diff --git a/docs/BOT-CVARS.md b/docs/BOT-CVARS.md index c975278..81af27c 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -6,27 +6,22 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | Name | Origin | Default | Valid values | Description | |------|--------|---------|--------------|-------------| -| `bot_aasoptimize` | Vanilla | `0` | 0 or 1 | When `1`, allows the bot navigation library to optimize the map’s AAS data when it is built or loaded. | -| `bot_challenge` | Vanilla | `0` | 0 or 1 | Harder bots on **legacy** aim: steadier tracking and snap-to-target when locked on. No effect on `bot_enhanced_aim` (enhanced harness ignores this cvar). | +| `bot_aasoptimize` | Vanilla | `0` | 0 or 1 | When `1`, allows the bot navigation library to optimize the map's AAS data when it is built or loaded. | +| `bot_challenge` | Vanilla | `0` | 0 or 1 | Harder bots on **legacy** aim: steadier tracking and snap-to-target when locked on. No effect when `bot_enhanced` is on (enhanced harness ignores this cvar). | | `bot_developer` | Vanilla | `0` | 0 or 1 | Extra bot-library debug output. Requires `sv_cheats 1`. | | `bot_debugAim` | Devotion | `0` | 0 or 1 | Publishes bot aim on `ps.grapplePoint`, `STAT_EXTFLAGS` (`EXTFL_BOT_AIM_DEBUG`), and `ent->s.origin2` for `cg_debugBotAim`. Works with or without enhanced aim. Requires `sv_cheats 1`. | | `bot_enable` | Vanilla | `0` | 0 or 1 | Master switch: bots only load and play when `1`. Usually set in `server.cfg` (provided by the engine, not the game module). | -| `bot_enhanced` | Devotion | `0` | 0 or 1 | Master switch for Devotion bot AI upgrades (aim harness, smart weapons, tactics). When `0`, sub-cvars have no effect. Saved to config. | -| `bot_enhanced_aim` | Devotion | `0` | 0 or 1 | Smoother, more human-like bot aiming when `1` and `bot_enhanced` is `1` (`0` = classic snap aim). Independent of `bot_challenge`. Saved to config. | -| `bot_enhanced_tactics` | Devotion | `0` | 0 or 1 | Extra combat decisions when `1` and `bot_enhanced` is `1`: gauntlet rush/flee, react to third-party damage, finish wounded targets, prefer nearer threats. Saved to config. | -| `bot_enhanced_items` | Devotion | `1` | 0 or 1 | Visible mega/red/yellow armor pickup with committed goals when `1` and `bot_enhanced` is `1`. Saved to config. | -| `bot_enhanced_items_debug` | Devotion | `0` | 0 or 1 | Server console lines when bots commit to or abandon major item pickups. | -| `bot_enhanced_movement` | Devotion | `0` | 0 or 1 | Human-like movement behaviors when `1` and `bot_enhanced` is `1`: active rocket-jump maneuver prep/fire and walk-off ledge fall-damage avoidance. Saved to config. | -| `bot_enhanced_weapons` | Devotion | `0` | 0 or 1 | Smarter weapon picks by range and ammo when `1` and `bot_enhanced` is `1` (e.g. rail/MG at distance, rocket mid-range, shotgun up close). Saved to config. | -| `bot_fastchat` | Vanilla | `0` | 0 or 1 | When `1`, bots are more likely to use chat lines (skips some random “stay quiet” rolls). | +| `bot_enhanced` | Devotion | `0` | 0 or 1 | Master switch for all Devotion bot AI upgrades: aim harness, smart weapons, tactics, items, item timing (per gametype), movement (RJ + walkoff), position, opponent (1v1), nav guard, combat intent. When `0`, behavior matches vanilla. Saved to config. | +| `bot_enhanced_debug` | Devotion | `0` | 0 or 1 | Server-side debug logging for enhanced subsystems (item commits, nav guard, opponent inference, etc.). Requires `bot_enhanced 1`. | +| `bot_fastchat` | Vanilla | `0` | 0 or 1 | When `1`, bots are more likely to use chat lines (skips some random "stay quiet" rolls). | | `bot_forceclustering` | Vanilla | `0` | 0 or 1 | Forces the navigation system to rebuild area clusters for the current map (slow; map load / AAS build). | | `bot_forcereachability` | Vanilla | `0` | 0 or 1 | Forces reachability between areas to be recalculated (slow; map load / AAS build). | | `bot_forcewrite` | Vanilla | `0` | 0 or 1 | Forces the navigation `.aas` file to be written to disk when the map is processed. | | `bot_grapple` | Vanilla | `0` | 0 or 1 | When `1`, bots may use the off-hand grapple for movement where the mod supports it. | -| `bot_interbreedbots` | Vanilla | `10` | integer ≥ 1 | Number of bots spawned when a bot “interbreeding” run starts. | +| `bot_interbreedbots` | Vanilla | `10` | integer ≥ 1 | Number of bots spawned when a bot "interbreeding" run starts. | | `bot_interbreedchar` | Vanilla | `` | bot name string | Bot character file to use for interbreeding; setting this starts a tournament-style breeding session. Cleared after spawn. | -| `bot_interbreedcycle` | Vanilla | `20` | integer ≥ 1 | How many matches to run before the best bot’s AI is saved and a new generation is spawned. | -| `bot_interbreedwrite` | Vanilla | `` | filename string | If set, writes the winning bot’s goal logic to this file at the end of a breeding cycle, then clears. | +| `bot_interbreedcycle` | Vanilla | `20` | integer ≥ 1 | How many matches to run before the best bot's AI is saved and a new generation is spawned. | +| `bot_interbreedwrite` | Vanilla | `` | filename string | If set, writes the winning bot's goal logic to this file at the end of a breeding cycle, then clears. | | `bot_memorydump` | Vanilla | `0` | 0 or 1 | One-shot: dumps bot-library memory stats, then resets to `0`. Requires `sv_cheats 1`. | | `bot_minplayers` | Vanilla | `0` | integer ≥ 0 | Target player count per team (or FFA slot fill): server adds or removes bots about every 10 seconds to match. `0` = off. Shown in server info. | | `bot_nochat` | Vanilla | `0` | integer ≥ 0 | `0` = normal chat; `1` = no bot taunts/greetings; `2` or higher also blocks team orders/voice and bots talking in global chat. | @@ -36,32 +31,38 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | `bot_report` | Vanilla | `0` | 0 or 1 | Updates internal bot status info shown in server config strings. Requires `sv_cheats 1`. | | `bot_rocketjump` | Vanilla | `1` | 0 or 1 | When `1`, bots may rocket-jump for vertical movement; `0` disables it. | | `bot_saveroutingcache` | Vanilla | `0` | 0 or 1 | One-shot: saves the routing cache for the current map, then resets to `0`. Requires `sv_cheats 1`. | -| `bot_testclusters` | Vanilla | `0` | 0 or 1 | Debug: print AAS area/cluster under the bot’s view. Requires `sv_cheats 1`. | -| `bot_testichat` | Vanilla | `0` | 0 or 1 | Test mode: new bots fire their “enter game” chat line immediately. | +| `bot_testclusters` | Vanilla | `0` | 0 or 1 | Debug: print AAS area/cluster under the bot's view. Requires `sv_cheats 1`. | +| `bot_testichat` | Vanilla | `0` | 0 or 1 | Test mode: new bots fire their "enter game" chat line immediately. | | `bot_testrchat` | Vanilla | `0` | 0 or 1 | Test mode: triggers random chat handling in the bot library. | -| `bot_testsolid` | Vanilla | `0` | 0 or 1 | Debug: report whether the bot’s view point is in solid AAS. Requires `sv_cheats 1`. | +| `bot_testsolid` | Vanilla | `0` | 0 or 1 | Debug: report whether the bot's view point is in solid AAS. Requires `sv_cheats 1`. | | `bot_thinktime` | Vanilla | `100` | integer (ms), max 200 | Milliseconds between bot AI updates; lower = more reactive bots, higher = less CPU. Capped at 200. | | `bot_visualizejumppads` | Vanilla | `0` | 0 or 1 | Debug: draw jump-pad reachability in the navigation mesh. | | `bot_wigglefactor` | Vanilla | `0.0` | 0.0–1.0 | How often bots change strafe direction in combat; higher = more side-to-side movement. | ## Deprecated enhanced-bot cvars -These names are **not registered** by the game module anymore. On first load, if a new `bot_enhanced_*` cvar is still at its default and the old name is set in `server.cfg`, the value is copied once and `bot_enhanced` is turned on when needed. A one-line message is printed to the server console when any deprecated name is detected. +These names are **not registered** by the game module anymore. On first load, if any deprecated name is set in `server.cfg`, `bot_enhanced` is turned on and a one-line console notice is printed. Use `bot_enhanced` (and optionally `bot_enhanced_debug`) instead. | Deprecated | Replaced by | |------------|-------------| -| `bot_humanizeaim` | `bot_enhanced` + `bot_enhanced_aim` | -| `bot_smartWeaponChoice` | `bot_enhanced` + `bot_enhanced_weapons` | -| `bot_tacticalAI` | `bot_enhanced` + `bot_enhanced_tactics` | +| `bot_humanizeaim` | `bot_enhanced` | +| `bot_smartWeaponChoice` | `bot_enhanced` | +| `bot_tacticalAI` | `bot_enhanced` | +| `bot_enhanced_aim` | `bot_enhanced` | +| `bot_enhanced_weapons` | `bot_enhanced` | +| `bot_enhanced_tactics` | `bot_enhanced` | +| `bot_enhanced_items` | `bot_enhanced` | +| `bot_enhanced_items_debug` | `bot_enhanced_debug` | +| `bot_enhanced_items_timing` | `bot_enhanced` (timing active in FFA/Duel/TDM) | +| `bot_enhanced_movement` | `bot_enhanced` | +| `bot_enhanced_position` | `bot_enhanced` | +| `bot_enhanced_opponent` | `bot_enhanced` (opponent logic in 1v1 context) | -Example (equivalent to the old `set bot_humanizeaim 1`): +Example: ```text set bot_enhanced 1 -set bot_enhanced_aim 1 -set bot_enhanced_items 1 +set bot_enhanced_debug 0 ``` -Sub-cvars alone (`bot_enhanced_aim 1` without `bot_enhanced 1`) have no effect unless the master is enabled or legacy migration runs at init. - -For architecture, extension points, and the **Phase 8 parity test checklist**, see [BOT-ENHANCED-ARCHITECTURE.md](BOT-ENHANCED-ARCHITECTURE.md). +For architecture, extension points, and the **parity test checklist**, see [BOT-ENHANCED-ARCHITECTURE.md](BOT-ENHANCED-ARCHITECTURE.md). diff --git a/docs/BOT-ENHANCED-ARCHITECTURE.md b/docs/BOT-ENHANCED-ARCHITECTURE.md index eda009b..327ba12 100644 --- a/docs/BOT-ENHANCED-ARCHITECTURE.md +++ b/docs/BOT-ENHANCED-ARCHITECTURE.md @@ -40,22 +40,19 @@ flowchart TB think -.->|sets ideal_viewangles aimh_hold_fire| input ``` -**North-facing include for hooks:** `ai_bot_enhanced.h` (`BotEnhanced_*Active`, `BotEnhanced_OnThinkStart`, register/reset). +**North-facing include for hooks:** `ai_bot_enhanced.h` (`BotEnhanced_IsActive`, `BotEnhanced_DebugActive`, `BotEnhanced_OnThinkStart`, register/reset). --- ## Cvar matrix -| Active when | Cvar | Notes | -|-------------|------|--------| -| Master | `bot_enhanced` | Default `0`. Sub-cvars ignored if off. | -| Aim harness | `bot_enhanced` + `bot_enhanced_aim` | Facade: `BotEnhanced_AimActive()` (`bot_challenge` ignored) | -| Smart weapons | `bot_enhanced` + `bot_enhanced_weapons` | Facade: `BotEnhanced_WeaponsActive()` | -| Tactical AI | `bot_enhanced` + `bot_enhanced_tactics` | Facade: `BotEnhanced_TacticsActive()` | -| Movement | `bot_enhanced` + `bot_enhanced_movement` | Facade: `BotEnhanced_MovementActive()`. Gates: enhanced RJ maneuver prep/fire; walk-off ledge fall-damage avoidance. Enhanced RJ also requires `bot_enhanced_aim` (view bypass). | -| Debug (cheat) | `bot_debugAim` | Independent of master; client `cg_debugBotAim` | +| Cvar | Role | +|------|------| +| `bot_enhanced` | Master gate (default `0`). When `1`, all enhanced features are on: aim harness, weapons, tactics, items, item timing (FFA/Duel/TDM), movement (RJ + walkoff), position, opponent (1v1), nav guard, combat intent. | +| `bot_enhanced_debug` | Server-side debug logging for enhanced subsystems (item commits, nav guard, opponent inference). Requires `bot_enhanced 1`. | +| `bot_debugAim` | Client cheat debug (independent); see BOT-CVARS.md | -Legacy `bot_humanizeaim` / `bot_smartWeaponChoice` / `bot_tacticalAI` are migrated once at init if new cvars are still default (see BOT-CVARS.md). +Deprecated `bot_enhanced_*` sub-cvars and legacy `bot_humanizeaim` / `bot_smartWeaponChoice` / `bot_tacticalAI` are read once at init for migration (see BOT-CVARS.md). --- @@ -63,12 +60,12 @@ Legacy `bot_humanizeaim` / `bot_smartWeaponChoice` / `bot_tacticalAI` are migrat | File | Role | |------|------| -| `ai_bot_enhanced.c/h` | Master cvar, facade gates, `OnThinkStart`, register/reset orchestration, goal-stack overflow guards (`BotEnhanced_PushGoalSafe`, `BotEnhanced_ReserveGoalStackRoom`, `BotEnhanced_SanitizeGoalStack`) | -| `ai_bot_items.c/h` | Visible pickup commits, botlib item chooser wrappers (uses enhanced goal-stack API) | -| `ai_bot_combat.c/h` | `bot_combat_intent_t` on `bot_state_t`; stance/move/fire policy (defaults = legacy) | -| `ai_bot_events.c/h` | Ingress queue (`evt_*`); `BotEvents_Drain` → tactics scan/process | -| `ai_bot_move_harness.c/h` | Botlib movement-view bypass (`bot_enhanced_aim`); `bot_enhanced_movement` cvar + `BotEnhanced_MovementActive()` gate; enhanced RJ maneuver prep/fire (requires both aim + movement); walk-off ledge fall-damage avoidance; `BotMove_EffectiveTfl`; hooks think/input | -| `ai_bot_move_util.c/h` | Shared horiz walk, approach speed, view actuation, goal anchors for maneuvers | +| `ai_bot_enhanced.c/h` | Master cvar, `BotEnhanced_IsActive` / `BotEnhanced_DebugActive`, `OnThinkStart`, register/reset orchestration, goal-stack guards | +| `ai_bot_items.c/h` | Visible pickup commits, item timing (merged), botlib item chooser wrappers | +| `ai_bot_combat.c/h` | `bot_combat_intent_t`; stance/move/fire policy | +| `ai_bot_events.h` | Ingress API (`BotEvents_Push`); implementation in `ai_bot_tactics.c` | +| `ai_bot_move_harness.c/h` | Movement-view bypass, RJ maneuver, walk-off avoidance, `BotMove_BuildTravelFlags` / `BotMove_EffectiveTfl`, move util helpers | +| `ai_bot_move_util.h` | Shared geometry/view helper declarations (implemented in harness) | | `ai_aim_harness.c/h` | Humanized view motor; monotonic menu skill 0–5 accuracy ladder; suppressive fire (wide cone, all skills); rail/RL/SG shot urgency (reload+grace without firing widens track/trace tolerances); rail lead-and-wait + trace/urgency fire | | `ai_weapon_select.c/h` | Range/ammo weapon picker + roam selection; voluntary close combat (25%: SG > plasma > gauntlet) | | `ai_bot_tactics.c/h` | Gauntlet flee, hurt-by-other, closer threat (skill + distance-scaled swap chance), finish wounded | diff --git a/ratoa_gamecode/Makefile b/ratoa_gamecode/Makefile index d8539f1..24c2ce1 100644 --- a/ratoa_gamecode/Makefile +++ b/ratoa_gamecode/Makefile @@ -1794,13 +1794,15 @@ Q3GOBJ_ = \ $(B)/baseq3/game/ai_dmq3.o \ $(B)/baseq3/game/ai_bot_enhanced.o \ $(B)/baseq3/game/ai_bot_combat.o \ - $(B)/baseq3/game/ai_bot_events.o \ - $(B)/baseq3/game/ai_bot_move_util.o \ $(B)/baseq3/game/ai_bot_move_harness.o \ $(B)/baseq3/game/ai_aim_harness.o \ $(B)/baseq3/game/ai_weapon_select.o \ $(B)/baseq3/game/ai_bot_tactics.o \ $(B)/baseq3/game/ai_bot_items.o \ + $(B)/baseq3/game/ai_bot_position.o \ + $(B)/baseq3/game/ai_bot_opponent.o \ + $(B)/baseq3/game/ai_bot_nav_guard.o \ + $(B)/baseq3/game/ai_bot_dmnet.o \ $(B)/baseq3/game/ai_main.o \ $(B)/baseq3/game/ai_team.o \ $(B)/baseq3/game/ai_vcmd.o \ @@ -1863,12 +1865,15 @@ MPGOBJ_ = \ $(B)/missionpack/game/ai_dmq3.o \ $(B)/missionpack/game/ai_bot_enhanced.o \ $(B)/missionpack/game/ai_bot_combat.o \ - $(B)/missionpack/game/ai_bot_events.o \ - $(B)/missionpack/game/ai_bot_move_util.o \ $(B)/missionpack/game/ai_bot_move_harness.o \ $(B)/missionpack/game/ai_aim_harness.o \ $(B)/missionpack/game/ai_weapon_select.o \ $(B)/missionpack/game/ai_bot_tactics.o \ + $(B)/missionpack/game/ai_bot_items.o \ + $(B)/missionpack/game/ai_bot_position.o \ + $(B)/missionpack/game/ai_bot_opponent.o \ + $(B)/missionpack/game/ai_bot_nav_guard.o \ + $(B)/missionpack/game/ai_bot_dmnet.o \ $(B)/missionpack/game/ai_main.o \ $(B)/missionpack/game/ai_team.o \ $(B)/missionpack/game/ai_vcmd.o \ diff --git a/ratoa_gamecode/code/cgame/cg_unlagged.c b/ratoa_gamecode/code/cgame/cg_unlagged.c index 943187a..96f61a3 100644 --- a/ratoa_gamecode/code/cgame/cg_unlagged.c +++ b/ratoa_gamecode/code/cgame/cg_unlagged.c @@ -1821,7 +1821,9 @@ void CG_DrawBotAimFollowFirstPerson( void ) { } cent = &cg_entities[cg.snap->ps.clientNum]; - CG_BotAimDebugEye( cent, bodyStart, CG_BOTAIMDBG_BODY_HEIGHT ); + /* Chest height — eye origin hides rails under the follow crosshair. */ + VectorCopy(cg.snap->ps.origin, bodyStart); + bodyStart[2] += cg.snap->ps.viewheight * CG_BOTAIMDBG_BODY_HEIGHT; VectorCopy( cg.snap->ps.grapplePoint, aimPoint ); if (VectorLengthSquared( aimPoint ) < 64.0f ) { diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index 9eafbbd..b7b2063 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -18,9 +18,10 @@ BOT AIM HARNESS (v1) — see ai_aim_harness.h #include "ai_aim_harness.h" #include "ai_dmq3.h" #include "ai_bot_enhanced.h" +#include "ai_bot_combat.h" #include "ai_bot_move_harness.h" -vmCvar_t bot_enhanced_aim; +extern vmCvar_t bot_enhanced; vmCvar_t bot_debugAim; /* Forward — defined in ai_dmq3.c / ai_main.c */ @@ -32,6 +33,12 @@ void BotAI_Trace(bsp_trace_t *bsptrace, vec3_t start, vec3_t mins, vec3_t maxs, extern vmCvar_t bot_thinktime; extern bot_state_t *botstates[MAX_CLIENTS]; +static int BotAimHarness_UsingRocketLauncher(bot_state_t *bs); +static qboolean BotAimHarness_WeaponReady(bot_state_t *bs); +static void BotAimHarness_ClampTrackVelocity(vec3_t vel); +static void BotAimHarness_GetLiveTrackingAimPoint(bot_state_t *bs, vec3_t aimPoint); +static void BotAimHarness_ComputeRailLeadPoint(bot_state_t *bs, vec3_t leadPoint); + #define AIMH_FLICK_ANGLE 28.0f #define AIMH_STIFFNESS_TRACK 320.0f #define AIMH_STIFFNESS_FLICK 500.0f @@ -122,12 +129,22 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_MOTOR_SUBSTEP_DT 0.010f #define AIMH_MOTOR_SUBSTEP_MAX 12 -/* Suppressive fire: wide cone for all skills (low skill = spray, not picky trigger). */ +/* Suppressive fire: wide cone; trace-only rail fire at elite skill. */ #define AIMH_FIRE_TRACK_FOV 100.0f #define AIMH_FIRE_TRACK_SLACK 30.0f #define AIMH_FIRE_ANY_VISIBILITY 0.06f #define AIMH_FIRE_BLOCKED_TRACE_FRAC 0.18f #define AIMH_FIRE_RL_MAX_DIST 1024.0f +/* Blind suppressive fire at last-seen / predicted peek (RL/GL/plasma/BFG). */ +#define AIMH_BLIND_MIN_AIM_DIST 80.0f +#define AIMH_BLIND_SKILL_MIN 0.5f +#define AIMH_BLIND_RECENT_SEC 4.0f +#define AIMH_BLIND_RL_AMMO_MIN 3 +#define AIMH_BLIND_GL_AMMO_MIN 4 +#define AIMH_BLIND_PLASMA_AMMO_MIN 35 +#define AIMH_BLIND_BFG_AMMO_MIN 25 +#define AIMH_BLIND_TRACK_FOV 110.0f +#define AIMH_BLIND_TRACK_SLACK 35.0f /* Think-time pursuit bias: held until next BotAimAtEnemy; close fights = larger angular error. */ #define AIMH_PURSUIT_ERR_NEAR 4.0f #define AIMH_PURSUIT_ERR_FAR 0.85f @@ -145,32 +162,64 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_CATCHUP_ERR_MIN 0.8f #define AIMH_CATCHUP_ERR_MAX 26.0f #define AIMH_OVERSHOOT_DAMP_ACC 0.12f +/* MG/LG: per-frame track — live enemy origin + think trace offset + velocity lead. */ +#define AIMH_TRACK_LEAD_SEC_BASE 0.070f +#define AIMH_TRACK_LEAD_SEC_ELITE 0.110f +#define AIMH_TRACK_LAG_COMP_SEC 0.040f +#define AIMH_TRACK_BLEND_DELTA 0.35f +#define AIMH_TRACK_BLEND_REL 0.65f +#define AIMH_TRACK_MAX_SPEED 520.0f +#define AIMH_TRACK_DELTA_MIN_DT 0.04f +#define AIMH_TRACK_DELTA_MAX_STEP 96.0f +#define AIMH_TRACK_VEL_SMOOTH 0.55f +#define AIMH_TRACK_LIVE_VEL_BLEND 0.22f + +/* MG/LG hit-feedback lead trim: shoot -> observe -> adjust over hundreds of ms. */ +#define AIMH_RECAL_INTERVAL 0.52f +#define AIMH_RECAL_INITIAL_DELAY 0.38f +#define AIMH_RECAL_LEAD_STEP 0.034f +#define AIMH_RECAL_HIT_BLEND 0.11f +#define AIMH_RECAL_LEAD_MIN 0.76f +#define AIMH_RECAL_LEAD_MAX 1.26f +#define AIMH_RECAL_MIN_SPEED 72.0f +#define AIMH_RECAL_ALONG_DEADZONE 10.0f /* Rail lead-and-wait: park crosshair ahead on strafe path; fire on bbox intersection. */ #define AIMH_RAIL_CENTER_Z 24.0f -#define AIMH_RAIL_LEAD_SPEED_MIN 32.0f -#define AIMH_RAIL_LEAD_SPEED_FULL 400.0f -#define AIMH_RAIL_LEAD_BASE 56.0f -#define AIMH_RAIL_LEAD_SKILL 100.0f -#define AIMH_RAIL_LEAD_MAX 180.0f -#define AIMH_RAIL_STRAFE_SEC_BASE 0.45f -#define AIMH_RAIL_STRAFE_SEC_SKILL 0.55f -#define AIMH_RAIL_LATERAL_SCALE 0.28f +#define AIMH_RAIL_LEAD_SPEED_MIN 48.0f +#define AIMH_RAIL_LEAD_SPEED_FULL 320.0f +#define AIMH_RAIL_LEAD_SKILL_BONUS 28.0f +#define AIMH_RAIL_LEAD_MAX 56.0f +#define AIMH_RAIL_INTERCEPT_SEC_BASE 0.07f +#define AIMH_RAIL_INTERCEPT_SEC_SKILL 0.11f #define AIMH_RAIL_MOTOR_STIFF_MULT 1.22f #define AIMH_RAIL_MOTOR_NOISE_SCALE 0.45f -#define AIMH_RAIL_FIRE_ANGLE_TOL 1.2f -#define AIMH_RAIL_FIRE_ANGLE_MAX 3.5f -/* Menu tier >= 0.6 (skill 3+): rail trace-only; lower skills get small angle slack. */ -#define AIMH_RAIL_FIRE_SLACK_SCALE 0.45f -/* Menu skill 0-5 ladder; elite motor from skill 4+ (tier > 0.6). */ -#define AIMH_MENU_SKILL_MIN 0.0f -#define AIMH_MENU_SKILL_MAX 5.0f -#define AIMH_MENU_SKILL_MID_TIER 0.6f -#define AIMH_MENU_ACC_MIN 0.30f -#define AIMH_MENU_ACC_MAX 0.94f -#define AIMH_MENU_AIM_SKILL_MIN 0.28f -#define AIMH_MENU_AIM_SKILL_MAX 0.96f -#define AIMH_MENU_CHAR_BLEND_MAX 0.30f -#define AIMH_RAIL_FIRE_TRACE_TIER 0.6f +#define AIMH_RAIL_FIRE_ANGLE_TOL 2.8f +#define AIMH_RAIL_FIRE_ANGLE_SLACK 3.5f +#define AIMH_RAIL_FIRE_DELAY 0.14f +#define AIMH_RAIL_PERFECT_BASE 0.30f +#define AIMH_RAIL_PERFECT_SKILL 0.48f +#define AIMH_RAIL_COMMIT_ANGLE 5.0f +#define AIMH_RAIL_PURSUIT_ERR_SCALE 0.52f +#define AIMH_RAIL_PURSUIT_PITCH_BIAS 1.4f +#define AIMH_RAIL_PURSUIT_YAW_BIAS 0.32f +#define AIMH_RAIL_FLICK_VEL_SCALE 0.78f +#define AIMH_RAIL_FLICK_STIFF_SCALE 0.90f +#define AIMH_RAIL_VEL_SMOOTH 0.28f +#define AIMH_RAIL_VEL_SPIKE_DIST 64.0f +#define AIMH_RAIL_VEL_AAS_BLEND 0.25f +#define AIMH_RAIL_LEAD_STATIONARY_MAX 8.0f +/* Elite motor tuning; aim skill/accuracy from BotEnhanced_GetAim*. */ +#define AIMH_MOTOR_INACCURACY 0.06f +#define AIMH_PURSUIT_ERR_ELITE 0.18f +#define AIMH_MOVE_LEAD_ELITE 0.42f +#define AIMH_RAIL_LEAD_ELITE 1.0f +#define AIMH_MOTOR_STIFF_ELITE 1.34f +#define AIMH_MOTOR_DAMP_ELITE 1.12f +#define AIMH_MOTOR_VEL_ELITE 1.28f +#define AIMH_MOTOR_NOISE_ELITE 0.35f +#define AIMH_RAIL_MOTOR_STIFF_ELITE 1.18f +#define AIMH_RAIL_MOTOR_NOISE_ELITE 0.42f +#define AIMH_CATCHUP_ELITE 1.55f /* Slow weapons: build pressure to fire after reload + grace without a shot. */ #define AIMH_URGENCY_GRACE 0.12f #define AIMH_URGENCY_RAMP 0.40f @@ -179,8 +228,9 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_URGENCY_ANGLE_MAX 8.0f #define AIMH_URGENCY_MIN_RELOAD 0.05f -static int bot_enhanced_aim_last = -1; +static int bot_enhanced_last = -1; static int bot_debugAim_last = -1; +static float aimh_rail_shot_roll[MAX_CLIENTS]; void BotAimHarness_SyncAllBotsDebug(void); @@ -208,78 +258,6 @@ static float BotAimHarness_Smoothstep(float edge0, float edge1, float x) { return t * t * (3.0f - 2.0f * t); } -static float BotAimHarness_GetMenuSkillTier(bot_state_t *bs) { - float s; - - if (!bs) { - return AIMH_MENU_SKILL_MID_TIER; - } - s = bs->settings.skill; - if (s < AIMH_MENU_SKILL_MIN) { - s = AIMH_MENU_SKILL_MIN; - } - if (s > AIMH_MENU_SKILL_MAX) { - s = AIMH_MENU_SKILL_MAX; - } - return (s - AIMH_MENU_SKILL_MIN) / (AIMH_MENU_SKILL_MAX - AIMH_MENU_SKILL_MIN); -} - -/* 0 for menu skill <= 3; ramps 0..1 for skill 4-5 (elite motor / lead tightening). */ -static float BotAimHarness_GetEliteMotorTier(bot_state_t *bs) { - float t; - - t = BotAimHarness_GetMenuSkillTier(bs); - if (t <= AIMH_MENU_SKILL_MID_TIER) { - return 0.0f; - } - return (t - AIMH_MENU_SKILL_MID_TIER) / (1.0f - AIMH_MENU_SKILL_MID_TIER); -} - -static float BotAimHarness_GetMenuLadderAccuracy(bot_state_t *bs) { - float t; - - if (!bs) { - return 0.5f; - } - t = BotAimHarness_GetMenuSkillTier(bs); - return BotAimHarness_Lerp(AIMH_MENU_ACC_MIN, AIMH_MENU_ACC_MAX, - BotAimHarness_Smoothstep(0.0f, 1.0f, t)); -} - -static float BotAimHarness_GetMenuLadderAimSkill(bot_state_t *bs) { - float t; - - if (!bs) { - return 0.5f; - } - t = BotAimHarness_GetMenuSkillTier(bs); - return BotAimHarness_Lerp(AIMH_MENU_AIM_SKILL_MIN, AIMH_MENU_AIM_SKILL_MAX, - BotAimHarness_Smoothstep(0.0f, 1.0f, t)); -} - -/* - * Monotonic menu skill 0-5 ladder; up to 30% botlib characteristic bleed at skill 5. - */ -static void BotAimHarness_ApplyMenuSkillCurve(bot_state_t *bs, float *aimSkill, float *aimAccuracy) { - float t, ladderAcc, ladderSkill, charMix; - - if (!bs || !aimSkill || !aimAccuracy) { - return; - } - t = BotAimHarness_GetMenuSkillTier(bs); - ladderAcc = BotAimHarness_GetMenuLadderAccuracy(bs); - ladderSkill = BotAimHarness_GetMenuLadderAimSkill(bs); - charMix = BotAimHarness_Smoothstep(0.15f, 1.0f, t) * AIMH_MENU_CHAR_BLEND_MAX; - *aimAccuracy = BotAimHarness_Lerp(ladderAcc, *aimAccuracy, charMix); - *aimSkill = BotAimHarness_Lerp(ladderSkill, *aimSkill, charMix); - if (*aimAccuracy > 0.98f) { - *aimAccuracy = 0.98f; - } - if (*aimSkill > 0.99f) { - *aimSkill = 0.99f; - } -} - /* * How "far/calm" vs "close/urgent" the motor should be (0 = close, 1 = far). * Angular goal rate: fast on-screen motion -> urgent (low value) even at medium range. @@ -517,9 +495,20 @@ static float BotAimHarness_GetCombatPursuitDist(bot_state_t *bs) { * Close target -> larger bias (degrees); far -> smaller. Scaled by (1 - aim_accuracy). * Sampled once per think in SetCombatGoal; motor pursues biased smooth_goal until next think. */ +static void BotAimHarness_ClearRailVel(bot_state_t *bs) { + if (!bs) { + return; + } + VectorClear(bs->aimh_rail_smooth_vel); + VectorClear(bs->aimh_rail_last_origin); + bs->aimh_rail_vel_sample_time = 0.0f; + bs->aimh_rail_vel_valid = qfalse; +} + static void BotAimHarness_ClearRailLead(bot_state_t *bs) { VectorClear(bs->aimh_rail_lead_point); bs->aimh_rail_lead_valid = qfalse; + BotAimHarness_ClearRailVel(bs); } static int BotAimHarness_UsingRailgun(bot_state_t *bs) { @@ -631,6 +620,35 @@ static qboolean BotAimHarness_IsRailInterceptActive(bot_state_t *bs) { return qtrue; } +static void BotAimHarness_ResetRailShotRoll(bot_state_t *bs) { + if (bs && bs->client >= 0 && bs->client < MAX_CLIENTS) { + aimh_rail_shot_roll[bs->client] = -1.0f; + } +} + +static void BotAimHarness_ApplyRailPursuitSample(bot_state_t *bs, const vec3_t trueAngles) { + float dist, closeFactor, maxErr, accScale, pitchOff, yawOff; + + dist = BotAimHarness_GetCombatPursuitDist(bs); + closeFactor = 1.0f - BotAimHarness_Smoothstep(AIMH_PURSUIT_ERR_DIST_NEAR, + AIMH_PURSUIT_ERR_DIST_FAR, dist); + maxErr = BotAimHarness_Lerp(AIMH_PURSUIT_ERR_FAR, AIMH_PURSUIT_ERR_NEAR, closeFactor); + accScale = 0.04f + 1.08f * (1.0f - bs->aimh_aim_accuracy); + maxErr *= accScale * AIMH_RAIL_PURSUIT_ERR_SCALE; + + pitchOff = (crandom() + crandom()) * 0.5f * maxErr * AIMH_PURSUIT_PITCH_SCALE * + AIMH_RAIL_PURSUIT_PITCH_BIAS; + yawOff = (crandom() + crandom()) * 0.5f * maxErr * AIMH_RAIL_PURSUIT_YAW_BIAS; + + bs->aimh_pursuit_pitch_off = pitchOff; + bs->aimh_pursuit_yaw_off = yawOff; + bs->aimh_true_goal_pitch = trueAngles[PITCH]; + bs->aimh_true_goal_yaw = trueAngles[YAW]; + bs->aimh_pursuit_set_time = FloatTime(); + bs->aimh_smooth_goal_pitch = BotAimHarness_ClampPitch(trueAngles[PITCH] + pitchOff); + bs->aimh_smooth_goal_yaw = AngleMod(trueAngles[YAW] + yawOff); +} + static void BotAimHarness_RefreshThinkPursuitGoal(bot_state_t *bs) { vec3_t trueAngles, dir; float dist, closeFactor, maxErr, accScale, pitchOff, yawOff; @@ -648,6 +666,15 @@ static void BotAimHarness_RefreshThinkPursuitGoal(bot_state_t *bs) { } trueAngles[PITCH] = BotAimHarness_ClampPitch(trueAngles[PITCH]); trueAngles[YAW] = AngleMod(trueAngles[YAW]); + BotAimHarness_ApplyRailPursuitSample(bs, trueAngles); + return; + } + + BotAimHarness_GetCombatAimAngles(bs, trueAngles); + trueAngles[PITCH] = BotAimHarness_ClampPitch(trueAngles[PITCH]); + trueAngles[YAW] = AngleMod(trueAngles[YAW]); + + if (BotAimHarness_UsingTrackingHitscan(bs)) { bs->aimh_pursuit_pitch_off = 0.0f; bs->aimh_pursuit_yaw_off = 0.0f; bs->aimh_true_goal_pitch = trueAngles[PITCH]; @@ -658,17 +685,13 @@ static void BotAimHarness_RefreshThinkPursuitGoal(bot_state_t *bs) { return; } - BotAimHarness_GetCombatAimAngles(bs, trueAngles); - trueAngles[PITCH] = BotAimHarness_ClampPitch(trueAngles[PITCH]); - trueAngles[YAW] = AngleMod(trueAngles[YAW]); - dist = BotAimHarness_GetCombatPursuitDist(bs); closeFactor = 1.0f - BotAimHarness_Smoothstep(AIMH_PURSUIT_ERR_DIST_NEAR, AIMH_PURSUIT_ERR_DIST_FAR, dist); maxErr = BotAimHarness_Lerp(AIMH_PURSUIT_ERR_FAR, AIMH_PURSUIT_ERR_NEAR, closeFactor); accScale = 0.04f + 1.08f * (1.0f - bs->aimh_aim_accuracy); maxErr *= accScale; - maxErr *= BotAimHarness_Lerp(1.0f, 0.18f, BotAimHarness_GetEliteMotorTier(bs)); + maxErr *= AIMH_PURSUIT_ERR_ELITE; pitchOff = (crandom() + crandom()) * 0.5f * maxErr * AIMH_PURSUIT_PITCH_SCALE; yawOff = (crandom() + crandom()) * 0.5f * maxErr; @@ -689,20 +712,25 @@ static void BotAimHarness_GetCombatTrueAimAngles(bot_state_t *bs, vec3_t angles) angles[ROLL] = 0.0f; } -/* Input frames: eye moves with bot; re-aim from live eye + think-sampled pursuit offset. */ +/* Input frames: eye moves with bot; rail lead recomputed live from enemy travel. */ static void BotAimHarness_GetLiveCombatMotorGoal(bot_state_t *bs, vec3_t goal) { - vec3_t trueAngles, dir; + vec3_t trueAngles, dir, leadPoint; - if (BotAimHarness_IsRailInterceptActive(bs) && bs->aimh_rail_lead_valid) { - VectorSubtract(bs->aimh_rail_lead_point, bs->eye, dir); - if (VectorLengthSquared(dir) < 1.0f) { - BotAimHarness_GetCombatTrueAimAngles(bs, goal); + if (BotAimHarness_IsRailInterceptActive(bs)) { + BotAimHarness_RefreshEye(bs); + BotAimHarness_ComputeRailLeadPoint(bs, leadPoint); + if (bs->aimh_rail_lead_valid) { + VectorSubtract(leadPoint, bs->eye, dir); + if (VectorLengthSquared(dir) < 1.0f) { + BotAimHarness_GetCombatTrueAimAngles(bs, goal); + return; + } + vectoangles(dir, goal); + goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH] + + bs->aimh_pursuit_pitch_off); + goal[YAW] = AngleMod(goal[YAW] + bs->aimh_pursuit_yaw_off); return; } - vectoangles(dir, goal); - goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); - goal[YAW] = AngleMod(goal[YAW]); - return; } BotAimHarness_GetCombatTrueAimAngles(bs, trueAngles); @@ -738,8 +766,7 @@ static qboolean BotAimHarness_InThinkSettleWindow(bot_state_t *bs) { } thinkSec = BotAimHarness_GetThinkIntervalSec(); elapsed = FloatTime() - bs->aimh_pursuit_set_time; - thinkSec *= 1.0f - BotAimHarness_Lerp(AIMH_SETTLE_THINK_FRAC, AIMH_SETTLE_THINK_FRAC_ELITE, - BotAimHarness_GetEliteMotorTier(bs)); + thinkSec *= 1.0f - AIMH_SETTLE_THINK_FRAC_ELITE; return elapsed >= thinkSec; } @@ -818,12 +845,7 @@ void BotAimHarness_ApplyMovementLead(bot_state_t *bs, vec3_t shotPoint, float ai return; } - if (aimSkill < 0.0f) { - aimSkill = 0.0f; - } - if (aimSkill > 1.0f) { - aimSkill = 1.0f; - } + aimSkill = BotEnhanced_GetAimSkill(bs); trap_Cvar_Update(&bot_thinktime); thinkSec = bot_thinktime.integer / 1000.0f; @@ -856,7 +878,7 @@ void BotAimHarness_ApplyMovementLead(bot_state_t *bs, vec3_t shotPoint, float ai if (leadDist > AIMH_LEAD_TOTAL_MAX) { leadDist = AIMH_LEAD_TOTAL_MAX; } - leadDist *= BotAimHarness_Lerp(1.0f, 0.42f, BotAimHarness_GetEliteMotorTier(bs)); + leadDist *= AIMH_MOVE_LEAD_ELITE; leadDist /= 1.0f + 0.0016f * BotAimHarness_GetHorizBotSpeed(bs); if (leadDist < 1.0f) { return; @@ -876,6 +898,106 @@ static int BotAimHarness_SplashNearEnemy(vec3_t splash, aas_entityinfo_t *entinf return VectorLength(delta) <= AIMH_RL_SPLASH_NEAR_XY; } +static qboolean BotAimHarness_IsBlindSuppressiveWeapon(int wp) { + return (wp == WP_ROCKET_LAUNCHER || wp == WP_GRENADE_LAUNCHER || + wp == WP_PLASMAGUN || wp == WP_BFG); +} + +static qboolean BotAimHarness_BlindSuppressiveAmmoOk(bot_state_t *bs, int wp) { + int ammo; + + if (!bs || wp <= WP_NONE || wp >= WP_NUM_WEAPONS) { + return qfalse; + } + ammo = bs->cur_ps.ammo[wp]; + switch (wp) { + case WP_ROCKET_LAUNCHER: + return ammo >= AIMH_BLIND_RL_AMMO_MIN; + case WP_GRENADE_LAUNCHER: + return ammo >= AIMH_BLIND_GL_AMMO_MIN; + case WP_PLASMAGUN: + return ammo >= AIMH_BLIND_PLASMA_AMMO_MIN; + case WP_BFG: + return ammo >= AIMH_BLIND_BFG_AMMO_MIN; + default: + return qfalse; + } +} + +static qboolean BotAimHarness_IsTrackingBlindAimTarget(bot_state_t *bs) { + vec3_t dir, angles; + + if (!BotAimHarness_AimTargetValid(bs)) { + return qfalse; + } + VectorSubtract(bs->aimtarget, bs->eye, dir); + if (VectorLengthSquared(dir) < Square(AIMH_BLIND_MIN_AIM_DIST)) { + return qfalse; + } + vectoangles(dir, angles); + return InFieldOfVision(bs->viewangles, AIMH_BLIND_TRACK_FOV + AIMH_BLIND_TRACK_SLACK, + angles); +} + +static qboolean BotAimHarness_BlindAimPathClear(bot_state_t *bs) { + bsp_trace_t trace; + + if (!BotAimHarness_AimTargetValid(bs)) { + return qfalse; + } + BotAI_Trace(&trace, bs->eye, NULL, NULL, bs->aimtarget, bs->client, + CONTENTS_SOLID | CONTENTS_PLAYERCLIP); + if (trace.fraction < 1.0f && trace.ent != bs->enemy) { + return qfalse; + } + return qtrue; +} + +/* + * Doorway / last-known-area suppressive fire when MASK_SHOT LOS to the enemy is gone. + */ +static qboolean BotAimHarness_WantsBlindSuppressiveFire(bot_state_t *bs, + const weaponinfo_t *wi) { + if (!bs || !wi || !wi->valid) { + return qfalse; + } + if (!BotEnhanced_IsActive()) { + return qfalse; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + if (BotCombat_HasFightLOS(bs, bs->enemy)) { + return qfalse; + } + if (!BotAimHarness_IsBlindSuppressiveWeapon(bs->weaponnum)) { + return qfalse; + } + if (bs->weaponnum != wi->number) { + return qfalse; + } + if (bs->aimh_aim_skill < AIMH_BLIND_SKILL_MIN) { + return qfalse; + } + if (!BotAimHarness_BlindSuppressiveAmmoOk(bs, bs->weaponnum)) { + return qfalse; + } + if (bs->lastenemyareanum <= 0) { + return qfalse; + } + if (bs->enemyvisible_time <= 0.0f || + bs->enemyvisible_time < FloatTime() - AIMH_BLIND_RECENT_SEC) { + return qfalse; + } + if (!BotAimHarness_IsTrackingBlindAimTarget(bs)) { + return qfalse; + } + if (!BotAimHarness_BlindAimPathClear(bs)) { + return qfalse; + } + return qtrue; +} + static int BotAimHarness_ValidateRocketSplashShot(bot_state_t *bs, aas_entityinfo_t *entinfo, vec3_t splash) { bsp_trace_t trace; @@ -944,29 +1066,341 @@ static int BotAimHarness_TryRocketFeetPoint(bot_state_t *bs, aas_entityinfo_t *e return qtrue; } - BotAimHarness_SetRocketSplashPoint(entinfo, refPoint, feetPoint); - - return BotAimHarness_ValidateRocketSplashShot(bs, entinfo, feetPoint); -} + BotAimHarness_SetRocketSplashPoint(entinfo, refPoint, feetPoint); + + return BotAimHarness_ValidateRocketSplashShot(bs, entinfo, feetPoint); +} + +static int BotAimHarness_UsingRocketLauncher(bot_state_t *bs) { + if (bs->weaponnum == WP_ROCKET_LAUNCHER) { + return qtrue; + } + if (bs->cur_ps.weapon == WP_ROCKET_LAUNCHER) { + return qtrue; + } + return qfalse; +} + +static int BotAimHarness_UsingPlasmagun(bot_state_t *bs) { + if (bs->weaponnum == WP_PLASMAGUN) { + return qtrue; + } + if (bs->cur_ps.weapon == WP_PLASMAGUN) { + return qtrue; + } + return qfalse; +} + +static int BotAimHarness_UsingMachinegun(bot_state_t *bs) { + if (bs->weaponnum == WP_MACHINEGUN) { + return qtrue; + } + if (bs->cur_ps.weapon == WP_MACHINEGUN) { + return qtrue; + } + return qfalse; +} + +static int BotAimHarness_UsingLightning(bot_state_t *bs) { + if (bs->weaponnum == WP_LIGHTNING) { + return qtrue; + } + if (bs->cur_ps.weapon == WP_LIGHTNING) { + return qtrue; + } + return qfalse; +} + +int BotAimHarness_UsingTrackingHitscan(bot_state_t *bs) { + if (!bs || !BotAimHarness_IsActive()) { + return qfalse; + } + return BotAimHarness_UsingMachinegun(bs) || BotAimHarness_UsingLightning(bs); +} + +static void BotAimHarness_ClearRecalState(bot_state_t *bs) { + if (!bs) { + return; + } + bs->aimh_recal_lead_scale = 1.0f; + bs->aimh_recal_next_time = 0.0f; + bs->aimh_recal_fire_since = 0.0f; + bs->aimh_recal_last_hits = 0; +} + +static void BotAimHarness_ClearTrackingAimState(bot_state_t *bs) { + if (!bs) { + return; + } + VectorClear(bs->aimh_prev_aimtarget); + VectorClear(bs->aimh_track_vel); + VectorClear(bs->aimh_track_offset); + bs->aimh_prev_aimtarget_time = 0.0f; + bs->aimh_aimtarget_sample_time = 0.0f; + bs->aimh_prev_aimtarget_valid = qfalse; +} + +static void BotAimHarness_RefreshTrackVelocityLive(bot_state_t *bs) { + aas_entityinfo_t entinfo; + vec3_t relDir, vRel; + float relSpeed, blend; + + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return; + } + if (!BotAimHarness_GetRelativeLeadDir(bs, &entinfo, relDir, &relSpeed)) { + return; + } + VectorScale(relDir, relSpeed, vRel); + vRel[2] *= 0.20f; + BotAimHarness_ClampTrackVelocity(vRel); + blend = AIMH_TRACK_LIVE_VEL_BLEND; + bs->aimh_track_vel[0] += (vRel[0] - bs->aimh_track_vel[0]) * blend; + bs->aimh_track_vel[1] += (vRel[1] - bs->aimh_track_vel[1]) * blend; + bs->aimh_track_vel[2] += (vRel[2] - bs->aimh_track_vel[2]) * blend; + BotAimHarness_ClampTrackVelocity(bs->aimh_track_vel); +} + +static void BotAimHarness_ClampTrackVelocity(vec3_t vel) { + float speed; + + speed = VectorLength(vel); + if (speed > AIMH_TRACK_MAX_SPEED) { + VectorScale(vel, AIMH_TRACK_MAX_SPEED / speed, vel); + } +} + +static void BotAimHarness_UpdateTrackVelocity(bot_state_t *bs) { + aas_entityinfo_t entinfo; + vec3_t vDelta, vRel, vBlend, relDir; + float dt, deltaSpeed, relSpeed, wDelta, wRel, smooth; + + if (!BotAimHarness_UsingTrackingHitscan(bs) || !BotAimHarness_AimTargetValid(bs)) { + BotAimHarness_ClearTrackingAimState(bs); + return; + } + + VectorClear(vDelta); + dt = 0.0f; + if (bs->aimh_prev_aimtarget_valid && bs->aimh_prev_aimtarget_time > 0.0f) { + dt = bs->aimh_aimtarget_sample_time - bs->aimh_prev_aimtarget_time; + if (dt >= AIMH_TRACK_DELTA_MIN_DT) { + VectorSubtract(bs->aimtarget, bs->aimh_prev_aimtarget, vDelta); + if (VectorLength(vDelta) <= AIMH_TRACK_DELTA_MAX_STEP) { + VectorScale(vDelta, 1.0f / dt, vDelta); + vDelta[2] *= 0.35f; + } else { + VectorClear(vDelta); + } + } + } + + VectorClear(vRel); + if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + BotEntityInfo(bs->enemy, &entinfo); + if (entinfo.valid && + BotAimHarness_GetRelativeLeadDir(bs, &entinfo, relDir, &relSpeed)) { + VectorScale(relDir, relSpeed, vRel); + vRel[2] *= 0.20f; + } + } + + deltaSpeed = VectorLength(vDelta); + relSpeed = VectorLength(vRel); + if (deltaSpeed < 1.0f && relSpeed < 1.0f) { + VectorClear(bs->aimh_track_vel); + return; + } + + wDelta = AIMH_TRACK_BLEND_DELTA; + wRel = AIMH_TRACK_BLEND_REL; + if (deltaSpeed < 1.0f) { + wDelta = 0.0f; + wRel = 1.0f; + } else if (relSpeed < 1.0f) { + wDelta = 1.0f; + wRel = 0.0f; + } + + VectorScale(vDelta, wDelta, vBlend); + VectorMA(vBlend, wRel, vRel, vBlend); + BotAimHarness_ClampTrackVelocity(vBlend); + + smooth = AIMH_TRACK_VEL_SMOOTH; + if (!bs->aimh_prev_aimtarget_valid) { + smooth = 1.0f; + } + bs->aimh_track_vel[0] += (vBlend[0] - bs->aimh_track_vel[0]) * smooth; + bs->aimh_track_vel[1] += (vBlend[1] - bs->aimh_track_vel[1]) * smooth; + bs->aimh_track_vel[2] += (vBlend[2] - bs->aimh_track_vel[2]) * smooth; + BotAimHarness_ClampTrackVelocity(bs->aimh_track_vel); +} + +void BotAimHarness_PreserveAimTargetSample(bot_state_t *bs) { + if (!BotAimHarness_IsActive() || !bs) { + return; + } + if (!BotAimHarness_AimTargetValid(bs)) { + bs->aimh_prev_aimtarget_valid = qfalse; + return; + } + VectorCopy(bs->aimtarget, bs->aimh_prev_aimtarget); + bs->aimh_prev_aimtarget_time = bs->aimh_aimtarget_sample_time; + bs->aimh_prev_aimtarget_valid = qtrue; +} + +void BotAimHarness_CommitAimTargetSample(bot_state_t *bs) { + aas_entityinfo_t entinfo; + + if (!BotAimHarness_IsActive() || !bs) { + return; + } + if (!BotAimHarness_AimTargetValid(bs)) { + BotAimHarness_ClearTrackingAimState(bs); + return; + } + bs->aimh_aimtarget_sample_time = FloatTime(); + if (BotAimHarness_UsingTrackingHitscan(bs) && + bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + BotEntityInfo(bs->enemy, &entinfo); + if (entinfo.valid) { + VectorSubtract(bs->aimtarget, entinfo.origin, bs->aimh_track_offset); + } + } + BotAimHarness_UpdateTrackVelocity(bs); +} + +static float BotAimHarness_GetTrackLeadSec(bot_state_t *bs) { + float leadSec; + + leadSec = AIMH_TRACK_LEAD_SEC_ELITE + AIMH_TRACK_LAG_COMP_SEC; + leadSec *= (0.55f + 0.45f * bs->aimh_aim_skill); + leadSec *= bs->aimh_recal_lead_scale; + return leadSec; +} + +/* + * Slow closed-loop lead trim for MG/LG: compare PERS_HITS over each observe window, + * nudge lead scale forward/back along enemy travel when missing a moving target. + */ +static void BotAimHarness_TickHitRecalibration(bot_state_t *bs) { + aas_entityinfo_t entinfo; + vec3_t aimPoint, offset, velHoriz; + float now, relSpeed, along, step; + int gotHit; + + if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim) { + return; + } + if (!BotAimHarness_UsingTrackingHitscan(bs)) { + return; + } + + now = FloatTime(); + if (!bs->aimh_hold_fire) { + bs->aimh_recal_fire_since = 0.0f; + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + if (now < bs->aimh_acquire_until) { + return; + } + + if (bs->aimh_recal_fire_since <= 0.0f) { + bs->aimh_recal_fire_since = now; + bs->aimh_recal_last_hits = bs->cur_ps.persistant[PERS_HITS]; + bs->aimh_recal_next_time = now + AIMH_RECAL_INITIAL_DELAY; + return; + } + + if (now < bs->aimh_recal_next_time) { + return; + } + bs->aimh_recal_next_time = now + AIMH_RECAL_INTERVAL; + + if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy) < 0.2f) { + return; + } + + gotHit = bs->cur_ps.persistant[PERS_HITS] > bs->aimh_recal_last_hits; + bs->aimh_recal_last_hits = bs->cur_ps.persistant[PERS_HITS]; + + if (gotHit) { + bs->aimh_recal_lead_scale += (1.0f - bs->aimh_recal_lead_scale) * AIMH_RECAL_HIT_BLEND; + return; + } + + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return; + } + + velHoriz[0] = bs->aimh_track_vel[0]; + velHoriz[1] = bs->aimh_track_vel[1]; + velHoriz[2] = 0.0f; + relSpeed = VectorLength(velHoriz); + if (relSpeed < AIMH_RECAL_MIN_SPEED) { + return; + } + + BotAimHarness_GetLiveTrackingAimPoint(bs, aimPoint); + VectorSubtract(aimPoint, entinfo.origin, offset); + offset[2] = 0.0f; + along = DotProduct(offset, velHoriz) / relSpeed; + + step = AIMH_RECAL_LEAD_STEP * (0.62f + 0.38f * bs->aimh_aim_skill); + if (along > AIMH_RECAL_ALONG_DEADZONE) { + bs->aimh_recal_lead_scale -= step; + } else if (along < -AIMH_RECAL_ALONG_DEADZONE) { + bs->aimh_recal_lead_scale += step; + } else { + return; + } + + if (bs->aimh_recal_lead_scale < AIMH_RECAL_LEAD_MIN) { + bs->aimh_recal_lead_scale = AIMH_RECAL_LEAD_MIN; + } else if (bs->aimh_recal_lead_scale > AIMH_RECAL_LEAD_MAX) { + bs->aimh_recal_lead_scale = AIMH_RECAL_LEAD_MAX; + } +} + +static void BotAimHarness_GetLiveTrackingAimPoint(bot_state_t *bs, vec3_t aimPoint) { + aas_entityinfo_t entinfo; + vec3_t base; + float leadSec; + + if (!BotAimHarness_UsingTrackingHitscan(bs) || !BotAimHarness_AimTargetValid(bs)) { + if (BotAimHarness_AimTargetValid(bs)) { + VectorCopy(bs->aimtarget, aimPoint); + } + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + VectorCopy(bs->aimtarget, aimPoint); + return; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + VectorCopy(bs->aimtarget, aimPoint); + return; + } + + BotAimHarness_RefreshTrackVelocityLive(bs); -static int BotAimHarness_UsingRocketLauncher(bot_state_t *bs) { - if (bs->weaponnum == WP_ROCKET_LAUNCHER) { - return qtrue; - } - if (bs->cur_ps.weapon == WP_ROCKET_LAUNCHER) { - return qtrue; - } - return qfalse; -} + VectorCopy(entinfo.origin, base); + VectorAdd(base, bs->aimh_track_offset, base); -static int BotAimHarness_UsingPlasmagun(bot_state_t *bs) { - if (bs->weaponnum == WP_PLASMAGUN) { - return qtrue; + leadSec = BotAimHarness_GetTrackLeadSec(bs); + VectorCopy(base, aimPoint); + if (VectorLengthSquared(bs->aimh_track_vel) > 1.0f) { + VectorMA(base, leadSec, bs->aimh_track_vel, aimPoint); } - if (bs->cur_ps.weapon == WP_PLASMAGUN) { - return qtrue; - } - return qfalse; } void BotAimHarness_ApplyPlasmaCenterMassAim(bot_state_t *bs, vec3_t aimPoint) { @@ -985,98 +1419,188 @@ void BotAimHarness_ApplyPlasmaCenterMassAim(bot_state_t *bs, vec3_t aimPoint) { aimPoint[2] = BotAimHarness_GetEnemyCenterMassZ(&entinfo); } -void BotAimHarness_ApplyRailInterceptAim(bot_state_t *bs, vec3_t aimPoint, - float aimSkill, float aimAccuracy) { - aas_entityinfo_t entinfo; - vec3_t velDir, toEnemy; - float speed, speedT, dist, distT, t, leadDist, thinkSec, along, lateral; +/* + * Smooth world-space horizontal enemy velocity for rail intercept (not relative to bot). + */ +static void BotAimHarness_UpdateRailSmoothVel(bot_state_t *bs, aas_entityinfo_t *entinfo) { + vec3_t instant, aasVel, delta, blended; + float dt, speed, aasSpeed, blend; - if (!BotAimHarness_IsActive() || !BotAimHarness_UsingRailgun(bs)) { - BotAimHarness_ClearRailLead(bs); + if (!bs || !entinfo || !entinfo->valid) { return; } - if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { - BotAimHarness_ClearRailLead(bs); + + if (!bs->aimh_rail_vel_valid) { + VectorCopy(entinfo->origin, bs->aimh_rail_last_origin); + bs->aimh_rail_vel_sample_time = FloatTime(); + bs->aimh_rail_vel_valid = qtrue; + VectorClear(bs->aimh_rail_smooth_vel); + } + + VectorSubtract(entinfo->origin, bs->aimh_rail_last_origin, delta); + dt = FloatTime() - bs->aimh_rail_vel_sample_time; + if (dt < 0.001f) { return; } - BotEntityInfo(bs->enemy, &entinfo); - if (!entinfo.valid) { - BotAimHarness_ClearRailLead(bs); + if (VectorLength(delta) > AIMH_RAIL_VEL_SPIKE_DIST) { + VectorCopy(entinfo->origin, bs->aimh_rail_last_origin); + bs->aimh_rail_vel_sample_time = FloatTime(); return; } - aimPoint[2] = entinfo.origin[2] + AIMH_RAIL_CENTER_Z; + instant[0] = delta[0] / dt; + instant[1] = delta[1] / dt; + instant[2] = 0.0f; - if (aimSkill < 0.0f) { - aimSkill = 0.0f; + VectorSubtract(entinfo->origin, entinfo->lastvisorigin, aasVel); + aasVel[2] = 0.0f; + if (entinfo->update_time > 0.001f) { + VectorScale(aasVel, 1.0f / entinfo->update_time, aasVel); + } else { + VectorClear(aasVel); } - if (aimSkill > 1.0f) { - aimSkill = 1.0f; + aasSpeed = VectorLength(aasVel); + if (aasSpeed > AIMH_RAIL_LEAD_SPEED_FULL * 1.5f) { + VectorClear(aasVel); + aasSpeed = 0.0f; } - if (aimAccuracy < 0.0f) { - aimAccuracy = 0.0f; + + blend = AIMH_RAIL_VEL_SMOOTH; + if (dt > 0.18f) { + blend = 1.0f; } - if (aimAccuracy > 1.0f) { - aimAccuracy = 1.0f; + VectorScale(bs->aimh_rail_smooth_vel, 1.0f - blend, blended); + VectorMA(blended, blend, instant, bs->aimh_rail_smooth_vel); + + if (aasSpeed > AIMH_RAIL_LEAD_SPEED_MIN) { + VectorScale(bs->aimh_rail_smooth_vel, 1.0f - AIMH_RAIL_VEL_AAS_BLEND, + blended); + VectorMA(blended, AIMH_RAIL_VEL_AAS_BLEND, aasVel, bs->aimh_rail_smooth_vel); } - if (!BotAimHarness_GetRelativeLeadDir(bs, &entinfo, velDir, &speed)) { - VectorCopy(aimPoint, bs->aimh_rail_lead_point); - bs->aimh_rail_lead_valid = qtrue; - return; + speed = VectorLength(bs->aimh_rail_smooth_vel); + if (speed > AIMH_RAIL_LEAD_SPEED_FULL * 1.25f) { + VectorScale(bs->aimh_rail_smooth_vel, (AIMH_RAIL_LEAD_SPEED_FULL * 1.25f) / speed, + bs->aimh_rail_smooth_vel); } - VectorCopy(aimPoint, bs->aimh_rail_lead_point); - bs->aimh_rail_lead_valid = qtrue; + VectorCopy(entinfo->origin, bs->aimh_rail_last_origin); + bs->aimh_rail_vel_sample_time = FloatTime(); +} - if (speed < AIMH_RAIL_LEAD_SPEED_MIN || speed < 0.001f) { - VectorCopy(aimPoint, bs->aimh_rail_lead_point); - return; +static qboolean BotAimHarness_GetRailRelativeLeadDir(bot_state_t *bs, vec3_t velDir, + float *speed) { + vec3_t relVel, botVel; + + if (!bs || !velDir || !speed) { + return qfalse; } - trap_Cvar_Update(&bot_thinktime); - thinkSec = bot_thinktime.integer / 1000.0f; - if (thinkSec < 0.001f) { - thinkSec = 0.1f; + VectorCopy(bs->aimh_rail_smooth_vel, relVel); + relVel[2] = 0.0f; + + VectorCopy(bs->velocity, botVel); + botVel[2] = 0.0f; + if (VectorLengthSquared(botVel) < Square(24.0f) && + BotAI_GetClientState(bs->client, &bs->cur_ps)) { + VectorCopy(bs->cur_ps.velocity, botVel); + botVel[2] = 0.0f; + } + VectorSubtract(relVel, botVel, relVel); + + *speed = VectorLength(relVel); + if (*speed < 0.001f) { + return qfalse; } + VectorScale(relVel, 1.0f / *speed, velDir); + return *speed >= AIMH_RAIL_LEAD_SPEED_MIN; +} - leadDist = AIMH_RAIL_LEAD_BASE + AIMH_RAIL_LEAD_SKILL * aimSkill; - leadDist *= 0.4f + 0.6f * aimAccuracy; +/* + * Lead-and-wait intercept point from live enemy origin + smoothed relative velocity. + * Recomputed every call (think + each input motor frame). + */ +static void BotAimHarness_ComputeRailLeadPoint(bot_state_t *bs, vec3_t leadPoint) { + aas_entityinfo_t entinfo; + vec3_t velDir, base; + float aimSkill, aimAccuracy, speed, speedT, leadDist, interceptSec; - speedT = BotAimHarness_Clamp01((speed - AIMH_RAIL_LEAD_SPEED_MIN) / - (AIMH_RAIL_LEAD_SPEED_FULL - AIMH_RAIL_LEAD_SPEED_MIN)); - VectorSubtract(entinfo.origin, bs->eye, toEnemy); - dist = VectorLength(toEnemy); - distT = BotAimHarness_Clamp01((dist - AIMH_LEAD_DIST_MIN) / - (AIMH_LEAD_DIST_FULL - AIMH_LEAD_DIST_MIN)); - t = speedT * distT * (0.5f + 0.5f * aimSkill); - leadDist += AIMH_RAIL_LEAD_MAX * 0.35f * t; + if (!leadPoint) { + return; + } + VectorClear(leadPoint); + if (!BotAimHarness_IsActive() || !BotAimHarness_UsingRailgun(bs)) { + bs->aimh_rail_lead_valid = qfalse; + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + bs->aimh_rail_lead_valid = qfalse; + return; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + bs->aimh_rail_lead_valid = qfalse; + return; + } - leadDist += speed * thinkSec * - (AIMH_RAIL_STRAFE_SEC_BASE + AIMH_RAIL_STRAFE_SEC_SKILL * aimSkill); + BotAimHarness_UpdateRailSmoothVel(bs, &entinfo); - toEnemy[2] = 0.0f; - if (VectorNormalize(toEnemy) > 0.1f) { - along = fabs(DotProduct(velDir, toEnemy)); - if (along > 1.0f) { - along = 1.0f; - } - lateral = speed * sqrt(1.0f - along * along); - leadDist += lateral * thinkSec * AIMH_RAIL_LATERAL_SCALE * - (0.5f + 0.5f * aimSkill); + VectorCopy(entinfo.origin, base); + base[2] = entinfo.origin[2] + AIMH_RAIL_CENTER_Z; + VectorCopy(base, leadPoint); + + aimSkill = BotEnhanced_GetAimSkill(bs); + aimAccuracy = BotEnhanced_GetAimAccuracy(bs); + + if (!BotAimHarness_GetRailRelativeLeadDir(bs, velDir, &speed)) { + VectorCopy(base, leadPoint); + VectorCopy(leadPoint, bs->aimh_rail_lead_point); + bs->aimh_rail_lead_valid = qtrue; + return; } + speedT = BotAimHarness_Clamp01((speed - AIMH_RAIL_LEAD_SPEED_MIN) / + (AIMH_RAIL_LEAD_SPEED_FULL - AIMH_RAIL_LEAD_SPEED_MIN)); + + interceptSec = AIMH_RAIL_INTERCEPT_SEC_BASE + + AIMH_RAIL_INTERCEPT_SEC_SKILL * aimSkill; + leadDist = speed * interceptSec * (0.35f + 0.65f * speedT); + leadDist += AIMH_RAIL_LEAD_SKILL_BONUS * aimSkill * speedT; + leadDist *= 0.45f + 0.55f * aimAccuracy; + if (leadDist > AIMH_RAIL_LEAD_MAX) { leadDist = AIMH_RAIL_LEAD_MAX; } - leadDist *= BotAimHarness_Lerp(1.0f, 0.50f, BotAimHarness_GetEliteMotorTier(bs)); - leadDist /= 1.0f + 0.0016f * BotAimHarness_GetHorizBotSpeed(bs); - if (leadDist < 1.0f) { + if (leadDist < AIMH_RAIL_LEAD_STATIONARY_MAX * speedT) { + if (speedT < 0.08f) { + VectorCopy(base, leadPoint); + VectorCopy(leadPoint, bs->aimh_rail_lead_point); + bs->aimh_rail_lead_valid = qtrue; + return; + } + leadDist = AIMH_RAIL_LEAD_STATIONARY_MAX * speedT; + } + leadDist *= AIMH_RAIL_LEAD_ELITE; + if (leadDist < 2.0f) { + VectorCopy(base, leadPoint); + VectorCopy(leadPoint, bs->aimh_rail_lead_point); + bs->aimh_rail_lead_valid = qtrue; return; } - VectorMA(aimPoint, leadDist, velDir, aimPoint); - VectorCopy(aimPoint, bs->aimh_rail_lead_point); + VectorMA(base, leadDist, velDir, leadPoint); + VectorCopy(leadPoint, bs->aimh_rail_lead_point); + bs->aimh_rail_lead_valid = qtrue; +} + +void BotAimHarness_ApplyRailInterceptAim(bot_state_t *bs, vec3_t aimPoint, + float aimSkill, float aimAccuracy) { + (void)aimSkill; + (void)aimAccuracy; + if (!bs || !aimPoint) { + return; + } + BotAimHarness_ComputeRailLeadPoint(bs, aimPoint); } void BotAimHarness_ApplyRocketFeetAim(bot_state_t *bs, vec3_t aimPoint) { @@ -1134,7 +1658,9 @@ static int BotAimHarness_GetCombatTarget(bot_state_t *bs, vec3_t target) { } } - BotAimHarness_ApplyMovementLead(bs, target, bs->aimh_aim_skill); + if (!BotAimHarness_UsingTrackingHitscan(bs)) { + BotAimHarness_ApplyMovementLead(bs, target, bs->aimh_aim_skill); + } VectorCopy(target, bs->aimh_combat_target); return qtrue; @@ -1155,7 +1681,7 @@ void BotAimHarness_ApplyThinkHitscanOrigin(bot_state_t *bs, vec3_t bestorigin, * lead-only combat_target only when aimtarget is not set (blind fire). */ static void BotAimHarness_GetCombatAimAngles(bot_state_t *bs, vec3_t angles) { - vec3_t dir, aimPoint; + vec3_t dir, aimPoint, leadPoint; aas_entityinfo_t entinfo; if (BotAimHarness_UsingRocketLauncher(bs) && bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { @@ -1179,17 +1705,24 @@ static void BotAimHarness_GetCombatAimAngles(bot_state_t *bs, vec3_t angles) { } } - if (BotAimHarness_IsRailInterceptActive(bs) && bs->aimh_rail_lead_valid) { - VectorCopy(bs->aimh_rail_lead_point, aimPoint); - VectorSubtract(aimPoint, bs->eye, dir); - vectoangles(dir, angles); - angles[PITCH] = BotAimHarness_ClampPitch(angles[PITCH]); - angles[YAW] = AngleMod(angles[YAW]); - return; + if (BotAimHarness_IsRailInterceptActive(bs)) { + BotAimHarness_RefreshEye(bs); + BotAimHarness_ComputeRailLeadPoint(bs, leadPoint); + if (bs->aimh_rail_lead_valid) { + VectorSubtract(leadPoint, bs->eye, dir); + vectoangles(dir, angles); + angles[PITCH] = BotAimHarness_ClampPitch(angles[PITCH]); + angles[YAW] = AngleMod(angles[YAW]); + return; + } } if (BotAimHarness_AimTargetValid(bs)) { - VectorCopy(bs->aimtarget, aimPoint); + if (BotAimHarness_UsingTrackingHitscan(bs)) { + BotAimHarness_GetLiveTrackingAimPoint(bs, aimPoint); + } else { + VectorCopy(bs->aimtarget, aimPoint); + } BotAimHarness_ApplyPlasmaCenterMassAim(bs, aimPoint); VectorSubtract(aimPoint, bs->eye, dir); } else if (bs->aimh_combat_aim && VectorLengthSquared(bs->aimh_combat_target) > 1.0f) { @@ -1279,6 +1812,21 @@ static qboolean BotAimHarness_ShotObviouslyBlocked(bot_state_t *bs) { return qtrue; } + if (BotEnhanced_IsActive()) { + if (BotCombat_HasFightLOS(bs, bs->enemy)) { + return qfalse; + } + { + weaponinfo_t wi; + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + if (BotAimHarness_WantsBlindSuppressiveFire(bs, &wi)) { + return qfalse; + } + } + return qtrue; + } + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy); if (vis > AIMH_FIRE_ANY_VISIBILITY) { return qfalse; @@ -1320,9 +1868,23 @@ static qboolean BotAimHarness_IsTrackingTarget(bot_state_t *bs, vec3_t firePoint return qfalse; } - vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy); - if (vis <= AIMH_FIRE_ANY_VISIBILITY) { - return qfalse; + if (BotEnhanced_IsActive()) { + if (BotCombat_HasFightLOS(bs, bs->enemy)) { + /* direct fight — fall through */ + } else { + weaponinfo_t blindWi; + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &blindWi); + if (BotAimHarness_WantsBlindSuppressiveFire(bs, &blindWi)) { + return qtrue; + } + return qfalse; + } + } else { + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy); + if (vis <= AIMH_FIRE_ANY_VISIBILITY) { + return qfalse; + } } trackFov = AIMH_FIRE_TRACK_FOV; @@ -1362,16 +1924,46 @@ static qboolean BotAimHarness_IsTrackingTarget(bot_state_t *bs, vec3_t firePoint return qfalse; } +/* + * Rail intercept: MASK_SHOT trace from muzzle along view must hit enemy bbox. + */ +static qboolean BotAimHarness_RailTraceHitsEnemy(bot_state_t *bs) { + bsp_trace_t trace; + vec3_t forward, right, start, end; + weaponinfo_t wi; + vec3_t mins = {-8, -8, -8}, maxs = {8, 8, 8}; + + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { + return qfalse; + } + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + VectorCopy(bs->origin, start); + start[2] += bs->cur_ps.viewheight; + AngleVectors(bs->viewangles, forward, right, NULL); + start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1]; + start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1]; + start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2]; + VectorMA(start, 8192, forward, end); + VectorMA(start, -12, forward, start); + BotAI_Trace(&trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT); + if (trace.ent != bs->enemy) { + return qfalse; + } + return qtrue; +} + /* * Legacy BotCheckAttack hit test: trace along actual viewangles from muzzle. */ static qboolean BotAimHarness_CrosshairHitsEnemy(bot_state_t *bs) { - aas_entityinfo_t entinfo; bsp_trace_t trace; - vec3_t forward, right, start, end, toEnemy, enemyAngles; + vec3_t forward, right, start, end; weaponinfo_t wi; vec3_t mins = {-8, -8, -8}, maxs = {8, 8, 8}; - float tol, pitchErr, yawErr; if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return qfalse; @@ -1379,6 +1971,9 @@ static qboolean BotAimHarness_CrosshairHitsEnemy(bot_state_t *bs) { if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { return qfalse; } + if (BotAimHarness_UsingRailgun(bs)) { + return BotAimHarness_RailTraceHitsEnemy(bs); + } trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); VectorCopy(bs->origin, start); @@ -1396,64 +1991,71 @@ static qboolean BotAimHarness_CrosshairHitsEnemy(bot_state_t *bs) { if (trace.ent >= 0 && trace.ent < MAX_CLIENTS && BotSameTeam(bs, trace.ent)) { return qfalse; } + return qfalse; +} + +static qboolean BotAimHarness_WantsRailFire(bot_state_t *bs) { + vec3_t trueAngles, dir, leadPoint; + float pitchErr, yawErr, magErr, coneTol, perfectThreshold; + float now; + float shotRoll; + + if (!bs || bs->client < 0 || bs->client >= MAX_CLIENTS) { + return qfalse; + } - BotEntityInfo(bs->enemy, &entinfo); - if (!entinfo.valid) { + now = FloatTime(); + if (now < bs->aimh_acquire_until + AIMH_RAIL_FIRE_DELAY) { return qfalse; } - VectorSubtract(entinfo.origin, bs->eye, toEnemy); - toEnemy[2] += AIMH_RAIL_CENTER_Z; - if (VectorLengthSquared(toEnemy) < 1.0f) { + + if (BotAimHarness_RailTraceHitsEnemy(bs)) { + BotAimHarness_ResetRailShotRoll(bs); return qtrue; } - vectoangles(toEnemy, enemyAngles); - pitchErr = fabs(BotAimHarness_PitchDiff(bs->viewangles[PITCH], enemyAngles[PITCH])); - yawErr = fabs(BotAimHarness_YawDiff(bs->viewangles[YAW], enemyAngles[YAW])); - tol = AIMH_RAIL_FIRE_ANGLE_TOL + - BotAimHarness_GetMenuSkillTier(bs) * - (AIMH_RAIL_FIRE_ANGLE_MAX - AIMH_RAIL_FIRE_ANGLE_TOL) * AIMH_RAIL_FIRE_SLACK_SCALE; - if (BotAimHarness_UsingRailgun(bs)) { - float urgency = BotAimHarness_GetShotUrgency(bs); - tol += urgency * AIMH_URGENCY_ANGLE_MAX; - if (pitchErr <= tol && yawErr <= tol) { - return qtrue; - } - /* High skill: trace-only until urgency builds, then accept tracking. */ - if (BotAimHarness_GetMenuSkillTier(bs) >= AIMH_RAIL_FIRE_TRACE_TIER) { - if (urgency >= 0.35f) { - vec3_t firePoint; - - BotAimHarness_GetEnemyFirePoint(bs, firePoint); - if (BotAimHarness_IsTrackingTarget(bs, firePoint)) { - return qtrue; - } - } - return qfalse; + if (BotAimHarness_IsRailInterceptActive(bs)) { + BotAimHarness_ComputeRailLeadPoint(bs, leadPoint); + if (bs->aimh_rail_lead_valid) { + VectorSubtract(leadPoint, bs->eye, dir); + } else { + BotAimHarness_GetCombatTrueAimAngles(bs, trueAngles); + AngleVectors(trueAngles, dir, NULL, NULL); } - return qfalse; + } else { + BotAimHarness_GetCombatTrueAimAngles(bs, trueAngles); + AngleVectors(trueAngles, dir, NULL, NULL); } - /* High menu skill: trace hit only (no angle slack). */ - if (BotAimHarness_GetMenuSkillTier(bs) >= AIMH_RAIL_FIRE_TRACE_TIER) { + if (VectorLengthSquared(dir) < 1.0f) { return qfalse; } - return pitchErr <= tol && yawErr <= tol; -} + vectoangles(dir, trueAngles); + pitchErr = BotAimHarness_PitchDiff(bs->viewangles[PITCH], trueAngles[PITCH]); + yawErr = BotAimHarness_YawDiff(bs->viewangles[YAW], trueAngles[YAW]); + magErr = sqrt(pitchErr * pitchErr + yawErr * yawErr); -static qboolean BotAimHarness_WantsRailFire(bot_state_t *bs) { - vec3_t firePoint; + coneTol = AIMH_RAIL_FIRE_ANGLE_TOL + + (1.0f - bs->aimh_aim_accuracy) * AIMH_RAIL_FIRE_ANGLE_SLACK; + if (magErr > AIMH_RAIL_COMMIT_ANGLE) { + return qfalse; + } - if (BotAimHarness_CrosshairHitsEnemy(bs)) { - return qtrue; + shotRoll = aimh_rail_shot_roll[bs->client]; + if (shotRoll < 0.0f) { + shotRoll = random(); + aimh_rail_shot_roll[bs->client] = shotRoll; } - if (BotAimHarness_GetShotUrgency(bs) < 0.35f) { + perfectThreshold = AIMH_RAIL_PERFECT_BASE + + AIMH_RAIL_PERFECT_SKILL * bs->aimh_aim_accuracy; + if (shotRoll <= perfectThreshold) { return qfalse; } - if (BotAimHarness_ShotObviouslyBlocked(bs)) { - return qfalse; + + if (magErr <= coneTol * 1.5f) { + BotAimHarness_ResetRailShotRoll(bs); + return qtrue; } - BotAimHarness_GetEnemyFirePoint(bs, firePoint); - return BotAimHarness_IsTrackingTarget(bs, firePoint); + return qfalse; } static qboolean BotAimHarness_WeaponReady(bot_state_t *bs) { @@ -1481,8 +2083,7 @@ static qboolean BotAimHarness_PassesThinkFireGates(bot_state_t *bs) { return qfalse; } - reactiontime = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, - 0, 1); + reactiontime = BotEnhanced_GetReactionTime(bs); if (bs->enemysight_time > FloatTime() - reactiontime) { return qfalse; } @@ -1493,10 +2094,6 @@ static qboolean BotAimHarness_PassesThinkFireGates(bot_state_t *bs) { return qfalse; } - if (bs->firethrottlewait_time > FloatTime()) { - return qfalse; - } - return qtrue; } @@ -1565,6 +2162,9 @@ static qboolean BotAimHarness_WantsSuppressiveFire(bot_state_t *bs, if (BotAimHarness_ShotObviouslyBlocked(bs)) { return qfalse; } + if (BotAimHarness_WantsBlindSuppressiveFire(bs, wi)) { + return qtrue; + } if (!BotAimHarness_IsTrackingTarget(bs, firePoint)) { return qfalse; } @@ -1582,33 +2182,33 @@ void BotAimHarness_CheckAttack(bot_state_t *bs) { bs->aimh_hold_fire = qfalse; if (!BotAimHarness_IsActive()) { - return; + goto recal_exit; } if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { - return; + goto recal_exit; } if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { - return; + goto recal_exit; } if (!BotAimHarness_PassesThinkFireGates(bs)) { - return; + goto recal_exit; } trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); BotAimHarness_GetEnemyFirePoint(bs, firePoint); if (BotAimHarness_IsRailInterceptActive(bs)) { - return; + goto recal_exit; } if (!BotAimHarness_WantsSuppressiveFire(bs, &wi, firePoint)) { - return; + goto recal_exit; } if (BotAimHarness_IsContinuousFireWeapon(bs, &wi)) { bs->aimh_hold_fire = qtrue; trap_EA_Attack(bs->client); - return; + goto recal_exit; } if (wi.flags & WFL_FIRERELEASED) { @@ -1617,6 +2217,9 @@ void BotAimHarness_CheckAttack(bot_state_t *bs) { } bs->flags ^= BFL_ATTACKED; } + +recal_exit: + BotAimHarness_TickHitRecalibration(bs); } /* @@ -1782,7 +2385,10 @@ static void BotAimHarness_OnEnemyChange(bot_state_t *bs) { bs->aimh_tracked_ideal_pitch = bs->viewangles[PITCH]; bs->aimh_tracked_ideal_yaw = bs->viewangles[YAW]; BotAimHarness_ClearRailLead(bs); + BotAimHarness_ResetRailShotRoll(bs); BotAimHarness_ResetShotUrgency(bs); + BotAimHarness_ClearTrackingAimState(bs); + BotAimHarness_ClearRecalState(bs); } static void BotAimHarness_ClearEntityDebug(gentity_t *ent) { @@ -1809,19 +2415,27 @@ static void BotAimHarness_AnglesToAimPoint(bot_state_t *bs, float pitch, float y } /* - * Debug aim point: combat = aimtarget (updated each bot think); roam = navigation - * ideal (never stale aimtarget left over from the last fight). + * Debug aim point: combat = live motor aim (tracking extrap for MG/LG, rail lead, else + * aimtarget); roam = navigation ideal. */ static int BotAimHarness_GetDebugAimPoint(bot_state_t *bs, vec3_t point) { - vec3_t wishAngles; + vec3_t wishAngles, leadPoint; if (bs->aimh_combat_aim) { - if (BotAimHarness_IsRailInterceptActive(bs) && bs->aimh_rail_lead_valid) { - VectorCopy(bs->aimh_rail_lead_point, point); - return qtrue; + if (BotAimHarness_IsRailInterceptActive(bs)) { + BotAimHarness_RefreshEye(bs); + BotAimHarness_ComputeRailLeadPoint(bs, leadPoint); + if (bs->aimh_rail_lead_valid) { + VectorCopy(leadPoint, point); + return qtrue; + } } if (BotAimHarness_AimTargetValid(bs)) { - VectorCopy(bs->aimtarget, point); + if (BotAimHarness_UsingTrackingHitscan(bs)) { + BotAimHarness_GetLiveTrackingAimPoint(bs, point); + } else { + VectorCopy(bs->aimtarget, point); + } return qtrue; } if (VectorLengthSquared(bs->aimh_combat_target) > 1.0f) { @@ -1941,21 +2555,18 @@ void BotAimHarness_SyncAllBotsDebug(void) { } void BotAimHarness_RegisterCvars(void) { - trap_Cvar_Register(&bot_enhanced_aim, "bot_enhanced_aim", "0", CVAR_ARCHIVE); - trap_Cvar_Register(&bot_debugAim, "bot_debugAim", "0", CVAR_CHEAT); - trap_Cvar_Update(&bot_enhanced_aim); trap_Cvar_Update(&bot_debugAim); } void BotAimHarness_ResetCvarLatch(void) { - bot_enhanced_aim_last = -1; + bot_enhanced_last = -1; bot_debugAim_last = -1; } void BotAimHarness_UpdateCvar(void) { int i; - trap_Cvar_Update(&bot_enhanced_aim); + trap_Cvar_Update(&bot_enhanced); trap_Cvar_Update(&bot_debugAim); if (bot_debugAim_last != bot_debugAim.integer) { @@ -1969,15 +2580,15 @@ void BotAimHarness_UpdateCvar(void) { } } - if (bot_enhanced_aim_last == bot_enhanced_aim.integer) { + if (bot_enhanced_last == bot_enhanced.integer) { return; } - bot_enhanced_aim_last = bot_enhanced_aim.integer; + bot_enhanced_last = bot_enhanced.integer; for (i = 0; i < MAX_CLIENTS; i++) { if (botstates[i] && botstates[i]->inuse) { BotAimHarness_Reset(botstates[i]); - if (!bot_enhanced_aim.integer) { + if (!bot_enhanced.integer) { botstates[i]->viewanglespeed[0] = 0; botstates[i]->viewanglespeed[1] = 0; BotAimHarness_ClearEntityDebug(&g_entities[i]); @@ -1987,7 +2598,7 @@ void BotAimHarness_UpdateCvar(void) { } int BotAimHarness_IsActive(void) { - return BotEnhanced_AimActive(); + return BotEnhanced_IsActive(); } void BotAimHarness_SyncMotorToView(bot_state_t *bs) { @@ -2016,6 +2627,15 @@ static void BotAimHarness_SyncViewAngles(bot_state_t *bs) { bs->viewangles[ROLL] = 0; } +void BotAimHarness_ReleaseCombat(bot_state_t *bs) { + if (!bs) { + return; + } + bs->aimh_combat_aim = qfalse; + bs->aimh_hold_fire = qfalse; + VectorClear(bs->aimh_combat_target); +} + void BotAimHarness_Reset(bot_state_t *bs) { BotAimHarness_SyncViewAngles(bs); VectorCopy(bs->viewangles, bs->aimh_goal); @@ -2048,51 +2668,29 @@ void BotAimHarness_Reset(bot_state_t *bs) { VectorClear(bs->aimh_combat_target); bs->aimh_hold_fire = qfalse; BotAimHarness_ClearRailLead(bs); + BotAimHarness_ResetRailShotRoll(bs); BotAimHarness_ResetShotUrgency(bs); + BotAimHarness_ClearTrackingAimState(bs); + BotAimHarness_ClearRecalState(bs); } void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, float aimSkill, float aimAccuracy, float weaponVSpread, float weaponHSpread) { - float inaccuracy; + float scale; (void)weaponVSpread; (void)weaponHSpread; + (void)aimSkill; + (void)aimAccuracy; VectorCopy(idealAngles, bs->aimh_goal); bs->aimh_goal[PITCH] = BotAimHarness_ClampPitch(bs->aimh_goal[PITCH]); bs->aimh_goal[YAW] = AngleMod(bs->aimh_goal[YAW]); - if (aimSkill < 0.0f) { - aimSkill = 0.0f; - } - if (aimSkill > 1.0f) { - aimSkill = 1.0f; - } - if (aimAccuracy < 0.0f) { - aimAccuracy = 0.0f; - } - if (aimAccuracy > 1.0f) { - aimAccuracy = 1.0f; - } - BotAimHarness_ApplyMenuSkillCurve(bs, &aimSkill, &aimAccuracy); - if (aimSkill < 0.0f) { - aimSkill = 0.0f; - } - if (aimSkill > 1.0f) { - aimSkill = 1.0f; - } - if (aimAccuracy < 0.0f) { - aimAccuracy = 0.0f; - } - if (aimAccuracy > 1.0f) { - aimAccuracy = 1.0f; - } - bs->aimh_aim_skill = aimSkill; - bs->aimh_aim_accuracy = aimAccuracy; - - inaccuracy = 1.0f - aimAccuracy; - bs->aimh_motor_inaccuracy = inaccuracy * - BotAimHarness_Lerp(0.62f, 0.06f, BotAimHarness_GetEliteMotorTier(bs)); + scale = BotEnhanced_SkillScale(bs); + bs->aimh_aim_skill = BotEnhanced_GetAimSkill(bs); + bs->aimh_aim_accuracy = BotEnhanced_GetAimAccuracy(bs); + bs->aimh_motor_inaccuracy = AIMH_MOTOR_INACCURACY * scale; bs->aimh_combat_aim = qtrue; VectorCopy(bs->aimh_goal, bs->ideal_viewangles); @@ -2336,6 +2934,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->aimh_combat_aim = qfalse; bs->aimh_hold_fire = qfalse; BotAimHarness_ClearRailLead(bs); + BotAimHarness_ResetRailShotRoll(bs); bs->aimh_motor_inaccuracy = 0.0f; bs->aimh_last_enemy_z = 0.0f; bs->aimh_last_sanity_enemy = -1; @@ -2353,8 +2952,8 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { BotAimHarness_GetMotorGoal(bs, goal, thinktime); motorNoise = bs->aimh_motor_inaccuracy; aimSkill = bs->aimh_aim_skill; - skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_FACTOR, 0.01f, 1.0f); - maxChange = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_MAXCHANGE, 1.0f, 1800.0f); + skill = BotEnhanced_GetViewFactor(bs); + maxChange = BotEnhanced_GetViewMaxChange(bs); if (maxChange < 240.0f) { maxChange = 240.0f; } @@ -2420,13 +3019,10 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } if (bs->aimh_combat_aim) { - float eliteTier = BotAimHarness_GetEliteMotorTier(bs); - stiffness *= (0.78f + 0.52f * aimSkill); - damping *= (0.88f + 0.32f * aimSkill); - stiffness *= BotAimHarness_Lerp(1.0f, 1.34f, eliteTier); - damping *= BotAimHarness_Lerp(1.0f, 1.12f, eliteTier); - maxVel *= BotAimHarness_Lerp(1.0f, 1.28f, eliteTier); - motorNoise *= BotAimHarness_Lerp(1.0f, 0.35f, eliteTier); + stiffness *= (0.78f + 0.52f * aimSkill) * AIMH_MOTOR_STIFF_ELITE; + damping *= (0.88f + 0.32f * aimSkill) * AIMH_MOTOR_DAMP_ELITE; + maxVel *= AIMH_MOTOR_VEL_ELITE; + motorNoise *= AIMH_MOTOR_NOISE_ELITE; } if (bs->aimh_combat_aim) { @@ -2436,15 +3032,22 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { (void)catchupMult; (void)ffDummy; } + if (bs->aimh_combat_aim && bs->aimh_acquire_until > FloatTime()) { + damping *= 0.91f; + } if (BotAimHarness_IsRailInterceptActive(bs) && bs->aimh_rail_lead_valid) { - float eliteTier = BotAimHarness_GetEliteMotorTier(bs); - stiffness *= BotAimHarness_Lerp(AIMH_RAIL_MOTOR_STIFF_MULT, 1.48f, eliteTier); - motorNoise *= BotAimHarness_Lerp(AIMH_RAIL_MOTOR_NOISE_SCALE, 0.22f, eliteTier); + stiffness *= AIMH_RAIL_MOTOR_STIFF_ELITE; + motorNoise *= AIMH_RAIL_MOTOR_NOISE_ELITE; + damping *= 0.96f; + maxVel *= 0.90f; + if (magErr > flickAngle * 1.35f) { + maxVel *= AIMH_RAIL_FLICK_VEL_SCALE; + stiffness *= AIMH_RAIL_FLICK_STIFF_SCALE; + } } if (bs->aimh_combat_aim && maxVel < 140.0f) { - maxVel = 140.0f + 80.0f * aimSkill + - 40.0f * BotAimHarness_GetEliteMotorTier(bs); + maxVel = 140.0f + 80.0f * aimSkill + 40.0f; } else if (maxVel < 90.0f) { maxVel = 90.0f; } @@ -2458,10 +3061,8 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } if (bs->aimh_combat_aim) { - float eliteTier = BotAimHarness_GetEliteMotorTier(bs); - minVel = AIMH_COMBAT_MIN_VEL + 45.0f * aimSkill + 35.0f * eliteTier; - catchupGain = AIMH_COMBAT_CATCHUP_GAIN * catchupMult * - BotAimHarness_Lerp(1.0f, 1.55f, eliteTier); + minVel = AIMH_COMBAT_MIN_VEL + 45.0f * aimSkill + 35.0f; + catchupGain = AIMH_COMBAT_CATCHUP_GAIN * catchupMult * AIMH_CATCHUP_ELITE; } else { minVel = AIMH_ROAM_MIN_VEL; catchupGain = 0.0f; diff --git a/ratoa_gamecode/code/game/ai_aim_harness.h b/ratoa_gamecode/code/game/ai_aim_harness.h index c20c634..ec47ce6 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -9,19 +9,21 @@ ai_main.h, ai_main.c, and ai_dmq3.c, and unset bot_enhanced / bot_enhanced_aim. Toggle at runtime: bot_enhanced 1 and bot_enhanced_aim 1 (default 0 = legacy aim motor). Was bot_humanizeaim; that name is migrated at init if bot_enhanced_aim is unset. Debug: bot_debugAim 1 (server, CVAR_CHEAT) publishes motor wish (ideal_viewangles -when roaming, aimtarget when fighting) via ps.grapplePoint + EXTFL_BOT_AIM_DEBUG; -cg_debugBotAim draws green = wish, yellow (bit 4) = crosshair. +when roaming, live aim point when fighting — MG/LG tracking extrap, rail intercept + * lead each input frame) via +ps.grapplePoint + EXTFL_BOT_AIM_DEBUG; cg_debugBotAim draws green = wish, yellow (bit 4) = crosshair. Combat fire: suppressive — fire when tracking unless LOS is obviously blocked; MG/LG -hold +attack each input frame (only drop hold when blocked). Rail/RL/SG shot urgency: -after weapon reload + grace on target without firing, tracking/trace tolerances widen so -bots take good-enough shots. Rail: lead-and-wait intercept; +attack when trace/urgency -allows. +hold +attack each input frame (only drop hold when blocked). MG/LG: slow hit-feedback +lead trim (observe ~0.5s windows) nudges tracking lead when shots miss a moving target. +Rail/RL/SG shot urgency: after weapon reload + grace on target without firing, +tracking/trace tolerances widen so bots take good-enough shots. Rail: live lead-and-wait +intercept; fire on bbox trace hit only. Think-time pursuit bias (bot_enhanced_aim): each BotAimAtEnemy samples a pitch/yaw offset from aim_accuracy; input frames re-aim from live eye to aimtarget plus that offset (settle window uses true aim only). Hitscan lead uses enemy-minus-bot horizontal velocity. -Menu skill 1-3 ~= legacy nightmare at skill 3; skill 4-5 tighten motor and fire. +All enhanced bots use elite aim constants; skill nerfs via BotEnhanced_SkillScale later. Motor frames use legacy delta-angle rebasing in BotUpdateInput, 10 ms integration sub-steps (stable at low sv_fps on dedicated), resync playerState only on large desync, @@ -46,6 +48,7 @@ float BotAimHarness_ClampPitchAngle(float pitch); void BotAimHarness_BeginMotorFrame(struct bot_state_s *bs); void BotAimHarness_Reset(struct bot_state_s *bs); +void BotAimHarness_ReleaseCombat(struct bot_state_s *bs); void BotAimHarness_SetCombatGoal(struct bot_state_s *bs, const float idealAngles[3], float aimSkill, float aimAccuracy, float weaponVSpread, float weaponHSpread); void BotAimHarness_ApplyThinkHitscanOrigin(struct bot_state_s *bs, float bestorigin[3], @@ -53,11 +56,15 @@ void BotAimHarness_ApplyThinkHitscanOrigin(struct bot_state_s *bs, float bestori /* Overpredict final shot point along enemy travel (once per bot think); all weapons. */ void BotAimHarness_ApplyMovementLead(struct bot_state_s *bs, float shotPoint[3], float aimSkill); +/* MG/LG: snapshot aimtarget before think overwrite; commit after new sample. */ +void BotAimHarness_PreserveAimTargetSample(struct bot_state_s *bs); +void BotAimHarness_CommitAimTargetSample(struct bot_state_s *bs); +int BotAimHarness_UsingTrackingHitscan(struct bot_state_s *bs); /* Splash rockets: aim at enemy feet (center Z - 28, bot_enhanced_aim). */ void BotAimHarness_ApplyRocketFeetAim(struct bot_state_s *bs, float aimPoint[3]); /* Plasma: torso center mass only (never feet / splash Z). */ void BotAimHarness_ApplyPlasmaCenterMassAim(struct bot_state_s *bs, float aimPoint[3]); -/* Rail: aim ahead along enemy travel; fire when view trace hits bbox (not suppressive). */ +/* Rail: live lead ahead along enemy-minus-bot travel; fire when view trace hits bbox. */ void BotAimHarness_ApplyRailInterceptAim(struct bot_state_s *bs, float aimPoint[3], float aimSkill, float aimAccuracy); int BotAimHarness_ChangeViewAngles(struct bot_state_s *bs, float thinktime); diff --git a/ratoa_gamecode/code/game/ai_bot_combat.c b/ratoa_gamecode/code/game/ai_bot_combat.c index 6311e33..53a4bd8 100644 --- a/ratoa_gamecode/code/game/ai_bot_combat.c +++ b/ratoa_gamecode/code/game/ai_bot_combat.c @@ -12,12 +12,200 @@ BOT COMBAT — intent reset/update and weapon-commit hook. #include "../botlib/be_ai_move.h" #include "../botlib/be_ai_weap.h" #include "ai_main.h" +#include "inv.h" #include "ai_bot_enhanced.h" #include "ai_bot_combat.h" #include "ai_bot_tactics.h" #include "ai_weapon_select.h" +#include "ai_aim_harness.h" +#include "ai_bot_opponent.h" +#include "ai_bot_position.h" +#include "ai_dmq3.h" +#include "chars.h" + +#define IDEAL_ATTACKDIST 140 + +#define BOT_LOADOUT_STACK_NOT_READY (-55) +#define BOT_LOADOUT_STACK_SITUATIONAL 18 +#define BOT_LOADOUT_STACK_READY 50 +#define BOT_LOADOUT_BIAS_NOT_READY (-0.48f) +#define BOT_LOADOUT_BIAS_SITUATIONAL (-0.14f) + +extern vmCvar_t bot_wigglefactor; + qboolean BotIsDead(bot_state_t *bs); qboolean BotIsObserver(bot_state_t *bs); +int BotFindEnemy(bot_state_t *bs, int curenemy); + +static int BotCombat_HasSituationalWeapon(const bot_state_t *bs) { + if (!bs) { + return 0; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_RAILGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_LIGHTNING)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_GRENADE_LAUNCHER)) { + return 1; + } +#ifdef MISSIONPACK + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_NAILGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_PROX_LAUNCHER)) { + return 1; + } +#endif + return 0; +} + +static int BotCombat_HasReadyWeapon(const bot_state_t *bs) { + if (!bs) { + return 0; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_BFG)) { + return 1; + } + if (bs->inventory[INVENTORY_QUAD]) { + return 1; + } + return 0; +} + +bot_loadout_tier_t BotCombat_GetLoadoutTier(const bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return BOT_LOADOUT_READY; + } + if (BotCombat_HasReadyWeapon(bs)) { + return BOT_LOADOUT_READY; + } + if (BotCombat_HasSituationalWeapon(bs)) { + return BOT_LOADOUT_SITUATIONAL; + } + return BOT_LOADOUT_NOT_READY; +} + +int BotCombat_LoadoutStackBonus(const bot_state_t *bs) { + switch (BotCombat_GetLoadoutTier(bs)) { + case BOT_LOADOUT_READY: + return BOT_LOADOUT_STACK_READY; + case BOT_LOADOUT_SITUATIONAL: + return BOT_LOADOUT_STACK_SITUATIONAL; + default: + return BOT_LOADOUT_STACK_NOT_READY; + } +} + +float BotCombat_LoadoutEngageBiasNudge(const bot_state_t *bs) { + switch (BotCombat_GetLoadoutTier(bs)) { + case BOT_LOADOUT_READY: + return 0.0f; + case BOT_LOADOUT_SITUATIONAL: + return BOT_LOADOUT_BIAS_SITUATIONAL; + default: + return BOT_LOADOUT_BIAS_NOT_READY; + } +} + +float BotCombat_HorizontalDistToClient(const bot_state_t *bs, int clientnum) { + aas_entityinfo_t entinfo; + vec3_t dir; + + if (!bs || clientnum < 0 || clientnum >= MAX_CLIENTS) { + return 99999.0f; + } + BotEntityInfo(clientnum, &entinfo); + if (!entinfo.valid) { + return 99999.0f; + } + VectorSubtract(entinfo.origin, bs->origin, dir); + dir[2] = 0.0f; + return VectorLength(dir); +} + +static int BotCombat_SituationalEngageOk(const bot_state_t *bs, float dist) { + int has_sg; + int has_pg; + int has_rail; + int has_lg; + int has_gl; + + has_sg = BotWpnSelect_HasWeaponAndAmmo(bs, WP_SHOTGUN); + has_pg = BotWpnSelect_HasWeaponAndAmmo(bs, WP_PLASMAGUN); + has_rail = BotWpnSelect_HasWeaponAndAmmo(bs, WP_RAILGUN); + has_lg = BotWpnSelect_HasWeaponAndAmmo(bs, WP_LIGHTNING); + has_gl = BotWpnSelect_HasWeaponAndAmmo(bs, WP_GRENADE_LAUNCHER); + + if (has_rail && !has_sg && !has_pg && !has_lg && !has_gl) { + return dist >= BOT_LOADOUT_RAIL_MIN_DIST; + } + if ((has_sg || has_pg) && !has_rail && !has_lg) { + return dist <= BOT_LOADOUT_SG_PLASMA_MAX_DIST; + } + if (has_lg && !has_rail && !has_sg && !has_pg) { + return dist <= BOT_LOADOUT_LG_MAX_DIST; + } + if (has_rail && dist < BOT_LOADOUT_RAIL_MIN_DIST) { + if (has_sg && dist <= 256.0f) { + return 1; + } + if (has_pg && dist <= 400.0f) { + return 1; + } + if (has_lg && dist <= BOT_LOADOUT_LG_MAX_DIST) { + return 1; + } + return 0; + } + if (BotWpnSelect_HasStrongCombatOption(bs, dist)) { + return 1; + } + return BotWpnSelect_CountCombatAlternatives(bs, dist) > 0; +} + +int BotCombat_CanEngageAtDistance(const bot_state_t *bs, float horizDist) { + if (!bs || !BotEnhanced_IsActive()) { + return 1; + } + switch (BotCombat_GetLoadoutTier(bs)) { + case BOT_LOADOUT_READY: + return 1; + case BOT_LOADOUT_NOT_READY: + return 0; + default: + return BotCombat_SituationalEngageOk(bs, horizDist); + } +} + +int BotCombat_CanEngageTrackedOpponent(const bot_state_t *bs) { + float dist; + int client; + + if (!bs || !BotOpponent_IsTracking(bs)) { + return 1; + } + client = bs->opponent_belief.client; + if (client < 0 || client >= MAX_CLIENTS) { + return 1; + } + dist = BotCombat_HorizontalDistToClient(bs, client); + return BotCombat_CanEngageAtDistance(bs, dist); +} + static int BotCombat_HorizontalDistToEnemy(bot_state_t *bs) { vec3_t dir; aas_entityinfo_t entinfo; @@ -30,7 +218,7 @@ static int BotCombat_HorizontalDistToEnemy(bot_state_t *bs) { return (int)VectorLength(dir); } static qboolean BotCombat_IsVoluntaryCloseCombatWeapon(int wp) { - return (wp == WP_GAUNTLET || wp == WP_SHOTGUN || wp == WP_PLASMAGUN); + return (wp == WP_GAUNTLET); } static qboolean BotCombat_GauntletChosen(bot_state_t *bs) { return (bs->weaponnum == WP_GAUNTLET || bs->cur_ps.weapon == WP_GAUNTLET); @@ -48,9 +236,7 @@ static qboolean BotCombat_HasGauntlet(bot_state_t *bs) { return (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GAUNTLET)) != 0; } static qboolean BotCombat_HasCloseCombatWeapon(bot_state_t *bs) { - return BotCombat_HasGauntlet(bs) || - (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_SHOTGUN)) != 0 || - (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_PLASMAGUN)) != 0; + return BotCombat_HasGauntlet(bs); } static qboolean BotCombat_CloseVoluntaryCloseCombat(bot_state_t *bs) { if (!BotCombat_VoluntaryCloseCombatChosen(bs)) { @@ -62,6 +248,10 @@ static qboolean BotCombat_CloseVoluntaryCloseCombat(bot_state_t *bs) { if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { return qfalse; } + if (!BotWpnSelect_VoluntaryGauntletWarranted(bs, + (float)BotCombat_HorizontalDistToEnemy(bs))) { + return qfalse; + } return BotCombat_HorizontalDistToEnemy(bs) <= BOT_COMBAT_GAUNTLET_RUSH_DIST; } static qboolean BotCombat_InGauntletEngageRange(bot_state_t *bs) { @@ -75,9 +265,13 @@ static qboolean BotCombat_InGauntletEngageRange(bot_state_t *bs) { } return horiz <= BOT_COMBAT_GAUNTLET_RUSH_DIST; } -static void BotCombat_ClearVoluntaryPursuit(bot_state_t *bs) { +static void BotCombat_ClearCloseTrack(bot_state_t *bs) { bs->combat.gauntlet_voluntary_since = 0.0f; bs->combat.gauntlet_voluntary_best_dist = 0; + bs->combat.close_stall_hits = 0; +} +static void BotCombat_ClearVoluntaryPursuit(bot_state_t *bs) { + BotCombat_ClearCloseTrack(bs); } static void BotCombat_StartVoluntaryPursuit(bot_state_t *bs) { int horiz; @@ -93,62 +287,68 @@ static void BotCombat_StartVoluntaryPursuit(bot_state_t *bs) { } bs->combat.gauntlet_voluntary_since = FloatTime(); bs->combat.gauntlet_voluntary_best_dist = horiz; + bs->combat.close_stall_hits = bs->cur_ps.persistant[PERS_HITS]; } -static qboolean BotCombat_EnemyBackingAway(bot_state_t *bs) { - aas_entityinfo_t entinfo; - vec3_t toBot, vel; - float speedAway; +static qboolean BotCombat_InCloseStallZone(bot_state_t *bs) { + int horiz; + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return qfalse; } - BotEntityInfo(bs->enemy, &entinfo); - if (!entinfo.valid || entinfo.update_time <= 0.001f) { + horiz = BotCombat_HorizontalDistToEnemy(bs); + if (horiz > BOT_COMBAT_CLOSE_STALL_MAX_DIST) { return qfalse; } - VectorSubtract(bs->origin, entinfo.origin, toBot); - toBot[2] = 0.0f; - if (VectorLengthSquared(toBot) < Square(16.0f)) { + if (!BotCombat_HasFightLOS(bs, bs->enemy)) { return qfalse; } - VectorNormalize(toBot); - VectorSubtract(entinfo.origin, entinfo.lastvisorigin, vel); - VectorScale(vel, 1.0f / entinfo.update_time, vel); - vel[2] = 0.0f; - speedAway = -DotProduct(vel, toBot); - return speedAway > BOT_COMBAT_ENEMY_BACKING_AWAY_SPEED; + if (BotCombat_VoluntaryCloseCombatChosen(bs)) { + return qtrue; + } + return BotTactics_IsGauntletOnly(bs) && BotCombat_GauntletChosen(bs); } -static qboolean BotCombat_VoluntaryGauntletPursuitActive(bot_state_t *bs) { - return bs->combat.gauntlet_voluntary_since > 0.0f; +static void BotCombat_AbandonCloseFight(bot_state_t *bs) { + bs->combat.gauntlet_voluntary_abandon_until = + FloatTime() + BOT_COMBAT_VOLUNTARY_GAUNTLET_ABANDON_COOLDOWN; + bs->combat.stance_until = FloatTime() + BOT_COMBAT_CLOSE_STALL_BACKOFF_SEC; + BotCombat_ClearCloseTrack(bs); + BotWpnSelect_OnVoluntaryGauntletAborted(bs); + bs->flags ^= BFL_STRAFERIGHT; + bs->attackstrafe_time = 0; } -static qboolean BotCombat_UpdateVoluntaryGauntletAbort(bot_state_t *bs) { +static qboolean BotCombat_UpdateCloseFightStall(bot_state_t *bs) { + int horiz, hits, gain; float elapsed; - int horiz; - if (!BotCombat_VoluntaryGauntletPursuitActive(bs)) { - return qfalse; - } - if (!BotCombat_VoluntaryCloseCombatChosen(bs) || BotTactics_IsGauntletOnly(bs)) { - BotCombat_ClearVoluntaryPursuit(bs); + + if (!BotCombat_InCloseStallZone(bs)) { + BotCombat_ClearCloseTrack(bs); return qfalse; } horiz = BotCombat_HorizontalDistToEnemy(bs); - if (horiz < bs->combat.gauntlet_voluntary_best_dist) { + hits = bs->cur_ps.persistant[PERS_HITS]; + if (bs->combat.gauntlet_voluntary_since <= 0.0f) { + bs->combat.gauntlet_voluntary_since = FloatTime(); bs->combat.gauntlet_voluntary_best_dist = horiz; + bs->combat.close_stall_hits = hits; + return qfalse; } - if (horiz <= BOT_COMBAT_GAUNTLET_ATTACK_DIST) { - BotCombat_ClearVoluntaryPursuit(bs); + if (horiz <= BOT_COMBAT_GAUNTLET_ATTACK_DIST || hits > bs->combat.close_stall_hits) { + BotCombat_ClearCloseTrack(bs); return qfalse; } + if (horiz < bs->combat.gauntlet_voluntary_best_dist) { + bs->combat.gauntlet_voluntary_best_dist = horiz; + } elapsed = FloatTime() - bs->combat.gauntlet_voluntary_since; - if (elapsed < BOT_COMBAT_VOLUNTARY_GAUNTLET_TIMEOUT) { + if (elapsed < BOT_COMBAT_CLOSE_STALL_TIMEOUT) { return qfalse; } - if (!BotCombat_EnemyBackingAway(bs)) { + gain = bs->combat.gauntlet_voluntary_best_dist - horiz; + if (gain >= BOT_COMBAT_CLOSE_STALL_CLOSE_GAIN) { + BotCombat_ClearCloseTrack(bs); return qfalse; } - bs->combat.gauntlet_voluntary_abandon_until = - FloatTime() + BOT_COMBAT_VOLUNTARY_GAUNTLET_ABANDON_COOLDOWN; - BotCombat_ClearVoluntaryPursuit(bs); - BotWpnSelect_OnVoluntaryGauntletAborted(bs); + BotCombat_AbandonCloseFight(bs); return qtrue; } static qboolean BotCombat_CloseCombatRushAllowed(bot_state_t *bs) { @@ -160,13 +360,16 @@ static qboolean BotCombat_CloseCombatRushAllowed(bot_state_t *bs) { return BotCombat_GauntletChosen(bs) && horiz <= BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST; } - /* Voluntary close combat (skill 4–5): SG/plasma/gauntlet charge. */ - if (BotCombat_VoluntaryCloseCombatChosen(bs) && + /* Voluntary close combat (skill 4–5): gauntlet charge when loadout warrants it. */ + if (BotCombat_GauntletChosen(bs) && horiz <= BOT_COMBAT_GAUNTLET_RUSH_DIST && BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { return qfalse; } + if (!BotWpnSelect_VoluntaryGauntletWarranted(bs, (float)horiz)) { + return qfalse; + } return qtrue; } if (bs->flags & BFL_TACTICS_SURVIVAL_FLEE) { @@ -190,10 +393,16 @@ static void BotCombat_UpdateCloseCombatRush(bot_state_t *bs) { BotCombat_ApplyCloseCombatRush(bs); } static void BotCombat_ResetStance(bot_state_t *bs) { + float backoff; + + backoff = 0.0f; + if (bs->combat.stance_until > FloatTime()) { + backoff = bs->combat.stance_until; + } bs->combat.stance = BOT_STANCE_NORMAL; bs->combat.move_policy = BOT_MOVE_POLICY_LEGACY; bs->combat.fire_policy = BOT_FIRE_POLICY_LEGACY; - bs->combat.stance_until = 0.0f; + bs->combat.stance_until = backoff; } void BotCombat_Reset(bot_state_t *bs) { if (!bs) { @@ -203,6 +412,102 @@ void BotCombat_Reset(bot_state_t *bs) { BotCombat_ClearVoluntaryPursuit(bs); bs->combat.gauntlet_voluntary_abandon_until = 0.0f; } +int BotCombat_HasFightLOS(bot_state_t *bs, int clientnum) { + aas_entityinfo_t entinfo; + bsp_trace_t trace; + vec3_t end; + + if (!bs || clientnum < 0 || clientnum >= MAX_CLIENTS) { + return 0; + } + BotEntityInfo(clientnum, &entinfo); + if (!entinfo.valid) { + return 0; + } + VectorCopy(entinfo.origin, end); + end[2] += 24.0f; + BotAI_Trace(&trace, bs->eye, NULL, NULL, end, bs->client, MASK_SHOT); + return (trace.fraction >= 1.0f || trace.ent == clientnum); +} + +int BotCombat_HasEnemyCombatContact(bot_state_t *bs) { + float vis; + + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 0; + } + if (BotCombat_HasFightLOS(bs, bs->enemy)) { + return 1; + } + if (BotOpponent_IsActive() && BotOpponent_IsTracking(bs) && + bs->enemy == bs->opponent_belief.client) { + return BotOpponent_HasCombatSight(bs, bs->enemy); + } + if (BotEnhanced_IsActive()) { + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, + bs->enemy); + return vis >= 0.15f; + } + return 0; +} + +void BotCombat_ReleaseEnemy(bot_state_t *bs) { + if (!bs || bs->enemy < 0) { + return; + } + bs->enemy = -1; + bs->enemydeath_time = 0; + BotCombat_ClearVoluntaryPursuit(bs); + if (BotEnhanced_IsActive()) { + BotAimHarness_ReleaseCombat(bs); + } +} +static void BotCombat_RefreshLastEnemySpot(bot_state_t *bs) { + aas_entityinfo_t entinfo; + vec3_t target; + int areanum; + + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + BotEntityInfo(bs->enemy, &entinfo); + VectorCopy(entinfo.origin, target); + areanum = BotPointAreaNum(target); + if (areanum && trap_AAS_AreaReachability(areanum)) { + VectorCopy(target, bs->lastenemyorigin); + bs->lastenemyareanum = areanum; + } +} +void BotCombat_TickEngagement(bot_state_t *bs) { + if (!BotEnhanced_IsActive()) { + return; + } + if (!bs || !bs->inuse || BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + BotCombat_ReleaseEnemy(bs); + return; + } + if (BotCombat_HasFightLOS(bs, bs->enemy)) { + bs->enemyvisible_time = FloatTime(); + BotCombat_RefreshLastEnemySpot(bs); + return; + } + if (bs->lastenemyareanum > 0) { + if (bs->enemyvisible_time >= FloatTime() - BOT_COMBAT_LOS_DROP_AREA_SEC) { + return; + } + } else if (bs->enemyvisible_time >= FloatTime() - BOT_COMBAT_LOS_DROP_SEC) { + return; + } + if (!BotFindEnemy(bs, -1)) { + BotCombat_ReleaseEnemy(bs); + } +} void BotCombat_UpdateIntent(bot_state_t *bs) { if (!bs) { return; @@ -219,21 +524,32 @@ void BotCombat_UpdateIntent(bot_state_t *bs) { return; } if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { - bs->enemy = -1; - BotCombat_ClearVoluntaryPursuit(bs); + BotCombat_ReleaseEnemy(bs); return; } - if (BotCombat_UpdateVoluntaryGauntletAbort(bs)) { + if (BotCombat_UpdateCloseFightStall(bs)) { return; } BotCombat_UpdateCloseCombatRush(bs); } +int BotCombat_WantsCloseBackoff(const bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return 0; + } + return bs->combat.stance_until > FloatTime(); +} int BotCombat_IsRushOpponent(const bot_state_t *bs) { if (!bs) { return 0; } return bs->combat.stance == BOT_STANCE_RUSH_OPPONENT; } +int BotCombat_IsLedgeHold(const bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return 0; + } + return bs->combat.stance == BOT_STANCE_LEDGE_HOLD; +} int BotCombat_ShouldEngageFromRetreat(bot_state_t *bs) { if (!bs || !BotEnhanced_IsActive()) { return 0; @@ -253,6 +569,13 @@ int BotCombat_ShouldEngageFromRetreat(bot_state_t *bs) { if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { return 0; } + if (BotOpponent_IsActive() && BotOpponent_WantsAvoidEngagement(bs)) { + return 0; + } + if (!BotWpnSelect_VoluntaryGauntletWarranted(bs, + (float)BotCombat_HorizontalDistToEnemy(bs))) { + return 0; + } return BotCombat_InGauntletEngageRange(bs); } void BotCombat_OnWeaponCommitted(bot_state_t *bs, int prev_wp, int new_wp) { @@ -278,3 +601,227 @@ void BotCombat_OnWeaponCommitted(bot_state_t *bs, int prev_wp, int new_wp) { BotCombat_ApplyCloseCombatRush(bs); } +int BotCombat_FindEnemy(bot_state_t *bs, int curenemy) { + int i, best; + float alertness, squaredist, bestsq, maxsq; + vec3_t dir; + aas_entityinfo_t entinfo; + + if (!bs || !BotEnhanced_IsActive()) { + return qfalse; + } + + best = -1; + bestsq = 1e12f; + alertness = BotEnhanced_GetAlertness(bs); + maxsq = Square(900.0f + alertness * 4000.0f); + + for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) { + if (i == bs->client) { + continue; + } + BotEntityInfo(i, &entinfo); + if (!entinfo.valid) { + continue; + } + if (EntityClientIsDead(i) || entinfo.number == bs->entitynum) { + continue; + } + if (EntityIsInvisible(&entinfo) && !EntityIsShooting(&entinfo)) { + continue; + } + if (g_entities[i].flags & FL_NOTARGET) { + continue; + } + if (!BotEnhanced_CanEngageClient(bs, i)) { + continue; + } + if (BotOpponent_IsActive() && BotOpponent_WantsAvoidEngagement(bs)) { + qboolean trackedOpp; + qboolean fightLos; + + trackedOpp = BotOpponent_IsTracking(bs) && + i == bs->opponent_belief.client; + fightLos = trackedOpp && BotCombat_HasFightLOS(bs, i); + if (!fightLos) { + if (!BotOpponent_WantsFleeEngaged(bs)) { + continue; + } + if (!trackedOpp) { + continue; + } + } + } + if (BotAI_IsOriginNearRecentTeleport(entinfo.origin)) { + continue; + } + if (BotSameTeam(bs, i)) { + continue; + } + if (BotOpponent_IsActive() && BotOpponent_IsTracking(bs) && + i == bs->opponent_belief.client) { + if (!BotOpponent_HasCombatSight(bs, i)) { + continue; + } + if (!BotOpponent_WantsFleeEngaged(bs) && + !BotCombat_CanEngageAtDistance(bs, + BotCombat_HorizontalDistToClient(bs, i))) { + continue; + } + } else if (!BotCombat_HasFightLOS(bs, i)) { + continue; + } + VectorSubtract(entinfo.origin, bs->origin, dir); + squaredist = VectorLengthSquared(dir); + if (squaredist > maxsq) { + continue; + } + if (EntityCarriesFlag(&entinfo)) { + best = i; + break; + } + if (squaredist < bestsq) { + bestsq = squaredist; + best = i; + } + } + if (best < 0 || best == curenemy) { + return qfalse; + } + bs->enemy = best; + if (curenemy >= 0) { + bs->enemysight_time = FloatTime() - 2; + } else { + bs->enemysight_time = FloatTime(); + } + bs->enemysuicide = qfalse; + bs->enemydeath_time = 0; + bs->enemyvisible_time = FloatTime(); + return qtrue; +} + +bot_moveresult_t BotCombat_AttackMove(bot_state_t *bs, int tfl) { + int movetype, i, attackentity, holdHighGround; + float attack_skill, croucher, dist, strafechange_time; + float attack_dist, attack_range; + vec3_t forward, backward, sideward, hordir, up = {0, 0, 1}; + aas_entityinfo_t entinfo; + bot_moveresult_t moveresult; + + attackentity = bs->enemy; + memset(&moveresult, 0, sizeof(bot_moveresult_t)); + attack_skill = BotEnhanced_GetAttackSkill(bs); + croucher = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CROUCHER, 0, 1); + if (attack_skill < 0.2f) { + return moveresult; + } + BotSetupForMovement(bs); + BotEntityInfo(attackentity, &entinfo); + VectorSubtract(entinfo.origin, bs->origin, forward); + dist = VectorNormalize(forward); + VectorNegate(forward, backward); + movetype = MOVE_WALK; + if (bs->attackcrouch_time < FloatTime() - 1 && random() < croucher) { + bs->attackcrouch_time = FloatTime() + croucher * 5; + } + if (bs->attackcrouch_time > FloatTime()) { + movetype = MOVE_CROUCH; + } + if (bs->cur_ps.weapon == WP_GAUNTLET) { + attack_dist = 0; + attack_range = 0; + } else if (bs->cur_ps.weapon == WP_SHOTGUN || + bs->cur_ps.weapon == WP_PLASMAGUN) { + attack_dist = (BOT_COMBAT_CLOSE_WEAPON_MIN_DIST + + BOT_COMBAT_CLOSE_WEAPON_MAX_DIST) * 0.5f; + attack_range = (BOT_COMBAT_CLOSE_WEAPON_MAX_DIST - + BOT_COMBAT_CLOSE_WEAPON_MIN_DIST) * 0.5f; + } else { + attack_dist = IDEAL_ATTACKDIST; + attack_range = 40; + } + if (BotCombat_WantsCloseBackoff(bs)) { + movetype = MOVE_WALK; + if (trap_BotMoveInDirection(bs->ms, backward, 400, movetype)) { + return moveresult; + } + } + holdHighGround = BotPosition_WantsLedgeStrafeOnly(bs); + if (BotCombat_IsLedgeHold(bs)) { + BotPosition_TickLedgePeek(bs); + movetype = bs->pos_ledge_peek_crouch ? MOVE_CROUCH : MOVE_WALK; + } else if (BotPosition_HasHeightAdvantage(bs)) { + if (bs->attackcrouch_time < FloatTime() - 0.5f) { + bs->attackcrouch_time = FloatTime() + 1.5f; + } + } + if (holdHighGround) { + tfl &= ~TFL_WALKOFFLEDGE; + } + if (BotPosition_ShouldSuppressDownhillCharge(bs) && + BotCombat_IsRushOpponent(bs)) { + bs->combat.move_policy = BOT_MOVE_POLICY_LEGACY; + bs->combat.stance = BOT_STANCE_NORMAL; + } + if (BotCombat_IsRushOpponent(bs) && + bs->combat.move_policy == BOT_MOVE_CLOSE_MELEE) { + movetype = MOVE_WALK; + if (trap_BotMoveInDirection(bs->ms, forward, 400, movetype)) { + return moveresult; + } + if (trap_BotMoveInDirection(bs->ms, forward, 400, MOVE_RUN)) { + return moveresult; + } + return moveresult; + } + if (attack_skill <= 0.4f) { + if (dist > attack_dist + attack_range && !holdHighGround) { + if (trap_BotMoveInDirection(bs->ms, forward, 400, movetype)) { + return moveresult; + } + } + if (dist < attack_dist - attack_range) { + if (trap_BotMoveInDirection(bs->ms, backward, 400, movetype)) { + return moveresult; + } + } + return moveresult; + } + bs->attackstrafe_time += bs->thinktime; + strafechange_time = 0.4f + (1 - attack_skill) * 0.2f; + if (attack_skill > 0.7f) { + strafechange_time += crandom() * 0.2f; + } + if (bs->attackstrafe_time > strafechange_time) { + if (random() > 0.935f * (1.0f - bot_wigglefactor.value)) { + bs->flags ^= BFL_STRAFERIGHT; + bs->attackstrafe_time = 0; + } + } + for (i = 0; i < 2; i++) { + hordir[0] = forward[0]; + hordir[1] = forward[1]; + hordir[2] = 0; + VectorNormalize(hordir); + CrossProduct(hordir, up, sideward); + if (bs->flags & BFL_STRAFERIGHT) { + VectorNegate(sideward, sideward); + } + if (!holdHighGround && random() > 0.9f) { + VectorAdd(sideward, backward, sideward); + } else { + if (dist > attack_dist + attack_range && !holdHighGround) { + VectorAdd(sideward, forward, sideward); + } else if (dist < attack_dist - attack_range && !holdHighGround) { + VectorAdd(sideward, backward, sideward); + } + } + if (trap_BotMoveInDirection(bs->ms, sideward, 400, movetype)) { + return moveresult; + } + bs->flags ^= BFL_STRAFERIGHT; + bs->attackstrafe_time = 0; + } + return moveresult; +} + diff --git a/ratoa_gamecode/code/game/ai_bot_combat.h b/ratoa_gamecode/code/game/ai_bot_combat.h index 4470ea3..d1a5ddf 100644 --- a/ratoa_gamecode/code/game/ai_bot_combat.h +++ b/ratoa_gamecode/code/game/ai_bot_combat.h @@ -11,49 +11,90 @@ Defaults match legacy behavior. Stance/policy logic in BotCombat_UpdateIntent. typedef enum { BOT_STANCE_NORMAL = 0, - BOT_STANCE_RUSH_OPPONENT + BOT_STANCE_RUSH_OPPONENT, + BOT_STANCE_LEDGE_HOLD /* peek / duck at ledge over lower enemy */ /* BOT_STANCE_SURVIVAL_FLEE — reserved; tactics retreat for now */ } bot_stance_t; typedef enum { BOT_MOVE_POLICY_LEGACY = 0, - BOT_MOVE_CLOSE_MELEE /* charge opponent (gauntlet / voluntary SG / plasma) */ + BOT_MOVE_CLOSE_MELEE /* charge opponent (gauntlet only) */ } bot_move_policy_t; typedef enum { BOT_FIRE_POLICY_LEGACY = 0 } bot_fire_policy_t; -/* Close combat rush (voluntary SG/plasma/gauntlet at skill 4–5). */ +/* Close combat rush (voluntary gauntlet when BotEnhanced_SkillScale high enough). */ #define BOT_COMBAT_GAUNTLET_RUSH_DIST 192 +/* Shotgun / plasmagun: standoff band while fighting (enhanced). */ +#define BOT_COMBAT_CLOSE_WEAPON_MIN_DIST 128 +#define BOT_COMBAT_CLOSE_WEAPON_MAX_DIST 256 /* Gauntlet-only last resort: rush/fight out to this range (tactics flee beyond). */ #define BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST 384 /* Gauntlet hit range in BotCheckAttack is 60; slight margin for intent */ #define BOT_COMBAT_GAUNTLET_ATTACK_DIST 72 -/* Voluntary gauntlet: abandon rush if enemy kites for this long without closing. */ -#define BOT_COMBAT_VOLUNTARY_GAUNTLET_TIMEOUT 2.0f +/* Close fight: abandon rush/strafe orbit if no hit and poor closure in this window. */ +#define BOT_COMBAT_CLOSE_STALL_MAX_DIST 256 +#define BOT_COMBAT_CLOSE_STALL_TIMEOUT 1.8f +#define BOT_COMBAT_CLOSE_STALL_CLOSE_GAIN 28 +#define BOT_COMBAT_CLOSE_STALL_BACKOFF_SEC 0.7f #define BOT_COMBAT_VOLUNTARY_GAUNTLET_ABANDON_COOLDOWN 4.0f -#define BOT_COMBAT_ENEMY_BACKING_AWAY_SPEED 80.0f +/* Drop stale engagement after this long without MASK_SHOT LOS (enhanced). */ +#define BOT_COMBAT_LOS_DROP_SEC 1.2f +/* With a latched last-enemy area, keep contact longer for chase / blind suppressive fire. */ +#define BOT_COMBAT_LOS_DROP_AREA_SEC 4.0f +#define BOT_COMBAT_CHASE_TIMEOUT_SEC 3.0f typedef struct { bot_stance_t stance; bot_move_policy_t move_policy; bot_fire_policy_t fire_policy; float stance_until; /* 0 = no timer */ - float gauntlet_voluntary_since; /* 0 = not in voluntary pursuit */ + float gauntlet_voluntary_since; /* close-fight track start (0 = off) */ int gauntlet_voluntary_best_dist; - float gauntlet_voluntary_abandon_until; /* no voluntary gauntlet until */ + int close_stall_hits; /* PERS_HITS at track start */ + float gauntlet_voluntary_abandon_until; /* no close rush until */ } bot_combat_intent_t; struct bot_state_s; +typedef enum { + BOT_LOADOUT_NOT_READY = 0, /* gauntlet / MG only */ + BOT_LOADOUT_SITUATIONAL, /* SG, plasma, rail, LG, GL — range-dependent */ + BOT_LOADOUT_READY /* RL, BFG, or quad */ +} bot_loadout_tier_t; + +#define BOT_LOADOUT_RAIL_MIN_DIST 360.0f +#define BOT_LOADOUT_SG_PLASMA_MAX_DIST 360.0f +#define BOT_LOADOUT_LG_MAX_DIST 660.0f + void BotCombat_Reset(struct bot_state_s *bs); +void BotCombat_TickEngagement(struct bot_state_s *bs); void BotCombat_UpdateIntent(struct bot_state_s *bs); void BotCombat_OnWeaponCommitted(struct bot_state_s *bs, int prev_wp, int new_wp); +int BotCombat_HasFightLOS(struct bot_state_s *bs, int clientnum); +/* Fight LOS or opponent-visible contact — retain enemy / opportunistic fire. */ +int BotCombat_HasEnemyCombatContact(struct bot_state_s *bs); +void BotCombat_ReleaseEnemy(struct bot_state_s *bs); + int BotCombat_IsRushOpponent(const struct bot_state_s *bs); +int BotCombat_IsLedgeHold(const struct bot_state_s *bs); +int BotCombat_WantsCloseBackoff(const struct bot_state_s *bs); /* Close enough to charge with gauntlet; pulls bot out of battle retreat. */ int BotCombat_ShouldEngageFromRetreat(struct bot_state_s *bs); +int BotCombat_FindEnemy(struct bot_state_s *bs, int curenemy); +struct bot_moveresult_s BotCombat_AttackMove(struct bot_state_s *bs, int tfl); + +/* 1v1 loadout readiness — gates opponent engage / chase / duel commit. */ +bot_loadout_tier_t BotCombat_GetLoadoutTier(const struct bot_state_s *bs); +int BotCombat_LoadoutStackBonus(const struct bot_state_s *bs); +float BotCombat_LoadoutEngageBiasNudge(const struct bot_state_s *bs); +int BotCombat_CanEngageAtDistance(const struct bot_state_s *bs, float horizDist); +int BotCombat_CanEngageTrackedOpponent(const struct bot_state_s *bs); +float BotCombat_HorizontalDistToClient(const struct bot_state_s *bs, int clientnum); + #endif /* AI_BOT_COMBAT_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_dmnet.c b/ratoa_gamecode/code/game/ai_bot_dmnet.c new file mode 100644 index 0000000..2e86b8c --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_dmnet.c @@ -0,0 +1,404 @@ +/* +=========================================================================== +BOT DMNET — thin hooks from legacy ai_dmnet.c seek/battle nodes. +=========================================================================== +*/ + +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "ai_dmnet.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_dmnet.h" +#include "ai_bot_items.h" +#include "ai_bot_combat.h" +#include "ai_bot_position.h" +#include "ai_bot_opponent.h" +#include "ai_bot_move_harness.h" +#include "ai_bot_item_timing.h" +#include "ai_dmq3.h" + +#define GFL_AIR 128 + +int BotFindEnemy(bot_state_t *bs, int curenemy); +int BotGetAirGoal(bot_state_t *bs, bot_goal_t *goal); + +void BotDmnet_ExecuteSeekCombat(bot_state_t *bs, bot_dmnet_seek_act_t act, + const char *reason, int abort_item_commit) { + if (!bs || act == BOT_DMNET_SEEK_NOP) { + return; + } + switch (act) { + case BOT_DMNET_SEEK_BATTLE_RETREAT: + AIEnter_Battle_Retreat(bs, (char *)reason); + break; + case BOT_DMNET_SEEK_BATTLE_NBG: + AIEnter_Battle_NBG(bs, (char *)reason); + break; + case BOT_DMNET_SEEK_BATTLE_HARASS: + BotPosition_BeginItemHarass(bs); + AIEnter_Battle_Fight(bs, (char *)reason); + break; + case BOT_DMNET_SEEK_BATTLE_FIGHT: + if (abort_item_commit) { + BotItems_AbortCommit(bs); + } + trap_BotResetLastAvoidReach(bs->ms); + trap_BotEmptyGoalStack(bs->gs); + AIEnter_Battle_Fight(bs, (char *)reason); + break; + default: + break; + } +} + +int BotDmnet_NearbyGoal(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { + if (BotEnhanced_IsActive()) { + return BotItems_NearbyGoal(bs, tfl, ltg, range); + } + return trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range); +} + +int BotDmnet_GoForAir(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { + bot_goal_t goal; + + if (bs->lastair_time >= FloatTime() - 6) { + return qfalse; + } + if (!BotEnhanced_IsActive()) { + if (BotGetAirGoal(bs, &goal)) { + trap_BotPushGoal(bs->gs, &goal); + return qtrue; + } + if (trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range)) { + trap_BotGetTopGoal(bs->gs, &goal); + if (!(trap_AAS_PointContents(goal.origin) & + (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA))) { + return qtrue; + } + trap_BotPopGoal(bs->gs); + } + trap_BotResetAvoidGoals(bs->gs); + return qfalse; + } + if (BotGetAirGoal(bs, &goal)) { + BotEnhanced_PushGoalSafe(bs, &goal); + return qtrue; + } + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + while (BotItems_ChooseNBGItem(bs, tfl, ltg, range)) { + BotEnhanced_SanitizeGoalStack(bs); + trap_BotGetTopGoal(bs->gs, &goal); + if (!(trap_AAS_PointContents(goal.origin) & + (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA))) { + return qtrue; + } + trap_BotPopGoal(bs->gs); + } + trap_BotResetAvoidGoals(bs->gs); + return qfalse; +} + +int BotDmnet_ReachedGoal(bot_state_t *bs, bot_goal_t *goal) { + int itemResult; + + if (!bs || !goal) { + return qfalse; + } + if (goal->flags & GFL_ITEM) { + if (BotEnhanced_IsActive()) { + itemResult = BotItems_HandleReachedGoal(bs, goal); + if (itemResult >= 0) { + return itemResult; + } + if (BotItems_TimingHoldsGoalReached(bs, goal)) { + return qfalse; + } + } + if (trap_BotTouchingGoal(bs->origin, goal)) { + if (!(goal->flags & GFL_DROPPED)) { + trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + } + return qtrue; + } + if (BotEnhanced_IsActive() && + BotItems_ItemGoalInVisButNotVisible(bs, goal)) { + return qtrue; + } + if (bs->areanum == goal->areanum) { + if (bs->origin[0] > goal->origin[0] + goal->mins[0] && + bs->origin[0] < goal->origin[0] + goal->maxs[0]) { + if (bs->origin[1] > goal->origin[1] + goal->mins[1] && + bs->origin[1] < goal->origin[1] + goal->maxs[1]) { + if (!trap_AAS_Swimming(bs->origin)) { + return qtrue; + } + } + } + } + } else if (goal->flags & GFL_AIR) { + if (trap_BotTouchingGoal(bs->origin, goal)) { + return qtrue; + } + if (bs->lastair_time > FloatTime() - 1) { + return qtrue; + } + } else if (trap_BotTouchingGoal(bs->origin, goal)) { + return qtrue; + } + return qfalse; +} + +int BotDmnet_ChooseLTGItem(bot_state_t *bs, int tfl) { + return BotItems_ChooseLTGItem(bs, tfl); +} + +int BotDmnet_ItemGoalGone(bot_state_t *bs, bot_goal_t *goal) { + if (!BotEnhanced_IsActive()) { + return qfalse; + } + return BotItems_ItemGoalInVisButNotVisible(bs, goal); +} + +void BotDmnet_OnRespawned(bot_state_t *bs) { + if (BotEnhanced_IsActive()) { + BotItemTiming_OnSpawn(bs); + } +} + +bot_dmnet_seek_act_t BotDmnet_SeekCombatContact(bot_state_t *bs, int from_ltg) { + if (!bs || !BotEnhanced_IsActive()) { + return BOT_DMNET_SEEK_NOP; + } + if (!BotEnhanced_OnSeekCombatContact(bs)) { + return BOT_DMNET_SEEK_NOP; + } + BotMove_CancelBypass(bs); + if (BotWantsToRetreat(bs)) { + return from_ltg ? BOT_DMNET_SEEK_BATTLE_NBG : BOT_DMNET_SEEK_BATTLE_RETREAT; + } + if (BotPosition_IsItemHarassActive(bs) || BotPosition_CanItemHarass(bs)) { + return BOT_DMNET_SEEK_BATTLE_HARASS; + } + return BOT_DMNET_SEEK_BATTLE_FIGHT; +} + +bot_dmnet_seek_act_t BotDmnet_SeekLTG_CombatContact(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return BOT_DMNET_SEEK_NOP; + } + if (!BotEnhanced_OnSeekCombatContact(bs)) { + return BOT_DMNET_SEEK_NOP; + } + if (BotWantsToRetreat(bs)) { + return BOT_DMNET_SEEK_BATTLE_RETREAT; + } + return BOT_DMNET_SEEK_BATTLE_FIGHT; +} + +bot_dmnet_seek_act_t BotDmnet_SeekLegacyNBGEnemy(bot_state_t *bs) { + if (!bs || BotEnhanced_IsActive()) { + return BOT_DMNET_SEEK_NOP; + } + if (!BotFindEnemy(bs, -1)) { + return BOT_DMNET_SEEK_NOP; + } + BotMove_CancelBypass(bs); + if (BotWantsToRetreat(bs)) { + return BOT_DMNET_SEEK_BATTLE_NBG; + } + return BOT_DMNET_SEEK_BATTLE_FIGHT; +} + +void BotDmnet_SeekLTG_TryCamp(bot_state_t *bs) { + if (BotEnhanced_AllowsCamping()) { + BotWantsToCamp(bs); + } +} + +void BotDmnet_SeekLTG_OnMoveFailure(bot_state_t *bs, bot_moveresult_t *mr) { + if (BotEnhanced_IsActive()) { + BotItems_OnMoveFailure(bs, mr); + } +} + +int BotDmnet_TryEnterItemPickup(bot_state_t *bs, const char *reason) { + (void)reason; + if (!bs || !BotEnhanced_IsActive() || !BotItems_ShouldRunPickupNode(bs)) { + return 0; + } + bs->nbg_time = BotItems_CommitNbgTime(bs); + return 1; +} + +int BotDmnet_NBG_ShouldClearReachTime(bot_state_t *bs, bot_goal_t *goal) { + if (!BotEnhanced_IsActive()) { + return 1; + } + return !BotItems_TimingHoldsGoalReached(bs, goal); +} + +void BotDmnet_NBG_OnItemsMoveFailure(bot_state_t *bs, bot_moveresult_t *mr) { + if (BotEnhanced_IsActive()) { + BotItems_OnMoveFailure(bs, mr); + } +} + +int BotDmnet_NBG_TimingHoldsCommit(bot_state_t *bs, bot_goal_t *goal) { + if (!BotEnhanced_IsActive() || !BotItems_TimingHoldsGoalReached(bs, goal)) { + return 0; + } + bs->nbg_time = BotItems_CommitNbgTime(bs); + return 1; +} + +void BotDmnet_OnNBGMoveFailure(bot_state_t *bs, bot_moveresult_t *mr) { + BotDmnet_NBG_OnItemsMoveFailure(bs, mr); +} + +void BotDmnet_NBG_PostMoveEnemyCheck(bot_state_t *bs, bot_moveresult_t *mr) { + bot_dmnet_seek_act_t act; + + (void)mr; + act = BotDmnet_SeekLegacyNBGEnemy(bs); + if (act != BOT_DMNET_SEEK_NOP) { + BotDmnet_ExecuteSeekCombat(bs, act, "seek nbg: found enemy", 1); + } +} + +void BotDmnet_OnNBGPostMove(bot_state_t *bs, bot_moveresult_t *mr, bot_goal_t *goal) { + (void)goal; + if (!bs || !mr) { + return; + } + BotMove_OnPostMoveToGoal(bs, mr); + BotDmnet_NBG_PostMoveEnemyCheck(bs, mr); +} + +int BotDmnet_BattleFightEnemyDeadQuick(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return 0; + } + if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS && EntityClientIsDead(bs->enemy)) { + bs->enemy = -1; + bs->enemydeath_time = 0; + return 1; + } + return 0; +} + +bot_dmnet_fight_vis_t BotDmnet_BattleFightEnemyVisibility(bot_state_t *bs, + vec3_t target) { + int areanum; + + if (!bs) { + return BOT_DMNET_FIGHT_VIS_LTG; + } + if (!BotEnhanced_IsActive()) { + if (!BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) { + if (BotWantsToChase(bs)) { + return BOT_DMNET_FIGHT_VIS_CHASE; + } + return BOT_DMNET_FIGHT_VIS_LTG; + } + areanum = BotPointAreaNum(target); + if (areanum && trap_AAS_AreaReachability(areanum)) { + VectorCopy(target, bs->lastenemyorigin); + bs->lastenemyareanum = areanum; + } + return BOT_DMNET_FIGHT_VIS_OK; + } + if (!BotCombat_HasFightLOS(bs, bs->enemy)) { + if (BotWantsToChase(bs)) { + return BOT_DMNET_FIGHT_VIS_CHASE; + } + return BOT_DMNET_FIGHT_VIS_LTG; + } + areanum = BotPointAreaNum(target); + if (areanum && trap_AAS_AreaReachability(areanum)) { + VectorCopy(target, bs->lastenemyorigin); + bs->lastenemyareanum = areanum; + } + return BOT_DMNET_FIGHT_VIS_OK; +} + +void BotDmnet_BattleFight_PreAttack(bot_state_t *bs) { + if (BotEnhanced_IsActive()) { + BotCombat_UpdateIntent(bs); + } +} + +int BotDmnet_BattleFight_WantsRetreat(bot_state_t *bs) { + if (BotEnhanced_ShouldSuppressFightRetreat(bs)) { + return 0; + } + return BotWantsToRetreat(bs); +} + +float BotDmnet_BattleChaseAttackSec(bot_state_t *bs) { + if (BotOpponent_IsActive() && bs && BotOpponent_WantsDuelCommit(bs)) { + return BOT_COMBAT_CHASE_TIMEOUT_SEC; + } + return 2.0f; +} + +int BotDmnet_BattleChaseHasShootContact(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive() || bs->enemy < 0 || + bs->enemy >= MAX_CLIENTS) { + return 0; + } + return BotCombat_HasEnemyCombatContact(bs); +} + +int BotDmnet_BattleChaseTryEnterFight(bot_state_t *bs) { + if (!bs || bs->enemy < 0) { + return 0; + } + if (BotEnhanced_IsActive()) { + if (!BotDmnet_BattleChaseHasShootContact(bs)) { + return 0; + } + } else if (!BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, + bs->enemy)) { + return 0; + } + if (EntityClientIsDead(bs->enemy)) { + bs->enemy = -1; + return -1; + } + return 1; +} + +float BotDmnet_BattleChaseTimeoutSec(bot_state_t *bs) { + if (BotEnhanced_IsActive()) { + return BOT_COMBAT_CHASE_TIMEOUT_SEC; + } + return 10.0f; +} + +int BotDmnet_Retreat_TryChargeFight(bot_state_t *bs) { + if (!BotEnhanced_IsActive() || !BotCombat_ShouldEngageFromRetreat(bs)) { + return 0; + } + bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; + return 1; +} + +int BotDmnet_Retreat_WantsFleeEngaged(bot_state_t *bs) { + if (!BotOpponent_IsActive()) { + return 0; + } + return BotOpponent_WantsFleeEngaged(bs); +} + +void BotDmnet_Retreat_AdjustFleeMovement(bot_state_t *bs, bot_moveresult_t *mr) { + if (BotOpponent_IsActive()) { + BotOpponent_AdjustFleeMovement(bs, mr); + } +} diff --git a/ratoa_gamecode/code/game/ai_bot_dmnet.h b/ratoa_gamecode/code/game/ai_bot_dmnet.h new file mode 100644 index 0000000..93dd3c9 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_dmnet.h @@ -0,0 +1,79 @@ +/* +=========================================================================== +BOT DMNET — thin hooks from legacy ai_dmnet.c seek/battle nodes. + +Gate: bot_enhanced = 1. Legacy nodes call these and branch on the result. +=========================================================================== +*/ + +#ifndef AI_BOT_DMNET_H +#define AI_BOT_DMNET_H + +struct bot_state_s; +struct bot_goal_s; +struct bot_moveresult_s; + +typedef enum { + BOT_DMNET_SEEK_NOP = 0, + BOT_DMNET_SEEK_BATTLE_RETREAT, + BOT_DMNET_SEEK_BATTLE_FIGHT, + BOT_DMNET_SEEK_BATTLE_HARASS, + BOT_DMNET_SEEK_BATTLE_NBG +} bot_dmnet_seek_act_t; + +typedef enum { + BOT_DMNET_FIGHT_VIS_OK = 0, + BOT_DMNET_FIGHT_VIS_CHASE, + BOT_DMNET_FIGHT_VIS_LTG +} bot_dmnet_fight_vis_t; + +int BotDmnet_NearbyGoal(struct bot_state_s *bs, int tfl, struct bot_goal_s *ltg, + float range); +int BotDmnet_GoForAir(struct bot_state_s *bs, int tfl, struct bot_goal_s *ltg, + float range); +int BotDmnet_ReachedGoal(struct bot_state_s *bs, struct bot_goal_s *goal); + +int BotDmnet_ChooseLTGItem(struct bot_state_s *bs, int tfl); +int BotDmnet_ItemGoalGone(struct bot_state_s *bs, struct bot_goal_s *goal); +void BotDmnet_OnRespawned(struct bot_state_s *bs); + +/* from_ltg: retreat routes to Battle_Retreat when false, Battle_NBG when true */ +bot_dmnet_seek_act_t BotDmnet_SeekCombatContact(struct bot_state_s *bs, int from_ltg); +bot_dmnet_seek_act_t BotDmnet_SeekLTG_CombatContact(struct bot_state_s *bs); +bot_dmnet_seek_act_t BotDmnet_SeekLegacyNBGEnemy(struct bot_state_s *bs); +void BotDmnet_ExecuteSeekCombat(struct bot_state_s *bs, bot_dmnet_seek_act_t act, + const char *reason, int abort_item_commit); + +void BotDmnet_SeekLTG_TryCamp(struct bot_state_s *bs); +void BotDmnet_SeekLTG_OnMoveFailure(struct bot_state_s *bs, + struct bot_moveresult_s *mr); + +int BotDmnet_TryEnterItemPickup(struct bot_state_s *bs, const char *reason); + +int BotDmnet_NBG_ShouldClearReachTime(struct bot_state_s *bs, struct bot_goal_s *goal); +void BotDmnet_NBG_OnItemsMoveFailure(struct bot_state_s *bs, + struct bot_moveresult_s *mr); +int BotDmnet_NBG_TimingHoldsCommit(struct bot_state_s *bs, struct bot_goal_s *goal); +void BotDmnet_NBG_PostMoveEnemyCheck(struct bot_state_s *bs, struct bot_moveresult_s *mr); + +void BotDmnet_OnNBGMoveFailure(struct bot_state_s *bs, struct bot_moveresult_s *mr); +void BotDmnet_OnNBGPostMove(struct bot_state_s *bs, struct bot_moveresult_s *mr, + struct bot_goal_s *goal); + +int BotDmnet_BattleFightEnemyDeadQuick(struct bot_state_s *bs); +bot_dmnet_fight_vis_t BotDmnet_BattleFightEnemyVisibility(struct bot_state_s *bs, + vec3_t target); +void BotDmnet_BattleFight_PreAttack(struct bot_state_s *bs); +int BotDmnet_BattleFight_WantsRetreat(struct bot_state_s *bs); + +float BotDmnet_BattleChaseAttackSec(struct bot_state_s *bs); +int BotDmnet_BattleChaseHasShootContact(struct bot_state_s *bs); +int BotDmnet_BattleChaseTryEnterFight(struct bot_state_s *bs); +float BotDmnet_BattleChaseTimeoutSec(struct bot_state_s *bs); + +int BotDmnet_Retreat_TryChargeFight(struct bot_state_s *bs); +int BotDmnet_Retreat_WantsFleeEngaged(struct bot_state_s *bs); +void BotDmnet_Retreat_AdjustFleeMovement(struct bot_state_s *bs, + struct bot_moveresult_s *mr); + +#endif /* AI_BOT_DMNET_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.c b/ratoa_gamecode/code/game/ai_bot_enhanced.c index 451e878..d86b59d 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.c +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -14,26 +14,49 @@ BOT ENHANCED — master cvar, feature gates, centralized register/reset. #include "../botlib/be_ai_weap.h" #include "ai_main.h" #include "ai_dmq3.h" +#include "chars.h" #include "ai_bot_enhanced.h" #include "ai_aim_harness.h" #include "ai_weapon_select.h" #include "ai_bot_tactics.h" #include "ai_bot_combat.h" +#include "ai_bot_opponent.h" #include "ai_bot_events.h" #include "ai_bot_move_harness.h" #include "ai_bot_items.h" +#include "ai_bot_item_timing.h" +#include "ai_bot_position.h" +#include "ai_bot_opponent.h" +#include "ai_bot_nav_guard.h" +#include "ai_dmq3.h" + +extern bot_state_t *botstates[MAX_CLIENTS]; vmCvar_t bot_enhanced; +vmCvar_t bot_enhanced_debug; + +extern vmCvar_t bot_debugAim; -extern vmCvar_t bot_enhanced_aim; -extern vmCvar_t bot_enhanced_weapons; -extern vmCvar_t bot_enhanced_tactics; -extern vmCvar_t bot_enhanced_movement; +static bot_state_t *botenh_think_bs; #define BOT_ENHANCED_LEGACY_AIM "bot_humanizeaim" #define BOT_ENHANCED_LEGACY_WEAPONS "bot_smartWeaponChoice" #define BOT_ENHANCED_LEGACY_TACTICS "bot_tacticalAI" +/* Deprecated sub-cvars — read once at init via trap_Cvar_VariableValue. */ +static const char *botenh_deprecated_subcvars[] = { + "bot_enhanced_aim", + "bot_enhanced_weapons", + "bot_enhanced_tactics", + "bot_enhanced_items", + "bot_enhanced_items_debug", + "bot_enhanced_items_timing", + "bot_enhanced_movement", + "bot_enhanced_position", + "bot_enhanced_opponent", + NULL +}; + #ifdef Q3_VM void Botlib_RawPushGoal(int goalstate, bot_goal_t *goal) { trap_BotPushGoal(goalstate, goal); @@ -54,43 +77,31 @@ static int BotEnhanced_LegacyCvarActive(const char *name) { * sub-cvar is migrated so pre-refactor configs keep working. */ static void BotEnhanced_MigrateLegacyCvars(void) { + int i; int migrated; int legacy_used; migrated = 0; legacy_used = 0; - trap_Cvar_Update(&bot_enhanced_aim); - if (!bot_enhanced_aim.integer) { - if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_AIM)) { - trap_Cvar_Set("bot_enhanced_aim", "1"); - migrated = 1; - legacy_used = 1; - } - } else if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_AIM)) { + if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_AIM)) { + migrated = 1; legacy_used = 1; } - - trap_Cvar_Update(&bot_enhanced_weapons); - if (!bot_enhanced_weapons.integer) { - if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_WEAPONS)) { - trap_Cvar_Set("bot_enhanced_weapons", "1"); - migrated = 1; - legacy_used = 1; - } - } else if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_WEAPONS)) { + if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_WEAPONS)) { + migrated = 1; + legacy_used = 1; + } + if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_TACTICS)) { + migrated = 1; legacy_used = 1; } - trap_Cvar_Update(&bot_enhanced_tactics); - if (!bot_enhanced_tactics.integer) { - if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_TACTICS)) { - trap_Cvar_Set("bot_enhanced_tactics", "1"); + for (i = 0; botenh_deprecated_subcvars[i]; i++) { + if (BotEnhanced_LegacyCvarActive(botenh_deprecated_subcvars[i])) { migrated = 1; legacy_used = 1; } - } else if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_TACTICS)) { - legacy_used = 1; } trap_Cvar_Update(&bot_enhanced); @@ -100,69 +111,109 @@ static void BotEnhanced_MigrateLegacyCvars(void) { if (legacy_used) { G_Printf( - "Bot enhanced: deprecated cvars %s / %s / %s detected; " - "use bot_enhanced and bot_enhanced_aim|weapons|tactics.\n", - BOT_ENHANCED_LEGACY_AIM, - BOT_ENHANCED_LEGACY_WEAPONS, - BOT_ENHANCED_LEGACY_TACTICS); + "Bot enhanced: deprecated bot_enhanced_* / bot_humanizeaim / " + "bot_smartWeaponChoice / bot_tacticalAI detected in config; " + "use bot_enhanced 1 (and bot_enhanced_debug for logging).\n"); } trap_Cvar_Update(&bot_enhanced); - trap_Cvar_Update(&bot_enhanced_aim); - trap_Cvar_Update(&bot_enhanced_weapons); - trap_Cvar_Update(&bot_enhanced_tactics); } void BotEnhanced_RegisterCvars(void) { + static qboolean registered; + + if (registered) { + return; + } + registered = qtrue; + trap_Cvar_Register(&bot_enhanced, "bot_enhanced", "0", CVAR_ARCHIVE); + trap_Cvar_Register(&bot_enhanced_debug, "bot_enhanced_debug", "0", 0); + trap_Cvar_Register(&bot_debugAim, "bot_debugAim", "0", CVAR_CHEAT); + trap_Cvar_Update(&bot_enhanced); - BotAimHarness_RegisterCvars(); - BotWpnSelect_RegisterCvars(); - BotTactics_RegisterCvars(); - BotItems_RegisterCvars(); - BotMoveHarness_RegisterCvars(); + trap_Cvar_Update(&bot_enhanced_debug); + trap_Cvar_Update(&bot_debugAim); + + BotPosition_RegisterCvars(); + BotOpponent_RegisterCvars(); + BotEnhanced_MigrateLegacyCvars(); } int BotEnhanced_IsActive(void) { + if (botenh_think_bs && botenh_think_bs->inuse) { + return botenh_think_bs->enh_cached_active; + } trap_Cvar_Update(&bot_enhanced); return bot_enhanced.integer != 0; } -int BotEnhanced_AimActive(void) { +int BotEnhanced_DebugActive(void) { if (!BotEnhanced_IsActive()) { return 0; } - trap_Cvar_Update(&bot_enhanced_aim); - return bot_enhanced_aim.integer != 0; + if (botenh_think_bs && botenh_think_bs->inuse) { + return botenh_think_bs->enh_cached_debug; + } + trap_Cvar_Update(&bot_enhanced_debug); + return bot_enhanced_debug.integer != 0; } -int BotEnhanced_WeaponsActive(void) { - if (!BotEnhanced_IsActive()) { +static int BotEnhanced_WantsOpponentThink(bot_state_t *bs) { + opponent_belief_t *ob; + + if (!bs || !BotEnhanced_IsActive()) { return 0; } - trap_Cvar_Update(&bot_enhanced_weapons); - return bot_enhanced_weapons.integer != 0; + ob = &bs->opponent_belief; + return ob->tracking && ob->client >= 0; } -int BotEnhanced_TacticsActive(void) { - if (!BotEnhanced_IsActive()) { +static int BotEnhanced_WantsNavGuardThink(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { return 0; } - trap_Cvar_Update(&bot_enhanced_tactics); - return bot_enhanced_tactics.integer != 0; + if (BotItems_HasActiveCommit(bs)) { + return 1; + } + if (bs->timing_pursue_track >= 0) { + return 1; + } + if (BotEnhanced_GoalStackDepth(bs) > 0 && bs->enemy < 0) { + return 1; + } + return 0; } -int BotEnhanced_MovementActive(void) { - if (!BotEnhanced_IsActive()) { - return 0; +void BotEnhanced_OnObservedItemPickup(bot_state_t *bs, int pickerClient, + int itemIndex, const vec3_t eventOrigin) { + gentity_t *ent; + opponent_belief_t *ob; + + if (!bs || itemIndex < 0) { + return; } - trap_Cvar_Update(&bot_enhanced_movement); - return bot_enhanced_movement.integer != 0; -} -int BotEnhanced_ItemsActive(void) { - return BotItems_IsActive(); + if (BotEnhanced_IsActive() && pickerClient >= 0 && + pickerClient < level.maxclients) { + ent = &g_entities[pickerClient]; + ob = &bs->opponent_belief; + if (ent->inuse && pickerClient != bs->client && + pickerClient != bs->entitynum && + pickerClient == ob->heard_pickup_event_picker && + itemIndex == ob->heard_pickup_event_parm && + ent->eventTime == ob->heard_pickup_event_time) { + return; + } + ob->heard_pickup_event_picker = pickerClient; + ob->heard_pickup_event_parm = itemIndex; + ob->heard_pickup_event_time = ent->inuse ? ent->eventTime : 0; + } + + if (BotItemTiming_IsActive()) { + BotItemTiming_OnEntityPickup(bs, pickerClient, itemIndex, eventOrigin); + } } int BotEnhanced_ClientIsChatting(int clientnum) { @@ -465,19 +516,139 @@ void BotEnhanced_SanitizeGoalStack(bot_state_t *bs) { } void BotEnhanced_OnThinkStart(bot_state_t *bs) { + if (!bs || !bs->inuse || BotIsObserver(bs) || BotIsDead(bs)) { + if (bs) { + bs->tact_pending = 0; + BotEvents_Reset(bs); + } + botenh_think_bs = NULL; + return; + } + + trap_Cvar_Update(&bot_enhanced); + trap_Cvar_Update(&bot_enhanced_debug); + bs->enh_cached_active = bot_enhanced.integer != 0; + bs->enh_cached_debug = bot_enhanced_debug.integer != 0; + botenh_think_bs = bs; + + if (!bs->enh_cached_active) { + return; + } + + bs->enh_travel_tfl = BotMove_BuildTravelFlags(bs); + bs->enh_travel_tfl_valid = qtrue; + BotEvents_Drain(bs); - if (bs && bs->inuse) { - BotEnhanced_SanitizeGoalStack(bs); + BotEnhanced_SanitizeGoalStack(bs); + + bs->firethrottlewait_time = 0.0f; + bs->firethrottleshoot_time = 0.0f; + BotEnhanced_DropDeadEnemy(bs); + BotEnhanced_DropChattingEnemy(bs); + BotEnhanced_CancelCampLongTermGoal(bs); + BotPosition_OnThinkStart(bs); + BotCombat_TickEngagement(bs); + if (BotEnhanced_WantsOpponentThink(bs)) { + BotOpponent_OnThinkStart(bs); + BotOpponent_TickFleeEngagement(bs); + } + BotCombat_UpdateIntent(bs); + BotPosition_TickItemHarass(bs); + BotPosition_UpdateCombat(bs); + BotItems_Tick(bs); + if (BotEnhanced_WantsNavGuardThink(bs)) { + BotNavGuard_OnThinkStart(bs); + } + if (BotOpponent_IsTracking(bs) && BotOpponent_WantsAvoidEngagement(bs) && + bs->enemy >= 0 && BotItems_HasActiveCommit(bs) && + !BotCombat_HasEnemyCombatContact(bs) && + !BotOpponent_WantsFleeEngaged(bs) && + !BotOpponent_WantsDuelCommit(bs)) { + BotCombat_ReleaseEnemy(bs); + } +} + +void BotEnhanced_AfterCheckSnapshot(bot_state_t *bs) { + if (!bs) { + return; + } + if (BotEnhanced_IsActive() && bs->enemy < 0 && !BotIsObserver(bs)) { + BotWpnSelect_TickRoaming(bs); } if (BotEnhanced_IsActive()) { - BotEnhanced_DropDeadEnemy(bs); - BotEnhanced_DropChattingEnemy(bs); - BotEnhanced_CancelCampLongTermGoal(bs); - BotCombat_UpdateIntent(bs); - BotItems_Tick(bs); + BotItemTiming_PostSnapshot(bs); + } +} + +void BotEnhanced_OnSnapshotClientEvent(bot_state_t *bs, struct entityState_s *state, + int event) { + if (!bs || !state) { + return; + } + switch (event) { + case EV_ITEM_PICKUP: + BotEnhanced_OnObservedItemPickup(bs, BotAI_EventPickerClient((entityState_t *)state), + state->eventParm, state->origin); + break; + case EV_GLOBAL_ITEM_PICKUP: + BotItemTiming_OnGlobalItemPickup(bs, state->eventParm, state->origin); + break; + case EV_ITEM_RESPAWN: + BotItemTiming_OnItemRespawn(bs, state->modelindex, state->origin); + break; + default: + break; } } +void BotEnhanced_OnPowerupRespawnSound(bot_state_t *bs, const vec3_t origin) { + if (BotEnhanced_IsActive()) { + BotItemTiming_OnPowerupSpawnSound(bs, origin); + } +} + +int BotEnhanced_SuppressBlockedAvoid(bot_state_t *bs) { + if (!BotEnhanced_IsActive()) { + return 0; + } + return BotItems_SuppressBlockedAvoid(bs); +} + +int BotEnhanced_OnSeekCombatContact(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return 0; + } + + if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + if (!BotEnhanced_CanEngageClient(bs, bs->enemy) || + EntityClientIsDead(bs->enemy)) { + BotCombat_ReleaseEnemy(bs); + } + } + + if (BotOpponent_IsTracking(bs)) { + BotOpponent_TryLatchCombatEnemy(bs); + } + + if (BotFindEnemy(bs, -1)) { + return 1; + } + + if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + if (BotOpponent_IsTracking(bs) && + bs->enemy == bs->opponent_belief.client) { + if (BotOpponent_HasCombatSight(bs, bs->enemy)) { + return 1; + } + } else if (BotCombat_HasEnemyCombatContact(bs)) { + return 1; + } + BotCombat_ReleaseEnemy(bs); + } + + return 0; +} + int BotEnhanced_ShouldSuppressFightRetreat(bot_state_t *bs) { if (!bs || !BotEnhanced_IsActive()) { return 0; @@ -485,22 +656,94 @@ int BotEnhanced_ShouldSuppressFightRetreat(bot_state_t *bs) { if (BotCombat_IsRushOpponent(bs)) { return 1; } - if (BotEnhanced_TacticsActive() && BotTactics_BattleFightSuppressRetreat(bs)) { + if (BotPosition_IsItemHarassActive(bs)) { + return 1; + } + if (BotEnhanced_IsActive() && BotTactics_BattleFightSuppressRetreat(bs)) { + return 1; + } + if (BotOpponent_IsTracking(bs) && BotOpponent_WantsDuelCommit(bs)) { return 1; } return 0; } +float BotEnhanced_SkillScale(bot_state_t *bs) { + (void)bs; + return 1.0f; +} + +static float BotEnhanced_EliteScaled(bot_state_t *bs, float elite) { + return elite * BotEnhanced_SkillScale(bs); +} + +static float BotEnhanced_GetEliteOrChar(bot_state_t *bs, float elite, + int charId, float minVal, float maxVal) { + if (BotEnhanced_IsActive()) { + return BotEnhanced_EliteScaled(bs, elite); + } + return trap_Characteristic_BFloat(bs->character, charId, minVal, maxVal); +} + +float BotEnhanced_GetReactionTime(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_REACTION_TIME, + CHARACTERISTIC_REACTIONTIME, 0, 1); +} + +float BotEnhanced_GetViewFactor(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_VIEW_FACTOR, + CHARACTERISTIC_VIEW_FACTOR, 0.01f, 1.0f); +} + +float BotEnhanced_GetViewMaxChange(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_VIEW_MAXCHANGE, + CHARACTERISTIC_VIEW_MAXCHANGE, 1.0f, 1800.0f); +} + +float BotEnhanced_GetAimSkill(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_AIM_SKILL, + CHARACTERISTIC_AIM_SKILL, 0, 1); +} + +float BotEnhanced_GetAimAccuracy(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_AIM_ACCURACY, + CHARACTERISTIC_AIM_ACCURACY, 0, 1); +} + +float BotEnhanced_GetAttackSkill(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_ATTACK_SKILL, + CHARACTERISTIC_ATTACK_SKILL, 0, 1); +} + +float BotEnhanced_GetFireThrottle(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_FIRE_THROTTLE, + CHARACTERISTIC_FIRETHROTTLE, 0, 1); +} + +float BotEnhanced_GetAlertness(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_ALERTNESS, + CHARACTERISTIC_ALERTNESS, 0, 1); +} + +float BotEnhanced_GetEasyFragger(bot_state_t *bs) { + return BotEnhanced_GetEliteOrChar(bs, BOTENH_ELITE_EASY_FRAGGER, + CHARACTERISTIC_EASY_FRAGGER, 0, 1); +} + int BotEnhanced_AllowsVoluntaryCloseGauntlet(bot_state_t *bs) { if (!bs || !BotEnhanced_IsActive()) { return 0; } - return bs->settings.skill >= 4.0f; + return BotEnhanced_SkillScale(bs) >= 0.75f; } void BotEnhanced_ResetBot(bot_state_t *bs) { if (bs) { bs->enh_goal_last_push_time = 0.0f; + bs->enh_cached_active = 0; + bs->enh_cached_debug = 0; + bs->enh_travel_tfl = 0; + bs->enh_travel_tfl_valid = qfalse; } BotMoveHarness_Reset(bs); BotEvents_Reset(bs); @@ -509,4 +752,29 @@ void BotEnhanced_ResetBot(bot_state_t *bs) { BotWpnSelect_Reset(bs); BotTactics_Reset(bs); BotItems_Reset(bs); + BotItemTiming_Reset(bs); + BotPosition_Reset(bs); + BotOpponent_Reset(bs); + BotNavGuard_Reset(bs); +} + +void BotEnhanced_OnArenaEntry(int clientNum) { + bot_state_t *bs; + + if (clientNum < 0 || clientNum >= MAX_CLIENTS) { + return; + } + bs = botstates[clientNum]; + if (!bs || !bs->inuse) { + return; + } + bs->entergame_time = FloatTime(); + bs->entergamechat = qfalse; + BotEnhanced_ResetBot(bs); + if (BotItemTiming_IsActive()) { + BotItemTiming_OnSpawn(bs); + } + if (BotEnhanced_IsActive()) { + BotOpponent_OnSpawn(bs); + } } diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.h b/ratoa_gamecode/code/game/ai_bot_enhanced.h index 62cedec..e0dbc02 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.h +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.h @@ -6,10 +6,11 @@ North-facing include for legacy hooks: register, reset, and feature-active check go through this header. Implementation modules keep their own logic in ai_aim_harness.c, ai_weapon_select.c, ai_bot_tactics.c. -Master: bot_enhanced (default 0). Sub-cvars only apply when master is 1. +Master: bot_enhanced (default 0). bot_enhanced_debug gates server-side logging. -Legacy names (read once at init if new cvars are still default): bot_humanizeaim, -bot_smartWeaponChoice, bot_tacticalAI — see BotEnhanced_RegisterCvars migration. +Legacy names (read once at init): bot_humanizeaim, bot_smartWeaponChoice, +bot_tacticalAI, and deprecated bot_enhanced_* sub-cvars — see migration in +BotEnhanced_RegisterCvars. =========================================================================== */ @@ -18,25 +19,68 @@ bot_smartWeaponChoice, bot_tacticalAI — see BotEnhanced_RegisterCvars migratio struct bot_state_s; struct bot_goal_s; +struct entityState_s; void BotEnhanced_RegisterCvars(void); void BotEnhanced_ResetBot(struct bot_state_s *bs); +/* Fresh enhanced-module init when a bot enters active play (not connect/queue). */ +void BotEnhanced_OnArenaEntry(int clientNum); int BotEnhanced_IsActive(void); -int BotEnhanced_AimActive(void); -int BotEnhanced_WeaponsActive(void); -int BotEnhanced_TacticsActive(void); -int BotEnhanced_MovementActive(void); +int BotEnhanced_DebugActive(void); void BotEnhanced_OnThinkStart(struct bot_state_s *bs); +/* Seek/NBG: sanitize enemy, latch contact, find enemy; 1 if combat should start. */ +int BotEnhanced_OnSeekCombatContact(struct bot_state_s *bs); /* Battle fight: suppress BotWantsToRetreat when rushing or gauntlet-only tactics. */ int BotEnhanced_ShouldSuppressFightRetreat(struct bot_state_s *bs); -/* bs->settings.skill (1–5): voluntary close gauntlet only at skill 4 or 5. */ +/* + * 0..1 combat quality scale for enhanced modules (1 = full strength). + * Fixed at 1.0 during development; lower later to nerf low-menu-skill bots. + */ +float BotEnhanced_SkillScale(struct bot_state_s *bs); + +/* + * Elite personality values used when bot_enhanced is on (botlib characteristics + * ignored). Each is multiplied by BotEnhanced_SkillScale for future nerfs. + */ +#define BOTENH_ELITE_REACTION_TIME 0.075f +#define BOTENH_ELITE_VIEW_FACTOR 1.0f +#define BOTENH_ELITE_VIEW_MAXCHANGE 1200.0f +#define BOTENH_ELITE_AIM_SKILL 0.96f +#define BOTENH_ELITE_AIM_ACCURACY 0.94f +#define BOTENH_ELITE_ATTACK_SKILL 1.0f +#define BOTENH_ELITE_FIRE_THROTTLE 1.0f +#define BOTENH_ELITE_ALERTNESS 1.0f +#define BOTENH_ELITE_EASY_FRAGGER 1.0f + +float BotEnhanced_GetReactionTime(struct bot_state_s *bs); +float BotEnhanced_GetViewFactor(struct bot_state_s *bs); +float BotEnhanced_GetViewMaxChange(struct bot_state_s *bs); +float BotEnhanced_GetAimSkill(struct bot_state_s *bs); +float BotEnhanced_GetAimAccuracy(struct bot_state_s *bs); +float BotEnhanced_GetAttackSkill(struct bot_state_s *bs); +float BotEnhanced_GetFireThrottle(struct bot_state_s *bs); +float BotEnhanced_GetAlertness(struct bot_state_s *bs); +float BotEnhanced_GetEasyFragger(struct bot_state_s *bs); + int BotEnhanced_AllowsVoluntaryCloseGauntlet(struct bot_state_s *bs); -int BotEnhanced_ItemsActive(void); +/* + * Synchronous world-event ingress for major item pickups (snapshot EV_ITEM_PICKUP). + * Updates item-timing pad beliefs and opponent stack via BotItemTiming_OnWitnessedPadTaken. + * BotEvents queue is for deferred tactics only; do not route pickups through it. + */ +void BotEnhanced_OnObservedItemPickup(struct bot_state_s *bs, int pickerClient, + int itemIndex, const vec3_t eventOrigin); + +void BotEnhanced_AfterCheckSnapshot(struct bot_state_s *bs); +void BotEnhanced_OnSnapshotClientEvent(struct bot_state_s *bs, + struct entityState_s *state, int event); +void BotEnhanced_OnPowerupRespawnSound(struct bot_state_s *bs, const vec3_t origin); +int BotEnhanced_SuppressBlockedAvoid(struct bot_state_s *bs); /* EF_TALK on client — typing in chat (chat balloon). */ int BotEnhanced_ClientIsChatting(int clientnum); diff --git a/ratoa_gamecode/code/game/ai_bot_events.c b/ratoa_gamecode/code/game/ai_bot_events.c deleted file mode 100644 index cf26fd1..0000000 --- a/ratoa_gamecode/code/game/ai_bot_events.c +++ /dev/null @@ -1,73 +0,0 @@ -/* -=========================================================================== -BOT EVENTS — ingress queue; drain delegates scan/process to ai_bot_tactics.c. -=========================================================================== -*/ - -#include "g_local.h" -#include "../botlib/botlib.h" -#include "../botlib/be_aas.h" -#include "../botlib/be_ea.h" -#include "../botlib/be_ai_char.h" -#include "../botlib/be_ai_goal.h" -#include "../botlib/be_ai_move.h" -#include "../botlib/be_ai_weap.h" -#include "ai_main.h" -#include "ai_bot_enhanced.h" -#include "ai_bot_events.h" -#include "ai_bot_tactics.h" - -void BotEvents_Reset(bot_state_t *bs) { - if (!bs) { - return; - } - bs->evt_pending = 0; - bs->evt_attacker = -1; - bs->evt_damage = 0; - bs->evt_mod = MOD_UNKNOWN; -} - -void BotEvents_Push(bot_state_t *bs, int evt_bits, int ent, int parm) { - if (!bs || !evt_bits) { - return; - } - bs->evt_pending |= evt_bits; - if (evt_bits & BOT_EVT_HURT_BY_OTHER) { - bs->evt_attacker = ent; - bs->evt_damage = parm; - /* mod: use parm only for damage today; mod stays MOD_UNKNOWN until extended */ - } -} - -static void BotEvents_FlushToTactics(bot_state_t *bs) { - if (!bs->evt_pending) { - return; - } - bs->tact_pending |= bs->evt_pending; - if (bs->evt_pending & BOT_EVT_HURT_BY_OTHER) { - bs->tact_evt_attacker = bs->evt_attacker; - bs->tact_evt_damage = bs->evt_damage; - bs->tact_evt_mod = bs->evt_mod; - } - BotEvents_Reset(bs); -} - -void BotEvents_Drain(bot_state_t *bs) { - if (!bs) { - return; - } - if (!BotEnhanced_IsActive()) { - bs->tact_pending = 0; - BotEvents_Reset(bs); - return; - } - if (!BotEnhanced_TacticsActive()) { - bs->tact_pending = 0; - BotEvents_Reset(bs); - return; - } - - BotTactics_ScanEvents(bs); - BotEvents_FlushToTactics(bs); - BotTactics_ProcessPending(bs); -} diff --git a/ratoa_gamecode/code/game/ai_bot_events.h b/ratoa_gamecode/code/game/ai_bot_events.h index 30533d4..b3553f0 100644 --- a/ratoa_gamecode/code/game/ai_bot_events.h +++ b/ratoa_gamecode/code/game/ai_bot_events.h @@ -7,6 +7,7 @@ BOT EVENTS — per-bot world-event ingress (bounded queue, no malloc). inside tactics during drain until damage scan moves here). - BotEvents_Drain runs once per think from BotEnhanced_OnThinkStart only. - Pending bits and payload fields live on bot_state_t (evt_*); cleared on drain. + - Major item pickups use BotEnhanced_OnObservedItemPickup at snapshot time instead. BOT_EVT_* bits match BOT_TACT_EVT_* while tactics still owns handlers. =========================================================================== diff --git a/ratoa_gamecode/code/game/ai_bot_item_timing.h b/ratoa_gamecode/code/game/ai_bot_item_timing.h new file mode 100644 index 0000000..3788cef --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_item_timing.h @@ -0,0 +1,81 @@ +/* +=========================================================================== +BOT ITEM TIMING — per-bot belief about specific item spawn instances. + +Each bot tracks up to BOT_TIMING_TRACK_COUNT map items by origin (not class). +Gate: bot_enhanced = 1. Gametype FFA/Duel/TDM when enhanced. +gametype FFA / Duel / TDM only. +=========================================================================== +*/ + +#ifndef AI_BOT_ITEM_TIMING_H +#define AI_BOT_ITEM_TIMING_H + +struct bot_state_s; + +#define BOT_TIMING_TRACK_COUNT 3 + +#define BOT_TIMING_STATE_NONE 0 +#define BOT_TIMING_STATE_SPAWNED 1 +#define BOT_TIMING_STATE_COOLDOWN 2 + +typedef struct timing_belief_s { + int state; + vec3_t origin; + int areanum; + int goal_number; /* botlib level item goal */ + int kind; /* internal: entity lookup only */ + float respawn_interval; + float believed_spawn_at; + float pickup_latch_time; + float next_spawn_check; + float detour_block_until; /* don't re-snag this instance soon */ + float preempt_block_until; /* recently abandoned for higher item */ +} timing_belief_t; + +void BotItemTiming_RegisterCvars(void); +int BotItemTiming_IsActive(void); +void BotItemTiming_Reset(struct bot_state_s *bs); +/* Select / refresh tracked items from current spawn (arena entry or death respawn). */ +void BotItemTiming_OnSpawn(struct bot_state_s *bs); +void BotItemTiming_PostSnapshot(struct bot_state_s *bs); + +int BotItemTiming_HasTrack(const struct bot_state_s *bs); +/* trackIndex 0..BOT_TIMING_TRACK_COUNT-1; 0 = not tracking / spawned now */ +int BotItemTiming_GetSecondsUntil(const struct bot_state_s *bs, int trackIndex); +/* True while pursuing a cooldown track due to respawn within TIMING_IMMINENT_WAIT_SEC. */ +int BotItemTiming_ShouldWaitAtPad(struct bot_state_s *bs); + +void BotItemTiming_OnEntityPickup(struct bot_state_s *bs, int pickerClient, + int itemIndex, const vec3_t eventOrigin); +/* Major item taken at or near a pad — updates timing belief and opponent stack. */ +void BotItemTiming_OnWitnessedPadTaken(struct bot_state_s *bs, int pickerClient, + int itemIndex, const vec3_t eventOrigin, const char *reason); +void BotItemTiming_OnWitnessedPadTakenCooldown(struct bot_state_s *bs, + int pickerClient, int itemIndex, const vec3_t eventOrigin, + const char *reason, float cooldownSec); +void BotItemTiming_OnGlobalItemPickup(struct bot_state_s *bs, int itemIndex, + const vec3_t eventOrigin); +/* Bot heard the denied-reward sting near a major item pad. */ +void BotItemTiming_OnDeniedReward(struct bot_state_s *bs); +void BotItemTiming_OnPowerupSpawnSound(struct bot_state_s *bs, const vec3_t eventOrigin); +void BotItemTiming_OnItemRespawn(struct bot_state_s *bs, int itemIndex, + const vec3_t eventOrigin); + +/* Called when a timing-driven item commit ends (pickup, timeout, abort). */ +void BotItemTiming_OnTimingCommitEnd(struct bot_state_s *bs, int gotItem, + int trackIndex); +/* Drop latched pursuit without touching item commit (e.g. urgent health). */ +void BotItemTiming_AbortPursuit(struct bot_state_s *bs); +/* Block re-pursuit of the current timing goal (e.g. nav-guard stair loop). */ +void BotItemTiming_BlockPursuitAtGoal(struct bot_state_s *bs, float blockSec); +/* After a timing detour ends and primary pursuit resumes. */ +void BotItemTiming_OnDetourEnded(struct bot_state_s *bs, int trackIndex); +/* Bot picked up a tracked item themselves (commit success or pickup event). */ +void BotItemTiming_OnSelfPickup(struct bot_state_s *bs, int trackIndex); + +/* Death-site inference for opponent stack beliefs (opponent module). */ +void BotItemTiming_InferOpponentPickupNearDeath(struct bot_state_s *bs, + const vec3_t deathOrigin); + +#endif /* AI_BOT_ITEM_TIMING_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_items.c b/ratoa_gamecode/code/game/ai_bot_items.c index fb8d795..035f984 100644 --- a/ratoa_gamecode/code/game/ai_bot_items.c +++ b/ratoa_gamecode/code/game/ai_bot_items.c @@ -21,18 +21,39 @@ BOT ITEMS — visible high-value pickup with committed goal persistence. #include "inv.h" #include "ai_bot_items.h" #include "ai_bot_enhanced.h" +#include "ai_bot_item_timing.h" #include "ai_bot_move_harness.h" +#include "ai_bot_position.h" +#include "ai_bot_opponent.h" #include "ai_dmq3.h" #include "ai_team.h" -vmCvar_t bot_enhanced_items; -vmCvar_t bot_enhanced_items_debug; +static qboolean BotItems_GoalVisibleToBot(bot_state_t *bs, bot_goal_t *goal); #define BOT_ITEMS_COMMIT_DURATION 12.0f +#define BOT_ITEMS_COMMIT_PAD_SEC 5.0f +#define BOT_ITEMS_NEAR_GOAL_EXTEND_SEC 8.0f +#define BOT_ITEMS_NEAR_GOAL_EXTEND_SOON 4.0f #define BOT_ITEMS_SCAN_INTERVAL 0.35f +#define BOT_ITEMS_OPPORTUNE_COMMIT_SEC 3.0f +#define BOT_ITEMS_OPPORTUNE_MAX_TRAVEL 250 /* AAS units (~2.5s) */ +#define BOT_ITEMS_OPPORTUNE_SCAN_INTERVAL 0.5f +#define BOT_ITEMS_OPPORTUNE_COOLDOWN_SEC 8.0f +#define BOT_ITEMS_OPPORTUNE_PRIMARY_MIN 250 /* don't opportune if this close to primary */ +#define BOT_ITEMS_WEAPON_DETOUR_MAX_TRAVEL 420 /* AAS units (~4.2s) */ +#define BOT_ITEMS_WEAPON_REACH_MAX_TRAVEL 480 /* idle reachable weapon grab */ +#define BOT_ITEMS_WEAPON_DETOUR_SEC 4.5f #define BOT_ITEMS_GONE_AVOID_TIME 20.0f #define BOT_ITEMS_STUCK_DIST 48.0f #define BOT_ITEMS_STUCK_TIME 1.75f + +#define BOT_ITEMS_LJ_MAX_HORIZ 320.0f +#define BOT_ITEMS_LJ_MIN_DZ 20.0f +#define BOT_ITEMS_LJ_MAX_DZ 64.0f +#define BOT_ITEMS_LJ_MAX_ATTEMPTS 3 +#define BOT_ITEMS_LJ_LIP_TIME 0.35f +#define BOT_ITEMS_LJ_JUMP_LATCH 0.45f +#define BOT_ITEMS_LJ_PICKUP_HORIZ 40.0f #define BOT_ITEM_NONE 0 #define BOT_ITEM_QUAD 1 #define BOT_ITEM_RED_FLAG 2 @@ -61,6 +82,21 @@ vmCvar_t bot_enhanced_items_debug; #define BOT_ITEMS_DBG_RESET 4 #define BOT_ITEMS_DBG_STUCK 5 +static void BotItems_BeginCommitEx(bot_state_t *bs, bot_goal_t *goal, int kind, + float until, qboolean timing); +static void BotItems_FinishOpportuneCommit(bot_state_t *bs, int endEvent, + qboolean resumePrimary); +static qboolean BotItems_TickOpportuneScan(bot_state_t *bs); +static int BotItems_CountMissingWeapons(bot_state_t *bs); +static float BotItems_EffectivePickupScale(bot_state_t *bs, int kind); +static qboolean BotItems_FindBestAmongKinds(bot_state_t *bs, bot_goal_t *bestGoal, + int *bestKindOut, const int *kinds, int nKinds, int maxTravel, int primaryTravel, + qboolean requireVisible, qboolean useTravelMetric); +static qboolean BotItems_TryAcquireReachableWeapon(bot_state_t *bs); +static qboolean BotItems_TickWeaponDetourScan(bot_state_t *bs); +static qboolean BotItems_CommitInventoryImproved(bot_state_t *bs); +static int BotItems_SuspendActivePrimary(bot_state_t *bs); + static const int botItemsScanKinds[] = { @@ -97,22 +133,37 @@ typedef struct { } botItemWeaponDef_t; static const botItemWeaponDef_t botItemWeaponDefs[] = { - { INVENTORY_SHOTGUN, "Shotgun", "Shotgun", 1.0f }, - { INVENTORY_GRENADELAUNCHER, "Grenade Launcher", "Grenade Launcher", 1.0f }, - { INVENTORY_ROCKETLAUNCHER, "Rocket Launcher", "Rocket Launcher", 1.0f }, - { INVENTORY_PLASMAGUN, "Plasma Gun", "Plasma Gun", 1.0f }, - { INVENTORY_RAILGUN, "Railgun", "Railgun", 1.0f }, - { INVENTORY_LIGHTNING, "Lightning Gun", "Lightning Gun", 1.0f }, - { INVENTORY_BFG10K, "BFG10K", "BFG10K", 1.0f }, - { INVENTORY_MACHINEGUN, "Machinegun", "Machinegun", 1.0f }, - { INVENTORY_NAILGUN, "Nailgun", "Nailgun", 1.0f }, - { INVENTORY_PROXLAUNCHER, "Prox Launcher", "Prox Launcher", 1.0f }, - { INVENTORY_CHAINGUN, "Chaingun", "Chaingun", 1.0f } + { INVENTORY_SHOTGUN, "Shotgun", "Shotgun", 0.50f }, + { INVENTORY_GRENADELAUNCHER, "Grenade Launcher", "Grenade Launcher", 0.52f }, + { INVENTORY_ROCKETLAUNCHER, "Rocket Launcher", "Rocket Launcher", 0.42f }, + { INVENTORY_PLASMAGUN, "Plasma Gun", "Plasma Gun", 0.48f }, + { INVENTORY_RAILGUN, "Railgun", "Railgun", 0.40f }, + { INVENTORY_LIGHTNING, "Lightning Gun", "Lightning Gun", 0.44f }, + { INVENTORY_BFG10K, "BFG10K", "BFG10K", 0.55f }, + { INVENTORY_MACHINEGUN, "Machinegun", "Machinegun", 0.62f }, + { INVENTORY_NAILGUN, "Nailgun", "Nailgun", 0.50f }, + { INVENTORY_PROXLAUNCHER, "Prox Launcher", "Prox Launcher", 0.54f }, + { INVENTORY_CHAINGUN, "Chaingun", "Chaingun", 0.52f } +}; + +static const int botItemsWeaponKinds[] = { + BOT_ITEM_WEAPON_RAIL, + BOT_ITEM_WEAPON_ROCKET, + BOT_ITEM_WEAPON_LIGHTNING, + BOT_ITEM_WEAPON_PLASMA, + BOT_ITEM_WEAPON_SHOTGUN, + BOT_ITEM_WEAPON_GRENADE, + BOT_ITEM_WEAPON_BFG, + BOT_ITEM_WEAPON_MACHINEGUN, + BOT_ITEM_WEAPON_NAILGUN, + BOT_ITEM_WEAPON_PROX, + BOT_ITEM_WEAPON_CHAINGUN }; +#define BOT_ITEMS_WEAPON_KIND_COUNT (sizeof(botItemsWeaponKinds) / sizeof(botItemsWeaponKinds[0])) + static int BotItems_DebugEnabled(void) { - trap_Cvar_Update(&bot_enhanced_items_debug); - return bot_enhanced_items_debug.integer != 0; + return BotEnhanced_DebugActive(); } static const botItemWeaponDef_t *BotItems_WeaponDef(int kind) { @@ -242,7 +293,7 @@ static void BotItems_DebugLine(bot_state_t *bs, int kind, const char *event) { BotItems_KindLabel(kind, itemName, sizeof(itemName)); team = BotItems_ClientTeam(bs); if (kind == BOT_ITEM_RED_FLAG || kind == BOT_ITEM_BLUE_FLAG) { - G_Printf("BotItems: %s %s %s (session %s)\n", + G_Printf("BotItems: %s %s %s team %s\n", botName, event, itemName, BotItems_TeamLabel(team)); return; } @@ -356,34 +407,46 @@ static float BotItems_PriorityScale(int kind) { } +static int BotItems_CountMissingWeapons(bot_state_t *bs) { + int i; + int count; - -void BotItems_RegisterCvars(void) { - - trap_Cvar_Register(&bot_enhanced_items, "bot_enhanced_items", "1", CVAR_ARCHIVE); - - trap_Cvar_Update(&bot_enhanced_items); - - trap_Cvar_Register(&bot_enhanced_items_debug, "bot_enhanced_items_debug", "0", 0); - - trap_Cvar_Update(&bot_enhanced_items_debug); - + if (!bs) { + return 0; + } + count = 0; + for (i = 0; i < (int)BOT_ITEM_WEAPON_DEF_COUNT; i++) { + if (bs->inventory[botItemWeaponDefs[i].inventoryIndex] <= 0) { + count++; + } + } + return count; } +static float BotItems_EffectivePickupScale(bot_state_t *bs, int kind) { + float scale; + int missing; - -int BotItems_IsActive(void) { - - if (!BotEnhanced_IsActive()) { - - return 0; - + scale = BotItems_PriorityScale(kind); + if (!BotItems_WeaponDef(kind)) { + return BotOpponent_ItemPriorityScale(bs, scale); } + missing = BotItems_CountMissingWeapons(bs); + if (missing >= 4) { + scale *= 0.78f; + } else if (missing >= 3) { + scale *= 0.86f; + } else if (missing >= 2) { + scale *= 0.92f; + } + return BotOpponent_ItemPriorityScale(bs, scale); +} - trap_Cvar_Update(&bot_enhanced_items); - - return bot_enhanced_items.integer != 0; +void BotItems_RegisterCvars(void) { +} +int BotItems_IsActive(void) { + return BotEnhanced_IsActive(); } @@ -398,6 +461,14 @@ void BotItems_Reset(bot_state_t *bs) { bs->item_commit_active = qfalse; + bs->item_commit_timing = qfalse; + + bs->item_commit_detour = qfalse; + + bs->item_commit_opportune = qfalse; + + bs->item_commit_suspended = qfalse; + bs->item_commit_kind = BOT_ITEM_NONE; bs->item_commit_until = 0.0f; @@ -406,6 +477,18 @@ void BotItems_Reset(bot_state_t *bs) { memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); + memset(&bs->item_commit_suspended_goal, 0, sizeof(bot_goal_t)); + + bs->item_commit_suspended_kind = BOT_ITEM_NONE; + + bs->item_commit_suspended_until = 0.0f; + + bs->item_commit_suspended_timing = qfalse; + + bs->item_commit_suspended_progress_time = 0.0f; + + VectorClear(bs->item_commit_suspended_progress_origin); + bs->item_commit_snap_health = 0; bs->item_commit_snap_armor = 0; @@ -422,1507 +505,5721 @@ void BotItems_Reset(bot_state_t *bs) { VectorClear(bs->item_commit_progress_origin); + bs->item_lj_attempts = 0; + bs->item_lj_lip_since = 0.0f; + bs->item_lj_jump_until = 0.0f; + bs->item_next_scan_time = 0.0f; -} + bs->item_opportune_next_scan_time = 0.0f; + bs->item_opportune_block_until = 0.0f; +} -static void BotItems_ClearCommit(bot_state_t *bs, int endEvent) { - bot_goal_t top; - int kind; +static void BotItems_ResetLedgeJump(bot_state_t *bs) { - qboolean wasActive; + if (!bs) { + return; + } - int goalNumber; + bs->item_lj_attempts = 0; + bs->item_lj_lip_since = 0.0f; + bs->item_lj_jump_until = 0.0f; +} - if (!bs) { +static qboolean BotItems_LedgeJumpEligible(bot_state_t *bs, const bot_goal_t *goal) { - return; + vec3_t delta; + float horiz, dz; + if (!BotEnhanced_IsActive() || !bs || !goal || !(goal->flags & GFL_ITEM)) { + return qfalse; } - wasActive = bs->item_commit_active; + VectorSubtract(goal->origin, bs->origin, delta); + dz = delta[2]; + if (dz <= BOT_ITEMS_LJ_MIN_DZ || dz > BOT_ITEMS_LJ_MAX_DZ) { + return qfalse; + } - kind = bs->item_commit_kind; + delta[2] = 0.0f; + horiz = VectorLength(delta); + if (horiz > BOT_ITEMS_LJ_MAX_HORIZ) { + return qfalse; + } - goalNumber = bs->item_commit_goal.number; + return BotItems_GoalVisibleToBot(bs, (bot_goal_t *) goal); +} - if (bs->item_commit_active && trap_BotGetTopGoal(bs->gs, &top)) { - if (top.number == bs->item_commit_goal.number) { - trap_BotPopGoal(bs->gs); +static qboolean BotItems_LedgeJumpRetainCommit(bot_state_t *bs) { - } + if (!bs || !bs->item_commit_active) { + return qfalse; + } + if (!BotItems_LedgeJumpEligible(bs, &bs->item_commit_goal)) { + return qfalse; } - bs->item_commit_active = qfalse; + return bs->item_lj_attempts < BOT_ITEMS_LJ_MAX_ATTEMPTS; +} - bs->item_commit_kind = BOT_ITEM_NONE; - bs->item_commit_until = 0.0f; - memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); +static void BotItems_ClearSuspended(bot_state_t *bs) { - bs->item_commit_snap_health = 0; + if (!bs) { - bs->item_commit_snap_armor = 0; + return; - bs->item_commit_snap_quad = 0; + } - bs->item_commit_snap_redflag = 0; + bs->item_commit_suspended = qfalse; - bs->item_commit_snap_blueflag = 0; + memset(&bs->item_commit_suspended_goal, 0, sizeof(bot_goal_t)); - bs->item_commit_snap_weapon = 0; + bs->item_commit_suspended_kind = BOT_ITEM_NONE; + bs->item_commit_suspended_until = 0.0f; + bs->item_commit_suspended_timing = qfalse; - if (wasActive && endEvent == BOT_ITEMS_DBG_GONE && goalNumber) { + bs->item_commit_suspended_progress_time = 0.0f; - trap_BotSetAvoidGoalTime(bs->gs, goalNumber, BOT_ITEMS_GONE_AVOID_TIME); + VectorClear(bs->item_commit_suspended_progress_origin); - } +} - if (wasActive && endEvent) { +static void BotItems_SuspendPrimaryCommit(bot_state_t *bs) { - switch (endEvent) { + bot_goal_t top; - case BOT_ITEMS_DBG_GOT: - BotItems_DebugLine(bs, kind, "picked up"); - break; + if (!bs || !bs->item_commit_active || !bs->item_commit_timing) { - case BOT_ITEMS_DBG_TIMEOUT: + return; - BotItems_DebugLine(bs, kind, "abandoned (timeout)"); + } - break; + memcpy(&bs->item_commit_suspended_goal, &bs->item_commit_goal, + sizeof(bot_goal_t)); - case BOT_ITEMS_DBG_GONE: + bs->item_commit_suspended_kind = bs->item_commit_kind; - BotItems_DebugLine(bs, kind, "abandoned (gone)"); + bs->item_commit_suspended_until = bs->item_commit_until; - break; + bs->item_commit_suspended_timing = bs->item_commit_timing; - case BOT_ITEMS_DBG_RESET: + bs->item_commit_suspended_progress_time = bs->item_commit_progress_time; - BotItems_DebugLine(bs, kind, "abandoned (reset)"); + VectorCopy(bs->item_commit_progress_origin, + bs->item_commit_suspended_progress_origin); - break; + bs->item_commit_suspended = qtrue; - case BOT_ITEMS_DBG_STUCK: - BotItems_DebugLine(bs, kind, "abandoned (stuck)"); - break; + if (trap_BotGetTopGoal(bs->gs, &top) && + top.number == bs->item_commit_goal.number) { - default: + trap_BotPopGoal(bs->gs); - break; + } - } + bs->item_commit_active = qfalse; - } + bs->item_commit_timing = qfalse; -} + bs->item_commit_kind = BOT_ITEM_NONE; + bs->item_commit_until = 0.0f; + memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); -static qboolean BotItems_CanConsider(bot_state_t *bs) { +} - if (!bs || !bs->inuse) { - return qfalse; - } +static void BotItems_RestoreSuspendedCommit(bot_state_t *bs) { - if (BotIsDead(bs) || BotIsObserver(bs)) { + if (!bs || !bs->item_commit_suspended) { - return qfalse; + return; } - if (gametype == GT_CTF || gametype == GT_CTF_ELIMINATION) { + BotItems_BeginCommitEx(bs, &bs->item_commit_suspended_goal, + bs->item_commit_suspended_kind, bs->item_commit_suspended_until, + bs->item_commit_suspended_timing); - if (BotCTFCarryingFlag(bs)) { + if (bs->item_commit_active) { - return qfalse; + bs->item_commit_progress_time = bs->item_commit_suspended_progress_time; - } + VectorCopy(bs->item_commit_suspended_progress_origin, + bs->item_commit_progress_origin); + + bs->nbg_time = bs->item_commit_until; } - return qtrue; + BotItems_ClearSuspended(bs); } -static qboolean BotItems_DenialPickupGametype(void) { +static void BotItems_FinishDetourCommit(bot_state_t *bs, int endEvent, + qboolean resumePrimary) { - return gametype == GT_FFA || gametype == GT_TOURNAMENT; + bot_goal_t top; -} + int goalNumber; + int detourTrack; -static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { - const botItemWeaponDef_t *wdef; + if (!bs || !bs->item_commit_detour) { + return; + } - if (!bs) { + detourTrack = bs->timing_detour_track; - return qfalse; + goalNumber = bs->item_commit_goal.number; + if (endEvent == BOT_ITEMS_DBG_GOT && detourTrack >= 0) { + BotItemTiming_OnSelfPickup(bs, detourTrack); } + if (bs->item_commit_active && trap_BotGetTopGoal(bs->gs, &top)) { + if (top.number == goalNumber) { - wdef = BotItems_WeaponDef(kind); - - if (wdef) { + trap_BotPopGoal(bs->gs); - return bs->inventory[wdef->inventoryIndex] <= 0; + } } + bs->item_commit_active = qfalse; + bs->item_commit_timing = qfalse; - if (BotItems_DenialPickupGametype()) { + bs->item_commit_detour = qfalse; - switch (kind) { + bs->item_commit_kind = BOT_ITEM_NONE; - case BOT_ITEM_MEGA_HEALTH: + bs->item_commit_until = 0.0f; - case BOT_ITEM_RED_ARMOR: + memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); - case BOT_ITEM_YELLOW_ARMOR: + BotItems_ResetLedgeJump(bs); - return qtrue; + bs->timing_detour_track = -1; - case BOT_ITEM_QUAD: - return qtrue; - case BOT_ITEM_HEALTH_SMALL: - case BOT_ITEM_HEALTH: - case BOT_ITEM_HEALTH_LARGE: - return BotItems_NeedsHealthPickup(bs); - default: + if (resumePrimary && bs->item_commit_suspended) { - return qfalse; + BotItems_RestoreSuspendedCommit(bs); - } + BotItemTiming_OnDetourEnded(bs, detourTrack); + + } else { + + BotItems_ClearSuspended(bs); } +} - switch (kind) { - case BOT_ITEM_QUAD: +static void BotItems_FinishOpportuneCommit(bot_state_t *bs, int endEvent, + qboolean resumePrimary) { - return qtrue; + bot_goal_t top; - case BOT_ITEM_RED_FLAG: - case BOT_ITEM_BLUE_FLAG: - if (!BotItems_FlagCaptureGametype()) { - return qfalse; - } - if (!BotItems_FlagIsEnemy(bs, kind)) { - return qfalse; - } - return BotItems_FlagAtBase(bs, kind); + int goalNumber; - case BOT_ITEM_MEGA_HEALTH: + float now; - return bs->inventory[INVENTORY_HEALTH] < 150; - case BOT_ITEM_RED_ARMOR: - return bs->inventory[INVENTORY_ARMOR] < 140; + if (!bs || !bs->item_commit_opportune) { - case BOT_ITEM_YELLOW_ARMOR: + return; - return bs->inventory[INVENTORY_ARMOR] < 80; + } - case BOT_ITEM_HEALTH_SMALL: - case BOT_ITEM_HEALTH: - case BOT_ITEM_HEALTH_LARGE: - return BotItems_NeedsHealthPickup(bs); + now = FloatTime(); - default: + goalNumber = bs->item_commit_goal.number; - return qfalse; + if (bs->item_commit_active && trap_BotGetTopGoal(bs->gs, &top)) { + + if (top.number == goalNumber) { + + trap_BotPopGoal(bs->gs); + + } } -} + bs->item_commit_active = qfalse; + bs->item_commit_timing = qfalse; + bs->item_commit_detour = qfalse; -static void BotItems_SnapshotCommitInventory(bot_state_t *bs, int kind) { + bs->item_commit_opportune = qfalse; - const botItemWeaponDef_t *wdef; + bs->item_commit_kind = BOT_ITEM_NONE; + + bs->item_commit_until = 0.0f; + memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); + BotItems_ResetLedgeJump(bs); - if (!bs) { + if (endEvent == BOT_ITEMS_DBG_GOT) { - return; + bs->item_opportune_block_until = now + BOT_ITEMS_OPPORTUNE_COOLDOWN_SEC; } - bs->item_commit_snap_health = bs->inventory[INVENTORY_HEALTH]; - bs->item_commit_snap_armor = bs->inventory[INVENTORY_ARMOR]; - bs->item_commit_snap_quad = bs->inventory[INVENTORY_QUAD]; + if (resumePrimary && bs->item_commit_suspended) { - bs->item_commit_snap_redflag = bs->inventory[INVENTORY_REDFLAG]; + BotItems_RestoreSuspendedCommit(bs); - bs->item_commit_snap_blueflag = bs->inventory[INVENTORY_BLUEFLAG]; + } else { - wdef = BotItems_WeaponDef(kind); + BotItems_ClearSuspended(bs); - bs->item_commit_snap_weapon = wdef ? bs->inventory[wdef->inventoryIndex] : 0; + } } -/* Goal achieved: no longer need item, or inventory improved since commit (denial modes). */ +static int BotItems_ItemIndexFromKind(int kind) { -static qboolean BotItems_CommitAchieved(bot_state_t *bs) { + gitem_t *item; - const botItemWeaponDef_t *wdef; - int kind; + item = NULL; + switch (kind) { - if (!bs || !bs->item_commit_active) { + case BOT_ITEM_QUAD: - return qfalse; + item = BG_FindItem("item_quad"); - } + break; - kind = bs->item_commit_kind; + case BOT_ITEM_MEGA_HEALTH: - if (kind == BOT_ITEM_NONE) { + item = BG_FindItem("item_health_mega"); - return qfalse; + break; - } + case BOT_ITEM_RED_ARMOR: - /* Flag left base is not success; only picked up the committed enemy flag. */ - if (BotItems_IsFlagKind(kind)) { - if (kind == BOT_ITEM_RED_FLAG) { - return bs->inventory[INVENTORY_REDFLAG] > bs->item_commit_snap_redflag; - } - return bs->inventory[INVENTORY_BLUEFLAG] > bs->item_commit_snap_blueflag; - } - - if (!BotItems_NeedsKind(bs, kind)) { - return qtrue; - } + item = BG_FindItem("item_armor_body"); - wdef = BotItems_WeaponDef(kind); + break; - if (wdef) { + case BOT_ITEM_YELLOW_ARMOR: - return bs->inventory[wdef->inventoryIndex] > bs->item_commit_snap_weapon; + item = BG_FindItem("item_armor_combat"); - } + break; - switch (kind) { + case BOT_ITEM_HEALTH_SMALL: - case BOT_ITEM_QUAD: + item = BG_FindItem("item_health_small"); - return bs->inventory[INVENTORY_QUAD] && !bs->item_commit_snap_quad; + break; - case BOT_ITEM_MEGA_HEALTH: - case BOT_ITEM_HEALTH_SMALL: case BOT_ITEM_HEALTH: - case BOT_ITEM_HEALTH_LARGE: - return bs->inventory[INVENTORY_HEALTH] > bs->item_commit_snap_health; + item = BG_FindItem("item_health"); - case BOT_ITEM_RED_ARMOR: + break; - case BOT_ITEM_YELLOW_ARMOR: + case BOT_ITEM_HEALTH_LARGE: - return bs->inventory[INVENTORY_ARMOR] > bs->item_commit_snap_armor; + item = BG_FindItem("item_health_large"); + + break; default: - return qfalse; + return -1; } -} + if (!item) { + return -1; + } -/* Level item goals often use entitynum -1 when no spawned pickup is linked in AAS. */ + return ITEM_INDEX(item); -static qboolean BotItems_GoalHasPickupEntity(const bot_goal_t *goal) { - if (!goal) { - return qfalse; - } - if (goal->entitynum <= MAX_CLIENTS || goal->entitynum >= level.num_entities) { - return qfalse; - } - return qtrue; } -int BotItems_ItemGoalInVisButNotVisible(bot_state_t *bs, bot_goal_t *goal) { - if (!bs || !goal) { - return qtrue; - } - if (!(goal->flags & GFL_ITEM)) { - return qfalse; - } - if (!BotItems_GoalHasPickupEntity(goal)) { - return qtrue; - } - return trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, - goal); -} -/* Pickup still on the map (not taken / hidden for respawn). */ -static qboolean BotItems_PickupEntityActive(int entnum) { +static void BotItems_NotifyOpponentItemGone(bot_state_t *bs, int kind, + const bot_goal_t *goal) { - gentity_t *ent; + int itemIndex; - if (entnum <= MAX_CLIENTS || entnum >= level.num_entities) { + if (!bs || !goal) { - return qfalse; + return; } - ent = &g_entities[entnum]; + itemIndex = BotItems_ItemIndexFromKind(kind); - if (!ent->inuse || !ent->item) { + if (itemIndex < 0) { - return qfalse; + return; } - if (ent->s.eType != ET_ITEM && ent->s.eType != ET_GENERAL) { + BotItemTiming_OnWitnessedPadTaken(bs, -1, itemIndex, goal->origin, + "witnessed item gone"); - return qfalse; +} - } - if (ent->s.eFlags & EF_NODRAW) { - return qfalse; +static void BotItems_ClearCommit(bot_state_t *bs, int endEvent) { - } + bot_goal_t top; - if (ent->r.svFlags & SVF_NOCLIENT) { + int kind; - return qfalse; + qboolean wasActive; - } + int goalNumber; - return qtrue; -} + if (!bs) { + return; -static qboolean BotItems_GoalEntityMatchesFlagKind(bot_goal_t *goal, int kind) { - gentity_t *ent; - if (!goal || !BotItems_IsFlagKind(kind)) { - return qtrue; - } - if (!BotItems_PickupEntityActive(goal->entitynum)) { - return qfalse; } - ent = &g_entities[goal->entitynum]; - if (kind == BOT_ITEM_RED_FLAG) { - return ent->item->giTag == PW_REDFLAG; - } - return ent->item->giTag == PW_BLUEFLAG; -} + if (endEvent == BOT_ITEMS_DBG_GONE && bs->item_commit_active) { + if (!BotItems_CommitInventoryImproved(bs)) { -static qboolean BotItems_RefreshGoalByNumber(bot_state_t *bs, bot_goal_t *goal, int kind) { + BotItems_NotifyOpponentItemGone(bs, bs->item_commit_kind, + &bs->item_commit_goal); - char goalname[64]; + } - int index; + } - bot_goal_t tmp; + if (endEvent == BOT_ITEMS_DBG_GOT && bs->item_commit_active) { + int itemIndex; + itemIndex = BotItems_ItemIndexFromKind(bs->item_commit_kind); - if (!goal || kind == BOT_ITEM_NONE) { + if (itemIndex >= 0) { - return qfalse; + BotOpponent_OnSelfMajorPickup(bs, itemIndex); + + } } - BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + if (bs->item_commit_detour) { - if (!goalname[0]) { + BotItems_FinishDetourCommit(bs, endEvent, + endEvent == BOT_ITEMS_DBG_GOT || + endEvent == BOT_ITEMS_DBG_TIMEOUT || + endEvent == BOT_ITEMS_DBG_STUCK || + endEvent == BOT_ITEMS_DBG_GONE); - return qfalse; + return; } - index = -1; + if (bs->item_commit_opportune) { - while ((index = trap_BotGetLevelItemGoal(index, goalname, &tmp)) >= 0) { + BotItems_FinishOpportuneCommit(bs, endEvent, + endEvent == BOT_ITEMS_DBG_GOT || + endEvent == BOT_ITEMS_DBG_TIMEOUT || + endEvent == BOT_ITEMS_DBG_STUCK || + endEvent == BOT_ITEMS_DBG_GONE); - if (tmp.number == goal->number) { + return; - memcpy(goal, &tmp, sizeof(bot_goal_t)); + } - return qtrue; + wasActive = bs->item_commit_active; - } + kind = bs->item_commit_kind; - } + goalNumber = bs->item_commit_goal.number; - return qfalse; + if (wasActive && bs->item_commit_timing) { -} + int pursueTrack; + pursueTrack = bs->timing_pursue_track; + bs->item_commit_timing = qfalse; -/* Same LOS test as BotNearestVisibleItem — trace only, no view FOV. */ + BotItemTiming_OnTimingCommitEnd(bs, + endEvent == BOT_ITEMS_DBG_GOT, pursueTrack); -static qboolean BotItems_GoalVisibleToBot(bot_state_t *bs, bot_goal_t *goal) { + } - bsp_trace_t trace; + BotItems_ClearSuspended(bs); + if (bs->item_commit_active && trap_BotGetTopGoal(bs->gs, &top)) { + if (top.number == bs->item_commit_goal.number) { - if (!bs || !goal) { + trap_BotPopGoal(bs->gs); - return qfalse; + } } + bs->item_commit_active = qfalse; + bs->item_commit_timing = qfalse; - BotAI_Trace(&trace, bs->eye, NULL, NULL, goal->origin, bs->client, + bs->item_commit_detour = qfalse; - CONTENTS_SOLID | CONTENTS_PLAYERCLIP); + bs->item_commit_opportune = qfalse; - return trace.fraction >= 1.0f; + bs->item_commit_kind = BOT_ITEM_NONE; -} + bs->item_commit_until = 0.0f; + memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); + bs->item_commit_snap_health = 0; -static qboolean BotItems_GoalIsPresent(bot_state_t *bs, bot_goal_t *goal) { + bs->item_commit_snap_armor = 0; - if (!bs || !goal || !(goal->flags & GFL_ITEM)) { + bs->item_commit_snap_quad = 0; - return qfalse; + bs->item_commit_snap_redflag = 0; - } + bs->item_commit_snap_blueflag = 0; - if (!BotItems_PickupEntityActive(goal->entitynum)) { + bs->item_commit_snap_weapon = 0; - return qfalse; + BotItems_ResetLedgeJump(bs); - } - if (BotItems_ItemGoalInVisButNotVisible(bs, goal)) { - return qfalse; + if (wasActive && endEvent == BOT_ITEMS_DBG_GONE && goalNumber) { + + trap_BotSetAvoidGoalTime(bs->gs, goalNumber, BOT_ITEMS_GONE_AVOID_TIME); } - return qtrue; -} + if (wasActive && endEvent) { + switch (endEvent) { -/* - * Travel flags for item acquire / movement — match Seek NBG (RJ, lava) and - * BotJumppad_EffectiveTfl so we do not commit to jumppad-only paths while pads - * are avoided after a trigger_push flight. - */ -static int BotItems_TravelFlags(bot_state_t *bs) { - int tfl = TFL_DEFAULT; + case BOT_ITEMS_DBG_GOT: - if (bot_grapple.integer) { - tfl |= TFL_GRAPPLEHOOK; - } - if (BotInLavaOrSlime(bs)) { - tfl |= TFL_LAVA | TFL_SLIME; - } - if (BotCanAndWantsToRocketJump(bs)) { - tfl |= TFL_ROCKETJUMP; - } - if (bs->jumppad_avoid_until > FloatTime()) { - tfl &= ~TFL_JUMPPAD; - } - /* - * Mirror the movement harness walkoff-avoid flag so that acquisition - * reachability checks stay consistent with what BotMove_EffectiveTfl - * will allow during actual movement (avoids committing to a goal whose - * only route is through a currently-blocked walkoff ledge). - */ - if (bs->movej_no_walkoff_until > FloatTime()) { - tfl &= ~TFL_WALKOFFLEDGE; - } - return tfl; -} + BotItems_DebugLine(bs, kind, "picked up"); -/* Acquire-time AAS reachability (func_button pattern in ai_dmq3.c). */ + break; -static qboolean BotItems_GoalReachable(bot_state_t *bs, bot_goal_t *goal) { + case BOT_ITEMS_DBG_TIMEOUT: - int t; + BotItems_DebugLine(bs, kind, "abandoned timeout"); + + break; + case BOT_ITEMS_DBG_GONE: + BotItems_DebugLine(bs, kind, "abandoned gone"); - if (!bs || !goal || !goal->areanum) { + break; - return qfalse; + case BOT_ITEMS_DBG_RESET: - } + BotItems_DebugLine(bs, kind, "abandoned reset"); - if (!trap_AAS_AreaReachability(bs->areanum)) { + break; - return qfalse; + case BOT_ITEMS_DBG_STUCK: - } + BotItems_DebugLine(bs, kind, "abandoned stuck"); - t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, - BotItems_TravelFlags(bs)); + break; - return t > 0; + default: -} + break; + } + } -static void BotItems_BeginCommit(bot_state_t *bs, bot_goal_t *goal, int kind) { +} - if (!bs || !goal) { - return; - } +static qboolean BotItems_CanConsider(bot_state_t *bs) { - if (!BotEnhanced_PushGoalSafe(bs, goal)) { + if (!bs || !bs->inuse) { - return; + return qfalse; } + if (BotIsDead(bs) || BotIsObserver(bs)) { + return qfalse; - memcpy(&bs->item_commit_goal, goal, sizeof(bot_goal_t)); + } - bs->item_commit_active = qtrue; + if (gametype == GT_CTF || gametype == GT_CTF_ELIMINATION) { - bs->item_commit_kind = kind; + if (BotCTFCarryingFlag(bs)) { - bs->item_commit_until = FloatTime() + BOT_ITEMS_COMMIT_DURATION; + return qfalse; - BotItems_SnapshotCommitInventory(bs, kind); + } + } + return qtrue; - trap_BotRemoveFromAvoidGoals(bs->gs, goal->number); +} - bs->nbg_time = bs->item_commit_until; - bs->item_commit_progress_time = FloatTime(); - VectorCopy(bs->origin, bs->item_commit_progress_origin); +static qboolean BotItems_DenialPickupGametype(void) { - BotItems_DebugLine(bs, kind, "going for"); + return gametype == GT_FFA || gametype == GT_TOURNAMENT; } -static void BotItems_EnsureGoalOnStack(bot_state_t *bs) { +static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { - if (!bs || !bs->item_commit_active) { + const botItemWeaponDef_t *wdef; - return; - } - if (BotEnhanced_GoalStackHasEquivalent(bs, &bs->item_commit_goal)) { + if (!bs) { - return; + return qfalse; } - if (!BotEnhanced_PushGoalSafe(bs, &bs->item_commit_goal)) { - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); - } + wdef = BotItems_WeaponDef(kind); -} + if (wdef) { + return bs->inventory[wdef->inventoryIndex] <= 0; + } -static void BotItems_CheckStuck(bot_state_t *bs) { - vec3_t delta; - float dist; + if (BotItems_DenialPickupGametype()) { + switch (kind) { + case BOT_ITEM_MEGA_HEALTH: - if (!bs || !bs->item_commit_active) { + case BOT_ITEM_RED_ARMOR: - return; + case BOT_ITEM_YELLOW_ARMOR: - } + return qtrue; - /* - * While airborne (jump pad, fall) the bot cannot steer and movement failures - * are expected. Slide the progress window forward so the stuck timer starts - * fresh on landing rather than counting flight time as idle. - */ - if (bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + case BOT_ITEM_QUAD: + return qtrue; - bs->item_commit_progress_time = FloatTime(); + case BOT_ITEM_HEALTH_SMALL: + case BOT_ITEM_HEALTH: + case BOT_ITEM_HEALTH_LARGE: + return BotItems_NeedsHealthPickup(bs); - VectorCopy(bs->origin, bs->item_commit_progress_origin); + default: - return; + return qfalse; + + } } - VectorSubtract(bs->origin, bs->item_commit_progress_origin, delta); - dist = VectorLength(delta); - if (dist >= BOT_ITEMS_STUCK_DIST) { + switch (kind) { - bs->item_commit_progress_time = FloatTime(); + case BOT_ITEM_QUAD: - VectorCopy(bs->origin, bs->item_commit_progress_origin); + return qtrue; - return; + case BOT_ITEM_RED_FLAG: + case BOT_ITEM_BLUE_FLAG: + if (!BotItems_FlagCaptureGametype()) { + return qfalse; + } + if (!BotItems_FlagIsEnemy(bs, kind)) { + return qfalse; + } + return BotItems_FlagAtBase(bs, kind); - } + case BOT_ITEM_MEGA_HEALTH: - if (FloatTime() - bs->item_commit_progress_time >= BOT_ITEMS_STUCK_TIME) { + return bs->inventory[INVENTORY_HEALTH] < 150; - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_STUCK); + case BOT_ITEM_RED_ARMOR: - } + return bs->inventory[INVENTORY_ARMOR] < 140; -} + case BOT_ITEM_YELLOW_ARMOR: + + return bs->inventory[INVENTORY_ARMOR] < 80; + case BOT_ITEM_HEALTH_SMALL: + case BOT_ITEM_HEALTH: + case BOT_ITEM_HEALTH_LARGE: + return BotItems_NeedsHealthPickup(bs); + default: -static qboolean BotItems_TryAcquireVisible(bot_state_t *bs) { + return qfalse; - int kind, index, k; + } - char goalname[64]; +} - vec3_t dir; - bot_goal_t goal, bestGoal; - int bestKind; +static void BotItems_SnapshotCommitInventory(bot_state_t *bs, int kind) { - float dist, bestDist, effDist, scale; + const botItemWeaponDef_t *wdef; - if (!BotItems_CanConsider(bs)) { + if (!bs) { - return qfalse; + return; } - if (bs->item_commit_active) { + bs->item_commit_snap_health = bs->inventory[INVENTORY_HEALTH]; - return qfalse; + bs->item_commit_snap_armor = bs->inventory[INVENTORY_ARMOR]; - } + bs->item_commit_snap_quad = bs->inventory[INVENTORY_QUAD]; - if (FloatTime() < bs->item_next_scan_time) { + bs->item_commit_snap_redflag = bs->inventory[INVENTORY_REDFLAG]; - return qfalse; + bs->item_commit_snap_blueflag = bs->inventory[INVENTORY_BLUEFLAG]; - } + wdef = BotItems_WeaponDef(kind); - bs->item_next_scan_time = FloatTime() + BOT_ITEMS_SCAN_INTERVAL; + bs->item_commit_snap_weapon = wdef ? bs->inventory[wdef->inventoryIndex] : 0; +} - bestKind = BOT_ITEM_NONE; - bestDist = 999999.0f; +/* Goal achieved: no longer need item, or inventory improved since commit (denial modes). */ +static qboolean BotItems_CommitInventoryImproved(bot_state_t *bs) { + const botItemWeaponDef_t *wdef; - for (k = 0; k < BOT_ITEMS_SCAN_KIND_COUNT; k++) { + int kind; - kind = botItemsScanKinds[k]; - if (!BotItems_NeedsKind(bs, kind)) { - continue; + if (!bs || !bs->item_commit_active) { - } + return qfalse; - BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + } - if (!goalname[0]) { + kind = bs->item_commit_kind; - continue; + if (kind == BOT_ITEM_NONE) { + + return qfalse; + + } + if (BotItems_IsFlagKind(kind)) { + if (kind == BOT_ITEM_RED_FLAG) { + return bs->inventory[INVENTORY_REDFLAG] > bs->item_commit_snap_redflag; } + return bs->inventory[INVENTORY_BLUEFLAG] > bs->item_commit_snap_blueflag; + } + + wdef = BotItems_WeaponDef(kind); + if (wdef) { + return bs->inventory[wdef->inventoryIndex] > bs->item_commit_snap_weapon; - index = -1; + } - while ((index = trap_BotGetLevelItemGoal(index, goalname, &goal)) >= 0) { + switch (kind) { - VectorSubtract(goal.origin, bs->origin, dir); + case BOT_ITEM_QUAD: - dist = VectorLength(dir); + return bs->inventory[INVENTORY_QUAD] && !bs->item_commit_snap_quad; - scale = BotItems_PriorityScale(kind); + case BOT_ITEM_MEGA_HEALTH: + case BOT_ITEM_HEALTH_SMALL: + case BOT_ITEM_HEALTH: + case BOT_ITEM_HEALTH_LARGE: - effDist = dist * scale; + return bs->inventory[INVENTORY_HEALTH] > bs->item_commit_snap_health; - if (effDist >= bestDist) { + case BOT_ITEM_RED_ARMOR: - continue; + case BOT_ITEM_YELLOW_ARMOR: - } + return bs->inventory[INVENTORY_ARMOR] > bs->item_commit_snap_armor; - if (!BotItems_GoalVisibleToBot(bs, &goal)) { + default: - continue; + return qfalse; - } + } - if (!BotItems_GoalIsPresent(bs, &goal)) { +} - continue; - } - if (!BotItems_GoalReachable(bs, &goal)) { +static qboolean BotItems_CommitNearGoal(bot_state_t *bs) { - continue; + vec3_t delta; - } + float dist; - if (!BotItems_GoalEntityMatchesFlagKind(&goal, kind)) { - continue; - } + if (!bs || !bs->item_commit_active) { + return qfalse; + } - bestDist = effDist; + VectorSubtract(bs->origin, bs->item_commit_goal.origin, delta); - bestKind = kind; + dist = VectorLength(delta); - memcpy(&bestGoal, &goal, sizeof(bot_goal_t)); + return dist <= 384.0f; - } +} - } +static qboolean BotItems_TimingCommitAchieved(bot_state_t *bs) { - if (bestKind == BOT_ITEM_NONE) { + if (!bs || !bs->item_commit_active) { return qfalse; } - if (!BotItems_GoalHasPickupEntity(&bestGoal)) { + if (!bs->item_commit_timing && !bs->item_commit_detour && + !bs->item_commit_opportune) { return qfalse; } + if (!BotItems_CommitNearGoal(bs)) { + return qfalse; - BotItems_BeginCommit(bs, &bestGoal, bestKind); + } - return qtrue; + return BotItems_CommitInventoryImproved(bs); } -static void BotItems_DebugConfigOnce(void) { +static qboolean BotItems_CommitAchieved(bot_state_t *bs) { - static qboolean done; + int kind; - if (done || !BotItems_DebugEnabled()) { + if (!bs || !bs->item_commit_active) { - return; + return qfalse; } - done = qtrue; - - if (!BotEnhanced_IsActive()) { - - G_Printf("BotItems: debug on but bot_enhanced is 0 — no scans\n"); + if (bs->item_commit_timing || bs->item_commit_detour || + bs->item_commit_opportune) { - return; + return BotItems_TimingCommitAchieved(bs); } - trap_Cvar_Update(&bot_enhanced_items); - - if (!bot_enhanced_items.integer) { + kind = bs->item_commit_kind; - G_Printf("BotItems: debug on but bot_enhanced_items is 0 — no scans\n"); + if (kind == BOT_ITEM_NONE) { - return; + return qfalse; } - G_Printf("BotItems: debug active (quad, red/blue flag, armor, mega, weapons)\n"); - -} + /* Flag left base is not success; only picked up the committed enemy flag. */ + if (BotItems_IsFlagKind(kind)) { + if (kind == BOT_ITEM_RED_FLAG) { + return bs->inventory[INVENTORY_REDFLAG] > bs->item_commit_snap_redflag; + } + return bs->inventory[INVENTORY_BLUEFLAG] > bs->item_commit_snap_blueflag; + } + if (!BotItems_NeedsKind(bs, kind)) { + return qtrue; + } + return BotItems_CommitInventoryImproved(bs); -static qboolean BotItems_TryAcquireHealthKinds(bot_state_t *bs, const int *kinds, - int nKinds) { - int i, kind, index, k; - char goalname[64]; - vec3_t dir; - bot_goal_t goal, bestGoal; - int bestKind; - float dist, bestDist, effDist, scale; +} - if (!BotItems_CanConsider(bs)) { - return qfalse; - } - bestKind = BOT_ITEM_NONE; - bestDist = 999999.0f; - for (k = 0; k < nKinds; k++) { - kind = kinds[k]; - if (!BotItems_NeedsKind(bs, kind)) { - continue; - } - BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); - if (!goalname[0]) { - continue; - } - index = -1; - while ((index = trap_BotGetLevelItemGoal(index, goalname, &goal)) >= 0) { - VectorSubtract(goal.origin, bs->origin, dir); - dist = VectorLength(dir); - scale = BotItems_PriorityScale(kind); - effDist = dist * scale; - if (effDist >= bestDist) { - continue; - } - if (!BotItems_GoalVisibleToBot(bs, &goal)) { - continue; - } - if (!BotItems_GoalIsPresent(bs, &goal)) { - continue; - } - if (!BotItems_GoalReachable(bs, &goal)) { - continue; - } - bestDist = effDist; - bestKind = kind; - memcpy(&bestGoal, &goal, sizeof(bot_goal_t)); - } - } +/* Level item goals often use entitynum -1 when no spawned pickup is linked in AAS. */ - if (bestKind == BOT_ITEM_NONE) { +static qboolean BotItems_GoalHasPickupEntity(const bot_goal_t *goal) { + if (!goal) { return qfalse; } - if (!BotItems_GoalHasPickupEntity(&bestGoal)) { + if (goal->entitynum <= MAX_CLIENTS || goal->entitynum >= level.num_entities) { return qfalse; } - BotItems_BeginCommit(bs, &bestGoal, bestKind); return qtrue; } -void BotItems_RequestUrgentHealth(bot_state_t *bs) { - static const int urgentHealthKinds[] = { - BOT_ITEM_MEGA_HEALTH, - BOT_ITEM_HEALTH_LARGE, - BOT_ITEM_HEALTH, - BOT_ITEM_HEALTH_SMALL - }; - - if (!BotItems_IsActive() || !bs) { - return; +int BotItems_ItemGoalInVisButNotVisible(bot_state_t *bs, bot_goal_t *goal) { + if (!bs || !goal) { + return qtrue; } - if (!BotItems_NeedsHealthPickup(bs)) { - return; + if (!(goal->flags & GFL_ITEM)) { + return qfalse; } - if (bs->item_commit_active) { - return; + if (!BotItems_GoalHasPickupEntity(goal)) { + return qtrue; } - (void)BotItems_TryAcquireHealthKinds(bs, urgentHealthKinds, - sizeof(urgentHealthKinds) / sizeof(urgentHealthKinds[0])); + return trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, + goal); } -void BotItems_Tick(bot_state_t *bs) { - - BotItems_DebugConfigOnce(); +/* Pickup still on the map (not taken / hidden for respawn). */ - if (!BotItems_IsActive()) { +static qboolean BotItems_PickupEntityActive(int entnum) { - return; + gentity_t *ent; - } - if (!BotItems_CanConsider(bs)) { - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); + if (entnum <= MAX_CLIENTS || entnum >= level.num_entities) { - return; + return qfalse; } + ent = &g_entities[entnum]; + if (!ent->inuse || !ent->item) { + + return qfalse; - if (BotMove_WantsUrgentHealth(bs) && - !bs->item_commit_active && BotItems_NeedsHealthPickup(bs)) { - static const int urgentKinds[] = { - BOT_ITEM_MEGA_HEALTH, - BOT_ITEM_HEALTH_LARGE, - BOT_ITEM_HEALTH, - BOT_ITEM_HEALTH_SMALL - }; - bs->item_next_scan_time = 0.0f; - if (BotItems_TryAcquireHealthKinds(bs, urgentKinds, - sizeof(urgentKinds) / sizeof(urgentKinds[0]))) { - return; - } } - if (bs->item_commit_active) { + if (ent->s.eType != ET_ITEM && ent->s.eType != ET_GENERAL) { - if (FloatTime() >= bs->item_commit_until) { + return qfalse; - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_TIMEOUT); + } - return; + if (ent->s.eFlags & EF_NODRAW) { - } + return qfalse; - if (BotItems_CommitAchieved(bs)) { + } - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); + if (ent->r.svFlags & SVF_NOCLIENT) { - return; + return qfalse; - } + } - if (!BotItems_RefreshGoalByNumber(bs, &bs->item_commit_goal, bs->item_commit_kind)) { + return qtrue; - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); +} - return; - } - if (!BotItems_GoalIsPresent(bs, &bs->item_commit_goal)) { +static qboolean BotItems_GoalEntityMatchesFlagKind(bot_goal_t *goal, int kind) { + gentity_t *ent; + if (!goal || !BotItems_IsFlagKind(kind)) { + return qtrue; + } + if (!BotItems_PickupEntityActive(goal->entitynum)) { + return qfalse; + } + ent = &g_entities[goal->entitynum]; + if (kind == BOT_ITEM_RED_FLAG) { + return ent->item->giTag == PW_REDFLAG; + } + return ent->item->giTag == PW_BLUEFLAG; +} - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); - return; - } +static qboolean BotItems_RefreshGoalByNumber(bot_state_t *bs, bot_goal_t *goal, int kind) { - BotItems_CheckStuck(bs); + char goalname[64]; - BotItems_EnsureGoalOnStack(bs); + int index; - return; + bot_goal_t tmp; - } + if (!goal || kind == BOT_ITEM_NONE) { - BotItems_TryAcquireVisible(bs); + return qfalse; -} + } + BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + if (!goalname[0]) { -int BotItems_HasActiveCommit(const bot_state_t *bs) { + return qfalse; - if (!bs || !BotItems_IsActive()) { + } - return 0; + index = -1; - } + while ((index = trap_BotGetLevelItemGoal(index, goalname, &tmp)) >= 0) { - return bs->item_commit_active && FloatTime() < bs->item_commit_until; + if (tmp.number == goal->number) { -} + memcpy(goal, &tmp, sizeof(bot_goal_t)); + return qtrue; + } -int BotItems_ShouldRunPickupNode(bot_state_t *bs) { + } - return BotItems_HasActiveCommit(bs); + return qfalse; } -float BotItems_CommitNbgTime(bot_state_t *bs) { +/* Same LOS test as BotNearestVisibleItem — trace only, no view FOV. */ - if (!bs || !BotItems_HasActiveCommit(bs)) { +static qboolean BotItems_GoalVisibleToBot(bot_state_t *bs, bot_goal_t *goal) { - return FloatTime(); + bsp_trace_t trace; - } - return bs->item_commit_until; -} + if (!bs || !goal) { + return qfalse; + } -int BotItems_IsCommittedGoal(const bot_state_t *bs, const bot_goal_t *goal) { - if (!bs || !goal || !bs->item_commit_active) { - return 0; + BotAI_Trace(&trace, bs->eye, NULL, NULL, goal->origin, bs->client, - } + CONTENTS_SOLID | CONTENTS_PLAYERCLIP); - return goal->number == bs->item_commit_goal.number; + return trace.fraction >= 1.0f; } -int BotItems_HandleReachedGoal(bot_state_t *bs, bot_goal_t *goal) { +static qboolean BotItems_GoalIsPresent(bot_state_t *bs, bot_goal_t *goal) { - if (!BotItems_IsActive() || !goal || !(goal->flags & GFL_ITEM)) { + if (!bs || !goal || !(goal->flags & GFL_ITEM)) { - return -1; + return qfalse; } - if (!BotItems_IsCommittedGoal(bs, goal)) { + if (!BotItems_PickupEntityActive(goal->entitynum)) { - return -1; + return qfalse; } + if (BotItems_ItemGoalInVisButNotVisible(bs, goal)) { + return qfalse; - if (trap_BotTouchingGoal(bs->origin, goal)) { + } - trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + return qtrue; - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); +} - return 1; - } +/* + * Travel flags for item acquire / movement — match Seek NBG (RJ, lava) and + * BotJumppad_EffectiveTfl so we do not commit to jumppad-only paths while pads + * are avoided after a trigger_push flight. + */ +static int BotItems_TravelFlags(bot_state_t *bs) { + return BotMove_EffectiveTfl(bs); +} +/* Acquire-time AAS reachability (func_button pattern in ai_dmq3.c). */ - if (BotItems_CommitAchieved(bs)) { +int BotItems_GoalReachable(bot_state_t *bs, bot_goal_t *goal) { - trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + int t; - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); - return 1; - } + if (!bs || !goal || !goal->areanum) { + return qfalse; + } - if (!BotItems_PickupEntityActive(goal->entitynum) || + if (!trap_AAS_AreaReachability(bs->areanum)) { - BotItems_ItemGoalInVisButNotVisible(bs, goal)) { + return qfalse; - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + } - return 1; + t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, + BotItems_TravelFlags(bs)); - } + return t > 0; +} - return 0; -} +int BotItems_TravelTimeToGoal(bot_state_t *bs, bot_goal_t *goal) { + if (!bs || !goal || !goal->areanum) { + return 0; -int BotItems_ShouldPreserveGoalStack(bot_state_t *bs) { + } - if (!BotItems_HasActiveCommit(bs)) { + if (!trap_AAS_AreaReachability(bs->areanum)) { return 0; } - return 1; + return trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, + BotItems_TravelFlags(bs)); } -void BotItems_AbortCommit(bot_state_t *bs) { +static float BotItems_GoalTravelCost(bot_state_t *bs, bot_goal_t *goal, int kind) { - if (!BotItems_HasActiveCommit(bs)) { + int travelTime; - return; + int bonus; - } + float scale, penalty, cost; - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); -} + if (!bs || !goal) { + return 999999.0f; -void BotItems_OnMoveFailure(bot_state_t *bs) { + } - if (!BotItems_HasActiveCommit(bs)) { + travelTime = BotItems_TravelTimeToGoal(bs, goal); - return; + if (travelTime <= 0) { + + return 999999.0f; } - /* - * Don't abandon the commit while the bot is airborne (e.g. immediately after - * a jump pad triggers). The movement failure is expected because TFL_JUMPPAD - * has just been stripped and botlib can't re-route in mid-air. The stuck check - * will handle genuine inability to progress once the bot lands. - */ - if (bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + scale = BotItems_EffectivePickupScale(bs, kind); - return; + penalty = BotOpponent_ItemRoutePenalty(bs, goal->origin); + + bonus = BotPosition_RouteElevationBonus(bs, goal, travelTime); + + cost = (float)travelTime - (float)bonus; + + if (cost < 1.0f) { + + cost = 1.0f; } - BotItems_ClearCommit(bs, BOT_ITEMS_DBG_STUCK); + return cost * scale * penalty; } -float BotItems_GetCommitGoalOriginZ(const bot_state_t *bs) { +static float BotItems_CommitDeadline(bot_state_t *bs, bot_goal_t *goal) { + float now; + float minUntil; + int travelTime; + float travelUntil; - if (!bs || !bs->item_commit_active) { + now = FloatTime(); + minUntil = now + BOT_ITEMS_COMMIT_DURATION; + if (!bs || !goal) { + return minUntil; + } - return -99999.0f; + travelTime = BotItems_TravelTimeToGoal(bs, goal); + if (travelTime <= 0) { + return minUntil; + } + travelUntil = now + ((float)travelTime / 100.0f) + BOT_ITEMS_COMMIT_PAD_SEC; + if (travelUntil > minUntil) { + return travelUntil; } + return minUntil; +} - return bs->item_commit_goal.origin[2]; +static void BotItems_TickNearGoalCommitExtend(bot_state_t *bs) { + float now; + + if (!bs || !bs->item_commit_active) { + return; + } + if (bs->item_commit_timing || bs->item_commit_detour || + bs->item_commit_opportune) { + return; + } + if (!BotItems_CommitNearGoal(bs)) { + return; + } + if (!BotItems_NeedsKind(bs, bs->item_commit_kind)) { + return; + } + + now = FloatTime(); + if (now + BOT_ITEMS_NEAR_GOAL_EXTEND_SOON <= bs->item_commit_until) { + return; + } + bs->item_commit_until = now + BOT_ITEMS_NEAR_GOAL_EXTEND_SEC; + bs->nbg_time = bs->item_commit_until; } +static void BotItems_BeginCommitEx(bot_state_t *bs, bot_goal_t *goal, int kind, + float until, qboolean timing) { + if (!bs || !goal) { -#define BOT_ITEMS_CHOOSER_MAX_TRIES 24 + return; -#define BOT_ITEMS_USELESS_GOAL_AVOID 15.0f + } + if (!BotEnhanced_PushGoalSafe(bs, goal)) { + return; -/* Returns a 'reasonable' max ammo capacity for a weapon -e.g. no point ever picking up 200 railgun slugs. */ + } -static int BotItems_AmmoPracticalMax(int weapon) { - switch (weapon) { - case WP_MACHINEGUN: - return 200; - case WP_SHOTGUN: - return 50; - case WP_GRENADE_LAUNCHER: - return 20; - case WP_ROCKET_LAUNCHER: - return 50; - case WP_LIGHTNING: - return 200; - case WP_RAILGUN: - return 50; - case WP_PLASMAGUN: - return 200; - case WP_BFG: - return 200; - #ifdef MISSIONPACK - case WP_NAILGUN: - return 100; - case WP_PROX_LAUNCHER: - return 50; - case WP_CHAINGUN: - return 200; - #endif - default: - return AMMO_CAPACITY; - } + memcpy(&bs->item_commit_goal, goal, sizeof(bot_goal_t)); + + bs->item_commit_active = qtrue; + + bs->item_commit_timing = timing; + + bs->item_commit_kind = kind; + + bs->item_commit_until = until; + + BotItems_SnapshotCommitInventory(bs, kind); + + + + trap_BotRemoveFromAvoidGoals(bs->gs, goal->number); + + bs->nbg_time = bs->item_commit_until; + + bs->item_commit_progress_time = FloatTime(); + + VectorCopy(bs->origin, bs->item_commit_progress_origin); + + BotItems_ResetLedgeJump(bs); + + BotItems_DebugLine(bs, kind, timing ? "timing route" : "going for"); + } -static qboolean BotItems_PlayerCanUsePickup(bot_state_t *bs, gentity_t *ent) { +static void BotItems_BeginCommit(bot_state_t *bs, bot_goal_t *goal, int kind) { - gitem_t *item; + BotItems_BeginCommitEx(bs, goal, kind, + BotItems_CommitDeadline(bs, goal), qfalse); - int weapon, qty; +} + + + +int BotItems_BeginTimingCommit(bot_state_t *bs, bot_goal_t *goal, int kind, + float until) { + + if (!bs || !goal || !BotItems_IsActive()) { + + return 0; - if (!bs || !ent || !ent->inuse || !ent->item) { - return qfalse; } - if (!BG_CanItemBeGrabbed(gametype, &ent->s, &bs->cur_ps)) { - return qfalse; + if (!BotItems_CanConsider(bs)) { + + return 0; + } - item = ent->item; - switch (item->giType) { - case IT_WEAPON: - weapon = item->giTag; - if (bs->cur_ps.stats[STAT_WEAPONS] & (1 << weapon)) { - qty = ent->count > 0 ? ent->count : item->quantity; - //Don't pick up the gun again unless you've got less than half the ammo - //it comes with - if (bs->cur_ps.ammo[weapon] >= qty*0.5) { - return qfalse; - } - } - return qtrue; + if (!BotItems_GoalReachable(bs, goal)) { + + return 0; - case IT_AMMO: - weapon = item->giTag; - if (!(bs->cur_ps.stats[STAT_WEAPONS] & (1 << weapon))) { - return qfalse; - } - if (bs->cur_ps.ammo[weapon] >= BotItems_AmmoPracticalMax(weapon)) { - return qfalse; - } - return qtrue; - case IT_TEAM: - if (!BotItems_FlagCaptureGametype()) { - return qtrue; - } - { - int team = BotItems_ClientTeam(bs); - if (team == TEAM_FREE) { - return qfalse; - } - if (item->giTag == PW_REDFLAG) { - if (team == TEAM_RED) { - return qfalse; - } - return qtrue; - } - if (item->giTag == PW_BLUEFLAG) { - if (team == TEAM_BLUE) { - return qfalse; - } - return qtrue; - } - } - return qfalse; - default: - return qtrue; } -} + if (until <= FloatTime()) { + until = FloatTime() + BOT_ITEMS_COMMIT_DURATION; -static qboolean BotItems_GoalWorthPursuing(bot_state_t *bs, const bot_goal_t *goal) { + } - if (!bs || !goal) { + if (bs->item_commit_active) { - return qtrue; + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); } - if (!(goal->flags & GFL_ITEM)) { + BotItems_BeginCommitEx(bs, goal, kind, until, qtrue); - return qtrue; + return bs->item_commit_active; + +} + + + +int BotItems_BeginDetourCommit(bot_state_t *bs, bot_goal_t *goal, int kind, + float until) { + + if (!bs || !goal || !BotItems_IsActive()) { + + return 0; } - if (!BotItems_PickupEntityActive(goal->entitynum)) { + if (!BotItems_CanConsider(bs)) { - return qtrue; + return 0; } - return BotItems_PlayerCanUsePickup(bs, &g_entities[goal->entitynum]); + if (!bs->item_commit_suspended) { -} + return 0; + } + if (!BotItems_GoalReachable(bs, goal)) { -static qboolean BotItems_RejectTopGoal(bot_state_t *bs) { + return 0; - bot_goal_t goal; + } + + if (!BotItems_GoalPickupPresent(bs, goal)) { + return 0; + } - if (!trap_BotGetTopGoal(bs->gs, &goal)) { + if (until <= FloatTime()) { - return qfalse; + until = FloatTime() + 5.0f; } - if (BotItems_GoalWorthPursuing(bs, &goal)) { + BotItems_BeginCommitEx(bs, goal, kind, until, qfalse); - return qfalse; + if (!bs->item_commit_active) { + + return 0; } - trap_BotSetAvoidGoalTime(bs->gs, goal.number, BOT_ITEMS_USELESS_GOAL_AVOID); + bs->item_commit_detour = qtrue; - trap_BotPopGoal(bs->gs); + BotItems_DebugLine(bs, kind, "detour"); - return qtrue; + return 1; } -int BotItems_ChooseNBGItem(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { +static int BotItems_BeginOpportuneCommit(bot_state_t *bs, bot_goal_t *goal, + int kind, float until) { - int tries; + if (!bs || !goal || !BotItems_IsActive()) { + return 0; + } - if (!bs) { + if (!BotItems_CanConsider(bs)) { - return qfalse; + return 0; } - if (!BotEnhanced_IsActive()) { + if (!bs->item_commit_suspended) { - BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + return 0; - if (trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range)) { - BotEnhanced_SanitizeGoalStack(bs); - return qtrue; - } - return qfalse; + } + + if (!BotItems_GoalReachable(bs, goal)) { + + return 0; } - BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + if (!BotItems_GoalPickupPresent(bs, goal)) { - for (tries = 0; tries < BOT_ITEMS_CHOOSER_MAX_TRIES; tries++) { + return 0; - if (!trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range)) { + } - return qfalse; + if (until <= FloatTime()) { - } + until = FloatTime() + BOT_ITEMS_OPPORTUNE_COMMIT_SEC; - BotEnhanced_SanitizeGoalStack(bs); + } - if (!BotItems_RejectTopGoal(bs)) { + BotItems_BeginCommitEx(bs, goal, kind, until, qfalse); - return qtrue; + if (!bs->item_commit_active) { - } + return 0; } - return qfalse; + bs->item_commit_opportune = qtrue; + + BotItems_DebugLine(bs, kind, "opportune"); + + return 1; } -int BotItems_ChooseLTGItem(bot_state_t *bs, int tfl) { +int BotItems_IsDetourCommit(const bot_state_t *bs) { - int tries; + if (!bs) { + return 0; + } - if (!bs) { + return bs->item_commit_detour && bs->item_commit_active; - return qfalse; +} + + + +int BotItems_RefreshItemGoal(bot_state_t *bs, bot_goal_t *goal, int kind) { + + if (!bs || !goal) { + + return 0; } - if (!BotEnhanced_IsActive()) { + return BotItems_RefreshGoalByNumber(bs, goal, kind) ? 1 : 0; - BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); +} - if (trap_BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl)) { - BotEnhanced_SanitizeGoalStack(bs); - return qtrue; - } - return qfalse; + + +int BotItems_GoalPickupPresent(bot_state_t *bs, bot_goal_t *goal) { + + if (!bs || !goal) { + + return 0; } - BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + return BotItems_GoalIsPresent(bs, goal) ? 1 : 0; - for (tries = 0; tries < BOT_ITEMS_CHOOSER_MAX_TRIES; tries++) { +} - if (!trap_BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl)) { - return qfalse; - } +int BotItems_SuspendTimingPrimary(bot_state_t *bs) { - BotEnhanced_SanitizeGoalStack(bs); + if (!bs || !bs->item_commit_active || !bs->item_commit_timing) { - if (!BotItems_RejectTopGoal(bs)) { + return 0; - return qtrue; + } - } + if (bs->item_commit_detour || bs->item_commit_opportune || + bs->item_commit_suspended) { + + return 0; } - return qfalse; + BotItems_SuspendPrimaryCommit(bs); + + return bs->item_commit_suspended ? 1 : 0; } + +static int BotItems_SuspendActivePrimary(bot_state_t *bs) { + + if (!bs || !bs->item_commit_active) { + + return 0; + + } + + if (bs->item_commit_detour || bs->item_commit_opportune || + bs->item_commit_suspended) { + + return 0; + + } + + BotItems_SuspendPrimaryCommit(bs); + + return bs->item_commit_suspended ? 1 : 0; + +} + + + +void BotItems_CancelDetourSuspend(bot_state_t *bs) { + + if (!bs || !bs->item_commit_suspended || bs->item_commit_detour || + bs->item_commit_opportune) { + + return; + + } + + BotItems_RestoreSuspendedCommit(bs); + +} + + + +static void BotItems_EnsureGoalOnStack(bot_state_t *bs) { + + if (!bs || !bs->item_commit_active) { + + return; + + } + + if (BotEnhanced_GoalStackHasEquivalent(bs, &bs->item_commit_goal)) { + + return; + + } + + if (!BotEnhanced_PushGoalSafe(bs, &bs->item_commit_goal)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); + + } + +} + + + +static void BotItems_CheckStuck(bot_state_t *bs) { + + vec3_t delta; + + float dist; + + + + if (!bs || !bs->item_commit_active) { + + return; + + } + + if (bs->item_commit_timing && BotItems_TimingHoldingNearGoal(bs)) { + + bs->item_commit_progress_time = FloatTime(); + + VectorCopy(bs->origin, bs->item_commit_progress_origin); + + return; + + } + + /* + * While airborne (jump pad, fall) the bot cannot steer and movement failures + * are expected. Slide the progress window forward so the stuck timer starts + * fresh on landing rather than counting flight time as idle. + */ + if (bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + + bs->item_commit_progress_time = FloatTime(); + + VectorCopy(bs->origin, bs->item_commit_progress_origin); + + return; + + } + + if (BotItems_LedgeJumpRetainCommit(bs)) { + + bs->item_commit_progress_time = FloatTime(); + + VectorCopy(bs->origin, bs->item_commit_progress_origin); + + return; + + } + + VectorSubtract(bs->origin, bs->item_commit_progress_origin, delta); + + dist = VectorLength(delta); + + if (dist >= BOT_ITEMS_STUCK_DIST) { + + bs->item_commit_progress_time = FloatTime(); + + VectorCopy(bs->origin, bs->item_commit_progress_origin); + + return; + + } + + if (FloatTime() - bs->item_commit_progress_time >= BOT_ITEMS_STUCK_TIME) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_STUCK); + + } + +} + + + +static qboolean BotItems_LedgeJumpAtLip(bot_state_t *bs, bot_moveresult_t *mr) { + + vec3_t delta; + float horiz; + qboolean lip; + + if (!bs || !mr) { + return qfalse; + } + + VectorSubtract(bs->item_commit_goal.origin, bs->origin, delta); + delta[2] = 0.0f; + horiz = VectorLength(delta); + if (horiz < BOT_ITEMS_LJ_PICKUP_HORIZ) { + return qfalse; + } + + lip = mr->blocked || mr->failure || + ((mr->flags & MOVERESULT_ONTOPOFOBSTACLE) != 0); + if (!lip) { + VectorSubtract(bs->origin, bs->item_commit_progress_origin, delta); + if (VectorLength(delta) < BOT_ITEMS_STUCK_DIST && + FloatTime() - bs->item_commit_progress_time >= 0.5f) { + lip = qtrue; + } + } + + return lip; +} + + + +static qboolean BotItems_TryAcquireVisible(bot_state_t *bs) { + + int kind, index, k; + + char goalname[64]; + + bot_goal_t goal, bestGoal; + + int bestKind; + + float bestCost, cost; + + + + if (!BotItems_CanConsider(bs)) { + + return qfalse; + + } + + if (bs->item_commit_active) { + + return qfalse; + + } + + if (FloatTime() < bs->item_next_scan_time) { + + return qfalse; + + } + + bs->item_next_scan_time = FloatTime() + BOT_ITEMS_SCAN_INTERVAL; + + + + bestKind = BOT_ITEM_NONE; + + bestCost = 999999.0f; + + + + for (k = 0; k < BOT_ITEMS_SCAN_KIND_COUNT; k++) { + + kind = botItemsScanKinds[k]; + + if (!BotItems_NeedsKind(bs, kind)) { + + continue; + + } + + BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + + if (!goalname[0]) { + + continue; + + } + + + + index = -1; + + while ((index = trap_BotGetLevelItemGoal(index, goalname, &goal)) >= 0) { + + if (!BotItems_GoalVisibleToBot(bs, &goal)) { + + continue; + + } + + if (!BotItems_GoalIsPresent(bs, &goal)) { + + continue; + + } + + if (!BotItems_GoalReachable(bs, &goal) && + !BotItems_LedgeJumpEligible(bs, &goal)) { + + continue; + + } + + if (!BotItems_GoalEntityMatchesFlagKind(&goal, kind)) { + + continue; + + } + + cost = BotItems_GoalTravelCost(bs, &goal, kind); + + if (cost >= bestCost) { + + continue; + + } + + + + bestCost = cost; + + bestKind = kind; + + memcpy(&bestGoal, &goal, sizeof(bot_goal_t)); + + } + + } + + + + if (bestKind == BOT_ITEM_NONE) { + + return qfalse; + + } + + if (!BotItems_GoalHasPickupEntity(&bestGoal)) { + + return qfalse; + + } + + + + BotItems_BeginCommit(bs, &bestGoal, bestKind); + + return qtrue; + +} + + + +static qboolean BotItems_FindBestAmongKinds(bot_state_t *bs, bot_goal_t *bestGoal, + int *bestKindOut, const int *kinds, int nKinds, int maxTravel, int primaryTravel, + qboolean requireVisible, qboolean useTravelMetric) { + + int kind, index, k, travelTime; + char goalname[64]; + vec3_t dir; + bot_goal_t goal; + int bestKind; + float metric, bestMetric, scale; + + if (!bs || !bestGoal || !bestKindOut || !kinds || nKinds <= 0) { + + return qfalse; + + } + + bestKind = BOT_ITEM_NONE; + + bestMetric = 999999.0f; + + for (k = 0; k < nKinds; k++) { + + kind = kinds[k]; + + if (!BotItems_NeedsKind(bs, kind)) { + + continue; + + } + + BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + + if (!goalname[0]) { + + continue; + + } + + index = -1; + + while ((index = trap_BotGetLevelItemGoal(index, goalname, &goal)) >= 0) { + + if (requireVisible && !BotItems_GoalVisibleToBot(bs, &goal)) { + + continue; + + } + + if (!BotItems_GoalIsPresent(bs, &goal)) { + + continue; + + } + + if (!BotItems_GoalReachable(bs, &goal) && + !BotItems_LedgeJumpEligible(bs, &goal)) { + + continue; + + } + + if (!BotItems_GoalHasPickupEntity(&goal)) { + + continue; + + } + + travelTime = BotItems_TravelTimeToGoal(bs, &goal); + + if (travelTime <= 0) { + + continue; + + } + + if (maxTravel > 0 && travelTime > maxTravel) { + + continue; + + } + + if (primaryTravel > 0 && travelTime >= primaryTravel) { + + continue; + + } + + if (useTravelMetric) { + + metric = BotItems_GoalTravelCost(bs, &goal, kind); + + } else { + + VectorSubtract(goal.origin, bs->origin, dir); + + metric = VectorLength(dir); + + scale = BotItems_EffectivePickupScale(bs, kind); + + metric = metric * scale * + BotOpponent_ItemRoutePenalty(bs, goal.origin); + + } + + if (metric >= bestMetric) { + + continue; + + } + + bestMetric = metric; + + bestKind = kind; + + memcpy(bestGoal, &goal, sizeof(bot_goal_t)); + + } + + } + + if (bestKind == BOT_ITEM_NONE) { + + return qfalse; + + } + + *bestKindOut = bestKind; + + return qtrue; + +} + + + +static qboolean BotItems_TryAcquireReachableWeapon(bot_state_t *bs) { + + bot_goal_t goal; + int kind; + + if (!BotItems_CanConsider(bs) || bs->item_commit_active) { + + return qfalse; + + } + + if (BotItems_CountMissingWeapons(bs) <= 0) { + + return qfalse; + + } + + if (!BotItems_FindBestAmongKinds(bs, &goal, &kind, botItemsWeaponKinds, + BOT_ITEMS_WEAPON_KIND_COUNT, BOT_ITEMS_WEAPON_REACH_MAX_TRAVEL, 0, + qfalse, qtrue)) { + + return qfalse; + + } + + BotItems_BeginCommit(bs, &goal, kind); + + return bs->item_commit_active; + +} + + + +static qboolean BotItems_TickWeaponDetourScan(bot_state_t *bs) { + + bot_goal_t goal; + int kind; + int primaryTravel; + float now; + float until; + + if (!bs || !BotItems_IsActive() || !BotItems_CanConsider(bs)) { + + return qfalse; + + } + + if (!bs->item_commit_active || bs->item_commit_detour || + bs->item_commit_opportune || bs->item_commit_suspended) { + + return qfalse; + + } + + if (BotItems_CountMissingWeapons(bs) <= 0) { + + return qfalse; + + } + + now = FloatTime(); + + if (now < bs->item_opportune_block_until) { + + return qfalse; + + } + + if (now < bs->item_opportune_next_scan_time) { + + return qfalse; + + } + + bs->item_opportune_next_scan_time = now + BOT_ITEMS_OPPORTUNE_SCAN_INTERVAL; + + primaryTravel = BotItems_TravelTimeToGoal(bs, &bs->item_commit_goal); + + if (primaryTravel > 0 && + primaryTravel <= BOT_ITEMS_OPPORTUNE_PRIMARY_MIN) { + + return qfalse; + + } + + if (!BotItems_FindBestAmongKinds(bs, &goal, &kind, botItemsWeaponKinds, + BOT_ITEMS_WEAPON_KIND_COUNT, BOT_ITEMS_WEAPON_DETOUR_MAX_TRAVEL, + primaryTravel, qfalse, qtrue)) { + + return qfalse; + + } + + if (!BotItems_SuspendActivePrimary(bs)) { + + return qfalse; + + } + + until = now + BOT_ITEMS_WEAPON_DETOUR_SEC; + + if (!BotItems_BeginOpportuneCommit(bs, &goal, kind, until)) { + + BotItems_CancelDetourSuspend(bs); + + return qfalse; + + } + + return qtrue; + +} + + + +static qboolean BotItems_FindOpportunePickup(bot_state_t *bs, bot_goal_t *bestGoal, + int *bestKindOut, int primaryTravel) { + + static const int opportuneKinds[] = { + BOT_ITEM_HEALTH_SMALL, + BOT_ITEM_HEALTH, + BOT_ITEM_HEALTH_LARGE, + BOT_ITEM_YELLOW_ARMOR + }; + + return BotItems_FindBestAmongKinds(bs, bestGoal, bestKindOut, opportuneKinds, + sizeof(opportuneKinds) / sizeof(opportuneKinds[0]), + BOT_ITEMS_OPPORTUNE_MAX_TRAVEL, primaryTravel, qtrue, qfalse); + +} + + + +static qboolean BotItems_TickOpportuneScan(bot_state_t *bs) { + + bot_goal_t goal; + int kind; + int primaryTravel; + float now; + float until; + + if (!bs || !BotItems_IsActive() || !BotItems_CanConsider(bs)) { + + return qfalse; + + } + + if (!bs->item_commit_timing || !bs->item_commit_active) { + + return qfalse; + + } + + if (bs->item_commit_detour || bs->item_commit_opportune || + bs->item_commit_suspended) { + + return qfalse; + + } + + if (BotItems_TimingHoldingNearGoal(bs)) { + + return qfalse; + + } + + now = FloatTime(); + + if (now < bs->item_opportune_block_until) { + + return qfalse; + + } + + if (now < bs->item_opportune_next_scan_time) { + + return qfalse; + + } + + bs->item_opportune_next_scan_time = now + BOT_ITEMS_OPPORTUNE_SCAN_INTERVAL; + + primaryTravel = BotItems_TravelTimeToGoal(bs, &bs->item_commit_goal); + + if (primaryTravel > 0 && + primaryTravel <= BOT_ITEMS_OPPORTUNE_PRIMARY_MIN) { + + return qfalse; + + } + + if (!BotItems_FindOpportunePickup(bs, &goal, &kind, primaryTravel)) { + + return qfalse; + + } + + if (!BotItems_SuspendTimingPrimary(bs)) { + + return qfalse; + + } + + until = now + BOT_ITEMS_OPPORTUNE_COMMIT_SEC; + + if (!BotItems_BeginOpportuneCommit(bs, &goal, kind, until)) { + + BotItems_CancelDetourSuspend(bs); + + return qfalse; + + } + + return qtrue; + +} + + + +static void BotItems_DebugConfigOnce(void) { + + static qboolean done; + + + + if (done || !BotItems_DebugEnabled()) { + + return; + + } + + done = qtrue; + + if (!BotEnhanced_IsActive()) { + + G_Printf("BotItems: debug on but bot_enhanced is 0 - no scans\n"); + + return; + + } + + if (!BotEnhanced_IsActive()) { + + G_Printf("BotItems: debug on but bot_enhanced is 0 - no scans\n"); + + return; + + } + + G_Printf("BotItems: debug active quad flag armor mega weapons\n"); + +} + + + +static qboolean BotItems_TryAcquireHealthKinds(bot_state_t *bs, const int *kinds, + int nKinds) { + int kind, index, k; + char goalname[64]; + bot_goal_t goal, bestGoal; + int bestKind; + float bestCost, cost; + + if (!BotItems_CanConsider(bs)) { + return qfalse; + } + + bestKind = BOT_ITEM_NONE; + bestCost = 999999.0f; + + for (k = 0; k < nKinds; k++) { + kind = kinds[k]; + if (!BotItems_NeedsKind(bs, kind)) { + continue; + } + BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + if (!goalname[0]) { + continue; + } + index = -1; + while ((index = trap_BotGetLevelItemGoal(index, goalname, &goal)) >= 0) { + if (!BotItems_GoalVisibleToBot(bs, &goal)) { + continue; + } + if (!BotItems_GoalIsPresent(bs, &goal)) { + continue; + } + if (!BotItems_GoalReachable(bs, &goal) && + !BotItems_LedgeJumpEligible(bs, &goal)) { + continue; + } + cost = BotItems_GoalTravelCost(bs, &goal, kind); + if (cost >= bestCost) { + continue; + } + bestCost = cost; + bestKind = kind; + memcpy(&bestGoal, &goal, sizeof(bot_goal_t)); + } + } + + if (bestKind == BOT_ITEM_NONE) { + return qfalse; + } + if (!BotItems_GoalHasPickupEntity(&bestGoal)) { + return qfalse; + } + BotItems_BeginCommit(bs, &bestGoal, bestKind); + return qtrue; +} + +void BotItems_RequestUrgentHealth(bot_state_t *bs) { + static const int urgentHealthKinds[] = { + BOT_ITEM_MEGA_HEALTH, + BOT_ITEM_HEALTH_LARGE, + BOT_ITEM_HEALTH, + BOT_ITEM_HEALTH_SMALL + }; + + if (!BotItems_IsActive() || !bs) { + return; + } + if (!BotItems_NeedsHealthPickup(bs)) { + return; + } + if (bs->item_commit_active) { + return; + } + (void)BotItems_TryAcquireHealthKinds(bs, urgentHealthKinds, + sizeof(urgentHealthKinds) / sizeof(urgentHealthKinds[0])); +} + +void BotItems_Tick(bot_state_t *bs) { + + BotItems_DebugConfigOnce(); + + if (!BotItems_IsActive()) { + + return; + + } + + if (!BotItems_CanConsider(bs)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); + + return; + + } + + + + if (BotMove_WantsUrgentHealth(bs) && BotItems_NeedsHealthPickup(bs)) { + static const int urgentKinds[] = { + BOT_ITEM_MEGA_HEALTH, + BOT_ITEM_HEALTH_LARGE, + BOT_ITEM_HEALTH, + BOT_ITEM_HEALTH_SMALL + }; + if (bs->item_commit_detour) { + BotItems_FinishDetourCommit(bs, BOT_ITEMS_DBG_RESET, qfalse); + } + if (bs->item_commit_opportune) { + BotItems_FinishOpportuneCommit(bs, BOT_ITEMS_DBG_RESET, qfalse); + } + if (bs->item_commit_timing) { + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); + } + BotItems_ClearSuspended(bs); + if (!bs->item_commit_active) { + bs->item_next_scan_time = 0.0f; + if (BotItems_TryAcquireHealthKinds(bs, urgentKinds, + sizeof(urgentKinds) / sizeof(urgentKinds[0]))) { + return; + } + } + } + + if (bs->item_commit_timing && bs->item_commit_active && + !bs->item_commit_detour && !bs->item_commit_opportune && + !bs->item_commit_suspended) { + if (BotItems_TickOpportuneScan(bs)) { + return; + } + } + + if (bs->item_commit_active && + !bs->item_commit_detour && !bs->item_commit_opportune && + !bs->item_commit_suspended) { + if (BotItems_TickWeaponDetourScan(bs)) { + return; + } + } + + if (bs->item_commit_active) { + + BotItems_TickNearGoalCommitExtend(bs); + + if (FloatTime() >= bs->item_commit_until) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_TIMEOUT); + + return; + + } + + if (BotItems_CommitAchieved(bs)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); + + return; + + } + + if (!BotItems_RefreshGoalByNumber(bs, &bs->item_commit_goal, bs->item_commit_kind)) { + + if (!bs->item_commit_timing) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + + return; + + } + + } + + if (!bs->item_commit_timing && + !BotItems_GoalIsPresent(bs, &bs->item_commit_goal)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + + return; + + } + + if (bs->item_commit_timing && BotItems_TimingHoldingNearGoal(bs) && + BotItemTiming_ShouldWaitAtPad(bs)) { + + if (FloatTime() + 4.0f > bs->item_commit_until) { + + bs->item_commit_until = FloatTime() + 8.0f; + + } + + bs->nbg_time = bs->item_commit_until; + + } + + BotItems_CheckStuck(bs); + + BotItems_EnsureGoalOnStack(bs); + + return; + + } + + + + if (!bs->item_commit_active) { + if (!BotItems_TryAcquireReachableWeapon(bs)) { + BotItems_TryAcquireVisible(bs); + } + } + +} + + + +int BotItems_HasActiveCommit(const bot_state_t *bs) { + + if (!bs || !BotItems_IsActive()) { + + return 0; + + } + + return bs->item_commit_active && FloatTime() < bs->item_commit_until; + +} + + + +int BotItems_ShouldRunPickupNode(bot_state_t *bs) { + + return BotItems_HasActiveCommit(bs); + +} + + + +float BotItems_CommitNbgTime(bot_state_t *bs) { + + if (!bs || !BotItems_HasActiveCommit(bs)) { + + return FloatTime(); + + } + + return bs->item_commit_until; + +} + + + +int BotItems_IsCommittedGoal(const bot_state_t *bs, const bot_goal_t *goal) { + + if (!bs || !goal || !bs->item_commit_active) { + + return 0; + + } + + return goal->number == bs->item_commit_goal.number; + +} + + + +int BotItems_TimingHoldingNearGoal(bot_state_t *bs) { + + vec3_t delta; + + float dist; + + + + if (!bs || !bs->item_commit_timing || !bs->item_commit_active) { + + return 0; + + } + + VectorSubtract(bs->item_commit_goal.origin, bs->origin, delta); + + dist = VectorLength(delta); + + return dist <= 384.0f; + +} + + + +int BotItems_TimingHoldsGoalReached(bot_state_t *bs, bot_goal_t *goal) { + + if (!bs || !goal || !bs->item_commit_timing || !bs->item_commit_active) { + return 0; + } + + if (!BotItems_IsCommittedGoal(bs, goal)) { + return 0; + } + + if (BotItems_PickupEntityActive(bs->item_commit_goal.entitynum)) { + return 0; + } + + if (!BotItems_TimingHoldingNearGoal(bs)) { + return 0; + } + + return BotItemTiming_ShouldWaitAtPad(bs); +} + + + +int BotItems_HandleReachedGoal(bot_state_t *bs, bot_goal_t *goal) { + + if (!BotItems_IsActive() || !goal || !(goal->flags & GFL_ITEM)) { + + return -1; + + } + + if (!BotItems_IsCommittedGoal(bs, goal)) { + + return -1; + + } + + + + if (trap_BotTouchingGoal(bs->origin, goal)) { + + if (bs->item_commit_timing || bs->item_commit_detour || + bs->item_commit_opportune) { + + if (!BotItems_CommitInventoryImproved(bs)) { + if (bs->item_commit_timing) { + if (BotItemTiming_ShouldWaitAtPad(bs)) { + return 0; + } + trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + return 1; + } + return 0; + } + + } + + trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); + + return 1; + + } + + + + if (BotItems_CommitAchieved(bs)) { + + trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); + + return 1; + + } + + + + if (bs->item_commit_timing) { + + return 0; + + } + + + + if (!BotItems_PickupEntityActive(goal->entitynum) || + + BotItems_ItemGoalInVisButNotVisible(bs, goal)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + + return 1; + + } + + + + return 0; + +} + + + +int BotItems_ShouldPreserveGoalStack(bot_state_t *bs) { + + if (!BotItems_HasActiveCommit(bs)) { + + return 0; + + } + + return 1; + +} + + + +void BotItems_AbortCommit(bot_state_t *bs) { + + if (!BotItems_HasActiveCommit(bs)) { + + return; + + } + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); + +} + + + +void BotItems_AbortTimingCommitQuiet(bot_state_t *bs) { + + bot_goal_t top; + + + + if (!bs || !bs->item_commit_active || !bs->item_commit_timing || + bs->item_commit_detour || bs->item_commit_opportune) { + + return; + + } + + + + if (trap_BotGetTopGoal(bs->gs, &top) && + + top.number == bs->item_commit_goal.number) { + + trap_BotPopGoal(bs->gs); + + } + + + + bs->item_commit_active = qfalse; + + bs->item_commit_timing = qfalse; + + bs->item_commit_kind = BOT_ITEM_NONE; + + bs->item_commit_until = 0.0f; + + memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); + + BotItems_ResetLedgeJump(bs); + +} + + + +void BotItems_OnMoveFailure(bot_state_t *bs, bot_moveresult_t *mr) { + + if (!BotItems_HasActiveCommit(bs)) { + + return; + + } + + /* + * Don't abandon the commit while the bot is airborne (e.g. immediately after + * a jump pad triggers). The movement failure is expected because TFL_JUMPPAD + * has just been stripped and botlib can't re-route in mid-air. The stuck check + * will handle genuine inability to progress once the bot lands. + */ + if (bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + + return; + + } + + if (BotItems_LedgeJumpRetainCommit(bs)) { + + return; + + } + + if (BotMove_ShouldDeferCommitMoveFailure(bs, mr)) { + + return; + + } + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_STUCK); + +} + + + +float BotItems_GetCommitGoalOriginZ(const bot_state_t *bs) { + + if (!bs || !bs->item_commit_active) { + + return -99999.0f; + + } + + return bs->item_commit_goal.origin[2]; + +} + + + +#define BOT_ITEMS_CHOOSER_MAX_TRIES 24 + +#define BOT_ITEMS_USELESS_GOAL_AVOID 15.0f + + + +/* Returns a 'reasonable' max ammo capacity for a weapon +e.g. no point ever picking up 200 railgun slugs. */ + +static int BotItems_AmmoPracticalMax(int weapon) { + switch (weapon) { + case WP_MACHINEGUN: + return 200; + case WP_SHOTGUN: + return 50; + case WP_GRENADE_LAUNCHER: + return 20; + case WP_ROCKET_LAUNCHER: + return 50; + case WP_LIGHTNING: + return 200; + case WP_RAILGUN: + return 50; + case WP_PLASMAGUN: + return 200; + case WP_BFG: + return 200; + + #ifdef MISSIONPACK + case WP_NAILGUN: + return 100; + case WP_PROX_LAUNCHER: + return 50; + case WP_CHAINGUN: + return 200; + #endif + + default: + return AMMO_CAPACITY; + } +} + + + +static qboolean BotItems_PlayerCanUsePickup(bot_state_t *bs, gentity_t *ent) { + + gitem_t *item; + + int weapon, qty; + + if (!bs || !ent || !ent->inuse || !ent->item) { + return qfalse; + } + + if (!BG_CanItemBeGrabbed(gametype, &ent->s, &bs->cur_ps)) { + return qfalse; + } + + item = ent->item; + switch (item->giType) { + case IT_WEAPON: + weapon = item->giTag; + if (bs->cur_ps.stats[STAT_WEAPONS] & (1 << weapon)) { + qty = ent->count > 0 ? ent->count : item->quantity; + //Don't pick up the gun again unless you've got less than half the ammo + //it comes with + if (bs->cur_ps.ammo[weapon] >= qty*0.5) { + return qfalse; + } + } + return qtrue; + + case IT_AMMO: + weapon = item->giTag; + if (!(bs->cur_ps.stats[STAT_WEAPONS] & (1 << weapon))) { + return qfalse; + } + if (bs->cur_ps.ammo[weapon] >= BotItems_AmmoPracticalMax(weapon)) { + return qfalse; + } + return qtrue; + case IT_TEAM: + if (!BotItems_FlagCaptureGametype()) { + return qtrue; + } + { + int team = BotItems_ClientTeam(bs); + if (team == TEAM_FREE) { + return qfalse; + } + if (item->giTag == PW_REDFLAG) { + if (team == TEAM_RED) { + return qfalse; + } + return qtrue; + } + if (item->giTag == PW_BLUEFLAG) { + if (team == TEAM_BLUE) { + return qfalse; + } + return qtrue; + } + } + return qfalse; + default: + return qtrue; + } +} + + + +static qboolean BotItems_GoalWorthPursuing(bot_state_t *bs, const bot_goal_t *goal) { + + if (!bs || !goal) { + + return qtrue; + + } + + if (!(goal->flags & GFL_ITEM)) { + + return qtrue; + + } + + if (!BotItems_PickupEntityActive(goal->entitynum)) { + + return qtrue; + + } + + return BotItems_PlayerCanUsePickup(bs, &g_entities[goal->entitynum]); + +} + + + +static qboolean BotItems_RejectTopGoal(bot_state_t *bs) { + + bot_goal_t goal; + + + + if (!trap_BotGetTopGoal(bs->gs, &goal)) { + + return qfalse; + + } + + if (BotItems_GoalWorthPursuing(bs, &goal)) { + + return qfalse; + + } + + trap_BotSetAvoidGoalTime(bs->gs, goal.number, BOT_ITEMS_USELESS_GOAL_AVOID); + + trap_BotPopGoal(bs->gs); + + return qtrue; + +} + + + +int BotItems_ChooseNBGItem(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { + + int tries; + + + + if (!bs) { + + return qfalse; + + } + + if (!BotEnhanced_IsActive()) { + + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + + if (trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range)) { + BotEnhanced_SanitizeGoalStack(bs); + return qtrue; + } + return qfalse; + + } + + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + + for (tries = 0; tries < BOT_ITEMS_CHOOSER_MAX_TRIES; tries++) { + + if (!trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range)) { + + return qfalse; + + } + + BotEnhanced_SanitizeGoalStack(bs); + + if (!BotItems_RejectTopGoal(bs)) { + + return qtrue; + + } + + } + + return qfalse; + +} + + + +int BotItems_NearbyGoal(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { + + bot_goal_t goal; + + + + if (!bs) { + + return qfalse; + + } + + + + if (bs->lastair_time < FloatTime() - 6) { + + if (BotGetAirGoal(bs, &goal)) { + + BotEnhanced_PushGoalSafe(bs, &goal); + + return qtrue; + + } + + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + + while (BotItems_ChooseNBGItem(bs, tfl, ltg, range)) { + + BotEnhanced_SanitizeGoalStack(bs); + + trap_BotGetTopGoal(bs->gs, &goal); + + if (!(trap_AAS_PointContents(goal.origin) & + + (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA))) { + + return qtrue; + + } + + trap_BotPopGoal(bs->gs); + + } + + trap_BotResetAvoidGoals(bs->gs); + + } + + + + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + + return BotItems_ChooseNBGItem(bs, BotMove_EffectiveTfl(bs), ltg, range); + +} + + + +int BotItems_ChooseLTGItem(bot_state_t *bs, int tfl) { + + int tries; + + + + if (!bs) { + + return qfalse; + + } + + if (!BotEnhanced_IsActive()) { + + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + + if (trap_BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl)) { + BotEnhanced_SanitizeGoalStack(bs); + return qtrue; + } + return qfalse; + + } + + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + + for (tries = 0; tries < BOT_ITEMS_CHOOSER_MAX_TRIES; tries++) { + + if (!trap_BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl)) { + + return qfalse; + + } + + BotEnhanced_SanitizeGoalStack(bs); + + if (!BotItems_RejectTopGoal(bs)) { + + return qtrue; + + } + + } + + return qfalse; + +} + + +void BotItems_OnPostMoveToGoal(bot_state_t *bs, bot_moveresult_t *mr) { + + float now; + + if (!bs || !mr || !bs->item_commit_active) { + return; + } + + if (!BotItems_LedgeJumpEligible(bs, &bs->item_commit_goal)) { + bs->item_lj_lip_since = 0.0f; + return; + } + + if (bs->item_lj_attempts >= BOT_ITEMS_LJ_MAX_ATTEMPTS) { + return; + } + + now = FloatTime(); + + if (bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + bs->item_lj_lip_since = 0.0f; + return; + } + + if (!BotItems_LedgeJumpAtLip(bs, mr)) { + bs->item_lj_lip_since = 0.0f; + return; + } + + if (bs->item_lj_lip_since <= 0.0f) { + bs->item_lj_lip_since = now; + return; + } + + if (now - bs->item_lj_lip_since < BOT_ITEMS_LJ_LIP_TIME) { + return; + } + + bs->item_lj_attempts++; + bs->item_lj_lip_since = 0.0f; + bs->item_lj_jump_until = now + BOT_ITEMS_LJ_JUMP_LATCH; + bs->item_commit_progress_time = now; + VectorCopy(bs->origin, bs->item_commit_progress_origin); +} + + + +void BotItems_OnInputFrame(bot_state_t *bs, bot_input_t *bi) { + + vec3_t hordir; + + if (!bs || !bi || !bs->item_commit_active) { + return; + } + + if (FloatTime() >= bs->item_lj_jump_until) { + return; + } + + if (!BotItems_LedgeJumpEligible(bs, &bs->item_commit_goal)) { + return; + } + + hordir[0] = bs->item_commit_goal.origin[0] - bs->origin[0]; + hordir[1] = bs->item_commit_goal.origin[1] - bs->origin[1]; + hordir[2] = 0.0f; + if (VectorNormalize(hordir) < 0.1f) { + return; + } + + bi->actionflags |= ACTION_JUMP; + VectorCopy(hordir, bi->dir); + bi->speed = 600.0f; +} + + + +int BotItems_SuppressBlockedAvoid(bot_state_t *bs) { + + if (!bs || !bs->item_commit_active) { + return 0; + } + + if (FloatTime() < bs->item_lj_jump_until) { + return 1; + } + + return BotItems_LedgeJumpRetainCommit(bs) ? 1 : 0; +} + +/* ========================================================================= + * ITEM TIMING (merged from ai_bot_item_timing.c) + * ========================================================================= */ + +#define TIMING_KIND_NONE 0 +#define TIMING_KIND_QUAD 1 +#define TIMING_KIND_MEGA_HEALTH 2 +#define TIMING_KIND_RED_ARMOR 3 +#define TIMING_KIND_YELLOW_ARMOR 4 + +#define TIMING_TYPE_COUNT 4 +#define TIMING_MAP_POOL_MAX 32 + +#define TIMING_ORIGIN_MATCH_DIST 96.0f +#define TIMING_PICKUP_MATCH_DIST 192.0f +#define TIMING_OPPONENT_HEARD_BIND_DIST 4096.0f +#define TIMING_PICKUP_LATCH_SEC 3.0f +#define TIMING_SPAWN_CHECK_INTERVAL 1.0f +#define TIMING_SPAWN_SIGHT_DIST 2048.0f +#define TIMING_PLAN_INTERVAL 5.0f +#define TIMING_TRAVEL_MARGIN_SEC 1.5f +#define TIMING_IMMINENT_WAIT_SEC 5.0f +#define TIMING_FRESH_SPAWN_SEC 12.0f +#define TIMING_COMMIT_PAD_SEC 12.0f +#define TIMING_SPAWN_MISS_SEC 5.0f +#define TIMING_FAR_PURSUE_ABORT_SEC 10.0f +#define TIMING_WAIT_ROAM_RADIUS 96.0f +#define TIMING_WAIT_NEAR_DIST 384.0f +#define TIMING_DETOUR_COMMIT_SEC 5.0f +#define TIMING_DETOUR_SCAN_INTERVAL 0.5f +#define TIMING_DETOUR_MAX_TRAVEL 300 /* AAS units (~3 sec) */ +#define TIMING_DETOUR_COOLDOWN_SEC 12.0f +#define TIMING_DETOUR_TRACK_COOLDOWN_SEC 45.0f +#define TIMING_PREEMPT_SCAN_INTERVAL 1.0f +#define TIMING_PREEMPT_COOLDOWN_SEC 12.0f +#define TIMING_PREEMPT_TRACK_BLOCK_SEC 20.0f +#define TIMING_PREEMPT_SCORE_MARGIN 100 +#define TIMING_PREEMPT_COMMIT_TRAVEL 250 /* AAS units (~2.5s) */ +#define TIMING_DETOUR_PRIORITY_SLACK 80 /* AAS units for higher-priority detour */ +#define TIMING_DEATH_ITEM_RADIUS 512.0f + +#define TIMING_PURPOSE_COOLDOWN 0 +#define TIMING_PURPOSE_SPAWNED 1 + +typedef struct { + const char *goalname; + const char *classname; + const char *label; + int priority; + int kind; +} timing_type_def_t; + +typedef struct { + vec3_t origin; + int areanum; + int goal_number; + int kind; + int priority; + float respawn_interval; + qboolean spawned_now; + float cooldown_sec; +} timing_map_instance_t; + +static const timing_type_def_t timing_type_defs[TIMING_TYPE_COUNT] = { + { "Quad Damage", "item_quad", "Quad Damage", 40, TIMING_KIND_QUAD }, + { "Mega Health", "item_health_mega", "Mega Health", 30, TIMING_KIND_MEGA_HEALTH }, + { "Heavy Armor", "item_armor_body", "Red Armor", 20, TIMING_KIND_RED_ARMOR }, + { "Armor", "item_armor_combat", "Yellow Armor", 10, TIMING_KIND_YELLOW_ARMOR } +}; + +static timing_map_instance_t timing_map_pool[TIMING_MAP_POOL_MAX]; +static int timing_map_pool_count; +static int timing_map_level_time = -1; +static int timing_denied_playerevents[MAX_CLIENTS]; + +#define TIMING_DENIED_MATCH_DIST 256.0f + +static int Timing_KindToBotItem(int kind); +static qboolean Timing_BuildGoalFromTrack(bot_state_t *bs, timing_belief_t *track, + bot_goal_t *goal); +static qboolean Timing_WantsItemKind(bot_state_t *bs, int kind); +static void Timing_StartPursuit(bot_state_t *bs, int trackIndex, + timing_belief_t *track, int travelTime); +static void Timing_PlanClosestSpawned(bot_state_t *bs); +static qboolean Timing_ShouldDeferSpawnedGrab(bot_state_t *bs, int kind); +static int Timing_PriorityForKind(int kind); +static int Timing_PursuitScore(timing_belief_t *track, int travelTime); +static int Timing_PursuitScoreWithGoal(bot_state_t *bs, timing_belief_t *track, + int travelTime, bot_goal_t *goal); +static int Timing_PickBestPursuitTrack(bot_state_t *bs, int excludeIndex, + int requireDepartable, int *bestTravelOut); +static void Timing_PlanNextTrack(bot_state_t *bs, int excludeIndex); +static void Timing_StartCooldown(bot_state_t *bs, timing_belief_t *track, + float cooldown_sec, const char *reason); +static int Timing_TrackIndex(const bot_state_t *bs, const timing_belief_t *track); +static qboolean Timing_TickPreemptScan(bot_state_t *bs); +static qboolean Timing_TrackReachable(bot_state_t *bs, const timing_belief_t *track); +static qboolean Timing_PoolInstanceReachable(bot_state_t *bs, + const timing_map_instance_t *inst); +static qboolean Timing_PoolInstanceTracked(bot_state_t *bs, + const timing_map_instance_t *inst); +static void Timing_AssignTrackFromPool(bot_state_t *bs, int slot, + const timing_map_instance_t *inst, const char *debugEvent); +static qboolean Timing_PickupLatched(timing_belief_t *track); +static void Timing_TouchPickupLatch(timing_belief_t *track); +static const timing_map_instance_t *Timing_FindPoolInstanceOfKind(int kind); + +static void Timing_TickFarPursueStall(bot_state_t *bs, int trackIndex, + timing_belief_t *track); +static float Timing_GetSpawnDueAt(bot_state_t *bs, timing_belief_t *track); +static void Timing_HandleMissedSpawn(bot_state_t *bs, int trackIndex, + timing_belief_t *track); +static void Timing_ResetTimer(bot_state_t *bs, timing_belief_t *track, + const char *reason); +static void Timing_MarkSpawned(bot_state_t *bs, timing_belief_t *track, + const char *reason); + +#define TIMING_SCORE_PRIORITY_SCALE 1000 +#define TIMING_SCORE_URGENCY_SCALE 10 +#define TIMING_SCORE_TRAVEL_DIV 10 +#define TIMING_SCORE_SPAWNED_BONUS 50 + +static const timing_type_def_t *Timing_DefForKind(int kind) { + int i; + + for (i = 0; i < TIMING_TYPE_COUNT; i++) { + if (timing_type_defs[i].kind == kind) { + return &timing_type_defs[i]; + } + } + return NULL; +} + +static const char *Timing_LabelForKind(int kind) { + const timing_type_def_t *def; + + def = Timing_DefForKind(kind); + if (def) { + return def->label; + } + return "item"; +} + +static int Timing_PriorityForKind(int kind) { + const timing_type_def_t *def; + + def = Timing_DefForKind(kind); + return def ? def->priority : 0; +} + +static float Timing_SecondsUntilRelevant(const timing_belief_t *track) { + float seconds; + + if (!track) { + return 9999.0f; + } + if (track->state == BOT_TIMING_STATE_SPAWNED) { + return 0.0f; + } + if (track->state == BOT_TIMING_STATE_COOLDOWN) { + seconds = track->believed_spawn_at - FloatTime(); + return seconds > 0.0f ? seconds : 0.0f; + } + return 9999.0f; +} + +static int Timing_PursuitScore(timing_belief_t *track, int travelTime) { + int score; + float secondsUntil; + + if (!track) { + return -999999; + } + + score = Timing_PriorityForKind(track->kind) * TIMING_SCORE_PRIORITY_SCALE; + secondsUntil = Timing_SecondsUntilRelevant(track); + score -= (int)(secondsUntil * (float)TIMING_SCORE_URGENCY_SCALE); + if (travelTime > 0) { + score -= travelTime / TIMING_SCORE_TRAVEL_DIV; + } + if (track->state == BOT_TIMING_STATE_SPAWNED) { + score += TIMING_SCORE_SPAWNED_BONUS; + } + return score; +} + +static int Timing_PursuitScoreWithGoal(bot_state_t *bs, timing_belief_t *track, + int travelTime, bot_goal_t *goal) { + int score; + + score = Timing_PursuitScore(track, travelTime); + if (bs && goal) { + score += BotPosition_RouteElevationBonus(bs, goal, travelTime); + } + return score; +} + +static int BotItemTiming_DebugEnabled(void) { + return BotEnhanced_DebugActive(); +} + +static int BotItemTiming_GametypeAllowed(void) { + int gt; + + gt = g_gametype.integer; + return gt == GT_FFA || gt == GT_TOURNAMENT || gt == GT_TEAM; +} + +void BotItemTiming_RegisterCvars(void) { +} + +int BotItemTiming_IsActive(void) { + if (!BotEnhanced_IsActive()) { + return 0; + } + return BotItemTiming_GametypeAllowed(); +} + +static float Timing_DefaultRespawn(int kind) { + switch (kind) { + case TIMING_KIND_QUAD: + return 120.0f; + case TIMING_KIND_MEGA_HEALTH: + return 35.0f; + case TIMING_KIND_RED_ARMOR: + case TIMING_KIND_YELLOW_ARMOR: + return 25.0f; + default: + return 25.0f; + } +} + +static float Timing_Dist(const vec3_t a, const vec3_t b) { + vec3_t delta; + + VectorSubtract(a, b, delta); + return VectorLength(delta); +} + +static gentity_t *Timing_FindSpawnEntity(const char *classname, const vec3_t origin) { + int i; + gentity_t *ent; + float dist; + + if (!classname || !origin) { + return NULL; + } + for (i = MAX_CLIENTS; i < level.num_entities; i++) { + ent = &g_entities[i]; + if (!ent->inuse || !ent->item) { + continue; + } + if (Q_stricmp(ent->classname, classname)) { + continue; + } + dist = Timing_Dist(ent->s.origin, origin); + if (dist <= TIMING_ORIGIN_MATCH_DIST) { + return ent; + } + } + return NULL; +} + +static float Timing_RespawnIntervalFromEntity(gentity_t *ent, int kind) { + if (ent && ent->wait > 0.0f) { + return ent->wait; + } + return Timing_DefaultRespawn(kind); +} + +static void Timing_ScanSpawnState(const timing_type_def_t *def, const vec3_t origin, + qboolean *spawned_out, float *cooldown_out, float *interval_out) { + gentity_t *ent; + float cooldown; + + ent = Timing_FindSpawnEntity(def->classname, origin); + if (interval_out) { + *interval_out = Timing_RespawnIntervalFromEntity(ent, def->kind); + } + if (spawned_out) { + *spawned_out = qfalse; + } + if (cooldown_out) { + *cooldown_out = 0.0f; + } + if (!ent) { + return; + } + if (!(ent->s.eFlags & EF_NODRAW) && ent->r.contents != 0) { + if (spawned_out) { + *spawned_out = qtrue; + } + return; + } + if (ent->nextthink > level.time) { + cooldown = (ent->nextthink - level.time) / 1000.0f; + if (cooldown < 0.0f) { + cooldown = 0.0f; + } + if (cooldown_out) { + *cooldown_out = cooldown; + } + } +} + +static qboolean Timing_PoolHasOrigin(const vec3_t origin) { + int i; + + for (i = 0; i < timing_map_pool_count; i++) { + if (Timing_Dist(timing_map_pool[i].origin, origin) <= TIMING_ORIGIN_MATCH_DIST) { + return qtrue; + } + } + return qfalse; +} + +static void Timing_SortMapPool(void) { + int i; + int j; + timing_map_instance_t tmp; + + for (i = 0; i < timing_map_pool_count - 1; i++) { + for (j = i + 1; j < timing_map_pool_count; j++) { + if (timing_map_pool[j].priority > timing_map_pool[i].priority) { + tmp = timing_map_pool[i]; + timing_map_pool[i] = timing_map_pool[j]; + timing_map_pool[j] = tmp; + } + } + } +} + +static void Timing_BuildMapPool(void) { + int i; + int goalIndex; + const timing_type_def_t *def; + timing_map_instance_t *inst; + bot_goal_t goal; + + if (timing_map_level_time == level.time) { + return; + } + timing_map_level_time = level.time; + timing_map_pool_count = 0; + + for (i = 0; i < TIMING_TYPE_COUNT; i++) { + def = &timing_type_defs[i]; + if (G_ItemDisabled(BG_FindItem(def->goalname))) { + continue; + } + + goalIndex = -1; + while (timing_map_pool_count < TIMING_MAP_POOL_MAX) { + goalIndex = untrap_BotGetLevelItemGoal(goalIndex, (char *)def->goalname, &goal); + if (goalIndex < 0) { + break; + } + if (Timing_PoolHasOrigin(goal.origin)) { + continue; + } + + inst = &timing_map_pool[timing_map_pool_count]; + VectorCopy(goal.origin, inst->origin); + inst->areanum = goal.areanum; + inst->goal_number = goal.number; + inst->kind = def->kind; + inst->priority = def->priority; + Timing_ScanSpawnState(def, inst->origin, &inst->spawned_now, + &inst->cooldown_sec, &inst->respawn_interval); + timing_map_pool_count++; + } + } + + Timing_SortMapPool(); +} + +static void BotItemTiming_DebugLine(bot_state_t *bs, const char *event, + const char *itemLabel, int seconds) { + char botName[64]; + + if (!BotItemTiming_DebugEnabled() || !bs) { + return; + } + ClientName(bs->client, botName, sizeof(botName)); + if (seconds > 0) { + G_Printf("BotItemTiming: %s %s %s in %i sec\n", botName, event, itemLabel, seconds); + } else { + G_Printf("BotItemTiming: %s %s %s\n", botName, event, itemLabel); + } +} + +static int Timing_KindFromItemIndex(int itemIndex) { + gitem_t *item; + + if (itemIndex < 0 || itemIndex >= bg_numItems) { + return TIMING_KIND_NONE; + } + item = &bg_itemlist[itemIndex]; + if (!Q_stricmp(item->classname, "item_quad")) { + return TIMING_KIND_QUAD; + } + if (!Q_stricmp(item->classname, "item_health_mega")) { + return TIMING_KIND_MEGA_HEALTH; + } + if (!Q_stricmp(item->classname, "item_armor_body")) { + return TIMING_KIND_RED_ARMOR; + } + if (!Q_stricmp(item->classname, "item_armor_combat")) { + return TIMING_KIND_YELLOW_ARMOR; + } + return TIMING_KIND_NONE; +} + +static int Timing_ItemIndexFromKind(int kind) { + gitem_t *item; + + item = NULL; + switch (kind) { + case TIMING_KIND_QUAD: + item = BG_FindItem("item_quad"); + break; + case TIMING_KIND_MEGA_HEALTH: + item = BG_FindItem("item_health_mega"); + break; + case TIMING_KIND_RED_ARMOR: + item = BG_FindItem("item_armor_body"); + break; + case TIMING_KIND_YELLOW_ARMOR: + item = BG_FindItem("item_armor_combat"); + break; + default: + return -1; + } + if (!item) { + return -1; + } + return ITEM_INDEX(item); +} + +static void Timing_NotifyOpponentInferredPickup(bot_state_t *bs, + timing_belief_t *track, const char *reason) { + int itemIndex; + + if (!bs || !track || !BotOpponent_IsActive()) { + return; + } + if (Timing_PickupLatched(track)) { + return; + } + itemIndex = Timing_ItemIndexFromKind(track->kind); + if (itemIndex < 0) { + return; + } + Timing_TouchPickupLatch(track); + BotOpponent_OnInferredItemPickup(bs, itemIndex, track->origin, reason); +} + +static qboolean Timing_TrackActive(const timing_belief_t *track) { + return track && track->state != BOT_TIMING_STATE_NONE; +} + +static timing_belief_t *Timing_FindTrackAtOrigin(bot_state_t *bs, const vec3_t origin) { + int i; + timing_belief_t *track; + + if (!bs || !origin) { + return NULL; + } + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track)) { + continue; + } + if (Timing_Dist(track->origin, origin) <= TIMING_ORIGIN_MATCH_DIST) { + return track; + } + } + return NULL; +} + +static qboolean Timing_TrackReachable(bot_state_t *bs, const timing_belief_t *track) { + bot_goal_t goal; + + if (!bs || !track || !Timing_TrackActive(track)) { + return qfalse; + } + if (!Timing_BuildGoalFromTrack(bs, (timing_belief_t *)track, &goal)) { + return qfalse; + } + return BotItems_GoalReachable(bs, &goal); +} + +static qboolean Timing_PoolInstanceReachable(bot_state_t *bs, + const timing_map_instance_t *inst) { + bot_goal_t goal; + + if (!bs || !inst) { + return qfalse; + } + memset(&goal, 0, sizeof(goal)); + VectorCopy(inst->origin, goal.origin); + goal.areanum = inst->areanum; + return BotItems_GoalReachable(bs, &goal); +} + +static qboolean Timing_PoolInstanceTracked(bot_state_t *bs, + const timing_map_instance_t *inst) { + if (!bs || !inst) { + return qfalse; + } + return Timing_FindTrackAtOrigin(bs, inst->origin) != NULL; +} + +static void Timing_AssignTrackFromPool(bot_state_t *bs, int slot, + const timing_map_instance_t *inst, const char *debugEvent) { + timing_belief_t *track; + const timing_type_def_t *def; + int seconds; + + if (!bs || !inst || slot < 0 || slot >= BOT_TIMING_TRACK_COUNT) { + return; + } + + track = &bs->timing_track[slot]; + track->kind = inst->kind; + VectorCopy(inst->origin, track->origin); + track->areanum = inst->areanum; + track->goal_number = inst->goal_number; + track->respawn_interval = inst->respawn_interval; + track->pickup_latch_time = 0.0f; + track->next_spawn_check = 0.0f; + track->detour_block_until = 0.0f; + track->preempt_block_until = 0.0f; + + if (inst->spawned_now) { + track->state = BOT_TIMING_STATE_SPAWNED; + track->believed_spawn_at = 0.0f; + seconds = 0; + } else if (inst->cooldown_sec > 0.0f) { + track->state = BOT_TIMING_STATE_COOLDOWN; + track->believed_spawn_at = FloatTime() + inst->cooldown_sec; + seconds = (int)inst->cooldown_sec; + } else { + track->state = BOT_TIMING_STATE_SPAWNED; + track->believed_spawn_at = 0.0f; + seconds = 0; + } + + def = Timing_DefForKind(inst->kind); + BotItemTiming_DebugLine(bs, debugEvent ? debugEvent : "tracking", + def ? def->label : "item", seconds); +} + +static timing_belief_t *Timing_FindTrackForPickup(bot_state_t *bs, int kind, + const vec3_t refOrigin) { + int i; + timing_belief_t *track; + timing_belief_t *best; + float dist; + float bestDist; + + if (!bs || kind == TIMING_KIND_NONE) { + return NULL; + } + + track = Timing_FindTrackAtOrigin(bs, refOrigin); + if (track && track->kind == kind) { + return track; + } + + if (!refOrigin) { + return NULL; + } + + best = NULL; + bestDist = TIMING_PICKUP_MATCH_DIST; + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track) || track->kind != kind) { + continue; + } + dist = Timing_Dist(track->origin, refOrigin); + if (dist < bestDist) { + bestDist = dist; + best = track; + } + } + return best; +} + +static const timing_map_instance_t *Timing_FindPoolInstanceNear(int kind, + const vec3_t refOrigin, float maxDist) { + int i; + const timing_map_instance_t *inst; + const timing_map_instance_t *best; + float dist; + float bestDist; + + if (!refOrigin || kind == TIMING_KIND_NONE || maxDist <= 0.0f) { + return NULL; + } + + best = NULL; + bestDist = maxDist; + for (i = 0; i < timing_map_pool_count; i++) { + inst = &timing_map_pool[i]; + if (inst->kind != kind) { + continue; + } + dist = Timing_Dist(inst->origin, refOrigin); + if (dist < bestDist) { + bestDist = dist; + best = inst; + } + } + return best; +} + +static int Timing_AllocTrackSlot(bot_state_t *bs) { + int i; + int lowSlot; + int lowPriority; + + if (!bs) { + return -1; + } + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + if (!Timing_TrackActive(&bs->timing_track[i])) { + return i; + } + } + + lowSlot = 0; + lowPriority = Timing_PriorityForKind(bs->timing_track[0].kind); + for (i = 1; i < BOT_TIMING_TRACK_COUNT; i++) { + if (Timing_PriorityForKind(bs->timing_track[i].kind) < lowPriority) { + lowPriority = Timing_PriorityForKind(bs->timing_track[i].kind); + lowSlot = i; + } + } + return lowSlot; +} + +static timing_belief_t *Timing_FindTrackForOpponentHeardPickup(bot_state_t *bs, + int kind, const vec3_t hintOrigin) { + int i; + timing_belief_t *track; + timing_belief_t *best; + timing_belief_t *anyKind; + float dist; + float bestDist; + int spawnedCount; + int spawnedOnly; + const timing_map_instance_t *inst; + + if (!bs || kind == TIMING_KIND_NONE) { + return NULL; + } + + spawnedCount = 0; + spawnedOnly = -1; + anyKind = NULL; + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track) || track->kind != kind) { + continue; + } + if (!anyKind) { + anyKind = track; + } + if (track->state == BOT_TIMING_STATE_SPAWNED) { + spawnedCount++; + spawnedOnly = i; + } + } + if (spawnedCount == 1 && spawnedOnly >= 0) { + return &bs->timing_track[spawnedOnly]; + } + if (anyKind && !hintOrigin) { + return anyKind; + } + + best = NULL; + bestDist = TIMING_OPPONENT_HEARD_BIND_DIST; + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track) || track->kind != kind) { + continue; + } + if (!hintOrigin) { + return track; + } + dist = Timing_Dist(track->origin, hintOrigin); + if (dist < bestDist) { + bestDist = dist; + best = track; + } + } + if (best) { + return best; + } + if (anyKind) { + return anyKind; + } + + Timing_BuildMapPool(); + inst = Timing_FindPoolInstanceOfKind(kind); + if (inst) { + track = Timing_FindTrackAtOrigin(bs, inst->origin); + if (track && track->kind == kind) { + return track; + } + } + + return NULL; +} + +static const timing_map_instance_t *Timing_FindPoolInstanceOfKind(int kind) { + int i; + const timing_map_instance_t *inst; + const timing_map_instance_t *only; + + if (kind == TIMING_KIND_NONE) { + return NULL; + } + only = NULL; + for (i = 0; i < timing_map_pool_count; i++) { + inst = &timing_map_pool[i]; + if (inst->kind != kind) { + continue; + } + if (only) { + return NULL; + } + only = inst; + } + return only; +} + +static timing_belief_t *Timing_EnsureTrackForObservedPickup(bot_state_t *bs, + int kind, const vec3_t eventOrigin) { + timing_belief_t *track; + const timing_map_instance_t *inst; + int slot; + + if (!bs || kind == TIMING_KIND_NONE) { + return NULL; + } + + Timing_BuildMapPool(); + + track = Timing_FindTrackForPickup(bs, kind, eventOrigin); + if (track) { + return track; + } + + if (!eventOrigin) { + return NULL; + } + + inst = Timing_FindPoolInstanceNear(kind, eventOrigin, TIMING_PICKUP_MATCH_DIST); + if (!inst && eventOrigin) { + inst = Timing_FindPoolInstanceNear(kind, eventOrigin, + TIMING_OPPONENT_HEARD_BIND_DIST); + } + if (!inst) { + inst = Timing_FindPoolInstanceOfKind(kind); + } + if (!inst) { + return NULL; + } + + track = Timing_FindTrackAtOrigin(bs, inst->origin); + if (track && track->kind == kind) { + return track; + } + + slot = Timing_AllocTrackSlot(bs); + if (slot < 0) { + return NULL; + } + + Timing_AssignTrackFromPool(bs, slot, inst, "witness bind"); + return &bs->timing_track[slot]; +} + +static int Timing_IsSelfPicker(const bot_state_t *bs, int pickerClient) { + if (!bs) { + return 0; + } + return pickerClient == bs->client || pickerClient == bs->entitynum; +} + +static qboolean Timing_BotNearTrack(const bot_state_t *bs, + const timing_belief_t *track) { + if (!bs || !track) { + return qfalse; + } + return Timing_Dist(bs->origin, track->origin) <= TIMING_PICKUP_MATCH_DIST; +} + +static qboolean Timing_BotAtCampPosition(const bot_state_t *bs, + const timing_belief_t *track) { + if (!bs || !track) { + return qfalse; + } + return Timing_Dist(bs->origin, track->origin) <= TIMING_WAIT_NEAR_DIST; +} + +static qboolean Timing_TrackNearPoint(const timing_belief_t *track, + const vec3_t point) { + if (!track || !point) { + return qfalse; + } + return Timing_Dist(track->origin, point) <= TIMING_PICKUP_MATCH_DIST; +} + +static void Beliefs_ApplyOpponentMajorPickup(bot_state_t *bs, int pickerClient, + int itemIndex, const vec3_t padOrigin, const char *reason) { + qboolean selfPickup; + + if (!bs || itemIndex < 0 || itemIndex >= bg_numItems) { + return; + } + + selfPickup = pickerClient >= 0 && Timing_IsSelfPicker(bs, pickerClient); + if (selfPickup) { + BotOpponent_OnSelfMajorPickup(bs, itemIndex); + return; + } + if (BotOpponent_IsActive()) { + BotOpponent_OnInferredItemPickup(bs, itemIndex, padOrigin, reason); + } +} + +static void Timing_ApplyWitnessedPadTiming(bot_state_t *bs, timing_belief_t *track, + qboolean selfPickup, const vec3_t eventOrigin, const char *reason, + float cooldownSec) { + if (!bs || !track || !Timing_TrackActive(track)) { + return; + } + if (Timing_PickupLatched(track)) { + return; + } + if (selfPickup && + !Timing_BotNearTrack(bs, track) && + (!eventOrigin || !Timing_TrackNearPoint(track, eventOrigin))) { + return; + } + + if (cooldownSec > 0.0f) { + Timing_TouchPickupLatch(track); + Timing_StartCooldown(bs, track, cooldownSec, reason); + } else { + Timing_ResetTimer(bs, track, reason); + } +} + +static timing_belief_t *Timing_FindTrackForSelfPickup(bot_state_t *bs, int kind, + const vec3_t eventOrigin) { + int idx; + timing_belief_t *track; + + if (!bs || kind == TIMING_KIND_NONE) { + return NULL; + } + + if (bs->item_commit_detour && bs->timing_detour_track >= 0) { + idx = bs->timing_detour_track; + track = &bs->timing_track[idx]; + if (Timing_TrackActive(track) && track->kind == kind && + (Timing_BotNearTrack(bs, track) || + (eventOrigin && Timing_TrackNearPoint(track, eventOrigin)))) { + return track; + } + } + + if (bs->timing_pursue_track >= 0) { + idx = bs->timing_pursue_track; + track = &bs->timing_track[idx]; + if (Timing_TrackActive(track) && track->kind == kind && + (Timing_BotNearTrack(bs, track) || + (eventOrigin && Timing_TrackNearPoint(track, eventOrigin)))) { + return track; + } + } + + if (eventOrigin) { + track = Timing_FindTrackForPickup(bs, kind, eventOrigin); + if (track) { + return track; + } + } + + return Timing_FindTrackForPickup(bs, kind, bs->origin); +} + +static qboolean Timing_PickupLatched(timing_belief_t *track) { + if (!track) { + return qfalse; + } + return FloatTime() - track->pickup_latch_time < TIMING_PICKUP_LATCH_SEC; +} + +static void Timing_TouchPickupLatch(timing_belief_t *track) { + if (track) { + track->pickup_latch_time = FloatTime(); + } +} + +static qboolean Timing_BotCanSeeOrigin(bot_state_t *bs, const vec3_t origin) { + trace_t tr; + vec3_t end; + + if (!bs || !origin) { + return qfalse; + } + if (Timing_Dist(bs->eye, origin) > TIMING_SPAWN_SIGHT_DIST) { + return qfalse; + } + VectorCopy(origin, end); + trap_Trace(&tr, bs->eye, NULL, NULL, end, bs->entitynum, MASK_SHOT); + return tr.fraction >= 1.0f; +} + +static void Timing_SetSpawned(timing_belief_t *track) { + if (!track) { + return; + } + track->state = BOT_TIMING_STATE_SPAWNED; + track->believed_spawn_at = 0.0f; +} + +static void Timing_SetCooldown(timing_belief_t *track, float respawn_at) { + if (!track) { + return; + } + track->state = BOT_TIMING_STATE_COOLDOWN; + track->believed_spawn_at = respawn_at; +} + +static void Timing_AbandonPadAfterCollect(bot_state_t *bs, int trackIndex) { + if (!bs || trackIndex < 0 || trackIndex >= BOT_TIMING_TRACK_COUNT) { + return; + } + if (trackIndex != bs->timing_pursue_track) { + return; + } + bs->timing_spawn_due_at = 0.0f; + if (bs->item_commit_timing && bs->item_commit_active) { + BotItems_AbortCommit(bs); + return; + } + bs->timing_pursue_track = -1; + Timing_PlanNextTrack(bs, trackIndex); +} + +static qboolean Timing_ItemPresentAtTrack(bot_state_t *bs, timing_belief_t *track) { + bot_goal_t goal; + int botKind; + + if (!bs || !track || !Timing_BuildGoalFromTrack(bs, track, &goal)) { + return qfalse; + } + botKind = Timing_KindToBotItem(track->kind); + if (!botKind) { + return qfalse; + } + (void)BotItems_RefreshItemGoal(bs, &goal, botKind); + return BotItems_GoalPickupPresent(bs, &goal); +} + +static void Timing_TickAbandonEmptyPad(bot_state_t *bs, int trackIndex, + timing_belief_t *track) { + const timing_type_def_t *def; + float cooldown; + float remaining; + qboolean spawned; + + if (!bs || !track || trackIndex < 0) { + return; + } + if (!Timing_BotNearTrack(bs, track) || Timing_PickupLatched(track)) { + return; + } + if (Timing_ItemPresentAtTrack(bs, track)) { + return; + } + + if (track->state == BOT_TIMING_STATE_COOLDOWN) { + remaining = track->believed_spawn_at - FloatTime(); + /* Still waiting for an upcoming spawn — item absent is expected. */ + if (remaining > 0.0f && remaining <= TIMING_IMMINENT_WAIT_SEC) { + return; + } + /* Overdue past grace: stop camping an empty cooldown pad. */ + if (remaining <= -TIMING_SPAWN_MISS_SEC) { + Timing_AbandonPadAfterCollect(bs, trackIndex); + return; + } + } + + if (track->state == BOT_TIMING_STATE_SPAWNED) { + def = Timing_DefForKind(track->kind); + cooldown = 0.0f; + spawned = qfalse; + if (def) { + Timing_ScanSpawnState(def, track->origin, &spawned, &cooldown, NULL); + } + if (!spawned) { + int itemIndex; + + itemIndex = Timing_ItemIndexFromKind(track->kind); + if (itemIndex >= 0) { + BotItemTiming_OnWitnessedPadTakenCooldown(bs, -1, itemIndex, + track->origin, "empty at pad", cooldown); + } else { + Timing_TouchPickupLatch(track); + Timing_StartCooldown(bs, track, cooldown, "empty at pad"); + } + return; + } + } + Timing_AbandonPadAfterCollect(bs, trackIndex); +} + +static float Timing_GetSpawnDueAt(bot_state_t *bs, timing_belief_t *track) { + if (!bs || !track || track->state != BOT_TIMING_STATE_COOLDOWN) { + return 0.0f; + } + if (bs->timing_spawn_due_at > 0.0f) { + return bs->timing_spawn_due_at; + } + if (track->believed_spawn_at > 0.0f) { + return track->believed_spawn_at; + } + return 0.0f; +} + +static void Timing_HandleMissedSpawn(bot_state_t *bs, int trackIndex, + timing_belief_t *track) { + const timing_type_def_t *def; + qboolean spawned; + float cooldown; + + if (!bs || !track || trackIndex < 0) { + return; + } + + def = Timing_DefForKind(track->kind); + spawned = qfalse; + cooldown = 0.0f; + if (def) { + Timing_ScanSpawnState(def, track->origin, &spawned, &cooldown, NULL); + } + if (spawned) { + Timing_MarkSpawned(bs, track, "spawn arrived late"); + bs->timing_spawn_due_at = 0.0f; + bs->timing_far_pursue_since = 0.0f; + return; + } + + Timing_NotifyOpponentInferredPickup(bs, track, "missed spawn infer"); + BotItemTiming_DebugLine(bs, "missed spawn", + Timing_LabelForKind(track->kind), (int)track->respawn_interval); + if (bs->item_commit_timing) { + BotItems_AbortCommit(bs); + } + Timing_StartCooldown(bs, track, + cooldown > 0.0f ? cooldown : track->respawn_interval, "missed spawn"); + Timing_PlanNextTrack(bs, trackIndex); +} + +static void Timing_TickFarPursueStall(bot_state_t *bs, int trackIndex, + timing_belief_t *track) { + float spawnDue; + + if (!bs || !track || trackIndex < 0) { + return; + } + + if (Timing_BotAtCampPosition(bs, track)) { + bs->timing_far_pursue_since = 0.0f; + return; + } + + spawnDue = Timing_GetSpawnDueAt(bs, track); + if (spawnDue > 0.0f && FloatTime() >= spawnDue + TIMING_SPAWN_MISS_SEC) { + if (track->state == BOT_TIMING_STATE_COOLDOWN && + Timing_BotNearTrack(bs, track)) { + Timing_HandleMissedSpawn(bs, trackIndex, track); + } else { + BotItemTiming_DebugLine(bs, "pursue overdue", + Timing_LabelForKind(track->kind), (int)TIMING_SPAWN_MISS_SEC); + if (bs->item_commit_timing && bs->item_commit_active) { + BotItems_AbortCommit(bs); + } else { + bs->timing_pursue_track = -1; + bs->timing_spawn_due_at = 0.0f; + Timing_PlanNextTrack(bs, trackIndex); + } + bs->timing_far_pursue_since = 0.0f; + } + return; + } + + if (bs->timing_far_pursue_since <= 0.0f) { + bs->timing_far_pursue_since = FloatTime(); + return; + } + if (FloatTime() - bs->timing_far_pursue_since < TIMING_FAR_PURSUE_ABORT_SEC) { + return; + } + + BotItemTiming_DebugLine(bs, "pursue stall", + Timing_LabelForKind(track->kind), (int)TIMING_FAR_PURSUE_ABORT_SEC); + if (bs->item_commit_timing && bs->item_commit_active) { + BotItems_AbortCommit(bs); + } else { + bs->timing_pursue_track = -1; + bs->timing_spawn_due_at = 0.0f; + Timing_PlanNextTrack(bs, trackIndex); + } + bs->timing_far_pursue_since = 0.0f; +} + +static void Timing_StartCooldown(bot_state_t *bs, timing_belief_t *track, + float cooldown_sec, const char *reason) { + int seconds; + int trackIndex; + + if (!track || !Timing_TrackActive(track)) { + return; + } + if (cooldown_sec <= 0.0f) { + cooldown_sec = track->respawn_interval; + } + seconds = (int)cooldown_sec; + Timing_SetCooldown(track, FloatTime() + cooldown_sec); + BotItemTiming_DebugLine(bs, reason, Timing_LabelForKind(track->kind), seconds); + trackIndex = Timing_TrackIndex(bs, track); + if (trackIndex >= 0) { + Timing_AbandonPadAfterCollect(bs, trackIndex); + } +} + +static void Timing_ResetTimer(bot_state_t *bs, timing_belief_t *track, const char *reason) { + int seconds; + + if (!track || !Timing_TrackActive(track)) { + return; + } + seconds = (int)track->respawn_interval; + Timing_SetCooldown(track, FloatTime() + track->respawn_interval); + Timing_TouchPickupLatch(track); + BotItemTiming_DebugLine(bs, reason, Timing_LabelForKind(track->kind), seconds); + Timing_AbandonPadAfterCollect(bs, Timing_TrackIndex(bs, track)); +} + +static void Timing_MarkSpawned(bot_state_t *bs, timing_belief_t *track, const char *reason) { + if (!track || !Timing_TrackActive(track)) { + return; + } + if (track->state == BOT_TIMING_STATE_SPAWNED) { + return; + } + Timing_SetSpawned(track); + BotItemTiming_DebugLine(bs, reason, Timing_LabelForKind(track->kind), 0); +} + +static const timing_map_instance_t *Timing_BestUntrackedReachable(bot_state_t *bs) { + int j; + const timing_map_instance_t *inst; + + if (!bs) { + return NULL; + } + for (j = 0; j < timing_map_pool_count; j++) { + inst = &timing_map_pool[j]; + if (Timing_PoolInstanceTracked(bs, inst)) { + continue; + } + if (!Timing_PoolInstanceReachable(bs, inst)) { + continue; + } + return inst; + } + return NULL; +} + +void BotItemTiming_OnSpawn(bot_state_t *bs) { + int i; + int swapSlot; + int swapSlotPriority; + int forceSwapSlot; + int forceSwapPriority; + int trackPriority; + const timing_map_instance_t *inst; + const timing_map_instance_t *candidate; + timing_belief_t *track; + qboolean hadTracks; + const char *fillEvent; + + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + + Timing_BuildMapPool(); + + hadTracks = BotItemTiming_HasTrack(bs) ? qtrue : qfalse; + fillEvent = hadTracks ? "spawn fill" : "tracking"; + + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + if (Timing_TrackActive(&bs->timing_track[i])) { + continue; + } + inst = Timing_BestUntrackedReachable(bs); + if (!inst) { + break; + } + Timing_AssignTrackFromPool(bs, i, inst, fillEvent); + } + + if (hadTracks) { + swapSlot = -1; + swapSlotPriority = 999999; + forceSwapSlot = -1; + forceSwapPriority = 999999; + + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track)) { + continue; + } + trackPriority = Timing_PriorityForKind(track->kind); + if (!Timing_TrackReachable(bs, track)) { + if (trackPriority < forceSwapPriority) { + forceSwapPriority = trackPriority; + forceSwapSlot = i; + } + } + if (trackPriority < swapSlotPriority) { + swapSlotPriority = trackPriority; + swapSlot = i; + } + } + + candidate = Timing_BestUntrackedReachable(bs); + if (candidate && swapSlot >= 0) { + if (forceSwapSlot >= 0) { + swapSlot = forceSwapSlot; + } else if (candidate->priority <= swapSlotPriority) { + candidate = NULL; + } + } else { + candidate = NULL; + } + + if (candidate) { + Timing_AssignTrackFromPool(bs, swapSlot, candidate, "spawn swap"); + } + } + + if (bs->item_commit_timing && bs->item_commit_active) { + BotItems_AbortCommit(bs); + } + BotItemTiming_AbortPursuit(bs); + bs->timing_next_plan_time = FloatTime() + 1.0f + (float)(rand() % 4); + bs->timing_spawn_due_at = 0.0f; + bs->timing_detour_track = -1; + bs->timing_next_detour_time = 0.0f; + bs->timing_detour_block_until = 0.0f; + bs->timing_next_preempt_time = 0.0f; + bs->timing_preempt_block_until = 0.0f; + + Timing_PlanClosestSpawned(bs); +} + +static void Timing_PlanClosestSpawned(bot_state_t *bs) { + int i; + int bestIndex; + int bestScore; + int bestTravel; + int travelTime; + int score; + timing_belief_t *track; + bot_goal_t goal; + + if (!bs || BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + if (bs->timing_pursue_track >= 0) { + return; + } + + bestIndex = -1; + bestScore = -999999; + + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track) || + track->state != BOT_TIMING_STATE_SPAWNED) { + continue; + } + if (!Timing_WantsItemKind(bs, track->kind)) { + continue; + } + if (Timing_ShouldDeferSpawnedGrab(bs, track->kind)) { + continue; + } + if (!Timing_BuildGoalFromTrack(bs, track, &goal)) { + continue; + } + (void)BotItems_RefreshItemGoal(bs, &goal, + Timing_KindToBotItem(track->kind)); + if (!BotItems_GoalPickupPresent(bs, &goal)) { + continue; + } + travelTime = BotItems_TravelTimeToGoal(bs, &goal); + if (travelTime <= 0) { + continue; + } + score = Timing_PursuitScoreWithGoal(bs, track, travelTime, &goal); + if (score <= bestScore) { + continue; + } + bestScore = score; + bestTravel = travelTime; + bestIndex = i; + } + + if (bestIndex >= 0) { + track = &bs->timing_track[bestIndex]; + Timing_StartPursuit(bs, bestIndex, track, bestTravel); + bs->timing_next_plan_time = FloatTime() + TIMING_PLAN_INTERVAL; + } +} + +static void Timing_BeginDetour(bot_state_t *bs, int detourIndex) { + timing_belief_t *track; + bot_goal_t goal; + int botKind; + float until; + + if (!bs || detourIndex < 0 || detourIndex >= BOT_TIMING_TRACK_COUNT) { + return; + } + if (detourIndex == bs->timing_pursue_track) { + return; + } + + track = &bs->timing_track[detourIndex]; + if (!Timing_TrackActive(track) || track->state != BOT_TIMING_STATE_SPAWNED) { + return; + } + if (!Timing_BuildGoalFromTrack(bs, track, &goal)) { + return; + } + + botKind = Timing_KindToBotItem(track->kind); + if (!botKind) { + return; + } + (void)BotItems_RefreshItemGoal(bs, &goal, botKind); + if (!BotItems_GoalPickupPresent(bs, &goal)) { + return; + } + if (!BotItems_SuspendTimingPrimary(bs)) { + return; + } + + until = FloatTime() + TIMING_DETOUR_COMMIT_SEC; + if (!BotItems_BeginDetourCommit(bs, &goal, botKind, until)) { + BotItems_CancelDetourSuspend(bs); + return; + } + + bs->timing_detour_track = detourIndex; + BotItemTiming_DebugLine(bs, "detour", + Timing_LabelForKind(track->kind), (int)(until - FloatTime())); +} + +static void Timing_TickDetourScan(bot_state_t *bs) { + int i; + int primary; + int travelTime; + int primaryTravel; + int primaryPriority; + int bestIndex; + int bestScore; + int bestTravel; + int score; + int priority; + timing_belief_t *track; + timing_belief_t *primaryTrack; + bot_goal_t goal; + bot_goal_t primaryGoal; + int botKind; + float now; + + if (!bs || bs->timing_pursue_track < 0) { + return; + } + if (BotItems_IsDetourCommit(bs)) { + return; + } + if (!bs->item_commit_timing || !bs->item_commit_active) { + return; + } + if (BotItems_TimingHoldingNearGoal(bs)) { + return; + } + + now = FloatTime(); + if (now < bs->timing_detour_block_until) { + return; + } + if (now < bs->timing_next_detour_time) { + return; + } + bs->timing_next_detour_time = now + TIMING_DETOUR_SCAN_INTERVAL; + + primary = bs->timing_pursue_track; + primaryTrack = &bs->timing_track[primary]; + primaryPriority = Timing_PriorityForKind(primaryTrack->kind); + primaryTravel = 0; + if (Timing_BuildGoalFromTrack(bs, primaryTrack, &primaryGoal)) { + primaryTravel = BotItems_TravelTimeToGoal(bs, &primaryGoal); + } + + bestIndex = -1; + bestScore = -999999; + bestTravel = 0; + + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + if (i == primary) { + continue; + } + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track) || + track->state != BOT_TIMING_STATE_SPAWNED) { + continue; + } + if (now < track->detour_block_until) { + continue; + } + if (now < track->preempt_block_until) { + continue; + } + if (!Timing_WantsItemKind(bs, track->kind)) { + continue; + } + if (Timing_ShouldDeferSpawnedGrab(bs, track->kind)) { + continue; + } + if (!Timing_BuildGoalFromTrack(bs, track, &goal)) { + continue; + } + botKind = Timing_KindToBotItem(track->kind); + (void)BotItems_RefreshItemGoal(bs, &goal, botKind); + if (!BotItems_GoalPickupPresent(bs, &goal)) { + continue; + } + travelTime = BotItems_TravelTimeToGoal(bs, &goal); + if (travelTime <= 0 || travelTime > TIMING_DETOUR_MAX_TRAVEL) { + continue; + } + priority = Timing_PriorityForKind(track->kind); + if (primaryTravel > 0 && travelTime >= primaryTravel) { + if (priority <= primaryPriority) { + continue; + } + if (travelTime > primaryTravel + TIMING_DETOUR_PRIORITY_SLACK) { + continue; + } + } + score = Timing_PursuitScoreWithGoal(bs, track, travelTime, &goal); + if (score <= bestScore) { + continue; + } + bestScore = score; + bestTravel = travelTime; + bestIndex = i; + } + + if (bestIndex >= 0) { + Timing_BeginDetour(bs, bestIndex); + } +} + +static void BotItemTiming_EnsureInit(bot_state_t *bs) { + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (!BotItemTiming_HasTrack(bs)) { + BotItemTiming_OnSpawn(bs); + } +} + +static void BotItemTiming_CheckEmptySpawns(bot_state_t *bs) { + int i; + int itemIndex; + timing_belief_t *track; + const timing_type_def_t *def; + float cooldown; + qboolean spawned; + + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track) || track->state != BOT_TIMING_STATE_SPAWNED) { + continue; + } + if (Timing_PickupLatched(track)) { + continue; + } + if (FloatTime() < track->next_spawn_check) { + continue; + } + track->next_spawn_check = FloatTime() + TIMING_SPAWN_CHECK_INTERVAL; + + if (!Timing_BotCanSeeOrigin(bs, track->origin)) { + continue; + } + + def = Timing_DefForKind(track->kind); + if (!def) { + continue; + } + + cooldown = 0.0f; + spawned = qfalse; + Timing_ScanSpawnState(def, track->origin, &spawned, &cooldown, NULL); + if (spawned) { + continue; + } + + itemIndex = Timing_ItemIndexFromKind(track->kind); + if (itemIndex >= 0) { + BotItemTiming_OnWitnessedPadTakenCooldown(bs, -1, itemIndex, + track->origin, "empty spawn sighting", cooldown); + } else { + Timing_NotifyOpponentInferredPickup(bs, track, + "empty spawn sighting"); + Timing_TouchPickupLatch(track); + Timing_StartCooldown(bs, track, cooldown, "empty at spawn"); + } + } +} + +static int Timing_KindToBotItem(int kind) { + switch (kind) { + case TIMING_KIND_QUAD: + return 1; /* BOT_ITEM_QUAD */ + case TIMING_KIND_MEGA_HEALTH: + return 4; /* BOT_ITEM_MEGA_HEALTH */ + case TIMING_KIND_RED_ARMOR: + return 5; /* BOT_ITEM_RED_ARMOR */ + case TIMING_KIND_YELLOW_ARMOR: + return 6; /* BOT_ITEM_YELLOW_ARMOR */ + default: + return 0; + } +} + +static qboolean Timing_BuildGoalFromTrack(bot_state_t *bs, timing_belief_t *track, + bot_goal_t *goal) { + int botKind; + + if (!bs || !track || !goal || !Timing_TrackActive(track)) { + return qfalse; + } + + botKind = Timing_KindToBotItem(track->kind); + if (!botKind) { + return qfalse; + } + + memset(goal, 0, sizeof(*goal)); + VectorCopy(track->origin, goal->origin); + goal->areanum = track->areanum; + goal->number = track->goal_number; + goal->flags = GFL_ITEM; + + return goal->areanum != 0; +} + +static qboolean Timing_WantsItemKind(bot_state_t *bs, int kind) { + if (!bs) { + return qfalse; + } + + switch (kind) { + case TIMING_KIND_QUAD: + return bs->inventory[INVENTORY_QUAD] <= 0; + case TIMING_KIND_MEGA_HEALTH: + return bs->inventory[INVENTORY_HEALTH] < 200; + case TIMING_KIND_RED_ARMOR: + return bs->inventory[INVENTORY_ARMOR] < 200; + case TIMING_KIND_YELLOW_ARMOR: + return bs->inventory[INVENTORY_ARMOR] < 150; + default: + return qfalse; + } +} + +static qboolean Timing_ShouldDeferPursuit(bot_state_t *bs, int kind, int purpose) { + int health; + + if (!bs) { + return qtrue; + } + + if (BotIsDead(bs) || BotIsObserver(bs)) { + return qtrue; + } + + if (BotMove_WantsUrgentHealth(bs)) { + return qtrue; + } + + health = bs->inventory[INVENTORY_HEALTH]; + if (health < 50) { + return qtrue; + } + + if (purpose == TIMING_PURPOSE_SPAWNED) { + return qfalse; + } + + if (FloatTime() - bs->entergame_time < TIMING_FRESH_SPAWN_SEC) { + return qtrue; + } + + switch (kind) { + case TIMING_KIND_MEGA_HEALTH: + case TIMING_KIND_RED_ARMOR: + case TIMING_KIND_YELLOW_ARMOR: + if (health < 80) { + return qtrue; + } + break; + case TIMING_KIND_QUAD: + if (health < 60) { + return qtrue; + } + if ((bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0 || + bs->inventory[INVENTORY_ROCKETS] < 5) && + (bs->inventory[INVENTORY_RAILGUN] <= 0 || + bs->inventory[INVENTORY_SLUGS] < 5) && + (bs->inventory[INVENTORY_PLASMAGUN] <= 0 || + bs->inventory[INVENTORY_CELLS] < 20)) { + return qtrue; + } + break; + default: + break; + } + + return qfalse; +} + +static qboolean Timing_ShouldDeferSpawnedGrab(bot_state_t *bs, int kind) { + return Timing_ShouldDeferPursuit(bs, kind, TIMING_PURPOSE_SPAWNED); +} + +static qboolean Timing_ShouldDepartForTrack(bot_state_t *bs, timing_belief_t *track, + int travelTime) { + float secondsUntil; + float travelSec; + + if (!bs || !track || !Timing_TrackActive(track) || travelTime <= 0) { + return qfalse; + } + + travelSec = (float)travelTime / 100.0f; + + if (track->state == BOT_TIMING_STATE_COOLDOWN) { + secondsUntil = track->believed_spawn_at - FloatTime(); + if (secondsUntil <= 0.0f) { + return qtrue; + } + return secondsUntil <= travelSec + TIMING_TRAVEL_MARGIN_SEC; + } + + if (track->state == BOT_TIMING_STATE_SPAWNED) { + return Timing_WantsItemKind(bs, track->kind); + } + + return qfalse; +} + +static void Timing_StartPursuit(bot_state_t *bs, int trackIndex, timing_belief_t *track, + int travelTime) { + bot_goal_t goal; + int botKind; + float until; + float travelSec; + + if (!bs || !track || trackIndex < 0 || trackIndex >= BOT_TIMING_TRACK_COUNT) { + return; + } + + if (BotItems_IsDetourCommit(bs)) { + return; + } + if (bs->item_commit_suspended) { + return; + } + + if (!Timing_BuildGoalFromTrack(bs, track, &goal)) { + return; + } + + if (FloatTime() < track->detour_block_until) { + return; + } + + botKind = Timing_KindToBotItem(track->kind); + if (!botKind) { + return; + } + + travelSec = (float)travelTime / 100.0f; + until = FloatTime() + travelSec + TIMING_COMMIT_PAD_SEC; + + if (!BotItems_BeginTimingCommit(bs, &goal, botKind, until)) { + return; + } + + bs->timing_pursue_track = trackIndex; + bs->timing_far_pursue_since = 0.0f; + if (track->state == BOT_TIMING_STATE_COOLDOWN && track->believed_spawn_at > 0.0f) { + bs->timing_spawn_due_at = track->believed_spawn_at; + } else { + bs->timing_spawn_due_at = 0.0f; + } + BotItemTiming_DebugLine(bs, "pursue", + Timing_LabelForKind(track->kind), (int)travelSec); +} + +static int Timing_TrackIndex(const bot_state_t *bs, const timing_belief_t *track) { + if (!bs || !track) { + return -1; + } + if (track < bs->timing_track || + track >= bs->timing_track + BOT_TIMING_TRACK_COUNT) { + return -1; + } + return (int)(track - bs->timing_track); +} + +static int Timing_PickBestPursuitTrack(bot_state_t *bs, int excludeIndex, + int requireDepartable, int *bestTravelOut) { + int i; + int bestIndex; + int bestScore; + int bestTravel; + int travelTime; + int score; + int purpose; + timing_belief_t *track; + bot_goal_t goal; + + if (!bs) { + return -1; + } + + bestIndex = -1; + bestScore = -999999; + bestTravel = 0; + + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + if (i == excludeIndex) { + continue; + } + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track)) { + continue; + } + if (FloatTime() < track->preempt_block_until) { + continue; + } + if (FloatTime() < track->detour_block_until) { + continue; + } + + purpose = track->state == BOT_TIMING_STATE_SPAWNED ? + TIMING_PURPOSE_SPAWNED : TIMING_PURPOSE_COOLDOWN; + if (Timing_ShouldDeferPursuit(bs, track->kind, purpose)) { + continue; + } + if (!Timing_BuildGoalFromTrack(bs, track, &goal)) { + continue; + } + travelTime = BotItems_TravelTimeToGoal(bs, &goal); + if (travelTime <= 0) { + continue; + } + if (requireDepartable && + !Timing_ShouldDepartForTrack(bs, track, travelTime)) { + continue; + } + + score = Timing_PursuitScoreWithGoal(bs, track, travelTime, &goal); + if (score <= bestScore) { + continue; + } + bestScore = score; + bestIndex = i; + bestTravel = travelTime; + } + + if (bestTravelOut) { + *bestTravelOut = bestTravel; + } + return bestIndex; +} + +static qboolean Timing_TickPreemptScan(bot_state_t *bs) { + int currentIndex; + int candidateIndex; + int currentTravel; + int candidateTravel; + int currentScore; + int candidateScore; + int currentPriority; + int candidatePriority; + timing_belief_t *current; + timing_belief_t *candidate; + bot_goal_t goal; + float now; + + if (!bs || bs->timing_pursue_track < 0) { + return qfalse; + } + if (BotItems_IsDetourCommit(bs) || bs->item_commit_suspended) { + return qfalse; + } + if (!bs->item_commit_timing || !bs->item_commit_active) { + return qfalse; + } + if (BotItems_TimingHoldingNearGoal(bs)) { + return qfalse; + } + + now = FloatTime(); + if (now < bs->timing_preempt_block_until) { + return qfalse; + } + if (now < bs->timing_next_preempt_time) { + return qfalse; + } + bs->timing_next_preempt_time = now + TIMING_PREEMPT_SCAN_INTERVAL; + + currentIndex = bs->timing_pursue_track; + current = &bs->timing_track[currentIndex]; + if (!Timing_BuildGoalFromTrack(bs, current, &goal)) { + return qfalse; + } + currentTravel = BotItems_TravelTimeToGoal(bs, &goal); + if (currentTravel <= 0) { + return qfalse; + } + if (currentTravel <= TIMING_PREEMPT_COMMIT_TRAVEL) { + return qfalse; + } + + currentPriority = Timing_PriorityForKind(current->kind); + currentScore = Timing_PursuitScoreWithGoal(bs, current, currentTravel, &goal); + + candidateIndex = Timing_PickBestPursuitTrack(bs, currentIndex, qtrue, + &candidateTravel); + if (candidateIndex < 0) { + return qfalse; + } + + candidate = &bs->timing_track[candidateIndex]; + candidatePriority = Timing_PriorityForKind(candidate->kind); + if (candidatePriority <= currentPriority) { + return qfalse; + } + + if (!Timing_BuildGoalFromTrack(bs, candidate, &goal)) { + return qfalse; + } + candidateScore = Timing_PursuitScoreWithGoal(bs, candidate, candidateTravel, + &goal); + if (candidateScore < currentScore + TIMING_PREEMPT_SCORE_MARGIN) { + return qfalse; + } + + BotItems_AbortTimingCommitQuiet(bs); + current->preempt_block_until = now + TIMING_PREEMPT_TRACK_BLOCK_SEC; + bs->timing_preempt_block_until = now + TIMING_PREEMPT_COOLDOWN_SEC; + bs->timing_spawn_due_at = 0.0f; + + Timing_StartPursuit(bs, candidateIndex, candidate, candidateTravel); + BotItemTiming_DebugLine(bs, "preempt", + Timing_LabelForKind(candidate->kind), 0); + return qtrue; +} + +static void Timing_PlanNextTrack(bot_state_t *bs, int excludeIndex) { + int trackIndex; + int travelTime; + timing_belief_t *track; + + if (!bs || BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + + bs->timing_pursue_track = -1; + bs->timing_spawn_due_at = 0.0f; + bs->timing_next_plan_time = FloatTime() + TIMING_PLAN_INTERVAL; + + trackIndex = Timing_PickBestPursuitTrack(bs, excludeIndex, qtrue, &travelTime); + if (trackIndex < 0) { + return; + } + + track = &bs->timing_track[trackIndex]; + Timing_StartPursuit(bs, trackIndex, track, travelTime); +} + +static void Timing_CheckMissedSpawn(bot_state_t *bs) { + int trackIndex; + timing_belief_t *track; + float spawnDue; + + if (!bs || bs->timing_pursue_track < 0) { + return; + } + + trackIndex = bs->timing_pursue_track; + if (trackIndex < 0 || trackIndex >= BOT_TIMING_TRACK_COUNT) { + return; + } + track = &bs->timing_track[trackIndex]; + if (!Timing_TrackActive(track)) { + bs->timing_spawn_due_at = 0.0f; + return; + } + + spawnDue = Timing_GetSpawnDueAt(bs, track); + if (spawnDue <= 0.0f) { + return; + } + if (FloatTime() < spawnDue + TIMING_SPAWN_MISS_SEC) { + return; + } + if (!Timing_BotAtCampPosition(bs, track)) { + return; + } + + Timing_HandleMissedSpawn(bs, trackIndex, track); +} + +static void Timing_TickPursuit(bot_state_t *bs) { + int trackIndex; + int travelTime; + timing_belief_t *track; + bot_goal_t goal; + float now; + + if (!bs || BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + + now = FloatTime(); + + if (bs->timing_pursue_track >= 0) { + trackIndex = bs->timing_pursue_track; + if (trackIndex < 0 || trackIndex >= BOT_TIMING_TRACK_COUNT) { + BotItemTiming_AbortPursuit(bs); + return; + } + track = &bs->timing_track[trackIndex]; + if (!Timing_TrackActive(track)) { + if (bs->item_commit_timing) { + BotItems_AbortCommit(bs); + } + BotItemTiming_AbortPursuit(bs); + return; + } + Timing_TickAbandonEmptyPad(bs, trackIndex, track); + if (bs->timing_pursue_track < 0) { + return; + } + Timing_TickFarPursueStall(bs, trackIndex, track); + if (bs->timing_pursue_track < 0) { + return; + } + { + int purpose; + + purpose = track->state == BOT_TIMING_STATE_SPAWNED ? + TIMING_PURPOSE_SPAWNED : TIMING_PURPOSE_COOLDOWN; + if (Timing_ShouldDeferPursuit(bs, track->kind, purpose)) { + if (bs->item_commit_timing) { + BotItems_AbortCommit(bs); + } + BotItemTiming_AbortPursuit(bs); + return; + } + } + if (BotItems_IsDetourCommit(bs)) { + return; + } + if (Timing_TickPreemptScan(bs)) { + return; + } + Timing_TickDetourScan(bs); + /* Detour uses a non-timing commit; do not fall through to StartPursuit. */ + if (BotItems_IsDetourCommit(bs) || bs->item_commit_suspended || + bs->item_commit_active) { + return; + } + if (!Timing_BuildGoalFromTrack(bs, track, &goal)) { + if (bs->item_commit_timing) { + BotItems_AbortCommit(bs); + } + BotItemTiming_AbortPursuit(bs); + return; + } + travelTime = BotItems_TravelTimeToGoal(bs, &goal); + if (travelTime <= 0) { + if (bs->item_commit_timing) { + BotItems_AbortCommit(bs); + } + BotItemTiming_AbortPursuit(bs); + return; + } + Timing_StartPursuit(bs, trackIndex, track, travelTime); + return; + } + + if (now < bs->timing_next_plan_time) { + return; + } + bs->timing_next_plan_time = now + TIMING_PLAN_INTERVAL; + + trackIndex = Timing_PickBestPursuitTrack(bs, -1, qtrue, &travelTime); + if (trackIndex < 0) { + return; + } + + track = &bs->timing_track[trackIndex]; + Timing_StartPursuit(bs, trackIndex, track, travelTime); +} + +void BotItemTiming_Reset(bot_state_t *bs) { + int i; + + if (!bs) { + return; + } + bs->timing_pursue_track = -1; + bs->timing_detour_track = -1; + bs->timing_next_plan_time = 0.0f; + bs->timing_next_detour_time = 0.0f; + bs->timing_detour_block_until = 0.0f; + bs->timing_spawn_due_at = 0.0f; + bs->timing_far_pursue_since = 0.0f; + bs->timing_next_preempt_time = 0.0f; + bs->timing_preempt_block_until = 0.0f; + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + bs->timing_track[i].state = BOT_TIMING_STATE_NONE; + VectorClear(bs->timing_track[i].origin); + bs->timing_track[i].areanum = 0; + bs->timing_track[i].goal_number = 0; + bs->timing_track[i].kind = TIMING_KIND_NONE; + bs->timing_track[i].respawn_interval = 0.0f; + bs->timing_track[i].believed_spawn_at = 0.0f; + bs->timing_track[i].pickup_latch_time = 0.0f; + bs->timing_track[i].next_spawn_check = 0.0f; + bs->timing_track[i].detour_block_until = 0.0f; + bs->timing_track[i].preempt_block_until = 0.0f; + } + if (bs->client >= 0 && bs->client < MAX_CLIENTS) { + timing_denied_playerevents[bs->client] = 0; + } +} + +void BotItemTiming_OnSelfPickup(bot_state_t *bs, int trackIndex) { + timing_belief_t *track; + + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (trackIndex < 0 || trackIndex >= BOT_TIMING_TRACK_COUNT) { + return; + } + + track = &bs->timing_track[trackIndex]; + if (!Timing_TrackActive(track) || Timing_PickupLatched(track)) { + return; + } + if (!Timing_BotNearTrack(bs, track)) { + return; + } + + Timing_ResetTimer(bs, track, "self pickup"); +} + +void BotItemTiming_OnDetourEnded(bot_state_t *bs, int trackIndex) { + float now; + timing_belief_t *track; + + if (!bs) { + return; + } + + now = FloatTime(); + bs->timing_detour_block_until = now + TIMING_DETOUR_COOLDOWN_SEC; + bs->timing_next_detour_time = bs->timing_detour_block_until; + + if (trackIndex < 0 || trackIndex >= BOT_TIMING_TRACK_COUNT) { + return; + } + + track = &bs->timing_track[trackIndex]; + track->detour_block_until = now + TIMING_DETOUR_TRACK_COOLDOWN_SEC; +} + +void BotItemTiming_BlockPursuitAtGoal(bot_state_t *bs, float blockSec) { + int trackIndex; + timing_belief_t *track; + float now; + + if (!bs || blockSec <= 0.0f) { + return; + } + + now = FloatTime(); + trackIndex = bs->timing_pursue_track; + if (trackIndex >= 0 && trackIndex < BOT_TIMING_TRACK_COUNT) { + track = &bs->timing_track[trackIndex]; + track->detour_block_until = now + blockSec; + } + bs->timing_detour_block_until = now + blockSec; + bs->timing_next_detour_time = bs->timing_detour_block_until; + + if (bs->item_commit_timing) { + BotItems_AbortCommit(bs); + } + BotItemTiming_AbortPursuit(bs); +} + +void BotItemTiming_AbortPursuit(bot_state_t *bs) { + if (!bs) { + return; + } + bs->timing_pursue_track = -1; + bs->timing_spawn_due_at = 0.0f; + bs->timing_far_pursue_since = 0.0f; + bs->timing_detour_track = -1; + bs->timing_next_detour_time = 0.0f; +} + +void BotItemTiming_OnTimingCommitEnd(bot_state_t *bs, int gotItem, + int trackIndex) { + if (!bs) { + return; + } + bs->timing_pursue_track = -1; + bs->timing_spawn_due_at = 0.0f; + bs->timing_far_pursue_since = 0.0f; + bs->timing_detour_track = -1; + bs->timing_next_detour_time = 0.0f; + if (gotItem && trackIndex >= 0) { + BotItemTiming_OnSelfPickup(bs, trackIndex); + Timing_PlanNextTrack(bs, trackIndex); + } else { + Timing_PlanNextTrack(bs, trackIndex); + } +} + +void BotItemTiming_PostSnapshot(bot_state_t *bs) { + int i; + timing_belief_t *track; + + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + + BotItemTiming_EnsureInit(bs); + + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track) || track->state != BOT_TIMING_STATE_COOLDOWN) { + continue; + } + if (FloatTime() >= track->believed_spawn_at) { + Timing_SetSpawned(track); + } + } + + BotItemTiming_CheckEmptySpawns(bs); + Timing_CheckMissedSpawn(bs); + Timing_TickPursuit(bs); + + if (BotOpponent_IsActive() && bs->client >= 0 && + bs->client < MAX_CLIENTS) { + int events; + + events = bs->cur_ps.persistant[PERS_PLAYEREVENTS]; + if ((events ^ timing_denied_playerevents[bs->client]) & + PLAYEREVENT_DENIEDREWARD) { + BotItemTiming_OnDeniedReward(bs); + } + timing_denied_playerevents[bs->client] = events; + } +} + +void BotItemTiming_InferOpponentPickupNearDeath(bot_state_t *bs, + const vec3_t deathOrigin) { + int i; + timing_belief_t *track; + const timing_type_def_t *def; + qboolean spawned; + float cooldown; + float now; + float until; + + if (!bs || !deathOrigin || !BotItemTiming_IsActive() || + !BotOpponent_IsActive()) { + return; + } + + now = FloatTime(); + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track)) { + continue; + } + if (Timing_Dist(deathOrigin, track->origin) > TIMING_DEATH_ITEM_RADIUS) { + continue; + } + + if (track->state == BOT_TIMING_STATE_COOLDOWN) { + until = track->believed_spawn_at - now; + if (until > TIMING_IMMINENT_WAIT_SEC && now < track->believed_spawn_at) { + continue; + } + } + + def = Timing_DefForKind(track->kind); + spawned = qfalse; + cooldown = 0.0f; + if (def) { + Timing_ScanSpawnState(def, track->origin, &spawned, &cooldown, NULL); + } + if (spawned) { + continue; + } + + Timing_NotifyOpponentInferredPickup(bs, track, "death near item infer"); + } +} + +int BotItemTiming_HasTrack(const bot_state_t *bs) { + int i; + + if (!bs) { + return 0; + } + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + if (bs->timing_track[i].state != BOT_TIMING_STATE_NONE) { + return 1; + } + } + return 0; +} + +int BotItemTiming_GetSecondsUntil(const bot_state_t *bs, int trackIndex) { + timing_belief_t *track; + float remaining; + + if (!bs || trackIndex < 0 || trackIndex >= BOT_TIMING_TRACK_COUNT) { + return 0; + } + track = (timing_belief_t *)&bs->timing_track[trackIndex]; + if (!Timing_TrackActive(track) || track->state == BOT_TIMING_STATE_SPAWNED) { + return 0; + } + remaining = track->believed_spawn_at - FloatTime(); + if (remaining <= 0.0f) { + return 0; + } + return (int)remaining; +} + +int BotItemTiming_ShouldWaitAtPad(bot_state_t *bs) { + timing_belief_t *track; + float remaining; + + if (!bs || !BotItemTiming_IsActive()) { + return 0; + } + if (bs->timing_pursue_track < 0 || + bs->timing_pursue_track >= BOT_TIMING_TRACK_COUNT) { + return 0; + } + track = &bs->timing_track[bs->timing_pursue_track]; + if (!Timing_TrackActive(track) || + track->state != BOT_TIMING_STATE_COOLDOWN) { + return 0; + } + if (!Timing_BotAtCampPosition(bs, track)) { + return 0; + } + remaining = track->believed_spawn_at - FloatTime(); + if (remaining > TIMING_IMMINENT_WAIT_SEC) { + return 0; + } + if (remaining <= -TIMING_SPAWN_MISS_SEC) { + return 0; + } + return 1; +} + +static timing_belief_t *Timing_EnsureTrackForOpponentHeard(bot_state_t *bs, + int kind) { + timing_belief_t *track; + const timing_map_instance_t *inst; + int slot; + + if (!bs || kind == TIMING_KIND_NONE) { + return NULL; + } + + track = Timing_FindTrackForOpponentHeardPickup(bs, kind, NULL); + if (track) { + return track; + } + + Timing_BuildMapPool(); + inst = Timing_FindPoolInstanceOfKind(kind); + if (!inst) { + return NULL; + } + + track = Timing_FindTrackAtOrigin(bs, inst->origin); + if (track && track->kind == kind) { + return track; + } + + slot = Timing_AllocTrackSlot(bs); + if (slot < 0) { + return NULL; + } + Timing_AssignTrackFromPool(bs, slot, inst, "heard opponent bind"); + return &bs->timing_track[slot]; +} + +void BotItemTiming_OnWitnessedPadTakenCooldown(bot_state_t *bs, int pickerClient, + int itemIndex, const vec3_t eventOrigin, const char *reason, float cooldownSec) { + int kind; + timing_belief_t *track = NULL; + qboolean selfPickup; + const char *applyReason; + vec3_t padOrigin; + + if (!bs || BotIsObserver(bs)) { + return; + } + if (itemIndex < 0 || itemIndex >= bg_numItems) { + return; + } + + kind = Timing_KindFromItemIndex(itemIndex); + applyReason = (reason && reason[0]) ? reason : + ((pickerClient >= 0 && Timing_IsSelfPicker(bs, pickerClient)) ? + "self pickup" : "witnessed pickup"); + + if (eventOrigin) { + VectorCopy(eventOrigin, padOrigin); + } else { + VectorClear(padOrigin); + } + + selfPickup = pickerClient >= 0 && Timing_IsSelfPicker(bs, pickerClient); + + if (!selfPickup && BotItemTiming_IsActive() && kind != TIMING_KIND_NONE) { + BotItemTiming_EnsureInit(bs); + track = Timing_FindTrackForOpponentHeardPickup(bs, kind, NULL); + } + + /* Opponent stack always updates first — never gated on timing track state. */ + Beliefs_ApplyOpponentMajorPickup(bs, pickerClient, itemIndex, + (track ? track->origin : + (eventOrigin ? padOrigin : NULL)), applyReason); + + if (!BotItemTiming_IsActive() || kind == TIMING_KIND_NONE) { + return; + } + + BotItemTiming_EnsureInit(bs); + + if (selfPickup) { + track = Timing_FindTrackForSelfPickup(bs, kind, eventOrigin); + } else if (!track) { + track = Timing_EnsureTrackForObservedPickup(bs, kind, eventOrigin); + if (!track) { + track = Timing_EnsureTrackForOpponentHeard(bs, kind); + } + } + if (!track) { + return; + } + + Timing_ApplyWitnessedPadTiming(bs, track, selfPickup, eventOrigin, + applyReason, cooldownSec); +} + +void BotItemTiming_OnWitnessedPadTaken(bot_state_t *bs, int pickerClient, + int itemIndex, const vec3_t eventOrigin, const char *reason) { + BotItemTiming_OnWitnessedPadTakenCooldown(bs, pickerClient, itemIndex, + eventOrigin, reason, 0.0f); +} + +void BotItemTiming_OnEntityPickup(bot_state_t *bs, int pickerClient, + int itemIndex, const vec3_t eventOrigin) { + const char *reason; + + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (pickerClient < 0 || pickerClient >= MAX_CLIENTS) { + return; + } + + if (Timing_IsSelfPicker(bs, pickerClient)) { + reason = "self pickup"; + } else { + reason = "heard opponent pickup"; + } + + BotItemTiming_OnWitnessedPadTaken(bs, pickerClient, itemIndex, eventOrigin, + reason); +} + +void BotItemTiming_OnDeniedReward(bot_state_t *bs) { + int i; + timing_belief_t *track; + float dist; + float bestDist; + int bestIndex; + + if (!bs || !BotItemTiming_IsActive() || !BotOpponent_IsActive()) { + return; + } + BotItemTiming_EnsureInit(bs); + + if (bs->timing_pursue_track >= 0 && + bs->timing_pursue_track < BOT_TIMING_TRACK_COUNT) { + track = &bs->timing_track[bs->timing_pursue_track]; + if (Timing_TrackActive(track) && + Timing_BotNearTrack(bs, track) && + !Timing_ItemPresentAtTrack(bs, track)) { + Timing_NotifyOpponentInferredPickup(bs, track, "denied pickup"); + return; + } + } + + bestIndex = -1; + bestDist = TIMING_DENIED_MATCH_DIST; + for (i = 0; i < BOT_TIMING_TRACK_COUNT; i++) { + track = &bs->timing_track[i]; + if (!Timing_TrackActive(track)) { + continue; + } + dist = Timing_Dist(bs->origin, track->origin); + if (dist > TIMING_DENIED_MATCH_DIST) { + continue; + } + if (Timing_ItemPresentAtTrack(bs, track)) { + continue; + } + if (dist < bestDist) { + bestDist = dist; + bestIndex = i; + } + } + if (bestIndex >= 0) { + Timing_NotifyOpponentInferredPickup(bs, &bs->timing_track[bestIndex], + "denied pickup"); + } +} + +void BotItemTiming_OnGlobalItemPickup(bot_state_t *bs, int itemIndex, + const vec3_t eventOrigin) { + int kind; + timing_belief_t *track; + + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (BotIsObserver(bs)) { + return; + } + BotItemTiming_EnsureInit(bs); + + kind = Timing_KindFromItemIndex(itemIndex); + track = Timing_EnsureTrackForObservedPickup(bs, kind, eventOrigin); + if (!track) { + return; + } + if (Timing_PickupLatched(track)) { + return; + } + + Timing_NotifyOpponentInferredPickup(bs, track, "heard global pickup"); + Timing_ResetTimer(bs, track, "heard global pickup"); +} + +void BotItemTiming_OnPowerupSpawnSound(bot_state_t *bs, const vec3_t eventOrigin) { + timing_belief_t *track; + + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (BotIsObserver(bs)) { + return; + } + BotItemTiming_EnsureInit(bs); + + track = Timing_FindTrackAtOrigin(bs, eventOrigin); + if (!track || track->kind != TIMING_KIND_QUAD) { + return; + } + + Timing_MarkSpawned(bs, track, "powerup spawn"); +} + +void BotItemTiming_OnItemRespawn(bot_state_t *bs, int itemIndex, + const vec3_t eventOrigin) { + int kind; + timing_belief_t *track; + + if (!bs || !BotItemTiming_IsActive()) { + return; + } + if (BotIsObserver(bs)) { + return; + } + BotItemTiming_EnsureInit(bs); + + kind = Timing_KindFromItemIndex(itemIndex); + track = Timing_FindTrackForPickup(bs, kind, eventOrigin); + if (!track) { + return; + } + + Timing_MarkSpawned(bs, track, "item respawn"); + if (Timing_TrackIndex(bs, track) == bs->timing_pursue_track) { + bs->timing_spawn_due_at = 0.0f; + } +} diff --git a/ratoa_gamecode/code/game/ai_bot_items.h b/ratoa_gamecode/code/game/ai_bot_items.h index 1affcfa..9349d86 100644 --- a/ratoa_gamecode/code/game/ai_bot_items.h +++ b/ratoa_gamecode/code/game/ai_bot_items.h @@ -23,6 +23,10 @@ void BotItems_Tick(struct bot_state_s *bs); int BotItems_HasActiveCommit(const struct bot_state_s *bs); int BotItems_ShouldRunPickupNode(struct bot_state_s *bs); float BotItems_CommitNbgTime(struct bot_state_s *bs); +/* Timing pursuit: item goal reached but pickup not spawned yet — keep waiting. */ +int BotItems_TimingHoldsGoalReached(struct bot_state_s *bs, struct bot_goal_s *goal); +/* Timing pursuit: bot is holding near the committed spawn point. */ +int BotItems_TimingHoldingNearGoal(struct bot_state_s *bs); /* -1 = use vanilla; 0 = not reached; 1 = reached (touch or commit done) */ int BotItems_HandleReachedGoal(struct bot_state_s *bs, struct bot_goal_s *goal); @@ -32,13 +36,15 @@ void BotItems_AbortCommit(struct bot_state_s *bs); /* After risky movement (e.g. ledge abort): commit nearest visible health if needed. */ void BotItems_RequestUrgentHealth(struct bot_state_s *bs); /* Drop commit when pathing to the pickup fails (Seek NBG/LTG). */ -void BotItems_OnMoveFailure(struct bot_state_s *bs); +void BotItems_OnMoveFailure(struct bot_state_s *bs, struct bot_moveresult_s *mr); /* Z coordinate of the committed item goal origin, or -99999 if no active commit. */ float BotItems_GetCommitGoalOriginZ(const struct bot_state_s *bs); /* botlib item chooser wrappers: skip useless goals when bot_enhanced is on */ int BotItems_ChooseNBGItem(struct bot_state_s *bs, int tfl, struct bot_goal_s *ltg, float range); +int BotItems_NearbyGoal(struct bot_state_s *bs, int tfl, struct bot_goal_s *ltg, + float range); int BotItems_ChooseLTGItem(struct bot_state_s *bs, int tfl); /* @@ -48,4 +54,28 @@ int BotItems_ChooseLTGItem(struct bot_state_s *bs, int tfl); int BotItems_ItemGoalInVisButNotVisible(struct bot_state_s *bs, struct bot_goal_s *goal); +/* AAS route exists from bot to item goal (same travel flags as item commit). */ +int BotItems_GoalReachable(struct bot_state_s *bs, struct bot_goal_s *goal); +/* AAS travel time in botlib units (hundredths of a second); 0 = unreachable. */ +int BotItems_TravelTimeToGoal(struct bot_state_s *bs, struct bot_goal_s *goal); +/* High-priority commit for item-timing pursuit (goal need not be spawned yet). */ +int BotItems_BeginTimingCommit(struct bot_state_s *bs, struct bot_goal_s *goal, + int kind, float until); +/* Brief detour while a timing primary commit is suspended. */ +int BotItems_BeginDetourCommit(struct bot_state_s *bs, struct bot_goal_s *goal, + int kind, float until); +int BotItems_SuspendTimingPrimary(struct bot_state_s *bs); +void BotItems_CancelDetourSuspend(struct bot_state_s *bs); +int BotItems_IsDetourCommit(const struct bot_state_s *bs); +/* Drop timing commit without replanning (timing preempt handoff). */ +void BotItems_AbortTimingCommitQuiet(struct bot_state_s *bs); +int BotItems_RefreshItemGoal(struct bot_state_s *bs, struct bot_goal_s *goal, + int kind); +int BotItems_GoalPickupPresent(struct bot_state_s *bs, struct bot_goal_s *goal); + +/* Ledge jump-up pickup (visible item <=64u above, game-only). */ +void BotItems_OnPostMoveToGoal(struct bot_state_s *bs, struct bot_moveresult_s *mr); +void BotItems_OnInputFrame(struct bot_state_s *bs, struct bot_input_s *bi); +int BotItems_SuppressBlockedAvoid(struct bot_state_s *bs); + #endif /* AI_BOT_ITEMS_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_move_harness.c b/ratoa_gamecode/code/game/ai_bot_move_harness.c index 44275d3..d741085 100644 --- a/ratoa_gamecode/code/game/ai_bot_move_harness.c +++ b/ratoa_gamecode/code/game/ai_bot_move_harness.c @@ -4,9 +4,9 @@ BOT MOVE HARNESS — botlib movement bypass for enhanced aim; rocket jump maneuv walk-off ledge fall-damage avoidance. bot_enhanced_movement gates the enhanced rocket-jump maneuver and the walk-off -ledge fall-damage check. The aim-motor bypass (BotMoveHarness_IsActive) still -follows bot_enhanced_aim so botlib can own view during native RJ travel even when -movement enhancements are off. +ledge fall-damage check when bot_enhanced is on. The aim-motor bypass +(BotMoveHarness_IsActive) follows bot_enhanced so botlib can own view during +native RJ travel. =========================================================================== */ @@ -26,16 +26,29 @@ movement enhancements are off. #include "ai_bot_move_util.h" #include "ai_bot_move_harness.h" #include "ai_bot_items.h" +#include "ai_bot_position.h" +#include "ai_dmq3.h" -vmCvar_t bot_enhanced_movement; +extern vmCvar_t bot_grapple; #define BOTMOVE_BYPASS_LATCH_SEC 2.5f -#define BOTMOVE_WALKOFF_BLOCK_SEC 10.0f +/* Short routing ban after repeated/damage walkoff aborts (not every single step). */ +#define BOTMOVE_WALKOFF_BLOCK_SEC 2.5f +#define BOTMOVE_WALKOFF_BLOCK_DAMAGE_SEC 4.0f +#define BOTMOVE_WALKOFF_OSCILLATE_RADIUS 128.0f +#define BOTMOVE_WALKOFF_OSCILLATE_WINDOW 6.0f +#define BOTMOVE_WALKOFF_OSCILLATE_ABORTS 2 +#define BOTMOVE_WALKOFF_ALLOW_SEC 5.0f +#define BOTMOVE_WALKOFF_REQUIRED_SLACK 1.5f #define BOTMOVE_URGENT_HEALTH_SEC 14.0f #define BOTMOVE_WALKOFF_PRED_FRAMES 24 #define BOTMOVE_WALKOFF_PRED_FRAMETIME 0.1f -/* Abort a walkoff when the landing Z would be this far below a committed item goal. */ -#define BOTMOVE_COMMIT_ABOVE_THRESHOLD 96.0f +/* Max vertical drop (uu) before walk-off steps may be vetoed. */ +#define BOTMOVE_MAX_SAFE_WALKOFF_DROP 128.0f +/* Item goal Z vs landing Z (large drops only); same threshold as safe drop. */ +#define BOTMOVE_COMMIT_ABOVE_THRESHOLD BOTMOVE_MAX_SAFE_WALKOFF_DROP + +void BotMove_ClearWalkoffBlock(bot_state_t *bs); /* ---- Rocket jump tuning ---- */ #define BOTMOVE_RJ_VIEWTARGET_DIST 300.0f @@ -116,15 +129,13 @@ static int BotMove_SuppressEnhancedView(bot_state_t *bs) { /* ======================== Harness lifecycle ======================== */ void BotMoveHarness_RegisterCvars(void) { - trap_Cvar_Register(&bot_enhanced_movement, "bot_enhanced_movement", "0", CVAR_ARCHIVE); - trap_Cvar_Update(&bot_enhanced_movement); } void BotMoveHarness_Reset(bot_state_t *bs) { if (!bs) { return; } - bs->movej_no_walkoff_until = 0.0f; + BotMove_ClearWalkoffBlock(bs); bs->movej_urgent_health_until = 0.0f; bs->movej_moveresult_flags = 0; bs->movej_bypass_until = 0.0f; @@ -138,14 +149,14 @@ void BotMoveHarness_Reset(bot_state_t *bs) { BotMove_ClearRJ(bs); } -/* Aim-motor bypass gate: follows bot_enhanced_aim so botlib can own view. */ +/* Aim-motor bypass gate: active when bot_enhanced is on. */ int BotMoveHarness_IsActive(void) { - return BotEnhanced_AimActive(); + return BotEnhanced_IsActive(); } /* Movement-enhancement gate: rocket-jump maneuver and walk-off avoidance. */ static int BotMoveHarness_MovementActive(void) { - return BotEnhanced_MovementActive(); + return BotEnhanced_IsActive(); } void BotMove_CancelBypass(bot_state_t *bs) { @@ -632,16 +643,200 @@ static void BotMove_CancelWalkoffMoveresult(bot_state_t *bs, bot_moveresult_t *m trap_BotResetAvoidReach(bs->ms); } +int BotMove_WalkoffEscapeActive(bot_state_t *bs) { + if (!bs) { + return 0; + } + return bs->movej_walkoff_allow_until > FloatTime(); +} + +int BotMove_HasRecentWalkoffAbort(bot_state_t *bs) { + if (!bs || bs->movej_walkoff_abort_count <= 0) { + return 0; + } + return FloatTime() - bs->movej_walkoff_abort_window <= BOTMOVE_WALKOFF_OSCILLATE_WINDOW; +} + +void BotMove_TriggerWalkoffEscape(bot_state_t *bs) { + float now; + + if (!bs) { + return; + } + now = FloatTime(); + bs->movej_walkoff_allow_until = now + BOTMOVE_WALKOFF_ALLOW_SEC; + bs->movej_no_walkoff_until = 0.0f; + bs->movej_walkoff_abort_count = 0; + bs->movej_walkoff_abort_window = 0.0f; + VectorClear(bs->movej_walkoff_abort_origin); +} + +void BotMove_ClearWalkoffBlock(bot_state_t *bs) { + if (!bs) { + return; + } + bs->movej_no_walkoff_until = 0.0f; + bs->movej_walkoff_allow_until = 0.0f; + bs->movej_walkoff_abort_count = 0; + bs->movej_walkoff_abort_window = 0.0f; + VectorClear(bs->movej_walkoff_abort_origin); +} + +static int BotMove_GoalAreaForWalkoffCheck(bot_state_t *bs) { + bot_goal_t goal; + + if (!bs) { + return 0; + } + if (BotItems_HasActiveCommit(bs) && bs->item_commit_goal.areanum) { + return bs->item_commit_goal.areanum; + } + if (trap_BotGetTopGoal(bs->gs, &goal) && goal.areanum) { + return goal.areanum; + } + return 0; +} + +int BotMove_WalkoffRequiredForGoal(bot_state_t *bs, int routingTfl) { + int goalArea; + int travelFull; + int travelNoWalk; + int tflNoWalk; + + if (!bs || !BotMoveHarness_MovementActive()) { + return 0; + } + if (!(routingTfl & TFL_WALKOFFLEDGE)) { + return 0; + } + if (!bs->areanum || !trap_AAS_AreaReachability(bs->areanum)) { + return 0; + } + + goalArea = BotMove_GoalAreaForWalkoffCheck(bs); + if (!goalArea) { + return 0; + } + + tflNoWalk = routingTfl & ~TFL_WALKOFFLEDGE; + travelFull = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, + goalArea, routingTfl); + if (travelFull <= 0) { + return 0; + } + + travelNoWalk = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, + goalArea, tflNoWalk); + if (travelNoWalk <= 0) { + return 1; + } + if ((float)travelNoWalk > (float)travelFull * BOTMOVE_WALKOFF_REQUIRED_SLACK) { + return 1; + } + return 0; +} + +int BotMove_ShouldDeferCommitMoveFailure(bot_state_t *bs, bot_moveresult_t *mr) { + int routingTfl; + int tfl; + aas_clientmove_t move; + float drop; + int fallDamage; + int health; + + if (!bs || !BotMoveHarness_MovementActive()) { + return 0; + } + if (!BotItems_HasActiveCommit(bs)) { + return 0; + } + + tfl = bs->enh_travel_tfl_valid ? bs->enh_travel_tfl : BotMove_BuildTravelFlags(bs); + if (!(tfl & TFL_WALKOFFLEDGE)) { + BotMove_TriggerWalkoffEscape(bs); + bs->enh_travel_tfl = BotMove_BuildTravelFlags(bs); + bs->enh_travel_tfl_valid = qtrue; + return 1; + } + + if (mr && (mr->traveltype & TRAVELTYPE_MASK) == TRAVEL_WALKOFFLEDGE && + BotMove_RunWalkoffPredict(bs, mr->movedir, &move)) { + drop = bs->origin[2] - move.endpos[2]; + fallDamage = BotMove_EstimatePredictedFallDamage(&move, bs->origin); + health = bs->inventory[INVENTORY_HEALTH]; + if (health <= 0) { + health = bs->cur_ps.stats[STAT_HEALTH]; + } + if (drop > 0.0f && drop <= BOTMOVE_MAX_SAFE_WALKOFF_DROP && + (fallDamage <= 0 || (float)fallDamage < (float)health * 0.5f)) { + BotMove_TriggerWalkoffEscape(bs); + bs->enh_travel_tfl = BotMove_BuildTravelFlags(bs); + bs->enh_travel_tfl_valid = qtrue; + return 1; + } + } + + routingTfl = tfl | TFL_WALKOFFLEDGE; + if (BotMove_WalkoffRequiredForGoal(bs, routingTfl)) { + BotMove_TriggerWalkoffEscape(bs); + bs->enh_travel_tfl = BotMove_BuildTravelFlags(bs); + bs->enh_travel_tfl_valid = qtrue; + return 1; + } + return 0; +} + +static void BotMove_RecordWalkoffAbort(bot_state_t *bs, qboolean applyRoutingBan, + qboolean damageAbort) { + float now; + vec3_t delta; + + if (!bs) { + return; + } + now = FloatTime(); + + if (bs->movej_walkoff_abort_window <= 0.0f || + now - bs->movej_walkoff_abort_window > BOTMOVE_WALKOFF_OSCILLATE_WINDOW) { + bs->movej_walkoff_abort_window = now; + bs->movej_walkoff_abort_count = 0; + VectorCopy(bs->origin, bs->movej_walkoff_abort_origin); + } + + VectorSubtract(bs->origin, bs->movej_walkoff_abort_origin, delta); + delta[2] = 0.0f; + if (VectorLength(delta) > BOTMOVE_WALKOFF_OSCILLATE_RADIUS) { + bs->movej_walkoff_abort_window = now; + bs->movej_walkoff_abort_count = 0; + VectorCopy(bs->origin, bs->movej_walkoff_abort_origin); + } + + bs->movej_walkoff_abort_count++; + + if (bs->movej_walkoff_abort_count >= BOTMOVE_WALKOFF_OSCILLATE_ABORTS) { + BotMove_TriggerWalkoffEscape(bs); + return; + } + + if (applyRoutingBan && bs->movej_no_walkoff_until <= now) { + bs->movej_no_walkoff_until = now + (damageAbort ? + BOTMOVE_WALKOFF_BLOCK_DAMAGE_SEC : BOTMOVE_WALKOFF_BLOCK_SEC); + } +} + static qboolean BotMove_TryAbortRiskyWalkoff(bot_state_t *bs, bot_moveresult_t *mr, int travel) { aas_clientmove_t move; int health, fallDamage; qboolean abortForDamage, abortForItemZ; - float itemGoalZ; + float itemGoalZ, drop; if (!BotMoveHarness_MovementActive() || !bs || !mr) { return qfalse; } + if (BotMove_WalkoffEscapeActive(bs)) { + return qfalse; + } if ((g_dmflags.integer & DF_NO_FALLING) || (G_IsElimGT() && !g_elimination_selfdamage.integer)) { return qfalse; @@ -654,23 +849,25 @@ static qboolean BotMove_TryAbortRiskyWalkoff(bot_state_t *bs, bot_moveresult_t * return qfalse; } + drop = bs->origin[2] - move.endpos[2]; fallDamage = BotMove_EstimatePredictedFallDamage(&move, bs->origin); health = bs->inventory[INVENTORY_HEALTH]; if (health <= 0) { health = bs->cur_ps.stats[STAT_HEALTH]; } - /* Primary: abort when fall damage is at least half current health. */ + + /* Drops up to BOTMOVE_MAX_SAFE_WALKOFF_DROP are always allowed unless lethal. */ + if (drop <= BOTMOVE_MAX_SAFE_WALKOFF_DROP) { + if (fallDamage <= 0 || (float)fallDamage < (float)health * 0.5f) { + return qfalse; + } + } + abortForDamage = (fallDamage > 0 && (float)fallDamage >= (float)health * 0.5f); - /* - * Secondary: abort when the bot has an active item commit and the predicted - * landing Z would be significantly below the item goal's Z. Prevents the bot - * from dropping off a ledge to reach a lower area when its committed goal is - * above the landing point (common when jump-pad avoidance reroutes through a - * WALKOFFLEDGE reach). - */ abortForItemZ = qfalse; - if (!abortForDamage && BotItems_HasActiveCommit(bs)) { + if (!abortForDamage && drop > BOTMOVE_MAX_SAFE_WALKOFF_DROP && + BotItems_HasActiveCommit(bs)) { itemGoalZ = BotItems_GetCommitGoalOriginZ(bs); abortForItemZ = (itemGoalZ > move.endpos[2] + BOTMOVE_COMMIT_ABOVE_THRESHOLD); } @@ -680,32 +877,49 @@ static qboolean BotMove_TryAbortRiskyWalkoff(bot_state_t *bs, bot_moveresult_t * } BotMove_CancelWalkoffMoveresult(bs, mr); - bs->movej_no_walkoff_until = FloatTime() + BOTMOVE_WALKOFF_BLOCK_SEC; - bs->nbg_time = 0.0f; - bs->ltg_time = 0.0f; + if (abortForDamage) { - /* Health-threatening fall: also keep urgent-health seek active. */ + bs->nbg_time = 0.0f; + bs->ltg_time = 0.0f; bs->movej_urgent_health_until = FloatTime() + BOTMOVE_URGENT_HEALTH_SEC; BotItems_RequestUrgentHealth(bs); + BotMove_RecordWalkoffAbort(bs, qtrue, qtrue); + } else { + BotMove_RecordWalkoffAbort(bs, qfalse, qfalse); } return qtrue; } static int BotMove_ShouldAvoidWalkoffLedges(bot_state_t *bs) { + int routingTfl; + if (!bs || !BotMoveHarness_MovementActive()) { return 0; } - return bs->movej_no_walkoff_until > FloatTime(); -} + if (BotMove_WalkoffEscapeActive(bs)) { + return 0; + } + if (bs->movej_no_walkoff_until <= FloatTime()) { + return 0; + } -int BotMove_WantsUrgentHealth(bot_state_t *bs) { - if (!bs) { + routingTfl = TFL_DEFAULT; + if (bot_grapple.integer) { + routingTfl |= TFL_GRAPPLEHOOK; + } + if (BotInLavaOrSlime(bs)) { + routingTfl |= TFL_LAVA | TFL_SLIME; + } + if (BotCanAndWantsToRocketJump(bs)) { + routingTfl |= TFL_ROCKETJUMP; + } + if (BotMove_WalkoffRequiredForGoal(bs, routingTfl)) { return 0; } - return bs->movej_urgent_health_until > FloatTime(); + return 1; } -int BotMove_EffectiveTfl(bot_state_t *bs) { +int BotMove_BuildTravelFlags(bot_state_t *bs) { int tfl; if (!bs) { @@ -715,15 +929,63 @@ int BotMove_EffectiveTfl(bot_state_t *bs) { if (!tfl) { tfl = TFL_DEFAULT; } + if (bot_grapple.integer) { + tfl |= TFL_GRAPPLEHOOK; + } + if (BotInLavaOrSlime(bs)) { + tfl |= TFL_LAVA | TFL_SLIME; + } + if (BotCanAndWantsToRocketJump(bs)) { + tfl |= TFL_ROCKETJUMP; + } if (bs->jumppad_avoid_until > FloatTime()) { tfl &= ~TFL_JUMPPAD; } if (BotMove_ShouldAvoidWalkoffLedges(bs)) { tfl &= ~TFL_WALKOFFLEDGE; } + tfl = BotPosition_AdjustTravelFlags(bs, tfl); return tfl; } +int BotMove_IsAtLedgeEdge(bot_state_t *bs) { + aas_clientmove_t move; + + if (!bs) { + return 0; + } + if (bs->movej_travel_type == TRAVEL_WALKOFFLEDGE) { + return 1; + } + if (bs->movej_no_walkoff_until > FloatTime()) { + return 1; + } + if (VectorLengthSquared(bs->movej_movedir) > 0.01f && + BotMove_RunWalkoffPredict(bs, bs->movej_movedir, &move)) { + if (bs->origin[2] - move.endpos[2] > 48.0f) { + return 1; + } + } + return 0; +} + +int BotMove_WantsUrgentHealth(bot_state_t *bs) { + if (!bs) { + return 0; + } + return bs->movej_urgent_health_until > FloatTime(); +} + +int BotMove_EffectiveTfl(bot_state_t *bs) { + if (!bs) { + return TFL_DEFAULT; + } + if (bs->enh_travel_tfl_valid) { + return bs->enh_travel_tfl; + } + return BotMove_BuildTravelFlags(bs); +} + /* ======================== Think / input hooks ======================== */ void BotMove_OnPostMoveToGoal(bot_state_t *bs, bot_moveresult_t *mr) { @@ -747,6 +1009,7 @@ void BotMove_OnPostMoveToGoal(bot_state_t *bs, bot_moveresult_t *mr) { return; } BotMove_RJ_OnPostMove(bs, mr, travel); + BotItems_OnPostMoveToGoal(bs, mr); } void BotMove_OnInputFrame(bot_state_t *bs, int time, float thinktime) { @@ -771,6 +1034,8 @@ void BotMove_OnInputFrame(bot_state_t *bs, int time, float thinktime) { BotMove_RJ_UpdateView(bs, worldView); BotMove_RJ_TryFire(bs, &bi); + BotItems_OnInputFrame(bs, &bi); + if (bs->movej_rj_fired && bs->movej_rj_prep_view_until > FloatTime()) { BotMove_RJ_ApplyFireView(bs, worldView); bi.actionflags |= ACTION_ATTACK | ACTION_JUMP; @@ -781,3 +1046,188 @@ void BotMove_OnInputFrame(bot_state_t *bs, int time, float thinktime) { VectorCopy(worldView, bi.viewangles); BotInputToUserCommand(&bi, &bs->lastucmd, bs->cur_ps.delta_angles, time); } + +/* +=========================================================================== +BOT MOVE UTIL — shared helpers for movement harness maneuvers. +=========================================================================== +*/ + +float BotMoveUtil_HorizDist(const vec3_t a, const vec3_t b) { + vec3_t delta; + + VectorSubtract(b, a, delta); + delta[2] = 0.0f; + return VectorLength(delta); +} + +float BotMoveUtil_HorizSpeed(bot_state_t *bs) { + vec3_t vel; + + VectorCopy(bs->cur_ps.velocity, vel); + vel[2] = 0.0f; + return VectorLength(vel); +} + +int BotMoveUtil_HorizDir(const vec3_t from, const vec3_t to, vec3_t dir) { + VectorSubtract(to, from, dir); + dir[2] = 0.0f; + return VectorNormalize(dir) > 0.1f; +} + +float BotMoveUtil_ApproachSpeed(float dist, float hold, float slowRadius, float cap, + float minSpd) { + float speed, range; + + if (dist <= hold) { + return 0.0f; + } + if (dist >= slowRadius) { + return cap; + } + range = slowRadius - hold; + if (range < 1.0f) { + range = 1.0f; + } + speed = cap * (dist - hold) / range; + if (speed < minSpd) { + speed = minSpd; + } + return speed; +} + +float BotMoveUtil_ApproachSpeedVel(float dist, float hold, float slowRadius, float cap, + float minSpd, float hvel, float maxHvelNear) { + float speed, velCap; + + speed = BotMoveUtil_ApproachSpeed(dist, hold, slowRadius, cap, minSpd); + if (dist >= slowRadius || hvel <= maxHvelNear) { + return speed; + } + + velCap = maxHvelNear; + if (dist > hold) { + velCap = maxHvelNear * (dist - hold) / (slowRadius - hold); + } + if (velCap < 8.0f) { + velCap = 8.0f; + } + if (speed > velCap) { + speed = velCap; + } + if (dist <= hold + 8.0f && hvel > maxHvelNear * 0.9f) { + speed = 0.0f; + } + return speed; +} + +void BotMoveUtil_BiWalk(bot_input_t *bi, const vec3_t dir, float speed) { + bi->actionflags &= ~(ACTION_MOVEFORWARD | ACTION_MOVEBACK | + ACTION_MOVELEFT | ACTION_MOVERIGHT); + VectorCopy(dir, bi->dir); + bi->speed = speed; +} + +void BotMoveUtil_BiStopWalk(bot_input_t *bi) { + bi->actionflags &= ~(ACTION_MOVEFORWARD | ACTION_MOVEBACK | + ACTION_MOVELEFT | ACTION_MOVERIGHT); + VectorClear(bi->dir); + bi->speed = 0; +} + +static void BotMoveView_ResetSpeed(bot_state_t *bs) { + bs->viewanglespeed[0] = 0.0f; + bs->viewanglespeed[1] = 0.0f; +} + +void BotMoveView_WorldToStored(bot_state_t *bs, const vec3_t world, vec3_t stored) { + int j; + + VectorCopy(world, stored); + if (!BotEnhanced_IsActive()) { + return; + } + for (j = 0; j < 3; j++) { + stored[j] = AngleMod(stored[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); + } +} + +void BotMoveView_StoredToWorld(bot_state_t *bs, const vec3_t stored, vec3_t world) { + int j; + + VectorCopy(stored, world); + if (!BotEnhanced_IsActive()) { + return; + } + for (j = 0; j < 3; j++) { + world[j] = AngleMod(world[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); + } +} + +void BotMoveView_SetWorld(bot_state_t *bs, const vec3_t worldViewIn) { + vec3_t worldView, stored; + + VectorCopy(worldViewIn, worldView); + worldView[PITCH] = AngleMod(worldView[PITCH]); + worldView[YAW] = AngleMod(worldView[YAW]); + worldView[ROLL] = 0.0f; + + trap_EA_View(bs->client, worldView); + BotMoveView_WorldToStored(bs, worldView, stored); + VectorCopy(stored, bs->viewangles); + BotMoveView_ResetSpeed(bs); +} + +void BotMoveView_ApplyIdeal(bot_state_t *bs, const vec3_t ideal) { + VectorCopy(ideal, bs->ideal_viewangles); + VectorCopy(ideal, bs->movej_move_viewangles); + BotMoveView_SetWorld(bs, ideal); +} + +void BotMoveUtil_LatchBypass(bot_state_t *bs, float seconds) { + bs->movej_bypass_until = FloatTime() + seconds; +} + +int BotMoveUtil_BypassActive(bot_state_t *bs) { + return bs && bs->movej_bypass_until > FloatTime(); +} + +int BotMoveUtil_HasMovementView(int flags) { + return (flags & BOTMOVE_VIEW_FLAGS) != 0; +} + +int BotMoveUtil_IsWeaponJumpTravel(int travel) { + return travel == TRAVEL_ROCKETJUMP || travel == TRAVEL_BFGJUMP; +} + +void BotMoveUtil_CacheHorizMovedir(bot_state_t *bs, bot_moveresult_t *mr) { + vec3_t hordir; + + if (!mr || VectorLengthSquared(mr->movedir) < 0.01f) { + return; + } + VectorCopy(mr->movedir, hordir); + hordir[2] = 0.0f; + if (VectorNormalize(hordir) > 0.1f) { + VectorCopy(hordir, bs->movej_movedir); + } +} + +int BotMoveUtil_GetTopGoalOrigin(bot_state_t *bs, vec3_t origin) { + bot_goal_t goal; + + if (!bs || !trap_BotGetTopGoal(bs->gs, &goal)) { + return 0; + } + VectorCopy(goal.origin, origin); + return 1; +} + +int BotMoveUtil_MovementViewTarget(bot_state_t *bs, float maxDist, vec3_t target) { + bot_goal_t goal; + + if (!bs || !trap_BotGetTopGoal(bs->gs, &goal)) { + return 0; + } + return trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, maxDist, target); +} diff --git a/ratoa_gamecode/code/game/ai_bot_move_harness.h b/ratoa_gamecode/code/game/ai_bot_move_harness.h index ae57f67..60d5da0 100644 --- a/ratoa_gamecode/code/game/ai_bot_move_harness.h +++ b/ratoa_gamecode/code/game/ai_bot_move_harness.h @@ -3,15 +3,12 @@ BOT MOVE HARNESS — botlib movement view/weapon compatibility for enhanced aim; enhanced rocket-jump maneuver; walk-off ledge fall-damage avoidance. -BotMoveHarness_IsActive() follows bot_enhanced_aim: aim-motor bypass so botlib +BotMoveHarness_IsActive() follows bot_enhanced: aim-motor bypass so botlib owns view during native MOVERESULT_MOVEMENT* travel (rocket jump, swim, etc.). -bot_enhanced_movement gates the enhanced RJ maneuver prep/fire and the walk-off -ledge fall-damage check (BotEnhanced_MovementActive). Both gates must be true for -the enhanced RJ path; walk-off avoidance only needs movement. - -Shared geometry/view helpers: ai_bot_move_util.c. Maneuvers (rocket jump today) -live in ai_bot_move_harness.c; add new ones via OnPostMoveToGoal / OnInputFrame. +When bot_enhanced is on: enhanced RJ maneuver prep/fire and walk-off ledge +fall-damage check. Shared geometry/view helpers live in this file; see +ai_bot_move_util.h for declarations. =========================================================================== */ @@ -29,8 +26,19 @@ void BotMove_OnPostMoveToGoal(struct bot_state_s *bs, struct bot_moveresult_s *m void BotMove_CancelBypass(struct bot_state_s *bs); /* Travel flags for routing/move (jumppad avoid, walk-off block). */ +int BotMove_BuildTravelFlags(struct bot_state_s *bs); int BotMove_EffectiveTfl(struct bot_state_s *bs); int BotMove_WantsUrgentHealth(struct bot_state_s *bs); +int BotMove_IsAtLedgeEdge(struct bot_state_s *bs); + +int BotMove_WalkoffEscapeActive(struct bot_state_s *bs); +int BotMove_HasRecentWalkoffAbort(struct bot_state_s *bs); +void BotMove_TriggerWalkoffEscape(struct bot_state_s *bs); +void BotMove_ClearWalkoffBlock(struct bot_state_s *bs); + +int BotMove_WalkoffRequiredForGoal(struct bot_state_s *bs, int routingTfl); +int BotMove_ShouldDeferCommitMoveFailure(struct bot_state_s *bs, + struct bot_moveresult_s *mr); int BotMove_SuppressesAimMotor(struct bot_state_s *bs); int BotMove_SuppressRoamView(struct bot_state_s *bs); diff --git a/ratoa_gamecode/code/game/ai_bot_move_util.c b/ratoa_gamecode/code/game/ai_bot_move_util.c deleted file mode 100644 index f76f635..0000000 --- a/ratoa_gamecode/code/game/ai_bot_move_util.c +++ /dev/null @@ -1,197 +0,0 @@ -/* -=========================================================================== -BOT MOVE UTIL -=========================================================================== -*/ - -#include "g_local.h" -#include "../botlib/botlib.h" -#include "../botlib/be_aas.h" -#include "../botlib/be_ea.h" -#include "../botlib/be_ai_char.h" -#include "../botlib/be_ai_goal.h" -#include "../botlib/be_ai_move.h" -#include "../botlib/be_ai_weap.h" -#include "../botlib/aasfile.h" -#include "ai_main.h" -#include "ai_bot_enhanced.h" -#include "ai_bot_move_util.h" - -float BotMoveUtil_HorizDist(const vec3_t a, const vec3_t b) { - vec3_t delta; - - VectorSubtract(b, a, delta); - delta[2] = 0.0f; - return VectorLength(delta); -} - -float BotMoveUtil_HorizSpeed(bot_state_t *bs) { - vec3_t vel; - - VectorCopy(bs->cur_ps.velocity, vel); - vel[2] = 0.0f; - return VectorLength(vel); -} - -int BotMoveUtil_HorizDir(const vec3_t from, const vec3_t to, vec3_t dir) { - VectorSubtract(to, from, dir); - dir[2] = 0.0f; - return VectorNormalize(dir) > 0.1f; -} - -float BotMoveUtil_ApproachSpeed(float dist, float hold, float slowRadius, float cap, - float minSpd) { - float speed, range; - - if (dist <= hold) { - return 0.0f; - } - if (dist >= slowRadius) { - return cap; - } - range = slowRadius - hold; - if (range < 1.0f) { - range = 1.0f; - } - speed = cap * (dist - hold) / range; - if (speed < minSpd) { - speed = minSpd; - } - return speed; -} - -float BotMoveUtil_ApproachSpeedVel(float dist, float hold, float slowRadius, float cap, - float minSpd, float hvel, float maxHvelNear) { - float speed, velCap; - - speed = BotMoveUtil_ApproachSpeed(dist, hold, slowRadius, cap, minSpd); - if (dist >= slowRadius || hvel <= maxHvelNear) { - return speed; - } - - velCap = maxHvelNear; - if (dist > hold) { - velCap = maxHvelNear * (dist - hold) / (slowRadius - hold); - } - if (velCap < 8.0f) { - velCap = 8.0f; - } - if (speed > velCap) { - speed = velCap; - } - if (dist <= hold + 8.0f && hvel > maxHvelNear * 0.9f) { - speed = 0.0f; - } - return speed; -} - -void BotMoveUtil_BiWalk(bot_input_t *bi, const vec3_t dir, float speed) { - bi->actionflags &= ~(ACTION_MOVEFORWARD | ACTION_MOVEBACK | - ACTION_MOVELEFT | ACTION_MOVERIGHT); - VectorCopy(dir, bi->dir); - bi->speed = speed; -} - -void BotMoveUtil_BiStopWalk(bot_input_t *bi) { - bi->actionflags &= ~(ACTION_MOVEFORWARD | ACTION_MOVEBACK | - ACTION_MOVELEFT | ACTION_MOVERIGHT); - VectorClear(bi->dir); - bi->speed = 0; -} - -static void BotMoveView_ResetSpeed(bot_state_t *bs) { - bs->viewanglespeed[0] = 0.0f; - bs->viewanglespeed[1] = 0.0f; -} - -void BotMoveView_WorldToStored(bot_state_t *bs, const vec3_t world, vec3_t stored) { - int j; - - VectorCopy(world, stored); - if (!BotEnhanced_AimActive()) { - return; - } - for (j = 0; j < 3; j++) { - stored[j] = AngleMod(stored[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); - } -} - -void BotMoveView_StoredToWorld(bot_state_t *bs, const vec3_t stored, vec3_t world) { - int j; - - VectorCopy(stored, world); - if (!BotEnhanced_AimActive()) { - return; - } - for (j = 0; j < 3; j++) { - world[j] = AngleMod(world[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); - } -} - -void BotMoveView_SetWorld(bot_state_t *bs, const vec3_t worldViewIn) { - vec3_t worldView, stored; - - VectorCopy(worldViewIn, worldView); - worldView[PITCH] = AngleMod(worldView[PITCH]); - worldView[YAW] = AngleMod(worldView[YAW]); - worldView[ROLL] = 0.0f; - - trap_EA_View(bs->client, worldView); - BotMoveView_WorldToStored(bs, worldView, stored); - VectorCopy(stored, bs->viewangles); - BotMoveView_ResetSpeed(bs); -} - -void BotMoveView_ApplyIdeal(bot_state_t *bs, const vec3_t ideal) { - VectorCopy(ideal, bs->ideal_viewangles); - VectorCopy(ideal, bs->movej_move_viewangles); - BotMoveView_SetWorld(bs, ideal); -} - -void BotMoveUtil_LatchBypass(bot_state_t *bs, float seconds) { - bs->movej_bypass_until = FloatTime() + seconds; -} - -int BotMoveUtil_BypassActive(bot_state_t *bs) { - return bs && bs->movej_bypass_until > FloatTime(); -} - -int BotMoveUtil_HasMovementView(int flags) { - return (flags & BOTMOVE_VIEW_FLAGS) != 0; -} - -int BotMoveUtil_IsWeaponJumpTravel(int travel) { - return travel == TRAVEL_ROCKETJUMP || travel == TRAVEL_BFGJUMP; -} - -void BotMoveUtil_CacheHorizMovedir(bot_state_t *bs, bot_moveresult_t *mr) { - vec3_t hordir; - - if (!mr || VectorLengthSquared(mr->movedir) < 0.01f) { - return; - } - VectorCopy(mr->movedir, hordir); - hordir[2] = 0.0f; - if (VectorNormalize(hordir) > 0.1f) { - VectorCopy(hordir, bs->movej_movedir); - } -} - -int BotMoveUtil_GetTopGoalOrigin(bot_state_t *bs, vec3_t origin) { - bot_goal_t goal; - - if (!bs || !trap_BotGetTopGoal(bs->gs, &goal)) { - return 0; - } - VectorCopy(goal.origin, origin); - return 1; -} - -int BotMoveUtil_MovementViewTarget(bot_state_t *bs, float maxDist, vec3_t target) { - bot_goal_t goal; - - if (!bs || !trap_BotGetTopGoal(bs->gs, &goal)) { - return 0; - } - return trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, maxDist, target); -} diff --git a/ratoa_gamecode/code/game/ai_bot_nav_guard.c b/ratoa_gamecode/code/game/ai_bot_nav_guard.c new file mode 100644 index 0000000..266559c --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_nav_guard.c @@ -0,0 +1,473 @@ +/* +=========================================================================== +BOT NAV GUARD +=========================================================================== +*/ + +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "ai_dmnet.h" +#include "ai_dmq3.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_items.h" +#include "ai_bot_item_timing.h" +#include "ai_bot_position.h" +#include "ai_bot_combat.h" +#include "ai_bot_nav_guard.h" +#include "ai_bot_move_harness.h" + +#define BOTNAV_IDLE_DIST 48.0f +#define BOTNAV_IDLE_TIME 2.5f +#define BOTNAV_RING_INTERVAL 0.45f +#define BOTNAV_LOOP_MAX_PATH 300.0f +#define BOTNAV_LOOP_REVISIT_DIST 88.0f +#define BOTNAV_LOOP_MIN_REVISITS 3 +#define BOTNAV_BREAKOUT_COOLDOWN 3.0f +#define BOTNAV_STAIR_GOAL_MIN_BELOW 20.0f +#define BOTNAV_STAIR_NEAR_HORIZ 512.0f +#define BOTNAV_STAIR_Z_SWING 28.0f +#define BOTNAV_STAIR_LOOP_MAX_PATH 480.0f +#define BOTNAV_STAIR_MIN_REVISITS 2 +#define BOTNAV_PURSUIT_BLOCK_SEC 18.0f + +static int BotNavGuard_RingSampleIndex(const bot_state_t *bs, int order); +static int BotNavGuard_MaxPositionRevisits(bot_state_t *bs); + +static int BotNavGuard_IsActive(void) { + return BotEnhanced_IsActive(); +} + +int BotNavGuard_HasIdleOrLoopRisk(bot_state_t *bs) { + float now; + + if (!bs) { + return 0; + } + now = FloatTime(); + if (bs->nav_progress_time > 0.0f && + now - bs->nav_progress_time >= BOTNAV_IDLE_TIME * 0.5f) { + return 1; + } + if (bs->nav_ring_count >= BOTNAV_LOOP_MIN_REVISITS) { + return 1; + } + return 0; +} + +static float BotNavGuard_HorizSpeed(bot_state_t *bs) { + vec3_t vel; + + if (!bs || !BotAI_GetClientState(bs->client, &bs->cur_ps)) { + return 0.0f; + } + VectorCopy(bs->cur_ps.velocity, vel); + vel[2] = 0.0f; + return VectorLength(vel); +} + +static int BotNavGuard_OnExemptNode(bot_state_t *bs) { + if (!bs || !bs->ainode) { + return 0; + } + if (bs->ainode == AINode_Stand || + bs->ainode == AINode_Respawn || + bs->ainode == AINode_Intermission || + bs->ainode == AINode_Observer) { + return 1; + } + if (bs->ainode == AINode_Seek_ActivateEntity && bs->activatestack) { + return 1; + } + return 0; +} + +static int BotNavGuard_IsDeliberateStillness(bot_state_t *bs) { + if (!bs) { + return 1; + } + if (BotNavGuard_OnExemptNode(bs)) { + return 1; + } + if (BotAI_GetClientState(bs->client, &bs->cur_ps) && + bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + return 1; + } + if (BotItems_TimingHoldingNearGoal(bs)) { + return 1; + } + if (BotItemTiming_IsActive() && BotItemTiming_ShouldWaitAtPad(bs)) { + return 1; + } + if (BotPosition_IsItemHarassActive(bs)) { + return 1; + } + if (BotCombat_IsLedgeHold(bs)) { + return 1; + } + if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS && + (bs->ainode == AINode_Battle_Fight || + bs->ainode == AINode_Battle_Chase || + bs->ainode == AINode_Battle_Retreat || + bs->ainode == AINode_Battle_NBG)) { + return 1; + } + return 0; +} + +static void BotNavGuard_ClearRing(bot_state_t *bs) { + int i; + + if (!bs) { + return; + } + bs->nav_ring_count = 0; + bs->nav_ring_pos = 0; + bs->nav_next_ring_sample = 0.0f; + for (i = 0; i < BOTNAV_RING_SAMPLES; i++) { + bs->nav_ring_areanum[i] = 0; + VectorClear(bs->nav_ring_origin[i]); + } +} + +static void BotNavGuard_ResetProgress(bot_state_t *bs) { + if (!bs) { + return; + } + bs->nav_progress_time = FloatTime(); + VectorCopy(bs->origin, bs->nav_progress_origin); +} + +static void BotNavGuard_PushRingSample(bot_state_t *bs) { + int idx; + + if (!bs) { + return; + } + idx = bs->nav_ring_pos; + bs->nav_ring_areanum[idx] = bs->areanum; + VectorCopy(bs->origin, bs->nav_ring_origin[idx]); + bs->nav_ring_pos = (bs->nav_ring_pos + 1) % BOTNAV_RING_SAMPLES; + if (bs->nav_ring_count < BOTNAV_RING_SAMPLES) { + bs->nav_ring_count++; + } + bs->nav_next_ring_sample = FloatTime() + BOTNAV_RING_INTERVAL; +} + +static float BotNavGuard_RingPathLength(bot_state_t *bs) { + vec3_t delta; + float total; + int i; + int start; + int prevIdx; + int idx; + + if (!bs || bs->nav_ring_count < 2) { + return 0.0f; + } + + start = (bs->nav_ring_pos - bs->nav_ring_count + BOTNAV_RING_SAMPLES) % + BOTNAV_RING_SAMPLES; + prevIdx = start; + total = 0.0f; + for (i = 1; i < bs->nav_ring_count; i++) { + idx = (start + i) % BOTNAV_RING_SAMPLES; + VectorSubtract(bs->nav_ring_origin[idx], bs->nav_ring_origin[prevIdx], delta); + delta[2] = 0.0f; + total += VectorLength(delta); + prevIdx = idx; + } + return total; +} + +static float BotNavGuard_RingPathLength3D(bot_state_t *bs) { + vec3_t delta; + float total; + int i; + int start; + int prevIdx; + int idx; + + if (!bs || bs->nav_ring_count < 2) { + return 0.0f; + } + + start = (bs->nav_ring_pos - bs->nav_ring_count + BOTNAV_RING_SAMPLES) % + BOTNAV_RING_SAMPLES; + prevIdx = start; + total = 0.0f; + for (i = 1; i < bs->nav_ring_count; i++) { + idx = (start + i) % BOTNAV_RING_SAMPLES; + VectorSubtract(bs->nav_ring_origin[idx], bs->nav_ring_origin[prevIdx], delta); + total += VectorLength(delta); + prevIdx = idx; + } + return total; +} + +static float BotNavGuard_RingZSwing(bot_state_t *bs) { + float minZ; + float maxZ; + int i; + int idx; + + if (!bs || bs->nav_ring_count < 2) { + return 0.0f; + } + + idx = BotNavGuard_RingSampleIndex(bs, 0); + minZ = maxZ = bs->nav_ring_origin[idx][2]; + for (i = 1; i < bs->nav_ring_count; i++) { + idx = BotNavGuard_RingSampleIndex(bs, i); + if (bs->nav_ring_origin[idx][2] < minZ) { + minZ = bs->nav_ring_origin[idx][2]; + } + if (bs->nav_ring_origin[idx][2] > maxZ) { + maxZ = bs->nav_ring_origin[idx][2]; + } + } + return maxZ - minZ; +} + +static int BotNavGuard_CommittedGoalBelowNearby(bot_state_t *bs) { + vec3_t delta; + float goalZ; + float drop; + float horiz; + + if (!bs || !BotItems_HasActiveCommit(bs)) { + return 0; + } + goalZ = BotItems_GetCommitGoalOriginZ(bs); + drop = bs->origin[2] - goalZ; + if (drop < BOTNAV_STAIR_GOAL_MIN_BELOW) { + return 0; + } + VectorSubtract(bs->item_commit_goal.origin, bs->origin, delta); + delta[2] = 0.0f; + horiz = VectorLength(delta); + return horiz <= BOTNAV_STAIR_NEAR_HORIZ; +} + +static int BotNavGuard_DetectStairGoalLoop(bot_state_t *bs) { + float path3d; + float zSwing; + int revisits; + + if (!BotNavGuard_CommittedGoalBelowNearby(bs)) { + return 0; + } + if (bs->nav_ring_count < BOTNAV_STAIR_MIN_REVISITS + 1) { + return 0; + } + zSwing = BotNavGuard_RingZSwing(bs); + if (zSwing < BOTNAV_STAIR_Z_SWING) { + return 0; + } + path3d = BotNavGuard_RingPathLength3D(bs); + if (path3d > BOTNAV_STAIR_LOOP_MAX_PATH) { + return 0; + } + revisits = BotNavGuard_MaxPositionRevisits(bs); + return revisits >= BOTNAV_STAIR_MIN_REVISITS; +} + +static int BotNavGuard_RingSampleIndex(const bot_state_t *bs, int order) { + return (bs->nav_ring_pos - bs->nav_ring_count + order + BOTNAV_RING_SAMPLES) % + BOTNAV_RING_SAMPLES; +} + +static int BotNavGuard_MaxPositionRevisits(bot_state_t *bs) { + vec3_t delta; + int i; + int j; + int near; + int maxNear; + int idxI; + int idxJ; + + if (!bs || bs->nav_ring_count < BOTNAV_LOOP_MIN_REVISITS) { + return 0; + } + + maxNear = 0; + for (i = 0; i < bs->nav_ring_count; i++) { + idxI = BotNavGuard_RingSampleIndex(bs, i); + near = 0; + for (j = 0; j < bs->nav_ring_count; j++) { + idxJ = BotNavGuard_RingSampleIndex(bs, j); + VectorSubtract(bs->nav_ring_origin[idxI], bs->nav_ring_origin[idxJ], delta); + delta[2] = 0.0f; + if (VectorLength(delta) <= BOTNAV_LOOP_REVISIT_DIST) { + near++; + } + } + if (near > maxNear) { + maxNear = near; + } + } + return maxNear; +} + +static int BotNavGuard_DetectShortLoop(bot_state_t *bs) { + float pathLen; + int revisits; + + if (!bs || bs->nav_ring_count < BOTNAV_LOOP_MIN_REVISITS) { + return 0; + } + pathLen = BotNavGuard_RingPathLength(bs); + if (pathLen > BOTNAV_LOOP_MAX_PATH) { + return 0; + } + revisits = BotNavGuard_MaxPositionRevisits(bs); + return revisits >= BOTNAV_LOOP_MIN_REVISITS; +} + +static void BotNavGuard_BreakOut(bot_state_t *bs, const char *reason, int stairLoop) { + if (!bs) { + return; + } + + if (BotEnhanced_DebugActive() && reason) { + char netname[MAX_NETNAME]; + + ClientName(bs->client, netname, sizeof(netname)); + BotAI_Print(PRT_MESSAGE, "nav guard: %s breakout for %s\n", reason, netname); + } + + bs->nav_breakout_cooldown_until = FloatTime() + BOTNAV_BREAKOUT_COOLDOWN; + + BotPosition_CancelUplift(bs); + BotMove_ClearWalkoffBlock(bs); + + if (stairLoop) { + BotItemTiming_BlockPursuitAtGoal(bs, BOTNAV_PURSUIT_BLOCK_SEC); + } else if (BotItems_HasActiveCommit(bs)) { + BotItems_AbortCommit(bs); + } + + trap_BotResetAvoidReach(bs->ms); + trap_BotResetAvoidGoals(bs->gs); + bs->ltg_time = 0.0f; + bs->nbg_time = 0.0f; + BotEnhanced_DedupeGoalStack(bs); + BotNavGuard_ResetProgress(bs); + BotNavGuard_ClearRing(bs); +} + +static int BotNavGuard_LedgeWalkoffStuck(bot_state_t *bs, int idle, int loop) { + if (!idle && !loop) { + return 0; + } + if (!BotMove_IsAtLedgeEdge(bs)) { + return 0; + } + if (BotMove_WalkoffEscapeActive(bs)) { + return 0; + } + if (bs->movej_no_walkoff_until > FloatTime()) { + return 1; + } + if (BotMove_HasRecentWalkoffAbort(bs)) { + return 1; + } + return 0; +} + +static void BotNavGuard_BreakOutLedgeWalkoff(bot_state_t *bs, const char *reason) { + if (!bs) { + return; + } + + if (BotEnhanced_DebugActive() && reason) { + char netname[MAX_NETNAME]; + + ClientName(bs->client, netname, sizeof(netname)); + BotAI_Print(PRT_MESSAGE, "nav guard: %s ledge walkoff escape for %s\n", + reason, netname); + } + + bs->nav_breakout_cooldown_until = FloatTime() + BOTNAV_BREAKOUT_COOLDOWN; + BotMove_TriggerWalkoffEscape(bs); + trap_BotResetAvoidReach(bs->ms); + bs->ltg_time = 0.0f; + bs->nbg_time = 0.0f; + BotNavGuard_ResetProgress(bs); + BotNavGuard_ClearRing(bs); +} + +void BotNavGuard_Reset(bot_state_t *bs) { + if (!bs) { + return; + } + bs->nav_breakout_cooldown_until = 0.0f; + BotNavGuard_ResetProgress(bs); + BotNavGuard_ClearRing(bs); +} + +void BotNavGuard_OnThinkStart(bot_state_t *bs) { + vec3_t delta; + float dist; + float now; + int idle; + int loop; + int stairLoop; + + if (!BotNavGuard_IsActive() || !bs || !bs->inuse) { + return; + } + if (BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + + now = FloatTime(); + if (now < bs->nav_breakout_cooldown_until) { + return; + } + if (BotNavGuard_IsDeliberateStillness(bs)) { + BotNavGuard_ResetProgress(bs); + if (now >= bs->nav_next_ring_sample) { + BotNavGuard_PushRingSample(bs); + } + return; + } + + VectorSubtract(bs->origin, bs->nav_progress_origin, delta); + dist = VectorLength(delta); + if (dist >= BOTNAV_IDLE_DIST || BotNavGuard_HorizSpeed(bs) >= 24.0f) { + BotNavGuard_ResetProgress(bs); + } else if (bs->nav_progress_time <= 0.0f) { + BotNavGuard_ResetProgress(bs); + } + + if (now >= bs->nav_next_ring_sample) { + BotNavGuard_PushRingSample(bs); + } + + idle = (now - bs->nav_progress_time >= BOTNAV_IDLE_TIME); + loop = BotNavGuard_DetectShortLoop(bs); + stairLoop = BotNavGuard_DetectStairGoalLoop(bs); + + if (stairLoop) { + BotNavGuard_BreakOut(bs, "stair loop", 1); + return; + } + if ((loop || idle) && BotNavGuard_LedgeWalkoffStuck(bs, idle, loop)) { + BotNavGuard_BreakOutLedgeWalkoff(bs, idle ? "idle" : "loop"); + return; + } + if (loop) { + BotNavGuard_BreakOut(bs, "loop", 0); + return; + } + if (idle) { + BotNavGuard_BreakOut(bs, "idle", 0); + } +} diff --git a/ratoa_gamecode/code/game/ai_bot_nav_guard.h b/ratoa_gamecode/code/game/ai_bot_nav_guard.h new file mode 100644 index 0000000..5acefc4 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_nav_guard.h @@ -0,0 +1,24 @@ +/* +=========================================================================== +BOT NAV GUARD — idle and short-path loop breakout for enhanced bots. + +Gate: bot_enhanced = 1. + +Detects when a bot has not moved for a few seconds (and is not deliberately +camping / holding) or when it keeps revisiting the same small area, then resets +routing state and forces a fresh long-term goal pick. +=========================================================================== +*/ + +#ifndef AI_BOT_NAV_GUARD_H +#define AI_BOT_NAV_GUARD_H + +struct bot_state_s; + +#define BOTNAV_RING_SAMPLES 6 + +void BotNavGuard_Reset(struct bot_state_s *bs); +void BotNavGuard_OnThinkStart(struct bot_state_s *bs); +int BotNavGuard_HasIdleOrLoopRisk(struct bot_state_s *bs); + +#endif /* AI_BOT_NAV_GUARD_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_opponent.c b/ratoa_gamecode/code/game/ai_bot_opponent.c new file mode 100644 index 0000000..3a1efbf --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_opponent.c @@ -0,0 +1,1850 @@ +/* +=========================================================================== +BOT OPPONENT — human-like beliefs about the sole hostile opponent in 1v1. +=========================================================================== +*/ + +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "chars.h" +#include "inv.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_combat.h" +#include "ai_bot_item_timing.h" +#include "ai_bot_opponent.h" +#include "ai_dmq3.h" + +float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int ent); + +#define OPPONENT_STAT_MAX_HEALTH 100 +#define OPPONENT_SPAWN_ARMOR 0 +#define OPPONENT_STACK_OVERMAX_DECAY_PER_SEC 1.0f +#define OPPONENT_WITNESS_DIST 2048.0f +#define OPPONENT_TICK_INTERVAL 5.0f +#define OPPONENT_LOC_DECAY_PER_SEC 0.15f +#define OPPONENT_STACK_DECAY_PER_SEC 0.08f +#define OPPONENT_COMPARE_MARGIN 20 +#define OPPONENT_LOW_FITNESS_HEALTH_MAX 100 +#define OPPONENT_LOW_FITNESS_ARMOR_MAX 25 +#define OPPONENT_LOW_FITNESS_PRESS_DIFF (-20) +#define OPPONENT_LOW_FITNESS_AVOID_BIAS 0.42f +#define OPPONENT_HEARD_LOC_CONF 0.6f +#define OPPONENT_VISIBLE_THRESH 0.15f +#define OPPONENT_SPAWN_POOL_MAX 64 +#define OPPONENT_FAR_SPAWN_CANDIDATES 4 +#define OPPONENT_SPAWN_Z_BIAS 9.0f +#define OPPONENT_RESPAWN_GUESS_SEC 6.0f +#define OPPONENT_RESPAWN_ZONE_CONF 0.72f +#define OPPONENT_DOWN_AVOID_BASE 0.5f +#define OPPONENT_UP_PRESS_BASE 0.4f +#define OPPONENT_EVEN_DUEL_BASE 0.30f +#define OPPONENT_EVEN_SCORE_MAX_GAP 2 +#define OPPONENT_SCORE_BIAS_PER_FRAG 0.14f +#define OPPONENT_ENGAGE_AVOID_THRESH (-0.28f) +#define OPPONENT_ENGAGE_PRESS_THRESH 0.28f +#define OPPONENT_AVOID_SPOT_RADIUS 384.0f +#define OPPONENT_AVOID_SPOT_RADIUS_DOWN 560.0f +#define OPPONENT_AVOID_ITEM_RADIUS 640.0f +#define OPPONENT_AVOID_ITEM_RADIUS_DOWN 960.0f +#define OPPONENT_AVOID_LOC_MIN_CONF 0.20f +#define OPPONENT_AVOID_LOC_MIN_CONF_DOWN 0.08f +#define OPPONENT_POST_RESPAWN_FLEE_SEC 10.0f +#define OPPONENT_DEATH_CARRY_LOC_CONF 0.85f +#define OPPONENT_ITEM_PENALTY_DOWN_SCALE 3.4f +#define OPPONENT_INFER_PICKUP_LATCH_SEC 8.0f +#define OPPONENT_INFER_PICKUP_ORIGIN_DIST 256.0f +#define OPPONENT_SELF_PICKUP_LATCH_SEC 8.0f +#define OPPONENT_FLEE_ENGAGED_LOS_SEC 2.5f +#define OPPONENT_FLEE_TOWARD_DOT_THRESH 0.25f +#define OPPONENT_FLEE_FROM_SEC 12.0f +#define OPPONENT_DOWN_FLEE_SEC 8.0f +#define OPPONENT_COMBAT_COMPARE_INTERVAL 0.5f +#define OPPONENT_ITEM_PRIO_NOT_READY_SCALE 1.42f +#define OPPONENT_ITEM_PRIO_SITUATIONAL_SCALE 1.12f + +typedef struct { + vec3_t origin; +} opponent_map_spawn_t; + +typedef struct { + qboolean valid; + qboolean has_location; + int opponent_client; + vec3_t origin; + int areanum; + float loc_confidence; + int believed_health; + int believed_armor; + int powerups; + float stack_confidence; + int compare; + vec3_t drift_velocity; +} opponent_death_carry_t; + +static opponent_death_carry_t opponent_death_carry[MAX_CLIENTS]; + +static void Opponent_SetLocation(bot_state_t *bs, opponent_belief_t *ob, + const vec3_t origin, int source, float confidence); +static void Opponent_UpdateCompare(bot_state_t *bs, opponent_belief_t *ob); +static void Opponent_UpdateEngageBias(bot_state_t *bs, opponent_belief_t *ob); +static void Opponent_RefreshStackCompare(bot_state_t *bs, opponent_belief_t *ob); +static void Opponent_SnapshotCombatLastSeen(bot_state_t *bs, + opponent_belief_t *ob); +static qboolean Opponent_InActiveCombat(bot_state_t *bs, + const opponent_belief_t *ob); +static void Opponent_ApplyPickupBelief(opponent_belief_t *ob, int itemIndex); +static void Opponent_MergeGuess(bot_state_t *bs, opponent_belief_t *ob, + const vec3_t origin, float confidence); +static void Opponent_RefreshTracking(bot_state_t *bs, opponent_belief_t *ob); +static void Opponent_ApplyDamageDealt(bot_state_t *bs, opponent_belief_t *ob, + qboolean relaxedLatch); +static void Opponent_OnHeardOpponentRoaming(bot_state_t *bs, + opponent_belief_t *ob, const vec3_t origin); +static void Opponent_OnOpponentPositionalClue(bot_state_t *bs, + opponent_belief_t *ob, const vec3_t origin); +static qboolean Opponent_IsVisible(bot_state_t *bs, int clientnum); +static int Opponent_SelfStackScore(const bot_state_t *bs); +static int Opponent_BelievedStackScore(const opponent_belief_t *ob); + +static opponent_map_spawn_t opponent_spawn_pool[OPPONENT_SPAWN_POOL_MAX]; +static int opponent_spawn_pool_count; +static int opponent_spawn_pool_time = -1; + +static float Opponent_Dist(const vec3_t a, const vec3_t b) { + vec3_t delta; + + VectorSubtract(a, b, delta); + return VectorLength(delta); +} + +static float Opponent_FloatMin(float a, float b) { + return (a < b) ? a : b; +} + +static float Opponent_FloatMax(float a, float b) { + return (a > b) ? a : b; +} + +static int Opponent_SpawnHealthBelief(void) { + int bonus; + + trap_Cvar_Update(&g_spawnHealthBonus); + bonus = g_spawnHealthBonus.integer; + if (bonus < 0) { + bonus = 0; + } + return OPPONENT_STAT_MAX_HEALTH + bonus; +} + +static qboolean Opponent_StackOvermaxDecays(void) { + trap_Cvar_Update(&g_countDownHealthArmor); + return g_countDownHealthArmor.integer != 0; +} + +static int Opponent_DebugEnabled(void) { + return BotEnhanced_DebugActive(); +} + +static void Opponent_Debug(bot_state_t *bs, const char *msg) { + char botName[64]; + + if (!Opponent_DebugEnabled() || !bs || !msg) { + return; + } + ClientName(bs->client, botName, sizeof(botName)); + G_Printf("BotOpponent: %s %s\n", botName, msg); +} + +static qboolean Opponent_InActiveCombat(bot_state_t *bs, + const opponent_belief_t *ob) { + float now; + + if (!bs || !ob || ob->client < 0) { + return qfalse; + } + if (bs->enemy != ob->client) { + return qfalse; + } + now = FloatTime(); + if (BotCombat_HasFightLOS(bs, ob->client)) { + return qtrue; + } + if (bs->enemyvisible_time >= now - BOT_COMBAT_LOS_DROP_SEC) { + return qtrue; + } + if (bs->lastenemyareanum > 0 && + bs->enemyvisible_time >= now - BOT_COMBAT_LOS_DROP_AREA_SEC) { + return qtrue; + } + return qfalse; +} + +static int Opponent_CountHostiles(bot_state_t *bs, int *onlyClientOut) { + int i; + int count; + int only; + + if (!bs) { + return 0; + } + count = 0; + only = -1; + for (i = 0; i < level.maxclients; i++) { + if (i == bs->client) { + continue; + } + if (!g_entities[i].inuse || !g_entities[i].client) { + continue; + } + if (g_entities[i].client->sess.sessionTeam == TEAM_SPECTATOR) { + continue; + } + if (BotSameTeam(bs, i)) { + continue; + } + count++; + only = i; + } + if (onlyClientOut) { + *onlyClientOut = only; + } + return count; +} + +static int Opponent_IsSelfPicker(const bot_state_t *bs, int pickerClient) { + if (!bs || pickerClient < 0) { + return 0; + } + return pickerClient == bs->client || pickerClient == bs->entitynum; +} + +static void Opponent_FormatItemLabel(int itemIndex, char *buf, int bufsize) { + gitem_t *item; + + if (!buf || bufsize < 2) { + return; + } + if (itemIndex < 0 || itemIndex >= bg_numItems) { + Com_sprintf(buf, bufsize, "item %d", itemIndex); + return; + } + item = &bg_itemlist[itemIndex]; + if (item->pickup_name && item->pickup_name[0]) { + Q_strncpyz(buf, item->pickup_name, bufsize); + return; + } + if (item->classname && item->classname[0]) { + Q_strncpyz(buf, item->classname, bufsize); + return; + } + Com_sprintf(buf, bufsize, "item %d", itemIndex); +} + +static void Opponent_LatchOpponentEnemy(bot_state_t *bs, opponent_belief_t *ob) { + if (!bs || !ob || ob->client < 0) { + return; + } + if (!BotEnhanced_CanEngageClient(bs, ob->client)) { + return; + } + if (bs->enemy != ob->client) { + bs->enemy = ob->client; + bs->enemysight_time = FloatTime(); + bs->enemysuicide = qfalse; + bs->enemydeath_time = 0; + } + bs->enemyvisible_time = FloatTime(); +} + +/* + * Resolve which client picked up / made noise. See BotAI_EventPickerClient. + */ + +static int Opponent_IsLowFitness(const bot_state_t *bs) { + if (!bs) { + return 0; + } + if (bs->inventory[INVENTORY_HEALTH] >= OPPONENT_LOW_FITNESS_HEALTH_MAX) { + return 0; + } + return bs->inventory[INVENTORY_ARMOR] <= OPPONENT_LOW_FITNESS_ARMOR_MAX; +} + +static int Opponent_LowFitnessAvoidsFight(const bot_state_t *bs, + const opponent_belief_t *ob) { + int diff; + + if (!Opponent_IsLowFitness(bs) || !ob) { + return 0; + } + diff = Opponent_BelievedStackScore(ob) - Opponent_SelfStackScore(bs); + return diff > OPPONENT_LOW_FITNESS_PRESS_DIFF; +} + +static int Opponent_IsAvoiding(const bot_state_t *bs, const opponent_belief_t *ob) { + if (!bs || !ob) { + return 0; + } + if (ob->compare == BOT_OPPONENT_COMPARE_DOWN) { + return 1; + } + if (ob->flee_until > FloatTime()) { + return 1; + } + if (Opponent_LowFitnessAvoidsFight(bs, ob)) { + return 1; + } + return ob->engage_bias <= OPPONENT_ENGAGE_AVOID_THRESH; +} + +static int Opponent_WantsPureFlee(const opponent_belief_t *ob) { + if (!ob) { + return 0; + } + return ob->compare == BOT_OPPONENT_COMPARE_DOWN; +} + +static void Opponent_RefreshFleeFrom(opponent_belief_t *ob, const vec3_t origin) { + if (!ob || !origin) { + return; + } + VectorCopy(origin, ob->flee_from_origin); + ob->flee_from_until = FloatTime() + OPPONENT_FLEE_FROM_SEC; +} + +static qboolean Opponent_GetFleeAvoidOrigin(const opponent_belief_t *ob, + vec3_t originOut) { + float now; + + if (!ob || !originOut) { + return qfalse; + } + now = FloatTime(); + if (ob->flee_from_until > now) { + VectorCopy(ob->flee_from_origin, originOut); + return qtrue; + } + if (ob->loc_source == BOT_OPPONENT_LOC_UNKNOWN) { + return qfalse; + } + if (ob->loc_confidence < OPPONENT_AVOID_LOC_MIN_CONF_DOWN) { + return qfalse; + } + VectorCopy(ob->believed_origin, originOut); + return qtrue; +} + +static int Opponent_HasAvoidLocation(const bot_state_t *bs, + const opponent_belief_t *ob) { + vec3_t spot; + + if (!ob) { + return 0; + } + if (!Opponent_IsAvoiding(bs, ob) && + ob->compare != BOT_OPPONENT_COMPARE_DOWN) { + if (ob->loc_source == BOT_OPPONENT_LOC_UNKNOWN) { + return 0; + } + if (ob->loc_confidence < OPPONENT_AVOID_LOC_MIN_CONF) { + return 0; + } + return 1; + } + return Opponent_GetFleeAvoidOrigin(ob, spot); +} + +static float Opponent_AvoidSpotRadius(const bot_state_t *bs, + const opponent_belief_t *ob) { + if (Opponent_IsAvoiding(bs, ob) || + (ob && ob->compare == BOT_OPPONENT_COMPARE_DOWN)) { + return OPPONENT_AVOID_SPOT_RADIUS_DOWN; + } + return OPPONENT_AVOID_SPOT_RADIUS; +} + +static float Opponent_AvoidItemRadius(const bot_state_t *bs, + const opponent_belief_t *ob) { + if (Opponent_IsAvoiding(bs, ob) || + (ob && ob->compare == BOT_OPPONENT_COMPARE_DOWN)) { + return OPPONENT_AVOID_ITEM_RADIUS_DOWN; + } + return OPPONENT_AVOID_ITEM_RADIUS; +} + +static void Opponent_SaveDeathCarry(bot_state_t *bs, opponent_belief_t *ob) { + opponent_death_carry_t *carry; + vec3_t origin; + int areanum; + float conf; + + if (!bs || !ob || ob->client < 0 || + bs->client < 0 || bs->client >= MAX_CLIENTS) { + return; + } + + Opponent_SnapshotCombatLastSeen(bs, ob); + Opponent_ApplyDamageDealt(bs, ob, qtrue); + + if (BotItemTiming_IsActive()) { + BotItemTiming_InferOpponentPickupNearDeath(bs, bs->origin); + } + + carry = &opponent_death_carry[bs->client]; + memset(carry, 0, sizeof(*carry)); + carry->valid = qtrue; + carry->opponent_client = ob->client; + carry->believed_health = ob->believed_health; + carry->believed_armor = ob->believed_armor; + carry->powerups = ob->powerups; + carry->stack_confidence = ob->stack_confidence; + carry->compare = ob->compare; + VectorCopy(ob->drift_velocity, carry->drift_velocity); + + areanum = 0; + conf = 0.0f; + if (ob->loc_last_seen > 0.0f) { + VectorCopy(ob->last_seen_origin, origin); + areanum = ob->believed_areanum; + conf = OPPONENT_DEATH_CARRY_LOC_CONF; + } else if (bs->lastenemyareanum > 0) { + VectorCopy(bs->lastenemyorigin, origin); + areanum = bs->lastenemyareanum; + conf = OPPONENT_DEATH_CARRY_LOC_CONF; + } else if (ob->loc_source != BOT_OPPONENT_LOC_UNKNOWN && + ob->loc_confidence >= OPPONENT_AVOID_LOC_MIN_CONF_DOWN) { + VectorCopy(ob->believed_origin, origin); + areanum = ob->believed_areanum; + conf = ob->loc_confidence; + } + + if (conf > 0.0f) { + carry->has_location = qtrue; + VectorCopy(origin, carry->origin); + carry->areanum = areanum; + carry->loc_confidence = conf; + } + + if (Opponent_DebugEnabled()) { + char buf[128]; + if (carry->has_location) { + Com_sprintf(buf, sizeof(buf), + "death carry stack hp %d ar %d last seen conf %.2f", + carry->believed_health, carry->believed_armor, + carry->loc_confidence); + } else { + Com_sprintf(buf, sizeof(buf), + "death carry stack hp %d ar %d no last seen", + carry->believed_health, carry->believed_armor); + } + Opponent_Debug(bs, buf); + } +} + +static int Opponent_ApplyDeathCarry(bot_state_t *bs, opponent_belief_t *ob) { + opponent_death_carry_t *carry; + float now; + + if (!bs || !ob || ob->client < 0 || + bs->client < 0 || bs->client >= MAX_CLIENTS) { + return 0; + } + + carry = &opponent_death_carry[bs->client]; + if (!carry->valid || carry->opponent_client != ob->client) { + return 0; + } + carry->valid = qfalse; + + ob->believed_health = carry->believed_health; + ob->believed_armor = carry->believed_armor; + ob->powerups = carry->powerups; + ob->stack_confidence = carry->stack_confidence; + ob->compare = carry->compare; + VectorCopy(carry->drift_velocity, ob->drift_velocity); + + now = FloatTime(); + ob->flee_until = now + OPPONENT_POST_RESPAWN_FLEE_SEC; + + if (carry->has_location) { + Opponent_SetLocation(bs, ob, carry->origin, BOT_OPPONENT_LOC_SEEN, + carry->loc_confidence); + ob->loc_last_seen = now; + VectorCopy(carry->origin, ob->last_seen_origin); + if (carry->areanum > 0) { + ob->believed_areanum = carry->areanum; + } + ob->respawn_guess_until = 0.0f; + } + + Opponent_UpdateCompare(bs, ob); + Opponent_UpdateEngageBias(bs, ob); + Opponent_Debug(bs, "respawn restored death carry beliefs"); + return 1; +} + +static void Opponent_ReactSensoryContact(bot_state_t *bs, opponent_belief_t *ob) { + if (!bs || !ob || !Opponent_IsAvoiding(bs, ob)) { + return; + } + ob->flee_until = Opponent_FloatMax(ob->flee_until, + FloatTime() + 3.0f); + if (Opponent_WantsPureFlee(ob)) { + if (BotCombat_HasFightLOS(bs, ob->client)) { + ob->flee_engaged = qtrue; + Opponent_LatchOpponentEnemy(bs, ob); + } else { + ob->flee_engaged = qfalse; + if (bs->enemy == ob->client && !Opponent_InActiveCombat(bs, ob)) { + BotCombat_ReleaseEnemy(bs); + } + } + BotOpponent_ApplyAvoidSpot(bs); + return; + } + BotOpponent_ApplyAvoidSpot(bs); + if (BotCombat_HasFightLOS(bs, ob->client)) { + ob->flee_engaged = qtrue; + Opponent_LatchOpponentEnemy(bs, ob); + return; + } + if (ob->flee_engaged) { + return; + } + if (bs->enemy == ob->client && !Opponent_InActiveCombat(bs, ob)) { + BotCombat_ReleaseEnemy(bs); + } +} + +static int Opponent_IsMajorItemIndex(int itemIndex) { + gitem_t *item; + + if (itemIndex < 0 || itemIndex >= bg_numItems) { + return 0; + } + item = &bg_itemlist[itemIndex]; + if (item->giType == IT_HEALTH || item->giType == IT_ARMOR || + item->giType == IT_POWERUP) { + return 1; + } + return 0; +} + +static qboolean Opponent_InferPickupIsLatched(const opponent_belief_t *ob, + int itemIndex, const vec3_t itemOrigin, float now) { + if (!ob || now >= ob->infer_pickup_latch_until) { + return qfalse; + } + if (ob->infer_pickup_latch_index != itemIndex) { + return qfalse; + } + if (ob->infer_pickup_latch_has_origin && itemOrigin) { + if (Opponent_Dist(itemOrigin, ob->infer_pickup_latch_origin) > + OPPONENT_INFER_PICKUP_ORIGIN_DIST) { + return qfalse; + } + } + return qtrue; +} + +static void Opponent_LatchInferPickup(opponent_belief_t *ob, int itemIndex, + const vec3_t itemOrigin, float now) { + if (!ob) { + return; + } + ob->infer_pickup_latch_index = itemIndex; + ob->infer_pickup_latch_until = now + OPPONENT_INFER_PICKUP_LATCH_SEC; + if (itemOrigin) { + VectorCopy(itemOrigin, ob->infer_pickup_latch_origin); + ob->infer_pickup_latch_has_origin = qtrue; + } else { + ob->infer_pickup_latch_has_origin = qfalse; + } +} + +void BotOpponent_OnSelfMajorPickup(bot_state_t *bs, int itemIndex) { + opponent_belief_t *ob; + float now; + + if (!bs || !BotOpponent_IsActive() || itemIndex < 0) { + return; + } + if (!Opponent_IsMajorItemIndex(itemIndex)) { + return; + } + ob = &bs->opponent_belief; + now = FloatTime(); + ob->self_pickup_latch_index = itemIndex; + ob->self_pickup_latch_until = now + OPPONENT_SELF_PICKUP_LATCH_SEC; +} + +void BotOpponent_OnInferredItemPickup(bot_state_t *bs, int itemIndex, + const vec3_t itemOrigin, const char *reason) { + opponent_belief_t *ob; + float now; + char buf[128]; + char itemLabel[64]; + + if (!bs || !BotOpponent_IsActive() || itemIndex < 0) { + return; + } + if (!Opponent_IsMajorItemIndex(itemIndex)) { + return; + } + + Opponent_RefreshTracking(bs, &bs->opponent_belief); + ob = &bs->opponent_belief; + if (!ob->tracking || ob->client < 0) { + return; + } + + now = FloatTime(); + if (ob->self_pickup_latch_index == itemIndex && + now < ob->self_pickup_latch_until) { + return; + } + if (Opponent_InferPickupIsLatched(ob, itemIndex, itemOrigin, now)) { + return; + } + + Opponent_ApplyPickupBelief(ob, itemIndex); + Opponent_LatchInferPickup(ob, itemIndex, itemOrigin, now); + + if (itemOrigin) { + Opponent_MergeGuess(bs, ob, itemOrigin, 0.65f); + Opponent_RefreshFleeFrom(ob, itemOrigin); + } + Opponent_UpdateCompare(bs, ob); + Opponent_UpdateEngageBias(bs, ob); + if (Opponent_WantsPureFlee(ob)) { + ob->flee_until = Opponent_FloatMax(ob->flee_until, + FloatTime() + OPPONENT_DOWN_FLEE_SEC); + ob->flee_engaged = qfalse; + } + Opponent_ReactSensoryContact(bs, ob); + + if (Opponent_DebugEnabled() && reason) { + Opponent_FormatItemLabel(itemIndex, itemLabel, sizeof(itemLabel)); + Com_sprintf(buf, sizeof(buf), "%s %s", reason, itemLabel); + Opponent_Debug(bs, buf); + } +} + +static void Opponent_EnsureSpawnPool(void) { + gentity_t *spot; + + if (opponent_spawn_pool_time == level.time && opponent_spawn_pool_count > 0) { + return; + } + opponent_spawn_pool_time = level.time; + opponent_spawn_pool_count = 0; + spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) { + if (opponent_spawn_pool_count >= OPPONENT_SPAWN_POOL_MAX) { + break; + } + VectorCopy(spot->s.origin, + opponent_spawn_pool[opponent_spawn_pool_count].origin); + opponent_spawn_pool[opponent_spawn_pool_count].origin[2] += + OPPONENT_SPAWN_Z_BIAS; + opponent_spawn_pool_count++; + } +} + +/* + * Mirror SelectTournamentSpawnPoint: opponent respawns at one of the spawn + * pads furthest from the surviving player (us). Belief = centroid of the top + * candidate pads (up to four), matching the server's limited random pool. + */ +static int Opponent_ComputeFarRespawnZone(bot_state_t *bs, vec3_t zoneOrigin) { + float dist[OPPONENT_SPAWN_POOL_MAX]; + int order[OPPONENT_SPAWN_POOL_MAX]; + vec3_t delta; + int count; + int i; + int j; + int use; + int tmpi; + + if (!bs || !zoneOrigin) { + return 0; + } + Opponent_EnsureSpawnPool(); + count = opponent_spawn_pool_count; + if (count <= 0) { + return 0; + } + for (i = 0; i < count; i++) { + VectorSubtract(opponent_spawn_pool[i].origin, bs->origin, delta); + dist[i] = VectorLength(delta); + order[i] = i; + } + for (i = 0; i < count - 1; i++) { + for (j = 0; j < count - i - 1; j++) { + if (dist[order[j]] < dist[order[j + 1]]) { + tmpi = order[j]; + order[j] = order[j + 1]; + order[j + 1] = tmpi; + } + } + } + if (count <= 2) { + VectorCopy(opponent_spawn_pool[order[0]].origin, zoneOrigin); + return 1; + } + use = OPPONENT_FAR_SPAWN_CANDIDATES; + if (use > count) { + use = count; + } + VectorClear(zoneOrigin); + for (i = 0; i < use; i++) { + VectorAdd(zoneOrigin, opponent_spawn_pool[order[i]].origin, zoneOrigin); + } + VectorScale(zoneOrigin, 1.0f / (float)use, zoneOrigin); + return use; +} + +static void Opponent_ClearBelief(opponent_belief_t *ob) { + if (!ob) { + return; + } + memset(ob, 0, sizeof(*ob)); + ob->client = -1; + ob->believed_health = Opponent_SpawnHealthBelief(); + ob->believed_armor = OPPONENT_SPAWN_ARMOR; + ob->loc_source = BOT_OPPONENT_LOC_UNKNOWN; + ob->compare = BOT_OPPONENT_COMPARE_UNKNOWN; + ob->last_hitcount = -1; +} + +static void Opponent_SetSpawnBeliefs(opponent_belief_t *ob) { + if (!ob) { + return; + } + ob->believed_health = Opponent_SpawnHealthBelief(); + ob->believed_armor = OPPONENT_SPAWN_ARMOR; + ob->stack_confidence = 0.35f; + ob->powerups = 0; + ob->compare = BOT_OPPONENT_COMPARE_UNKNOWN; + ob->score_gap = 0; + ob->engage_bias = 0.0f; +} + +static int Opponent_SelfStackScore(const bot_state_t *bs) { + int score; + + if (!bs) { + return 0; + } + score = bs->inventory[INVENTORY_HEALTH] + bs->inventory[INVENTORY_ARMOR]; + if (bs->inventory[INVENTORY_QUAD]) { + score += 50; + } + if (BotEnhanced_IsActive()) { + score += BotCombat_LoadoutStackBonus(bs); + } + return score; +} + +static int Opponent_BelievedStackScore(const opponent_belief_t *ob) { + int score; + + if (!ob) { + return 0; + } + score = ob->believed_health + ob->believed_armor; + if (ob->powerups & (1 << PW_QUAD)) { + score += 50; + } + return score; +} + +static void Opponent_UpdateCompare(bot_state_t *bs, opponent_belief_t *ob) { + int diff; + + if (!bs || !ob || !ob->tracking) { + return; + } + diff = Opponent_BelievedStackScore(ob) - Opponent_SelfStackScore(bs); + if (diff >= OPPONENT_COMPARE_MARGIN) { + ob->compare = BOT_OPPONENT_COMPARE_UP; + } else if (diff <= -OPPONENT_COMPARE_MARGIN) { + ob->compare = BOT_OPPONENT_COMPARE_DOWN; + ob->flee_until = Opponent_FloatMax(ob->flee_until, + FloatTime() + OPPONENT_DOWN_FLEE_SEC); + ob->flee_engaged = qfalse; + } else { + ob->compare = BOT_OPPONENT_COMPARE_EVEN; + } +} + +static void Opponent_UpdateEngageBias(bot_state_t *bs, opponent_belief_t *ob) { + int selfScore; + int oppScore; + float bias; + + if (!bs || !ob || !ob->tracking || ob->client < 0) { + if (ob) { + ob->score_gap = 0; + ob->engage_bias = 0.0f; + } + return; + } + + selfScore = bs->cur_ps.persistant[PERS_SCORE]; + oppScore = 0; + if (g_entities[ob->client].inuse && g_entities[ob->client].client) { + oppScore = g_entities[ob->client].client->ps.persistant[PERS_SCORE]; + } + ob->score_gap = selfScore - oppScore; + + bias = 0.0f; + switch (ob->compare) { + case BOT_OPPONENT_COMPARE_DOWN: + bias = -OPPONENT_DOWN_AVOID_BASE; + break; + case BOT_OPPONENT_COMPARE_UP: + bias = OPPONENT_UP_PRESS_BASE; + break; + case BOT_OPPONENT_COMPARE_EVEN: + if (ob->score_gap >= -OPPONENT_EVEN_SCORE_MAX_GAP && + ob->score_gap <= OPPONENT_EVEN_SCORE_MAX_GAP) { + bias = OPPONENT_EVEN_DUEL_BASE; + } + break; + default: + break; + } + bias += (float)ob->score_gap * OPPONENT_SCORE_BIAS_PER_FRAG; + if (BotEnhanced_IsActive()) { + bias += BotCombat_LoadoutEngageBiasNudge(bs); + } + if (Opponent_LowFitnessAvoidsFight(bs, ob)) { + if (bias > -OPPONENT_LOW_FITNESS_AVOID_BIAS) { + bias = -OPPONENT_LOW_FITNESS_AVOID_BIAS; + } + } + if (bias > 1.0f) { + bias = 1.0f; + } else if (bias < -1.0f) { + bias = -1.0f; + } + ob->engage_bias = bias; +} + +static void Opponent_RefreshStackCompare(bot_state_t *bs, opponent_belief_t *ob) { + if (!bs || !ob || !ob->tracking) { + return; + } + Opponent_ApplyDamageDealt(bs, ob, qfalse); + Opponent_UpdateCompare(bs, ob); + Opponent_UpdateEngageBias(bs, ob); +} + +float BotOpponent_GetEngageBias(const bot_state_t *bs) { + if (!bs || !BotOpponent_IsTracking(bs)) { + return 0.0f; + } + return bs->opponent_belief.engage_bias; +} + +int BotOpponent_WantsAvoidEngagement(const bot_state_t *bs) { + if (!bs || !BotOpponent_IsTracking(bs)) { + return 0; + } + return Opponent_IsAvoiding(bs, &bs->opponent_belief); +} + +int BotOpponent_WantsFleeEngaged(const bot_state_t *bs) { + opponent_belief_t *ob; + float now; + + if (!bs || !BotOpponent_WantsAvoidEngagement(bs)) { + return 0; + } + ob = (opponent_belief_t *)&bs->opponent_belief; + if (!ob->flee_engaged || ob->client < 0) { + return 0; + } + now = FloatTime(); + if (BotCombat_HasFightLOS((bot_state_t *)bs, ob->client)) { + return 1; + } + if (bs->enemy == ob->client && + bs->enemyvisible_time >= now - OPPONENT_FLEE_ENGAGED_LOS_SEC) { + return 1; + } + return 0; +} + +int BotOpponent_WantsPressEngagement(const bot_state_t *bs) { + if (!bs || !BotOpponent_IsTracking(bs)) { + return 0; + } + return bs->opponent_belief.engage_bias >= OPPONENT_ENGAGE_PRESS_THRESH; +} + +static int Opponent_IsEvenDuelScore(const opponent_belief_t *ob) { + if (!ob) { + return 0; + } + if (ob->compare != BOT_OPPONENT_COMPARE_EVEN) { + return 0; + } + return ob->score_gap >= -OPPONENT_EVEN_SCORE_MAX_GAP && + ob->score_gap <= OPPONENT_EVEN_SCORE_MAX_GAP; +} + +static int Opponent_HasDuelContact(bot_state_t *bs, const opponent_belief_t *ob) { + float now; + + if (!bs || !ob || ob->client < 0) { + return 0; + } + if (BotCombat_HasFightLOS(bs, ob->client)) { + return 1; + } + now = FloatTime(); + if (bs->enemy == ob->client) { + if (bs->enemyvisible_time >= now - BOT_COMBAT_LOS_DROP_SEC) { + return 1; + } + if (bs->lastenemyareanum > 0 && + bs->enemyvisible_time >= now - BOT_COMBAT_LOS_DROP_AREA_SEC) { + return 1; + } + } + return 0; +} + +static void Opponent_TickEngageContact(bot_state_t *bs, opponent_belief_t *ob) { + if (!bs || !ob || ob->client < 0) { + return; + } + if (!BotEnhanced_CanEngageClient(bs, ob->client)) { + return; + } + if (!BotCombat_HasFightLOS(bs, ob->client) && + !Opponent_IsVisible(bs, ob->client)) { + return; + } + + if (Opponent_IsEvenDuelScore(ob) || BotOpponent_WantsPressEngagement(bs)) { + if (!BotCombat_CanEngageTrackedOpponent(bs)) { + return; + } + Opponent_LatchOpponentEnemy(bs, ob); + return; + } + + if (Opponent_WantsPureFlee(ob)) { + if (BotCombat_HasFightLOS(bs, ob->client)) { + ob->flee_engaged = qtrue; + Opponent_LatchOpponentEnemy(bs, ob); + } + return; + } + + if (!Opponent_IsAvoiding(bs, ob)) { + if (!BotCombat_CanEngageTrackedOpponent(bs)) { + return; + } + Opponent_LatchOpponentEnemy(bs, ob); + return; + } + + if (BotCombat_HasFightLOS(bs, ob->client)) { + ob->flee_engaged = qtrue; + Opponent_LatchOpponentEnemy(bs, ob); + } +} + +int BotOpponent_HasCombatSight(const bot_state_t *bs, int clientnum) { + const opponent_belief_t *ob; + + if (!bs || clientnum < 0 || clientnum >= MAX_CLIENTS) { + return 0; + } + if (BotCombat_HasFightLOS((bot_state_t *)bs, clientnum)) { + return 1; + } + if (!BotOpponent_IsTracking(bs)) { + return 0; + } + ob = &bs->opponent_belief; + if (clientnum != ob->client) { + return 0; + } + if (BotOpponent_WantsAvoidEngagement(bs)) { + return 0; + } + return Opponent_IsVisible((bot_state_t *)bs, clientnum); +} + +void BotOpponent_TryLatchCombatEnemy(bot_state_t *bs) { + opponent_belief_t *ob; + + if (!bs || !BotOpponent_IsActive()) { + return; + } + ob = &bs->opponent_belief; + if (!ob->tracking || ob->client < 0) { + return; + } + Opponent_TickEngageContact(bs, ob); +} + +int BotOpponent_WantsDuelCommit(const bot_state_t *bs) { + const opponent_belief_t *ob; + + if (!bs || !BotOpponent_IsTracking(bs)) { + return 0; + } + ob = &bs->opponent_belief; + if (Opponent_IsEvenDuelScore(ob) && + Opponent_HasDuelContact((bot_state_t *)bs, ob)) { + if (BotCombat_CanEngageTrackedOpponent(bs)) { + return 1; + } + return 0; + } + if (BotOpponent_WantsAvoidEngagement(bs)) { + return 0; + } + if (BotOpponent_WantsPressEngagement(bs)) { + if (!BotCombat_CanEngageTrackedOpponent(bs)) { + return 0; + } + return 1; + } + return 0; +} + +float BotOpponent_ItemRoutePenalty(const bot_state_t *bs, const vec3_t goalOrigin) { + const opponent_belief_t *ob; + vec3_t avoidOrigin; + float dist; + float u; + float radius; + float scale; + + if (!bs || !goalOrigin || !BotOpponent_WantsAvoidEngagement(bs)) { + return 1.0f; + } + ob = &bs->opponent_belief; + if (!Opponent_GetFleeAvoidOrigin(ob, avoidOrigin)) { + return 1.0f; + } + radius = Opponent_AvoidItemRadius(bs, ob); + dist = Opponent_Dist(goalOrigin, avoidOrigin); + if (dist >= radius) { + return 1.0f; + } + u = 1.0f - (dist / radius); + scale = 2.2f; + if (ob->compare == BOT_OPPONENT_COMPARE_DOWN || + ob->flee_until > FloatTime()) { + scale = OPPONENT_ITEM_PENALTY_DOWN_SCALE; + } + return 1.0f + u * scale; +} + +float BotOpponent_ItemPriorityScale(const bot_state_t *bs, float baseScale) { + if (!bs || baseScale <= 0.0f) { + return baseScale; + } + if (BotEnhanced_IsActive()) { + switch (BotCombat_GetLoadoutTier(bs)) { + case BOT_LOADOUT_NOT_READY: + return baseScale * OPPONENT_ITEM_PRIO_NOT_READY_SCALE; + case BOT_LOADOUT_SITUATIONAL: + return baseScale * OPPONENT_ITEM_PRIO_SITUATIONAL_SCALE; + default: + break; + } + } + if (BotOpponent_WantsAvoidEngagement(bs)) { + if (bs->opponent_belief.compare == BOT_OPPONENT_COMPARE_DOWN || + bs->opponent_belief.flee_until > FloatTime()) { + return baseScale * 0.45f; + } + return baseScale * 0.62f; + } + if (BotOpponent_WantsPressEngagement(bs)) { + return baseScale * 1.08f; + } + return baseScale; +} + +void BotOpponent_ApplyAvoidSpot(bot_state_t *bs) { + const opponent_belief_t *ob; + vec3_t spot; + float radius; + + if (!bs || !BotOpponent_WantsAvoidEngagement(bs)) { + return; + } + ob = &bs->opponent_belief; + if (!Opponent_GetFleeAvoidOrigin(ob, spot)) { + return; + } + radius = Opponent_AvoidSpotRadius(bs, ob); + trap_BotAddAvoidSpot(bs->ms, spot, radius, AVOID_ALWAYS); +} + +void BotOpponent_TickFleeEngagement(bot_state_t *bs) { + opponent_belief_t *ob; + float now; + + if (!bs || !BotOpponent_IsTracking(bs)) { + return; + } + ob = &bs->opponent_belief; + if (!BotOpponent_WantsAvoidEngagement(bs)) { + ob->flee_engaged = qfalse; + return; + } + + if (Opponent_WantsPureFlee(ob)) { + ob->flee_engaged = qfalse; + if (BotCombat_HasFightLOS(bs, ob->client)) { + ob->flee_engaged = qtrue; + Opponent_LatchOpponentEnemy(bs, ob); + } else if (bs->enemy == ob->client && !Opponent_InActiveCombat(bs, ob)) { + BotCombat_ReleaseEnemy(bs); + } + BotOpponent_ApplyAvoidSpot(bs); + return; + } + + now = FloatTime(); + if (BotCombat_HasFightLOS(bs, ob->client)) { + ob->flee_engaged = qtrue; + Opponent_LatchOpponentEnemy(bs, ob); + return; + } + if (bs->enemy == ob->client && + bs->enemyvisible_time >= now - OPPONENT_FLEE_ENGAGED_LOS_SEC) { + ob->flee_engaged = qtrue; + return; + } + if (ob->flee_engaged) { + return; + } + if (bs->enemy == ob->client && !Opponent_InActiveCombat(bs, ob)) { + BotCombat_ReleaseEnemy(bs); + } +} + +void BotOpponent_AdjustFleeMovement(bot_state_t *bs, bot_moveresult_t *moveresult) { + opponent_belief_t *ob; + vec3_t toOpp; + vec3_t flatMove; + vec3_t flatOpp; + vec3_t avoidOrigin; + float dot; + float radius; + + if (!bs || !moveresult || !BotOpponent_WantsAvoidEngagement(bs)) { + return; + } + ob = &bs->opponent_belief; + if (!Opponent_GetFleeAvoidOrigin(ob, avoidOrigin)) { + return; + } + if (VectorLengthSquared(moveresult->movedir) < 0.01f) { + return; + } + + VectorSubtract(avoidOrigin, bs->origin, toOpp); + toOpp[2] = 0.0f; + VectorCopy(moveresult->movedir, flatMove); + flatMove[2] = 0.0f; + if (VectorNormalize(flatMove) < 1.0f) { + return; + } + if (VectorNormalize2(toOpp, flatOpp) < 64.0f) { + return; + } + dot = DotProduct(flatMove, flatOpp); + if (dot <= OPPONENT_FLEE_TOWARD_DOT_THRESH) { + return; + } + + radius = Opponent_AvoidSpotRadius(bs, ob); + trap_BotAddAvoidSpot(bs->ms, avoidOrigin, radius, AVOID_ALWAYS); + trap_BotResetLastAvoidReach(bs->ms); +} + +static void Opponent_ClampBelievedStack(opponent_belief_t *ob) { + if (!ob) { + return; + } + if (ob->believed_health < 0) { + ob->believed_health = 0; + } + if (ob->believed_health > 200) { + ob->believed_health = 200; + } + if (ob->believed_armor < 0) { + ob->believed_armor = 0; + } + if (ob->believed_armor > 200) { + ob->believed_armor = 200; + } +} + +static void Opponent_ApplyPickupBelief(opponent_belief_t *ob, int itemIndex) { + gitem_t *item; + + if (!ob || itemIndex < 0 || itemIndex >= bg_numItems) { + return; + } + item = &bg_itemlist[itemIndex]; + if (item->giType == IT_HEALTH) { + ob->believed_health += item->quantity; + ob->stack_confidence = Opponent_FloatMin(1.0f, ob->stack_confidence + 0.2f); + } else if (item->giType == IT_ARMOR) { + ob->believed_armor += item->quantity; + ob->stack_confidence = Opponent_FloatMin(1.0f, ob->stack_confidence + 0.2f); + } else if (item->giType == IT_POWERUP) { + if (!Q_stricmp(item->classname, "item_quad")) { + ob->powerups |= (1 << PW_QUAD); + } else if (!Q_stricmp(item->classname, "item_haste")) { + ob->powerups |= (1 << PW_HASTE); + } else if (!Q_stricmp(item->classname, "item_invis")) { + ob->powerups |= (1 << PW_INVIS); + } else if (!Q_stricmp(item->classname, "item_regen")) { + ob->powerups |= (1 << PW_REGEN); + } else if (!Q_stricmp(item->classname, "item_enviro")) { + ob->powerups |= (1 << PW_BATTLESUIT); + } + } + Opponent_ClampBelievedStack(ob); +} + +static void Opponent_SetLocation(bot_state_t *bs, opponent_belief_t *ob, + const vec3_t origin, int source, float confidence) { + vec3_t point; + int areanum; + + if (!bs || !ob || !origin) { + return; + } + VectorCopy(origin, point); + VectorCopy(point, ob->believed_origin); + areanum = BotPointAreaNum(point); + if (areanum && trap_AAS_AreaReachability(areanum)) { + ob->believed_areanum = areanum; + } + ob->loc_source = source; + ob->loc_confidence = confidence; + ob->loc_last_update = FloatTime(); + if (source == BOT_OPPONENT_LOC_SEEN) { + ob->loc_last_seen = ob->loc_last_update; + VectorCopy(origin, ob->last_seen_origin); + } + if (source != BOT_OPPONENT_LOC_UNKNOWN) { + Opponent_RefreshFleeFrom(ob, point); + } +} + +static void Opponent_MergeGuess(bot_state_t *bs, opponent_belief_t *ob, + const vec3_t origin, float confidence) { + if (!bs || !ob || !origin) { + return; + } + if (confidence <= ob->loc_confidence && + ob->loc_source == BOT_OPPONENT_LOC_GUESSED && + FloatTime() - ob->loc_last_update < OPPONENT_TICK_INTERVAL) { + return; + } + Opponent_SetLocation(bs, ob, origin, BOT_OPPONENT_LOC_GUESSED, confidence); +} + +static void Opponent_OnHeardOpponentRoaming(bot_state_t *bs, + opponent_belief_t *ob, const vec3_t origin) { + vec3_t dir; + + if (!bs || !ob || !origin) { + return; + } + if (Opponent_InActiveCombat(bs, ob)) { + return; + } + Opponent_SetLocation(bs, ob, origin, BOT_OPPONENT_LOC_GUESSED, + OPPONENT_HEARD_LOC_CONF); + VectorSubtract(origin, bs->eye, dir); + vectoangles(dir, bs->ideal_viewangles); + bs->ideal_viewangles[2] *= 0.5f; + Opponent_RefreshFleeFrom(ob, origin); +} + +/* + * Shared reaction for opponent pickup sounds and movement/weapon noises. + * Mirrors the location + sensory hooks used on inferred major-item pickups. + */ +static void Opponent_OnOpponentPositionalClue(bot_state_t *bs, + opponent_belief_t *ob, const vec3_t origin) { + if (!bs || !ob || !origin) { + return; + } + if (!Opponent_InActiveCombat(bs, ob)) { + Opponent_OnHeardOpponentRoaming(bs, ob, origin); + } else { + Opponent_MergeGuess(bs, ob, origin, 0.55f); + } + Opponent_ReactSensoryContact(bs, ob); + if (!Opponent_IsAvoiding(bs, ob) && + (BotOpponent_WantsPressEngagement(bs) || + Opponent_IsEvenDuelScore(ob))) { + Opponent_LatchOpponentEnemy(bs, ob); + } +} + +static void Opponent_BelieveOpponentRespawned(bot_state_t *bs, + opponent_belief_t *ob) { + vec3_t zone; + int candidates; + char buf[128]; + + if (!bs || !ob) { + return; + } + Opponent_SetSpawnBeliefs(ob); + VectorClear(ob->drift_velocity); + ob->loc_last_seen = 0.0f; + ob->respawn_guess_until = FloatTime() + OPPONENT_RESPAWN_GUESS_SEC; + ob->combat_hold = qfalse; + ob->next_update_time = FloatTime(); + + candidates = Opponent_ComputeFarRespawnZone(bs, zone); + if (candidates > 0) { + Opponent_SetLocation(bs, ob, zone, BOT_OPPONENT_LOC_GUESSED, + OPPONENT_RESPAWN_ZONE_CONF); + if (Opponent_DebugEnabled()) { + char buf[128]; + Com_sprintf(buf, sizeof(buf), + "opponent died far spawn zone %d pads", candidates); + Opponent_Debug(bs, buf); + } + } else { + ob->loc_source = BOT_OPPONENT_LOC_UNKNOWN; + ob->loc_confidence = 0.0f; + Opponent_Debug(bs, "opponent died no spawn pads"); + } +} + +static qboolean Opponent_IsVisible(bot_state_t *bs, int clientnum) { + float vis; + + if (!bs || clientnum < 0 || clientnum >= MAX_CLIENTS) { + return qfalse; + } + if (BotCombat_HasFightLOS(bs, clientnum)) { + return qtrue; + } + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, + clientnum); + return vis >= OPPONENT_VISIBLE_THRESH; +} + +static void Opponent_UpdateVisibleLocation(bot_state_t *bs, opponent_belief_t *ob) { + aas_entityinfo_t entinfo; + + if (!bs || !ob || ob->client < 0) { + return; + } + if (!Opponent_IsVisible(bs, ob->client)) { + return; + } + BotEntityInfo(ob->client, &entinfo); + if (!entinfo.valid) { + return; + } + Opponent_SetLocation(bs, ob, entinfo.origin, BOT_OPPONENT_LOC_SEEN, 1.0f); + ob->powerups = entinfo.powerups; + ob->respawn_guess_until = 0.0f; + VectorCopy(bs->enemyvelocity, ob->drift_velocity); +} + +static void Opponent_DeadReckon(bot_state_t *bs, opponent_belief_t *ob) { + vec3_t guess; + float elapsed; + float now; + + if (!bs || !ob || ob->client < 0) { + return; + } + if (ob->respawn_guess_until > FloatTime() && + ob->loc_source == BOT_OPPONENT_LOC_GUESSED) { + return; + } + if (ob->loc_last_seen <= 0.0f) { + if (bs->lastenemyareanum > 0) { + Opponent_MergeGuess(bs, ob, bs->lastenemyorigin, 0.35f); + } + return; + } + now = FloatTime(); + elapsed = now - ob->loc_last_seen; + if (elapsed < 0.0f) { + elapsed = 0.0f; + } + VectorCopy(ob->last_seen_origin, guess); + if (VectorLengthSquared(ob->drift_velocity) > Square(8.0f)) { + VectorMA(guess, elapsed, ob->drift_velocity, guess); + } else if (VectorLengthSquared(bs->enemyvelocity) > Square(8.0f)) { + VectorMA(guess, elapsed, bs->enemyvelocity, guess); + } else if (bs->lastenemyareanum > 0) { + VectorCopy(bs->lastenemyorigin, guess); + } + Opponent_MergeGuess(bs, ob, guess, + Opponent_FloatMax(0.1f, 1.0f - elapsed * OPPONENT_LOC_DECAY_PER_SEC)); +} + +static void Opponent_ApplyDamageDealt(bot_state_t *bs, opponent_belief_t *ob, + qboolean relaxedLatch) { + int hits; + int delta; + int pack; + int attackeeHp; + int attackeeAr; + float now; + + if (!bs || !ob || !ob->tracking || ob->client < 0) { + return; + } + if (!relaxedLatch && bs->enemy != ob->client) { + return; + } + + hits = bs->cur_ps.persistant[PERS_HITS]; + if (ob->last_hitcount < 0) { + ob->last_hitcount = hits; + return; + } + + delta = hits - ob->last_hitcount; + if (delta > 0) { + now = FloatTime(); + if (ob->infer_pickup_latch_until > now) { + ob->last_hitcount = hits; + return; + } + + pack = bs->cur_ps.persistant[PERS_ATTACKEE_ARMOR]; + attackeeHp = (pack >> 8) & 0xFF; + attackeeAr = pack & 0xFF; + + if (attackeeHp < ob->believed_health) { + ob->believed_health = attackeeHp; + } + if (attackeeAr < ob->believed_armor) { + ob->believed_armor = attackeeAr; + } + Opponent_ClampBelievedStack(ob); + ob->stack_confidence = Opponent_FloatMin(1.0f, + ob->stack_confidence + 0.15f); + if (Opponent_DebugEnabled()) { + char buf[128]; + Com_sprintf(buf, sizeof(buf), + "hit snapshot hp %d ar %d believed hp %d ar %d", + attackeeHp, attackeeAr, + ob->believed_health, ob->believed_armor); + Opponent_Debug(bs, buf); + } + } + ob->last_hitcount = hits; +} + +static void Opponent_TickDamageDealt(bot_state_t *bs, opponent_belief_t *ob) { + Opponent_ApplyDamageDealt(bs, ob, qfalse); +} + +static void Opponent_SnapshotCombatLastSeen(bot_state_t *bs, + opponent_belief_t *ob) { + if (!bs || !ob || ob->client < 0) { + return; + } + if (Opponent_IsVisible(bs, ob->client)) { + Opponent_UpdateVisibleLocation(bs, ob); + return; + } + if (bs->lastenemyareanum > 0 && bs->enemyvisible_time > 0.0f) { + VectorCopy(bs->lastenemyorigin, ob->believed_origin); + ob->believed_areanum = bs->lastenemyareanum; + ob->loc_source = BOT_OPPONENT_LOC_SEEN; + ob->loc_confidence = 1.0f; + ob->loc_last_update = FloatTime(); + ob->loc_last_seen = bs->enemyvisible_time; + VectorCopy(bs->lastenemyorigin, ob->last_seen_origin); + VectorCopy(bs->enemyvelocity, ob->drift_velocity); + ob->respawn_guess_until = 0.0f; + } +} + +static void Opponent_OnCombatEnd(bot_state_t *bs, opponent_belief_t *ob) { + if (!bs || !ob) { + return; + } + Opponent_SnapshotCombatLastSeen(bs, ob); + Opponent_TickDamageDealt(bs, ob); + Opponent_UpdateCompare(bs, ob); + Opponent_UpdateEngageBias(bs, ob); +} + +static void Opponent_TickStackOvermaxDecay(opponent_belief_t *ob, float dt) { + int drop; + + if (!ob || dt <= 0.0f || !Opponent_StackOvermaxDecays()) { + return; + } + + drop = (int)(OPPONENT_STACK_OVERMAX_DECAY_PER_SEC * dt); + if (drop < 1) { + return; + } + + if (ob->believed_health > OPPONENT_STAT_MAX_HEALTH) { + ob->believed_health -= drop; + if (ob->believed_health < OPPONENT_STAT_MAX_HEALTH) { + ob->believed_health = OPPONENT_STAT_MAX_HEALTH; + } + } + if (ob->believed_armor > OPPONENT_STAT_MAX_HEALTH) { + ob->believed_armor -= drop; + if (ob->believed_armor < OPPONENT_STAT_MAX_HEALTH) { + ob->believed_armor = OPPONENT_STAT_MAX_HEALTH; + } + } +} + +static void Opponent_TickDecay(bot_state_t *bs, opponent_belief_t *ob, float dt) { + if (!bs || !ob || !ob->tracking || dt <= 0.0f) { + return; + } + Opponent_TickStackOvermaxDecay(ob, dt); + if (ob->loc_source == BOT_OPPONENT_LOC_GUESSED) { + if (ob->respawn_guess_until > FloatTime()) { + return; + } + ob->loc_confidence -= OPPONENT_LOC_DECAY_PER_SEC * dt; + if (ob->loc_confidence < 0.05f) { + ob->loc_confidence = 0.05f; + } + } + if (!Opponent_IsVisible(bs, ob->client)) { + ob->stack_confidence -= OPPONENT_STACK_DECAY_PER_SEC * dt; + if (ob->stack_confidence < 0.1f) { + ob->stack_confidence = 0.1f; + } + } +} + +static void Opponent_TickBeliefs(bot_state_t *bs, opponent_belief_t *ob, + float dt) { + Opponent_UpdateVisibleLocation(bs, ob); + if (!Opponent_IsVisible(bs, ob->client)) { + Opponent_DeadReckon(bs, ob); + } + Opponent_TickDamageDealt(bs, ob); + Opponent_TickDecay(bs, ob, dt); + Opponent_UpdateCompare(bs, ob); + Opponent_UpdateEngageBias(bs, ob); +} + +static void Opponent_RefreshTracking(bot_state_t *bs, opponent_belief_t *ob) { + int only; + int count; + + count = Opponent_CountHostiles(bs, &only); + if (count != 1 || only < 0) { + ob->tracking = qfalse; + ob->client = -1; + return; + } + ob->tracking = qtrue; + if (ob->client != only) { + Opponent_ClearBelief(ob); + ob->client = only; + ob->tracking = qtrue; + Opponent_SetSpawnBeliefs(ob); + ob->last_hitcount = bs->cur_ps.persistant[PERS_HITS]; + Opponent_Debug(bs, "latched opponent"); + } +} + +void BotOpponent_RegisterCvars(void) { +} + +int BotOpponent_IsActive(void) { + return BotEnhanced_IsActive(); +} + +int BotOpponent_IsTracking(const bot_state_t *bs) { + if (!bs || !BotOpponent_IsActive()) { + return 0; + } + return bs->opponent_belief.tracking; +} + +void BotOpponent_Reset(bot_state_t *bs) { + if (!bs) { + return; + } + Opponent_ClearBelief(&bs->opponent_belief); +} + +void BotOpponent_OnSpawn(bot_state_t *bs) { + opponent_belief_t *ob; + + if (!bs || !BotOpponent_IsActive()) { + return; + } + if (BotIsObserver(bs) || BotIsDead(bs)) { + return; + } + ob = &bs->opponent_belief; + Opponent_ClearBelief(ob); + Opponent_RefreshTracking(bs, ob); + if (ob->tracking) { + if (!Opponent_ApplyDeathCarry(bs, ob)) { + Opponent_SetSpawnBeliefs(ob); + Opponent_UpdateCompare(bs, ob); + Opponent_UpdateEngageBias(bs, ob); + } + ob->last_hitcount = bs->cur_ps.persistant[PERS_HITS]; + BotOpponent_ApplyAvoidSpot(bs); + } +} + +void BotOpponent_OnThinkStart(bot_state_t *bs) { + opponent_belief_t *ob; + float now; + qboolean inCombat; + + if (!bs || !BotOpponent_IsActive()) { + return; + } + if (BotIsObserver(bs) || BotIsDead(bs)) { + return; + } + + ob = &bs->opponent_belief; + now = FloatTime(); + + Opponent_RefreshTracking(bs, ob); + if (!ob->tracking || ob->client < 0) { + return; + } + + if (Opponent_WantsPureFlee(ob)) { + ob->flee_until = Opponent_FloatMax(ob->flee_until, + now + OPPONENT_DOWN_FLEE_SEC); + } + if (ob->flee_from_until <= now && bs->lastenemyareanum > 0) { + Opponent_RefreshFleeFrom(ob, bs->lastenemyorigin); + } + + Opponent_TickEngageContact(bs, ob); + + inCombat = Opponent_InActiveCombat(bs, ob); + if (inCombat) { + qboolean hasFightLos; + qboolean refreshCompare; + + hasFightLos = BotCombat_HasFightLOS(bs, ob->client); + refreshCompare = qfalse; + if (!ob->combat_hold) { + ob->next_combat_compare_time = now + OPPONENT_COMBAT_COMPARE_INTERVAL; + ob->combat_had_fight_los = hasFightLos; + } else if (ob->combat_had_fight_los && !hasFightLos) { + refreshCompare = qtrue; + } else if (now >= ob->next_combat_compare_time) { + refreshCompare = qtrue; + ob->next_combat_compare_time = now + OPPONENT_COMBAT_COMPARE_INTERVAL; + } + ob->combat_had_fight_los = hasFightLos; + if (refreshCompare) { + Opponent_RefreshStackCompare(bs, ob); + } + ob->combat_hold = qtrue; + if (Opponent_IsAvoiding(bs, ob)) { + BotOpponent_ApplyAvoidSpot(bs); + } + return; + } + + if (ob->combat_hold) { + ob->combat_hold = qfalse; + Opponent_OnCombatEnd(bs, ob); + ob->next_update_time = now + OPPONENT_TICK_INTERVAL; + BotOpponent_ApplyAvoidSpot(bs); + BotOpponent_TickFleeEngagement(bs); + return; + } + + BotOpponent_ApplyAvoidSpot(bs); + BotOpponent_TickFleeEngagement(bs); + + if (now < ob->next_update_time) { + return; + } + ob->next_update_time = now + OPPONENT_TICK_INTERVAL; + + Opponent_TickBeliefs(bs, ob, OPPONENT_TICK_INTERVAL); + + if (Opponent_DebugEnabled()) { + char buf[160]; + const char *cmp; + const char *loc; + const char *state; + + switch (ob->compare) { + case BOT_OPPONENT_COMPARE_UP: cmp = "up"; break; + case BOT_OPPONENT_COMPARE_DOWN: cmp = "down"; break; + case BOT_OPPONENT_COMPARE_EVEN: cmp = "even"; break; + default: cmp = "unknown"; break; + } + loc = (ob->loc_source == BOT_OPPONENT_LOC_SEEN) ? "seen" : + (ob->loc_source == BOT_OPPONENT_LOC_GUESSED) ? "guessed" : "unknown"; + if (ob->respawn_guess_until > now) { + state = "respawn zone"; + } else { + state = "tracking"; + } + Com_sprintf(buf, sizeof(buf), + "%s loc %s conf %.2f hp %d ar %d stack %s bias %.2f gap %d", + state, loc, ob->loc_confidence, + ob->believed_health, ob->believed_armor, cmp, + ob->engage_bias, ob->score_gap); + Opponent_Debug(bs, buf); + } +} + +static qboolean Opponent_IsNoiseEvent(int event) { + switch (event) { + /* footsteps */ + case EV_FOOTSTEP: + case EV_FOOTSTEP_METAL: + case EV_FOOTSTEP_WOOD: + case EV_FOOTSTEP_SNOW: + case EV_FOOTSPLASH: + case EV_FOOTWADE: + case EV_SWIM: + case EV_STEP_4: + case EV_STEP_8: + case EV_STEP_12: + case EV_STEP_16: + /* jump */ + case EV_JUMP_PAD: + case EV_JUMP: + /* fall */ + case EV_FALL_SHORT: + case EV_FALL_MEDIUM: + case EV_FALL_FAR: + /* taunt */ + case EV_TAUNT: + case EV_TAUNT_YES: + case EV_TAUNT_NO: + case EV_TAUNT_FOLLOWME: + case EV_TAUNT_GETFLAG: + case EV_TAUNT_GUARDBASE: + case EV_TAUNT_PATROL: + case EV_FIRE_WEAPON: + case EV_CHANGE_WEAPON: + return qtrue; + default: + return qfalse; + } +} + +void BotOpponent_OnClientEvent(bot_state_t *bs, entityState_t *state, int event) { + opponent_belief_t *ob; + int target; + int attacker; + + if (!bs || !state || !BotOpponent_IsActive()) { + return; + } + if (BotIsObserver(bs)) { + return; + } + if (BotIsDead(bs) && event != EV_OBITUARY) { + return; + } + + ob = &bs->opponent_belief; + Opponent_RefreshTracking(bs, ob); + if (!ob->tracking || ob->client < 0) { + return; + } + + if (event != EV_OBITUARY && event != EV_ITEM_PICKUP && + event != EV_GLOBAL_ITEM_PICKUP && + Opponent_InActiveCombat(bs, ob)) { + qboolean sensory; + + sensory = Opponent_IsNoiseEvent(event); + if (!sensory || !Opponent_IsAvoiding(bs, ob)) { + return; + } + } + + if (event == EV_OBITUARY) { + target = state->otherEntityNum; + attacker = state->otherEntityNum2; + if (target == ob->client) { + Opponent_BelieveOpponentRespawned(bs, ob); + } else if (target == bs->client && attacker == ob->client) { + Opponent_SaveDeathCarry(bs, ob); + ob->stack_confidence = Opponent_FloatMin(1.0f, + ob->stack_confidence + 0.1f); + } + return; + } + + if (event == EV_ITEM_PICKUP) { + int picker; + + picker = BotAI_EventPickerClient(state); + if (picker >= 0 && picker < level.maxclients && + Opponent_IsSelfPicker(bs, picker)) { + return; + } + if (picker >= 0 && picker < level.maxclients && + picker == ob->client) { + Opponent_OnOpponentPositionalClue(bs, ob, state->origin); + } + return; + } + + if (event == EV_GLOBAL_ITEM_PICKUP) { + if (state->eventParm >= 0 && state->eventParm < bg_numItems) { + if (ob->self_pickup_latch_index == state->eventParm && + FloatTime() < ob->self_pickup_latch_until) { + return; + } + } + return; + } + + if (Opponent_IsNoiseEvent(event)) { + int picker; + + picker = BotAI_EventPickerClient(state); + if (picker >= 0 && picker < level.maxclients && + !Opponent_IsSelfPicker(bs, picker) && + picker == ob->client) { + Opponent_OnOpponentPositionalClue(bs, ob, state->origin); + } + } +} diff --git a/ratoa_gamecode/code/game/ai_bot_opponent.h b/ratoa_gamecode/code/game/ai_bot_opponent.h new file mode 100644 index 0000000..c8e3d33 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_opponent.h @@ -0,0 +1,115 @@ +/* +=========================================================================== +BOT OPPONENT — per-bot beliefs about the sole hostile opponent in 1v1. + +Tracks estimated location and stack relative to self using observable evidence +only (visibility, sounds, witnessed pickups, damage dealt). No omniscient +reads of opponent health or hidden server state. + +Gate: bot_enhanced + bot_enhanced_opponent, exactly one hostile opponent. +=========================================================================== +*/ + +#ifndef AI_BOT_OPPONENT_H +#define AI_BOT_OPPONENT_H + +struct bot_state_s; +struct entityState_s; + +#define BOT_OPPONENT_LOC_UNKNOWN 0 +#define BOT_OPPONENT_LOC_SEEN 1 +#define BOT_OPPONENT_LOC_GUESSED 2 + +#define BOT_OPPONENT_COMPARE_UNKNOWN 0 +#define BOT_OPPONENT_COMPARE_DOWN 1 +#define BOT_OPPONENT_COMPARE_EVEN 2 +#define BOT_OPPONENT_COMPARE_UP 3 + +typedef struct opponent_belief_s { + int client; /* latched opponent client, else -1 */ + qboolean tracking; /* 1v1 context active */ + + vec3_t believed_origin; + int believed_areanum; + int loc_source; + float loc_confidence; /* 0..1 */ + float loc_last_update; + float loc_last_seen; + vec3_t last_seen_origin; + vec3_t drift_velocity; + float respawn_guess_until; /* hold far-spawn zone belief after obituary */ + + int believed_health; + int believed_armor; + float stack_confidence; /* 0..1 */ + int powerups; /* PW_* bits last seen on model */ + + int compare; /* DOWN / EVEN / UP vs self */ + + int score_gap; /* self frags minus opponent frags */ + float engage_bias; /* -1 avoid .. +1 press */ + + int last_hitcount; /* PERS_HITS latch for damage-dealt snapshots */ + float next_update_time; /* next periodic belief tick */ + qboolean combat_hold; /* skip ticks while engaged */ + qboolean combat_had_fight_los; /* prior think fight LOS (combat compare) */ + float next_combat_compare_time; /* periodic stack re-eval during combat */ + float flee_until; /* post-respawn / sensory panic flee */ + qboolean flee_engaged; /* retreating but keep opponent targeted */ + float infer_pickup_latch_until; + int infer_pickup_latch_index; + vec3_t infer_pickup_latch_origin; + qboolean infer_pickup_latch_has_origin; + int heard_pickup_event_picker; /* ingress dedup: last EV_ITEM_PICKUP */ + int heard_pickup_event_parm; + int heard_pickup_event_time; + vec3_t flee_from_origin; /* opponent bearing for flee routing */ + float flee_from_until; + int self_pickup_latch_index; /* bot took this item — block opponent infer */ + float self_pickup_latch_until; +} opponent_belief_t; + +void BotOpponent_RegisterCvars(void); +int BotOpponent_IsActive(void); +int BotOpponent_IsTracking(const struct bot_state_s *bs); + +void BotOpponent_Reset(struct bot_state_s *bs); +void BotOpponent_OnSpawn(struct bot_state_s *bs); +void BotOpponent_OnThinkStart(struct bot_state_s *bs); + +void BotOpponent_OnClientEvent(struct bot_state_s *bs, + struct entityState_s *state, int event); + +/* + * Opponent took a major item (witnessed pickup, empty spawn sighting, or + * death-site inference). itemIndex is bg_itemlist index (eventParm). + */ +void BotOpponent_OnInferredItemPickup(struct bot_state_s *bs, int itemIndex, + const vec3_t itemOrigin, const char *reason); +/* Bot picked up a major item — suppress opponent stack inference for a window. */ +void BotOpponent_OnSelfMajorPickup(struct bot_state_s *bs, int itemIndex); + +/* Behavior queries (1v1 opponent tracking only). */ +float BotOpponent_GetEngageBias(const struct bot_state_s *bs); +int BotOpponent_WantsAvoidEngagement(const struct bot_state_s *bs); +int BotOpponent_WantsPressEngagement(const struct bot_state_s *bs); +/* EVEN 1v1 or press bias: commit to fight/chase at normal duel pace. */ +int BotOpponent_WantsDuelCommit(const struct bot_state_s *bs); +float BotOpponent_ItemRoutePenalty(const struct bot_state_s *bs, + const vec3_t goalOrigin); +float BotOpponent_ItemPriorityScale(const struct bot_state_s *bs, float baseScale); +void BotOpponent_ApplyAvoidSpot(struct bot_state_s *bs); +/* 1 while fleeing with shoot-on-retreat (keeps enemy latched when visible). */ +int BotOpponent_WantsFleeEngaged(const struct bot_state_s *bs); +/* Drop latched opponent when fleeing and not in active fight. */ +void BotOpponent_TickFleeEngagement(struct bot_state_s *bs); +/* Nudge flee movement away from opponent when travel aligns with them. */ +void BotOpponent_AdjustFleeMovement(struct bot_state_s *bs, + struct bot_moveresult_s *moveresult); + +/* MASK_SHOT LOS, or entity-visible for non-avoiding tracked opponent (even/neutral). */ +int BotOpponent_HasCombatSight(const struct bot_state_s *bs, int clientnum); +/* Per-think latch from visibility / fight LOS (1v1 opponent module). */ +void BotOpponent_TryLatchCombatEnemy(struct bot_state_s *bs); + +#endif /* AI_BOT_OPPONENT_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_position.c b/ratoa_gamecode/code/game/ai_bot_position.c new file mode 100644 index 0000000..9e2e762 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_position.c @@ -0,0 +1,720 @@ +/* +=========================================================================== +BOT POSITION — high-ground preference behaviors for enhanced bots. +=========================================================================== +*/ + +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_items.h" +#include "ai_bot_combat.h" +#include "ai_bot_move_harness.h" +#include "ai_bot_item_timing.h" +#include "ai_bot_position.h" +#include "ai_bot_nav_guard.h" +#include "ai_dmq3.h" + +#define BOTPOS_HEIGHT_THRESHOLD 60.0f +#define BOTPOS_CHARGE_SUPPRESS_Z 100.0f +#define BOTPOS_HIGH_DECAY_SEC 10.0f +#define BOTPOS_REGAIN_THRESHOLD 80.0f +#define BOTPOS_GOAL_MEANINGFULLY_BELOW 24.0f +#define BOTPOS_NEAR_GOAL_HORIZ 640.0f + +#define BOTPOS_LEDGE_PEEK_Z 48.0f +#define BOTPOS_LEDGE_PEEK_MIN_HORIZ 160 +#define BOTPOS_LEDGE_PEEK_MAX_HORIZ 900 +#define BOTPOS_LEDGE_HOLD_SEC 2.5f +#define BOTPOS_LEDGE_PEEK_UP_SEC 0.85f +#define BOTPOS_LEDGE_PEEK_DOWN_SEC 0.55f + +#define BOTPOS_SCORE_HIGH_GOAL 20 +#define BOTPOS_SCORE_ELEVATED_GOAL 40 + +#define BOTPOS_EFFICIENCY_SCALE 12.0f /* units Z per second before bonus */ +#define BOTPOS_EFFICIENCY_MULT 2.5f +#define BOTPOS_EFFICIENCY_CAP 35 + +#define BOTPOS_ROUTE_AUDIT_INTERVAL 2.5f +#define BOTPOS_UPLIFT_MAX_SEC 6.0f +#define BOTPOS_UPLIFT_GOAL_NUMBER (-88001) +#define BOTPOS_DETOUR_MAX_TRAVEL 400 +#define BOTPOS_DETOUR_MAX_EXTRA 280 +#define BOTPOS_DETOUR_MAX_EXTRA_REGAIN 420 +#define BOTPOS_MIN_GAIN_Z 48.0f +#define BOTPOS_MIN_GAIN_Z_REGAIN 32.0f +#define BOTPOS_BBOX_HALF_HORIZ 640.0f +#define BOTPOS_BBOX_UP 320.0f +#define BOTPOS_MAX_AREA_CANDIDATES 24 +#define BOTPOS_UPLIFT_REACH_DIST 96.0f +#define BOTPOS_DETOUR_MIN_SCORE 80.0f + +#define BOTPOS_HARASS_MAX_HORIZ 700 +#define BOTPOS_HARASS_NEAR_ITEM 512.0f +#define BOTPOS_HARASS_FLED_DIST 600 +#define BOTPOS_HARASS_FLED_SEC 2.0f + +/* ========================================================================= + * Lifecycle + * ========================================================================= */ + +void BotPosition_RegisterCvars(void) { +} + +int BotPosition_IsActive(void) { + return BotEnhanced_IsActive(); +} + +void BotPosition_Reset(bot_state_t *bs) { + if (!bs) { + return; + } + bs->pos_enemy_z_delta = 0.0f; + bs->pos_last_high_z = bs->origin[2]; + bs->pos_high_sampled_at = 0.0f; + bs->pos_ledge_peek_until = 0.0f; + bs->pos_ledge_peek_crouch = qfalse; + bs->pos_item_harass_active = qfalse; + bs->pos_route_audit_time = 0.0f; + bs->pos_uplift_active = qfalse; + bs->pos_uplift_until = 0.0f; + memset(&bs->pos_uplift_goal, 0, sizeof(bs->pos_uplift_goal)); +} + +/* ========================================================================= + * Per-think state update + * ========================================================================= */ + +void BotPosition_OnThinkStart(bot_state_t *bs) { + float now; + + if (!bs || !BotPosition_IsActive()) { + return; + } + if (BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + + now = FloatTime(); + + if (bs->origin[2] > bs->pos_last_high_z) { + bs->pos_last_high_z = bs->origin[2]; + bs->pos_high_sampled_at = now; + } else if (now > bs->pos_high_sampled_at + BOTPOS_HIGH_DECAY_SEC) { + bs->pos_last_high_z = bs->origin[2]; + bs->pos_high_sampled_at = now; + } + + if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + bs->pos_enemy_z_delta = bs->origin[2] - bs->lastenemyorigin[2]; + } else { + bs->pos_enemy_z_delta = 0.0f; + } + + if (BotItems_HasActiveCommit(bs) || BotNavGuard_HasIdleOrLoopRisk(bs)) { + BotPosition_TickRouteElevation(bs); + } +} + +/* ========================================================================= + * Helpers + * ========================================================================= */ + +static int BotPosition_HorizontalDistToEnemy(const bot_state_t *bs) { + vec3_t delta; + + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 99999; + } + VectorSubtract(bs->lastenemyorigin, bs->origin, delta); + delta[2] = 0.0f; + return (int)sqrt(VectorLengthSquared(delta)); +} + +static int BotPosition_IsLedgePeekOpportunity(bot_state_t *bs) { + int horiz; + + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 0; + } + if (!BotCombat_HasFightLOS(bs, bs->enemy)) { + return 0; + } + if (bs->pos_enemy_z_delta < BOTPOS_LEDGE_PEEK_Z) { + return 0; + } + horiz = BotPosition_HorizontalDistToEnemy(bs); + if (horiz < BOTPOS_LEDGE_PEEK_MIN_HORIZ || + horiz > BOTPOS_LEDGE_PEEK_MAX_HORIZ) { + return 0; + } + return BotMove_IsAtLedgeEdge(bs); +} + +static int BotPosition_NearTimingItem(bot_state_t *bs) { + vec3_t delta; + + if (!bs) { + return 0; + } + if (BotItems_TimingHoldingNearGoal(bs)) { + return 1; + } + if (bs->item_commit_timing && bs->item_commit_active) { + VectorSubtract(bs->item_commit_goal.origin, bs->origin, delta); + return VectorLength(delta) <= BOTPOS_HARASS_NEAR_ITEM; + } + if (bs->item_commit_suspended && bs->item_commit_suspended_timing) { + VectorSubtract(bs->item_commit_suspended_goal.origin, bs->origin, delta); + return VectorLength(delta) <= BOTPOS_HARASS_NEAR_ITEM; + } + if (bs->timing_pursue_track >= 0 && + bs->timing_pursue_track < BOT_TIMING_TRACK_COUNT) { + VectorSubtract(bs->timing_track[bs->timing_pursue_track].origin, + bs->origin, delta); + return VectorLength(delta) <= BOTPOS_HARASS_NEAR_ITEM; + } + return 0; +} + +static int BotPosition_HasTimingPursuit(bot_state_t *bs) { + if (!bs) { + return 0; + } + if (bs->timing_pursue_track >= 0) { + return 1; + } + if (bs->item_commit_timing && bs->item_commit_active) { + return 1; + } + if (bs->item_commit_suspended && bs->item_commit_suspended_timing) { + return 1; + } + return 0; +} + +static int BotPosition_HasOverlook(bot_state_t *bs) { + if (!bs) { + return 0; + } + if (BotPosition_HasHeightAdvantage(bs)) { + return 1; + } + return BotMove_IsAtLedgeEdge(bs) && BotPosition_NearTimingItem(bs); +} + +static int BotPosition_EnemyFled(const bot_state_t *bs) { + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 1; + } + if (BotCombat_HasFightLOS((bot_state_t *)bs, bs->enemy)) { + return 0; + } + if (bs->enemyvisible_time >= FloatTime() - BOTPOS_HARASS_FLED_SEC) { + return 0; + } + return BotPosition_HorizontalDistToEnemy(bs) > BOTPOS_HARASS_FLED_DIST; +} + +static void BotPosition_EndItemHarass(bot_state_t *bs) { + if (!bs || !bs->pos_item_harass_active) { + return; + } + bs->pos_item_harass_active = qfalse; + if (bs->item_commit_suspended && !BotItems_IsDetourCommit(bs) && + !bs->item_commit_opportune) { + BotItems_CancelDetourSuspend(bs); + } +} + +int BotPosition_IsItemHarassActive(const bot_state_t *bs) { + if (!bs || !BotPosition_IsActive()) { + return 0; + } + return bs->pos_item_harass_active; +} + +int BotPosition_CanItemHarass(const bot_state_t *bs) { + int horiz; + + if (!bs || !BotPosition_IsActive() || !BotItemTiming_IsActive()) { + return 0; + } + if (BotItems_IsDetourCommit(bs) || bs->item_commit_opportune) { + return 0; + } + if (!BotPosition_HasTimingPursuit((bot_state_t *)bs) || + !BotPosition_NearTimingItem((bot_state_t *)bs)) { + return 0; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 0; + } + horiz = BotPosition_HorizontalDistToEnemy(bs); + if (horiz > BOTPOS_HARASS_MAX_HORIZ) { + return 0; + } + if (!BotCombat_HasFightLOS((bot_state_t *)bs, bs->enemy) && + bs->enemyvisible_time < FloatTime() - BOT_COMBAT_LOS_DROP_SEC) { + return 0; + } + return BotPosition_HasOverlook((bot_state_t *)bs); +} + +void BotPosition_BeginItemHarass(bot_state_t *bs) { + if (!bs || !BotPosition_IsActive() || !BotItemTiming_IsActive()) { + return; + } + if (BotItems_IsDetourCommit(bs) || bs->item_commit_opportune) { + return; + } + if (bs->pos_item_harass_active) { + return; + } + if (bs->item_commit_timing && bs->item_commit_active) { + if (!BotItems_SuspendTimingPrimary(bs)) { + return; + } + } else if (!bs->item_commit_suspended || !bs->item_commit_suspended_timing) { + return; + } + bs->pos_item_harass_active = qtrue; +} + +void BotPosition_TickItemHarass(bot_state_t *bs) { + if (!bs || !BotPosition_IsActive() || !BotItemTiming_IsActive()) { + return; + } + if (BotMove_WantsUrgentHealth(bs)) { + if (bs->pos_item_harass_active) { + BotPosition_EndItemHarass(bs); + } + return; + } + if (bs->pos_item_harass_active) { + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS || + EntityClientIsDead(bs->enemy) || + BotPosition_EnemyFled(bs)) { + BotPosition_EndItemHarass(bs); + } + return; + } + if (BotPosition_CanItemHarass(bs)) { + BotPosition_BeginItemHarass(bs); + } +} + +void BotPosition_UpdateCombat(bot_state_t *bs) { + if (!bs || !BotPosition_IsActive()) { + return; + } + if (BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + if (BotCombat_IsRushOpponent(bs)) { + return; + } + + if (bs->pos_item_harass_active) { + bs->combat.stance = BOT_STANCE_LEDGE_HOLD; + bs->combat.move_policy = BOT_MOVE_POLICY_LEGACY; + bs->combat.stance_until = FloatTime() + BOTPOS_LEDGE_HOLD_SEC; + return; + } + + if (BotPosition_IsLedgePeekOpportunity(bs)) { + bs->combat.stance = BOT_STANCE_LEDGE_HOLD; + bs->combat.move_policy = BOT_MOVE_POLICY_LEGACY; + bs->combat.stance_until = FloatTime() + BOTPOS_LEDGE_HOLD_SEC; + return; + } + + if (bs->combat.stance == BOT_STANCE_LEDGE_HOLD && + bs->combat.stance_until > FloatTime() && + BotPosition_HasHeightAdvantage(bs) && + BotCombat_HasFightLOS(bs, bs->enemy)) { + return; + } +} + +/* ========================================================================= + * Travel flags + * ========================================================================= */ + +int BotPosition_AdjustTravelFlags(bot_state_t *bs, int tfl) { + if (!bs || !BotPosition_IsActive()) { + return tfl; + } + + if (BotMove_WalkoffEscapeActive(bs)) { + return tfl; + } + + /* Item harass only: step-level 128uu / damage gates handle everything else. */ + if (bs->pos_item_harass_active) { + tfl &= ~TFL_WALKOFFLEDGE; + } + + return tfl; +} + +/* ========================================================================= + * Queries + * ========================================================================= */ + +int BotPosition_HasHeightAdvantage(const bot_state_t *bs) { + if (!bs || !BotPosition_IsActive()) { + return 0; + } + return bs->enemy >= 0 && bs->pos_enemy_z_delta > BOTPOS_HEIGHT_THRESHOLD; +} + +int BotPosition_ShouldSuppressDownhillCharge(const bot_state_t *bs) { + if (!bs || !BotPosition_IsActive()) { + return 0; + } + return bs->enemy >= 0 && bs->pos_enemy_z_delta > BOTPOS_CHARGE_SUPPRESS_Z; +} + +int BotPosition_BlocksWalkoffForRegain(const bot_state_t *bs) { + float goalZ; + + if (!bs || !BotPosition_IsActive()) { + return 0; + } + if (BotIsDead((bot_state_t *)bs) || BotIsObserver((bot_state_t *)bs)) { + return 0; + } + if (bs->enemy >= 0) { + return 0; + } + if (bs->pos_last_high_z - bs->origin[2] <= BOTPOS_REGAIN_THRESHOLD) { + return 0; + } + if (BotItems_HasActiveCommit(bs)) { + goalZ = BotItems_GetCommitGoalOriginZ(bs); + if (goalZ < bs->origin[2] - BOTPOS_GOAL_MEANINGFULLY_BELOW) { + return 0; + } + } + return 1; +} + +int BotPosition_PursuitGoalBonus(const bot_state_t *bs, const bot_goal_t *goal) { + float dz; + + if (!bs || !goal || !BotPosition_IsActive()) { + return 0; + } + dz = goal->origin[2] - bs->origin[2]; + if (dz > 32.0f) { + return BOTPOS_SCORE_ELEVATED_GOAL; + } + if (dz > -32.0f) { + return BOTPOS_SCORE_HIGH_GOAL; + } + return 0; +} + +int BotPosition_RouteElevationBonus(const bot_state_t *bs, const bot_goal_t *goal, + int travelTime) { + float dz; + float travelSec; + float efficiency; + int bonus; + int cap; + + if (!bs || !goal || !BotPosition_IsActive()) { + return 0; + } + + bonus = BotPosition_PursuitGoalBonus(bs, goal); + if (travelTime <= 0) { + return bonus; + } + + dz = goal->origin[2] - bs->origin[2]; + if (dz <= 0.0f) { + return bonus; + } + + travelSec = travelTime * 0.01f; + if (travelSec < 0.05f) { + travelSec = 0.05f; + } + efficiency = dz / travelSec; + if (efficiency > BOTPOS_EFFICIENCY_SCALE) { + bonus += (int)((efficiency - BOTPOS_EFFICIENCY_SCALE) * + BOTPOS_EFFICIENCY_MULT); + } + cap = BOTPOS_SCORE_ELEVATED_GOAL + BOTPOS_EFFICIENCY_CAP; + if (bonus > cap) { + bonus = cap; + } + return bonus; +} + +void BotPosition_CancelUplift(bot_state_t *bs) { + bot_goal_t top; + + if (!bs || !bs->pos_uplift_active) { + return; + } + bs->pos_uplift_active = qfalse; + bs->pos_uplift_until = 0.0f; + if (trap_BotGetTopGoal(bs->gs, &top) && + top.number == BOTPOS_UPLIFT_GOAL_NUMBER) { + trap_BotPopGoal(bs->gs); + } +} + +static void BotPosition_TickUpliftProgress(bot_state_t *bs) { + vec3_t delta; + + if (!bs || !bs->pos_uplift_active) { + return; + } + if (FloatTime() > bs->pos_uplift_until) { + BotPosition_CancelUplift(bs); + return; + } + VectorSubtract(bs->pos_uplift_goal.origin, bs->origin, delta); + if (VectorLength(delta) <= BOTPOS_UPLIFT_REACH_DIST) { + BotPosition_CancelUplift(bs); + return; + } + if (bs->areanum > 0 && bs->areanum == bs->pos_uplift_goal.areanum) { + BotPosition_CancelUplift(bs); + } +} + +static qboolean BotPosition_GetRouteDestination(bot_state_t *bs, bot_goal_t *dest) { + if (!bs || !dest) { + return qfalse; + } + if (BotItems_HasActiveCommit(bs)) { + memcpy(dest, &bs->item_commit_goal, sizeof(bot_goal_t)); + return dest->areanum > 0; + } + return trap_BotGetTopGoal(bs->gs, dest) && dest->areanum > 0; +} + +static int BotPosition_SkipUpliftForNearBelowGoal(bot_state_t *bs) { + vec3_t delta; + float goalZ; + float horiz; + + if (!bs || !BotItems_HasActiveCommit(bs)) { + return 0; + } + goalZ = BotItems_GetCommitGoalOriginZ(bs); + if (goalZ >= bs->origin[2] - BOTPOS_GOAL_MEANINGFULLY_BELOW) { + return 0; + } + VectorSubtract(bs->item_commit_goal.origin, bs->origin, delta); + delta[2] = 0.0f; + horiz = VectorLength(delta); + return horiz <= BOTPOS_NEAR_GOAL_HORIZ; +} + +static int BotPosition_ShouldAuditRoute(bot_state_t *bs) { + bot_goal_t goal; + + if (!bs || !BotPosition_IsActive() || BotIsDead(bs) || BotIsObserver(bs)) { + return 0; + } + if (BotPosition_IsItemHarassActive(bs)) { + return 0; + } + if (bs->enemy >= 0) { + return 0; + } + if (!bs->areanum || !trap_AAS_AreaReachability(bs->areanum)) { + return 0; + } + if (BotItems_HasActiveCommit(bs)) { + if (BotPosition_SkipUpliftForNearBelowGoal(bs)) { + return 0; + } + return 1; + } + return trap_BotGetTopGoal(bs->gs, &goal); +} + +static void BotPosition_TryStartUplift(bot_state_t *bs, const bot_goal_t *dest, + int tfl) { + int areas[BOTPOS_MAX_AREA_CANDIDATES]; + vec3_t absmins, absmaxs, areaOrigin; + aas_areainfo_t info; + int numareas; + int i; + int sideTime, viaTime, directTime, extraTime; + float dz, bestScore, score; + float minGain, maxExtra; + int bestArea; + float now; + + if (!bs || !dest || bs->pos_uplift_active) { + return; + } + + minGain = BOTPOS_MIN_GAIN_Z; + maxExtra = (float)BOTPOS_DETOUR_MAX_EXTRA; + if (BotPosition_BlocksWalkoffForRegain(bs)) { + minGain = BOTPOS_MIN_GAIN_Z_REGAIN; + maxExtra = (float)BOTPOS_DETOUR_MAX_EXTRA_REGAIN; + } + + directTime = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, + dest->areanum, tfl); + if (directTime <= 0) { + return; + } + + absmins[0] = bs->origin[0] - BOTPOS_BBOX_HALF_HORIZ; + absmins[1] = bs->origin[1] - BOTPOS_BBOX_HALF_HORIZ; + absmins[2] = bs->origin[2] + minGain; + absmaxs[0] = bs->origin[0] + BOTPOS_BBOX_HALF_HORIZ; + absmaxs[1] = bs->origin[1] + BOTPOS_BBOX_HALF_HORIZ; + absmaxs[2] = bs->origin[2] + BOTPOS_BBOX_UP; + + numareas = trap_AAS_BBoxAreas(absmins, absmaxs, areas, + BOTPOS_MAX_AREA_CANDIDATES); + if (numareas <= 0) { + return; + } + + bestScore = BOTPOS_DETOUR_MIN_SCORE; + bestArea = 0; + for (i = 0; i < numareas; i++) { + if (!areas[i] || areas[i] == bs->areanum) { + continue; + } + trap_AAS_AreaInfo(areas[i], &info); + dz = info.center[2] - bs->origin[2]; + if (dz < minGain) { + continue; + } + sideTime = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, + areas[i], tfl); + if (sideTime <= 0 || sideTime > BOTPOS_DETOUR_MAX_TRAVEL) { + continue; + } + viaTime = trap_AAS_AreaTravelTimeToGoalArea(areas[i], info.center, + dest->areanum, tfl); + if (viaTime <= 0) { + continue; + } + extraTime = sideTime + viaTime - directTime; + if (extraTime < 0) { + extraTime = 0; + } + if ((float)extraTime > maxExtra) { + continue; + } + score = dz * 8.0f - (float)extraTime * 0.35f; + if (BotPosition_BlocksWalkoffForRegain(bs)) { + score += dz * 2.0f; + } + if (score > bestScore) { + bestScore = score; + bestArea = areas[i]; + VectorCopy(info.center, areaOrigin); + } + } + + if (!bestArea) { + return; + } + + memset(&bs->pos_uplift_goal, 0, sizeof(bs->pos_uplift_goal)); + VectorCopy(areaOrigin, bs->pos_uplift_goal.origin); + bs->pos_uplift_goal.areanum = bestArea; + bs->pos_uplift_goal.number = BOTPOS_UPLIFT_GOAL_NUMBER; + bs->pos_uplift_goal.flags = 0; + + if (!BotEnhanced_PushGoalSafe(bs, &bs->pos_uplift_goal)) { + return; + } + + now = FloatTime(); + bs->pos_uplift_active = qtrue; + bs->pos_uplift_until = now + BOTPOS_UPLIFT_MAX_SEC; + bs->pos_route_audit_time = now + BOTPOS_ROUTE_AUDIT_INTERVAL; +} + +void BotPosition_TickRouteElevation(bot_state_t *bs) { + bot_goal_t dest; + float now; + int tfl; + + if (!bs || !BotPosition_IsActive()) { + return; + } + + BotPosition_TickUpliftProgress(bs); + if (bs->pos_uplift_active) { + return; + } + + now = FloatTime(); + if (now < bs->pos_route_audit_time) { + return; + } + bs->pos_route_audit_time = now + BOTPOS_ROUTE_AUDIT_INTERVAL; + + if (!BotPosition_ShouldAuditRoute(bs)) { + return; + } + if (!BotPosition_GetRouteDestination(bs, &dest)) { + return; + } + + tfl = BotMove_EffectiveTfl(bs); + BotPosition_TryStartUplift(bs, &dest, tfl); +} + +/* Ledge peek phase toggling — called from BotAttackMove. */ +void BotPosition_TickLedgePeek(bot_state_t *bs) { + float now; + float phase; + + if (!bs || !BotCombat_IsLedgeHold(bs)) { + return; + } + + now = FloatTime(); + if (now < bs->pos_ledge_peek_until) { + if (bs->pos_ledge_peek_crouch) { + bs->attackcrouch_time = now + 1.0f; + } + return; + } + + bs->pos_ledge_peek_crouch = !bs->pos_ledge_peek_crouch; + phase = bs->pos_ledge_peek_crouch ? + BOTPOS_LEDGE_PEEK_DOWN_SEC : BOTPOS_LEDGE_PEEK_UP_SEC; + bs->pos_ledge_peek_until = now + phase; + if (bs->pos_ledge_peek_crouch) { + bs->attackcrouch_time = now + phase + 0.2f; + } +} + +int BotPosition_WantsLedgeStrafeOnly(const bot_state_t *bs) { + if (!bs) { + return 0; + } + return BotCombat_IsLedgeHold(bs) || BotPosition_HasHeightAdvantage(bs) || + bs->pos_item_harass_active; +} diff --git a/ratoa_gamecode/code/game/ai_bot_position.h b/ratoa_gamecode/code/game/ai_bot_position.h new file mode 100644 index 0000000..6b1c627 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_position.h @@ -0,0 +1,82 @@ +/* +=========================================================================== +BOT POSITION — high-ground preference behaviors for enhanced bots. + +Gate: bot_enhanced = 1 AND bot_enhanced_position = 1. + +Behaviors provided: + • Travel policy: block walkoff drops when pursuing items at or above the bot, + and when the bot has fallen from a recent peak (unless the item is below). + • Height-advantage combat: suppress downhill rush, strafe at elevation. + • Ledge hold: when at a ledge edge with a lower visible enemy, latch a + peek-and-duck combat stance. + • Pursuit scoring bonus for elevated item goals (travel-time weighted). + • Mid-route uplift waypoints toward higher AAS areas while roaming. + +CPU notes: + • OnThinkStart / UpdateCombat: arithmetic + cached fields only. + • AdjustTravelFlags: one Z comparison when an item commit is active. + • TickRouteElevation: throttled AAS bbox scan (~every 2.5s while routing). +=========================================================================== +*/ + +#ifndef AI_BOT_POSITION_H +#define AI_BOT_POSITION_H + +struct bot_state_s; +struct bot_goal_s; + +/* Lifecycle ------------------------------------------------------------ */ +void BotPosition_RegisterCvars(void); +int BotPosition_IsActive(void); +void BotPosition_Reset(struct bot_state_s *bs); + +/* Called once per think before combat intent is finalized. */ +void BotPosition_OnThinkStart(struct bot_state_s *bs); + +/* Called after BotCombat_UpdateIntent to latch ledge-hold when appropriate. */ +void BotPosition_UpdateCombat(struct bot_state_s *bs); + +/* Travel flags -------------------------------------------------------- */ + +/* + * Apply position-related travel-flag adjustments (walkoff blocking for item + * goals and high-ground regain). Called from BotMove_EffectiveTfl and + * BotItems_TravelFlags. + */ +int BotPosition_AdjustTravelFlags(struct bot_state_s *bs, int tfl); + +/* Query --------------------------------------------------------------- */ + +int BotPosition_HasHeightAdvantage(const struct bot_state_s *bs); +int BotPosition_ShouldSuppressDownhillCharge(const struct bot_state_s *bs); +int BotPosition_BlocksWalkoffForRegain(const struct bot_state_s *bs); + +/* + * Flat bonus from goal endpoint height (legacy / display). + * Zero when position module is inactive. + */ +int BotPosition_PursuitGoalBonus(const struct bot_state_s *bs, + const struct bot_goal_s *goal); + +/* + * Travel-time-weighted elevation bonus for route scoring (items + timing). + * Includes PursuitGoalBonus plus efficiency for height gained per travel second. + */ +int BotPosition_RouteElevationBonus(const struct bot_state_s *bs, + const struct bot_goal_s *goal, int travelTime); + +/* Throttled mid-route audit: push brief uplift goals toward higher areas. */ +void BotPosition_TickRouteElevation(struct bot_state_s *bs); +void BotPosition_CancelUplift(struct bot_state_s *bs); + +void BotPosition_TickLedgePeek(struct bot_state_s *bs); +int BotPosition_WantsLedgeStrafeOnly(const struct bot_state_s *bs); + +/* Item overlook harass: suspend timing pursuit, fight from height, then resume. */ +void BotPosition_TickItemHarass(struct bot_state_s *bs); +int BotPosition_IsItemHarassActive(const struct bot_state_s *bs); +int BotPosition_CanItemHarass(const struct bot_state_s *bs); +void BotPosition_BeginItemHarass(struct bot_state_s *bs); + +#endif /* AI_BOT_POSITION_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.c b/ratoa_gamecode/code/game/ai_bot_tactics.c index 6c61fcd..386b8e6 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.c +++ b/ratoa_gamecode/code/game/ai_bot_tactics.c @@ -17,6 +17,8 @@ BOT TACTICAL AI — see ai_bot_tactics.h #include "ai_dmq3.h" #include "ai_bot_tactics.h" #include "ai_bot_enhanced.h" +#include "ai_bot_events.h" +#include "ai_dmq3.h" void AIEnter_Battle_Retreat(bot_state_t *bs, char *s); void AIEnter_Battle_Fight(bot_state_t *bs, char *s); @@ -24,13 +26,6 @@ int AINode_Battle_Fight(bot_state_t *bs); qboolean EntityCarriesFlag(aas_entityinfo_t *entinfo); -vmCvar_t bot_enhanced_tactics; - -#define BOT_TACTICS_CLOSER_MARGIN 128 -#define BOT_TACTICS_SWAP_NEAR_DIST 280.0f -#define BOT_TACTICS_SWAP_FAR_DIST 720.0f -#define BOT_TACTICS_SWAP_BOTH_FAR_MIN 680 -#define BOT_TACTICS_SWAP_MIN_ADV 64.0f #define BOT_TACTICS_FINISH_HEALTH 40 #define BOT_TACTICS_HURT_MIN_DAMAGE 8 #define BOT_TACTICS_FLEE_HEALTH 50 @@ -45,7 +40,7 @@ typedef enum { } tact_action_t; static int BotTactics_IsActive(void) { - return BotEnhanced_TacticsActive(); + return BotEnhanced_IsActive(); } static int BotTactics_HasUsableNonGauntletWeapon(bot_state_t *bs) { @@ -108,87 +103,6 @@ static int BotTactics_IsValidEnemyClient(bot_state_t *bs, int clientnum) { return 1; } -static float BotTactics_NormalizedSkill(bot_state_t *bs) { - float s; - - if (!bs) { - return 0.0f; - } - s = bs->settings.skill; - if (s < 1.0f) { - s = 1.0f; - } - if (s > 5.0f) { - s = 5.0f; - } - return (s - 1.0f) / 4.0f; -} - -/* - * Chance to swap from curhoriz target to a nearer candhoriz enemy (0..1). - * High skill + close threat -> near certain; both far -> unlikely. - */ -static float BotTactics_CloserSwapChance(bot_state_t *bs, int curhoriz, int candhoriz) { - float skillNorm, adv, advRatio, candNear, farScale, merit, chance; - int minDist; - - if (!bs || candhoriz >= curhoriz) { - return 0.0f; - } - adv = (float)(curhoriz - candhoriz); - if (adv < BOT_TACTICS_SWAP_MIN_ADV) { - return 0.0f; - } - - skillNorm = BotTactics_NormalizedSkill(bs); - advRatio = adv / ((float)curhoriz + 64.0f); - if (advRatio > 1.0f) { - advRatio = 1.0f; - } - - candNear = 1.0f - (float)candhoriz / BOT_TACTICS_SWAP_NEAR_DIST; - if (candNear < 0.0f) { - candNear = 0.0f; - } else if (candNear > 1.0f) { - candNear = 1.0f; - } - - minDist = curhoriz; - if (candhoriz < minDist) { - minDist = candhoriz; - } - farScale = 1.0f; - if (minDist > BOT_TACTICS_SWAP_BOTH_FAR_MIN) { - farScale = 1.0f - ((float)minDist - (float)BOT_TACTICS_SWAP_BOTH_FAR_MIN) / - (BOT_TACTICS_SWAP_FAR_DIST - (float)BOT_TACTICS_SWAP_BOTH_FAR_MIN + 1.0f); - if (farScale < 0.06f) { - farScale = 0.06f; - } - } - if (curhoriz > (int)BOT_TACTICS_SWAP_FAR_DIST && - candhoriz > (int)BOT_TACTICS_SWAP_NEAR_DIST) { - farScale *= 0.3f; - } - - merit = advRatio * 0.45f + candNear * 0.55f; - merit *= farScale; - - chance = (0.08f + skillNorm * 0.27f) + (0.42f + skillNorm * 0.58f) * merit; - if (chance > 1.0f) { - chance = 1.0f; - } - - if (skillNorm >= 0.75f && candhoriz <= (int)BOT_TACTICS_SWAP_NEAR_DIST && - adv >= 160.0f && advRatio >= 0.22f && chance < 0.92f) { - chance = 0.92f; - } - if (skillNorm >= 0.99f && candhoriz <= 192 && curhoriz >= 320 && adv >= 200.0f) { - chance = 1.0f; - } - - return chance; -} - static int BotTactics_HorizontalDist(bot_state_t *bs, int clientnum) { vec3_t dir; aas_entityinfo_t entinfo; @@ -236,9 +150,15 @@ static float BotTactics_ThreatScore(bot_state_t *bs, int clientnum, int damage, score += (float)damage * 2.5f; } score += BotTactics_ModThreatBonus(mod); - vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, clientnum); - if (vis > 0) { - score += 40.0f * vis; + if (BotEnhanced_IsActive()) { + if (BotCombat_HasFightLOS(bs, clientnum)) { + score += 40.0f; + } + } else { + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, clientnum); + if (vis > 0) { + score += 40.0f * vis; + } } if (EntityIsShooting(&entinfo)) { score += 35.0f; @@ -393,9 +313,6 @@ void BotTactics_ProcessPending(bot_state_t *bs) { } void BotTactics_RegisterCvars(void) { - trap_Cvar_Register(&bot_enhanced_tactics, "bot_enhanced_tactics", "0", - CVAR_ARCHIVE); - trap_Cvar_Update(&bot_enhanced_tactics); } void BotTactics_Reset(bot_state_t *bs) { @@ -478,88 +395,57 @@ int BotTactics_SkipAimAtEnemy(bot_state_t *bs) { return qtrue; } -void BotTactics_PreferCloserEnemy(bot_state_t *bs) { - int i, curenemy, bestenemy, curhoriz, candhoriz, besthoriz, minMargin; - float vis, swapChance; - vec3_t dir; - aas_entityinfo_t entinfo, cureinfo; +/* +=========================================================================== +BOT EVENTS — ingress queue; drain delegates scan/process to tactics. +=========================================================================== +*/ - if (!BotTactics_IsActive()) { - return; - } - curenemy = bs->enemy; - if (curenemy < 0 || curenemy >= MAX_CLIENTS) { +void BotEvents_Reset(bot_state_t *bs) { + if (!bs) { return; } - if (EntityClientIsDead(curenemy)) { + bs->evt_pending = 0; + bs->evt_attacker = -1; + bs->evt_damage = 0; + bs->evt_mod = MOD_UNKNOWN; +} + +void BotEvents_Push(bot_state_t *bs, int evt_bits, int ent, int parm) { + if (!bs || !evt_bits) { return; } - curhoriz = bs->inventory[ENEMY_HORIZONTAL_DIST]; - if (curhoriz <= 0) { - curhoriz = BotTactics_HorizontalDist(bs, curenemy); + bs->evt_pending |= evt_bits; + if (evt_bits & BOT_EVT_HURT_BY_OTHER) { + bs->evt_attacker = ent; + bs->evt_damage = parm; } - if (g_entities[curenemy].health > 0 && - g_entities[curenemy].health <= BOT_TACTICS_FINISH_HEALTH) { +} + +static void BotEvents_FlushToTactics(bot_state_t *bs) { + if (!bs->evt_pending) { return; } - BotEntityInfo(curenemy, &cureinfo); - if (EntityCarriesFlag(&cureinfo)) { - return; - } - - minMargin = (int)(BOT_TACTICS_CLOSER_MARGIN + - (1.0f - BotTactics_NormalizedSkill(bs)) * 96.0f); - - bestenemy = -1; - besthoriz = 999999; - for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) { - if (i == bs->client || i == curenemy) { - continue; - } - BotEntityInfo(i, &entinfo); - if (!entinfo.valid) { - continue; - } - if (EntityClientIsDead(i) || entinfo.number == bs->entitynum) { - continue; - } - if (EntityIsInvisible(&entinfo) && !EntityIsShooting(&entinfo)) { - continue; - } - if (g_entities[i].flags & FL_NOTARGET) { - continue; - } - if (BotSameTeam(bs, i)) { - continue; - } - if (BotEnhanced_IsActive() && BotEnhanced_ClientIsChatting(i)) { - continue; - } - VectorSubtract(entinfo.origin, bs->origin, dir); - dir[2] = 0; - candhoriz = (int)VectorLength(dir); - if (candhoriz + minMargin >= curhoriz) { - continue; - } - vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, i); - if (vis <= 0) { - continue; - } - if (bestenemy < 0 || candhoriz < besthoriz) { - bestenemy = i; - besthoriz = candhoriz; - } + bs->tact_pending |= bs->evt_pending; + if (bs->evt_pending & BOT_EVT_HURT_BY_OTHER) { + bs->tact_evt_attacker = bs->evt_attacker; + bs->tact_evt_damage = bs->evt_damage; + bs->tact_evt_mod = bs->evt_mod; } + BotEvents_Reset(bs); +} - if (bestenemy < 0 || bestenemy == curenemy) { +void BotEvents_Drain(bot_state_t *bs) { + if (!bs) { return; } - - swapChance = BotTactics_CloserSwapChance(bs, curhoriz, besthoriz); - if (swapChance <= 0.0f || random() >= swapChance) { + if (!BotEnhanced_IsActive() || BotIsObserver(bs) || BotIsDead(bs)) { + bs->tact_pending = 0; + BotEvents_Reset(bs); return; } - BotTactics_AssignEnemy(bs, bestenemy); - BotUpdateBattleInventory(bs, bestenemy); + BotTactics_ScanEvents(bs); + BotEvents_FlushToTactics(bs); + BotTactics_ProcessPending(bs); } diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.h b/ratoa_gamecode/code/game/ai_bot_tactics.h index 7f06073..8032e89 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.h +++ b/ratoa_gamecode/code/game/ai_bot_tactics.h @@ -37,7 +37,4 @@ int BotTactics_BattleFightSuppressRetreat(struct bot_state_s *bs); void BotTactics_RetreatAfterInventory(struct bot_state_s *bs); int BotTactics_SkipAimAtEnemy(struct bot_state_s *bs); -/* Prefer a nearer visible threat; swap chance scales with skill and distance gap */ -void BotTactics_PreferCloserEnemy(struct bot_state_s *bs); - #endif diff --git a/ratoa_gamecode/code/game/ai_dmnet.c b/ratoa_gamecode/code/game/ai_dmnet.c index ddfb643..3da5356 100644 --- a/ratoa_gamecode/code/game/ai_dmnet.c +++ b/ratoa_gamecode/code/game/ai_dmnet.c @@ -43,11 +43,13 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #include "ai_main.h" #include "ai_bot_enhanced.h" -#include "ai_bot_combat.h" +#include "ai_bot_dmnet.h" #include "ai_bot_items.h" +#include "ai_bot_item_timing.h" +#include "ai_bot_move_harness.h" #include "ai_bot_tactics.h" +#include "ai_weapon_select.h" #include "ai_dmq3.h" -#include "ai_bot_move_harness.h" #include "ai_chat.h" #include "ai_cmd.h" #include "ai_dmnet.h" @@ -161,35 +163,7 @@ BotGoForAir ================== */ int BotGoForAir(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { - bot_goal_t goal; - - //if the bot needs air - if (bs->lastair_time < FloatTime() - 6) { - // -#ifdef DEBUG - //BotAI_Print(PRT_MESSAGE, "going for air\n"); -#endif //DEBUG - //if we can find an air goal - if (BotGetAirGoal(bs, &goal)) { - BotEnhanced_PushGoalSafe(bs, &goal); - return qtrue; - } - else { - BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); - //get a nearby goal outside the water - while (BotItems_ChooseNBGItem(bs, tfl, ltg, range)) { - BotEnhanced_SanitizeGoalStack(bs); - trap_BotGetTopGoal(bs->gs, &goal); - //if the goal is not in water - if (!(trap_AAS_PointContents(goal.origin) & (CONTENTS_WATER|CONTENTS_SLIME|CONTENTS_LAVA))) { - return qtrue; - } - trap_BotPopGoal(bs->gs); - } - trap_BotResetAvoidGoals(bs->gs); - } - } - return qfalse; + return BotDmnet_GoForAir(bs, tfl, ltg, range); } /* @@ -216,8 +190,7 @@ int BotNearbyGoal(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { } } // - BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); - ret = BotItems_ChooseNBGItem(bs, BotJumppad_EffectiveTfl(bs), ltg, range); + ret = BotDmnet_NearbyGoal(bs, tfl, ltg, range); /* if (ret) { @@ -237,59 +210,7 @@ BotReachedGoal ================== */ int BotReachedGoal(bot_state_t *bs, bot_goal_t *goal) { - if (goal->flags & GFL_ITEM) { - { - int itemReached; - - itemReached = BotItems_HandleReachedGoal(bs, goal); - if (itemReached >= 0) { - return itemReached; - } - } - //if touching the goal - if (trap_BotTouchingGoal(bs->origin, goal)) { - if (!(goal->flags & GFL_DROPPED)) { - trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); - } - return qtrue; - } - //if the goal isn't there - if (BotItems_ItemGoalInVisButNotVisible(bs, goal)) { - /* - float avoidtime; - int t; - - avoidtime = trap_BotAvoidGoalTime(bs->gs, goal->number); - if (avoidtime > 0) { - t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, bs->tfl); - if ((float) t * 0.009 < avoidtime) - return qtrue; - } - */ - return qtrue; - } - //if in the goal area and below or above the goal and not swimming - if (bs->areanum == goal->areanum) { - if (bs->origin[0] > goal->origin[0] + goal->mins[0] && bs->origin[0] < goal->origin[0] + goal->maxs[0]) { - if (bs->origin[1] > goal->origin[1] + goal->mins[1] && bs->origin[1] < goal->origin[1] + goal->maxs[1]) { - if (!trap_AAS_Swimming(bs->origin)) { - return qtrue; - } - } - } - } - } - else if (goal->flags & GFL_AIR) { - //if touching the goal - if (trap_BotTouchingGoal(bs->origin, goal)) return qtrue; - //if the bot got air - if (bs->lastair_time > FloatTime() - 1) return qtrue; - } - else { - //if touching the goal - if (trap_BotTouchingGoal(bs->origin, goal)) return qtrue; - } - return qfalse; + return BotDmnet_ReachedGoal(bs, goal); } /* @@ -707,7 +628,7 @@ int BotGetLongTermGoal(bot_state_t *bs, int tfl, int retreat, bot_goal_t *goal) bs->ltgtype = 0; } // - if (BotItems_ItemGoalInVisButNotVisible(bs, goal)) { + if (BotEnhanced_IsActive() && BotItems_ItemGoalInVisButNotVisible(bs, goal)) { trap_BotGoalName(bs->teamgoal.number, buf, sizeof(buf)); BotAI_BotInitialChat(bs, "getitem_notthere", buf, NULL); trap_BotEnterChat(bs->cs, bs->decisionmaker, CHAT_TELL); @@ -1368,6 +1289,9 @@ int AINode_Respawn(bot_state_t *bs) { // if waiting for the actual respawn if (bs->respawn_wait) { if (!BotIsDead(bs)) { + if (BotEnhanced_IsActive()) { + BotItemTiming_OnSpawn(bs); + } AIEnter_Seek_LTG(bs, "respawn: respawned"); } else { @@ -1726,9 +1650,8 @@ int AINode_Seek_ActivateEntity(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } // if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { - bs->weaponnum = moveresult.weapon; - } + BotWpnSelect_ApplyMovementWeapon(bs, moveresult.weapon, + moveresult.flags & MOVERESULT_MOVEMENTWEAPON); BotMove_OnPostMoveToGoal(bs, &moveresult); // if there is an enemy if (BotFindEnemy(bs, -1)) { @@ -1802,14 +1725,26 @@ int AINode_Seek_NBG(bot_state_t *bs) { } //map specific code BotMapScripts(bs); - //no enemy - bs->enemy = -1; + if (BotEnhanced_IsActive()) { + bot_dmnet_seek_act_t seekAct; + + seekAct = BotDmnet_SeekCombatContact(bs, 1); + if (seekAct != BOT_DMNET_SEEK_NOP) { + BotDmnet_ExecuteSeekCombat(bs, seekAct, "seek nbg: found enemy", 1); + return qfalse; + } + } else { + //no enemy + bs->enemy = -1; + } //if the bot has no goal if (!trap_BotGetTopGoal(bs->gs, &goal)) bs->nbg_time = 0; //if the bot touches the current goal else if (BotReachedGoal(bs, &goal)) { BotChooseWeapon(bs); - bs->nbg_time = 0; + if (BotDmnet_NBG_ShouldClearReachTime(bs, &goal)) { + bs->nbg_time = 0; + } } // if (bs->nbg_time < FloatTime()) { @@ -1835,7 +1770,9 @@ int AINode_Seek_NBG(bot_state_t *bs) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach(bs->ms); } - BotItems_OnMoveFailure(bs); + if (BotEnhanced_IsActive()) { + BotItems_OnMoveFailure(bs, &moveresult); + } bs->nbg_time = 0; } //check if the bot is blocked @@ -1855,6 +1792,16 @@ int AINode_Seek_NBG(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } } + else if (BotDmnet_NBG_TimingHoldsCommit(bs, &goal)) { + if (random() < bs->thinktime * 0.6) { + VectorCopy(goal.origin, target); + target[0] += crandom() * 96.0f; + target[1] += crandom() * 96.0f; + VectorSubtract(target, bs->origin, dir); + vectoangles(dir, bs->ideal_viewangles); + bs->ideal_viewangles[2] *= 0.5; + } + } else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotMove_SuppressRoamView(bs)) { if (!trap_BotGetSecondGoal(bs->gs, &goal)) trap_BotGetTopGoal(bs->gs, &goal); if (trap_BotMovementViewTarget(bs->ms, &goal, BotJumppad_EffectiveTfl(bs), 300, target)) { @@ -1869,26 +1816,10 @@ int AINode_Seek_NBG(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { - bs->weaponnum = moveresult.weapon; - } + BotWpnSelect_ApplyMovementWeapon(bs, moveresult.weapon, + moveresult.flags & MOVERESULT_MOVEMENTWEAPON); BotMove_OnPostMoveToGoal(bs, &moveresult); - //if there is an enemy - if (BotFindEnemy(bs, -1)) { - BotMove_CancelBypass(bs); - if (BotWantsToRetreat(bs)) { - //keep the current long term goal and retreat - AIEnter_Battle_NBG(bs, "seek nbg: found enemy"); - } - else { - BotItems_AbortCommit(bs); - trap_BotResetLastAvoidReach(bs->ms); - //empty the goal stack - trap_BotEmptyGoalStack(bs->gs); - //go fight - AIEnter_Battle_Fight(bs, "seek nbg: found enemy"); - } - } + BotDmnet_NBG_PostMoveEnemyCheck(bs, &moveresult); return qtrue; } @@ -1956,26 +1887,31 @@ int AINode_Seek_LTG(bot_state_t *bs) } //map specific code BotMapScripts(bs); - //no enemy - bs->enemy = -1; // if (bs->killedenemy_time > FloatTime() - 2) { if (random() < bs->thinktime * 1) { trap_EA_Gesture(bs->client); } } - //if there is an enemy - if (BotFindEnemy(bs, -1)) { - if (BotWantsToRetreat(bs)) { - //keep the current long term goal and retreat - AIEnter_Battle_Retreat(bs, "seek ltg: found enemy"); + if (BotEnhanced_IsActive()) { + bot_dmnet_seek_act_t seekAct; + + seekAct = BotDmnet_SeekLTG_CombatContact(bs); + if (seekAct != BOT_DMNET_SEEK_NOP) { + BotDmnet_ExecuteSeekCombat(bs, seekAct, "seek ltg: found enemy", 0); return qfalse; } - else { + } else { + //no enemy + bs->enemy = -1; + //if there is an enemy + if (BotFindEnemy(bs, -1)) { + if (BotWantsToRetreat(bs)) { + AIEnter_Battle_Retreat(bs, "seek ltg: found enemy"); + return qfalse; + } trap_BotResetLastAvoidReach(bs->ms); - //empty the goal stack trap_BotEmptyGoalStack(bs->gs); - //go fight AIEnter_Battle_Fight(bs, "seek ltg: found enemy"); return qfalse; } @@ -1990,9 +1926,7 @@ int AINode_Seek_LTG(bot_state_t *bs) if (bs->check_time < FloatTime()) { bs->check_time = FloatTime() + 0.5; //check if the bot wants to camp - if (BotEnhanced_AllowsCamping()) { - BotWantsToCamp(bs); - } + BotDmnet_SeekLTG_TryCamp(bs); // if (bs->ltgtype == LTG_DEFENDKEYAREA) range = 400; else range = 150; @@ -2015,8 +1949,7 @@ int AINode_Seek_LTG(bot_state_t *bs) } */ // - if (BotItems_ShouldRunPickupNode(bs)) { - bs->nbg_time = BotItems_CommitNbgTime(bs); + if (BotDmnet_TryEnterItemPickup(bs, "items: committed pickup")) { AIEnter_Seek_NBG(bs, "items: committed pickup"); return qfalse; } @@ -2044,7 +1977,7 @@ int AINode_Seek_LTG(bot_state_t *bs) //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach(bs->ms); //BotAI_Print(PRT_MESSAGE, "movement failure %d\n", moveresult.traveltype); - BotItems_OnMoveFailure(bs); + BotDmnet_SeekLTG_OnMoveFailure(bs, &moveresult); bs->ltg_time = 0; } // @@ -2085,9 +2018,8 @@ int AINode_Seek_LTG(bot_state_t *bs) bs->ideal_viewangles[2] *= 0.5; } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { - bs->weaponnum = moveresult.weapon; - } + BotWpnSelect_ApplyMovementWeapon(bs, moveresult.weapon, + moveresult.flags & MOVERESULT_MOVEMENTWEAPON); BotMove_OnPostMoveToGoal(bs, &moveresult); // return qtrue; @@ -2123,7 +2055,6 @@ AINode_Battle_Fight ================== */ int AINode_Battle_Fight(bot_state_t *bs) { - int areanum; vec3_t target; aas_entityinfo_t entinfo; bot_moveresult_t moveresult; @@ -2157,9 +2088,7 @@ int AINode_Battle_Fight(bot_state_t *bs) { // BotEntityInfo(bs->enemy, &entinfo); //if the enemy is dead - if (BotEnhanced_IsActive() && EntityClientIsDead(bs->enemy)) { - bs->enemy = -1; - bs->enemydeath_time = 0; + if (BotDmnet_BattleFightEnemyDeadQuick(bs)) { AIEnter_Seek_LTG(bs, "battle fight: enemy dead"); return qfalse; } @@ -2202,20 +2131,12 @@ int AINode_Battle_Fight(bot_state_t *bs) { target[2] += 16; } } - //update the reachability area and origin if possible - areanum = BotPointAreaNum(target); - if (areanum && trap_AAS_AreaReachability(areanum)) { - VectorCopy(target, bs->lastenemyorigin); - bs->lastenemyareanum = areanum; - } //update the attack inventory values BotUpdateBattleInventory(bs, bs->enemy); - if (BotItems_ShouldRunPickupNode(bs)) { - bs->nbg_time = BotItems_CommitNbgTime(bs); + if (BotDmnet_TryEnterItemPickup(bs, "items: committed pickup")) { AIEnter_Battle_NBG(bs, "items: committed pickup"); return qfalse; } - BotTactics_PreferCloserEnemy(bs); if (BotTactics_BattleFightTryFlee(bs)) { return qfalse; } @@ -2236,18 +2157,23 @@ int AINode_Battle_Fight(bot_state_t *bs) { } } //if the enemy is not visible - if (!BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) { + { + bot_dmnet_fight_vis_t vis; + #ifdef MISSIONPACK - if (bs->enemy == redobelisk.entitynum || bs->enemy == blueobelisk.entitynum) { + if (!BotEnhanced_IsActive() && + (bs->enemy == redobelisk.entitynum || bs->enemy == blueobelisk.entitynum) && + !BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) { AIEnter_Battle_Chase(bs, "battle fight: obelisk out of sight"); return qfalse; } #endif - if (BotWantsToChase(bs)) { + vis = BotDmnet_BattleFightEnemyVisibility(bs, target); + if (vis == BOT_DMNET_FIGHT_VIS_CHASE) { AIEnter_Battle_Chase(bs, "battle fight: enemy out of sight"); return qfalse; } - else { + if (vis == BOT_DMNET_FIGHT_VIS_LTG) { AIEnter_Seek_LTG(bs, "battle fight: enemy out of sight"); return qfalse; } @@ -2265,9 +2191,7 @@ int AINode_Battle_Fight(bot_state_t *bs) { } //choose the best weapon to fight with BotChooseWeapon(bs); - if (BotEnhanced_IsActive()) { - BotCombat_UpdateIntent(bs); - } + BotDmnet_BattleFight_PreAttack(bs); //do attack movements moveresult = BotAttackMove(bs, bs->tfl); //if the movement failed @@ -2285,7 +2209,7 @@ int AINode_Battle_Fight(bot_state_t *bs) { BotCheckAttack(bs); //if the bot wants to retreat if (!(bs->flags & BFL_FIGHTSUICIDAL)) { - if (!BotEnhanced_ShouldSuppressFightRetreat(bs) && BotWantsToRetreat(bs)) { + if (BotDmnet_BattleFight_WantsRetreat(bs)) { AIEnter_Battle_Retreat(bs, "battle fight: wants to retreat"); return qtrue; } @@ -2315,6 +2239,11 @@ int AINode_Battle_Chase(bot_state_t *bs) vec3_t target, dir; bot_moveresult_t moveresult; float range; + float chaseAttackSec; + int chaseShoot; + + chaseAttackSec = BotDmnet_BattleChaseAttackSec(bs); + chaseShoot = BotDmnet_BattleChaseHasShootContact(bs); if (BotIsObserver(bs)) { AIEnter_Observer(bs, "battle chase: observer"); @@ -2336,14 +2265,18 @@ int AINode_Battle_Chase(bot_state_t *bs) return qfalse; } //if the enemy is visible - if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy)) { - if (!EntityClientIsDead(bs->enemy)) { + { + int chaseFight; + + chaseFight = BotDmnet_BattleChaseTryEnterFight(bs); + if (chaseFight > 0) { AIEnter_Battle_Fight(bs, "battle chase"); return qfalse; } - bs->enemy = -1; - AIEnter_Seek_LTG(bs, "battle chase: enemy dead"); - return qfalse; + if (chaseFight < 0) { + AIEnter_Seek_LTG(bs, "battle chase: enemy dead"); + return qfalse; + } } //if there is another enemy if (BotFindEnemy(bs, -1)) { @@ -2375,7 +2308,7 @@ int AINode_Battle_Chase(bot_state_t *bs) //if the last seen enemy spot is reached the enemy could not be found if (trap_BotTouchingGoal(bs->origin, &goal)) bs->chase_time = 0; //if there's no chase time left - if (!bs->chase_time || bs->chase_time < FloatTime() - 10) { + if (!bs->chase_time || bs->chase_time < FloatTime() - BotDmnet_BattleChaseTimeoutSec(bs)) { AIEnter_Seek_LTG(bs, "battle chase: time out"); return qfalse; } @@ -2384,8 +2317,7 @@ int AINode_Battle_Chase(bot_state_t *bs) bs->check_time = FloatTime() + 1; range = 150; // - if (BotItems_ShouldRunPickupNode(bs)) { - bs->nbg_time = BotItems_CommitNbgTime(bs); + if (BotDmnet_TryEnterItemPickup(bs, "items: committed pickup")) { AIEnter_Battle_NBG(bs, "items: committed pickup"); return qfalse; } @@ -2399,7 +2331,7 @@ int AINode_Battle_Chase(bot_state_t *bs) } // BotUpdateBattleInventory(bs, bs->enemy); - BotTactics_PreferCloserEnemy(bs); + BotChooseWeapon(bs); //initialize the movement state BotSetupForMovement(bs); //move towards the goal @@ -2418,7 +2350,7 @@ int AINode_Battle_Chase(bot_state_t *bs) VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles); } else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotMove_SuppressRoamView(bs)) { - if (bs->chase_time > FloatTime() - 2) { + if (chaseShoot || bs->chase_time > FloatTime() - chaseAttackSec) { BotAimAtEnemy(bs); } else { @@ -2433,10 +2365,12 @@ int AINode_Battle_Chase(bot_state_t *bs) bs->ideal_viewangles[2] *= 0.5; } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { - bs->weaponnum = moveresult.weapon; - } + BotWpnSelect_ApplyMovementWeapon(bs, moveresult.weapon, + moveresult.flags & MOVERESULT_MOVEMENTWEAPON); BotMove_OnPostMoveToGoal(bs, &moveresult); + if (chaseShoot || bs->chase_time > FloatTime() - chaseAttackSec) { + BotCheckAttack(bs); + } //if the bot is in the area the enemy was last seen in if (bs->areanum == bs->lastenemyareanum) bs->chase_time = 0; //if the bot wants to retreat (the bot could have been damage during the chase) @@ -2511,8 +2445,7 @@ int AINode_Battle_Retreat(bot_state_t *bs) { //update the attack inventory values BotUpdateBattleInventory(bs, bs->enemy); BotTactics_RetreatAfterInventory(bs); - if (BotCombat_ShouldEngageFromRetreat(bs)) { - bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; + if (BotDmnet_Retreat_TryChargeFight(bs)) { AIEnter_Battle_Fight(bs, "enhanced: close combat charge"); return qfalse; } @@ -2551,7 +2484,7 @@ int AINode_Battle_Retreat(bot_state_t *bs) { //else if the enemy is NOT visible else if (bs->enemyvisible_time < FloatTime()) { //if there is another enemy - if (BotFindEnemy(bs, -1)) { + if (!BotDmnet_Retreat_WantsFleeEngaged(bs) && BotFindEnemy(bs, -1)) { AIEnter_Battle_Fight(bs, "battle retreat: another enemy"); return qfalse; } @@ -2587,8 +2520,7 @@ int AINode_Battle_Retreat(bot_state_t *bs) { } */ // - if (BotItems_ShouldRunPickupNode(bs)) { - bs->nbg_time = BotItems_CommitNbgTime(bs); + if (BotDmnet_TryEnterItemPickup(bs, "items: committed pickup")) { AIEnter_Battle_NBG(bs, "items: committed pickup"); return qfalse; } @@ -2604,6 +2536,7 @@ int AINode_Battle_Retreat(bot_state_t *bs) { BotSetupForMovement(bs); //move towards the goal trap_BotMoveToGoal(&moveresult, bs->ms, &goal, BotJumppad_EffectiveTfl(bs)); + BotDmnet_Retreat_AdjustFleeMovement(bs, &moveresult); //if the movement failed if (moveresult.failure) { //reset the avoid reach, otherwise bot is stuck in current area @@ -2621,9 +2554,9 @@ int AINode_Battle_Retreat(bot_state_t *bs) { } else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET) && !(bs->flags & BFL_IDEALVIEWSET) && !BotMove_SuppressRoamView(bs) ) { - attack_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1); - //if the bot is skilled anough - if (attack_skill > 0.3) { + attack_skill = BotEnhanced_GetAttackSkill(bs); + //if the bot is skilled enough, or fleeing while engaged + if (attack_skill > 0.3 || BotDmnet_Retreat_WantsFleeEngaged(bs)) { BotAimAtEnemy(bs); } else { @@ -2638,9 +2571,8 @@ int AINode_Battle_Retreat(bot_state_t *bs) { } } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { - bs->weaponnum = moveresult.weapon; - } + BotWpnSelect_ApplyMovementWeapon(bs, moveresult.weapon, + moveresult.flags & MOVERESULT_MOVEMENTWEAPON); BotMove_OnPostMoveToGoal(bs, &moveresult); //attack the enemy if possible BotCheckAttack(bs); @@ -2768,7 +2700,7 @@ int AINode_Battle_NBG(bot_state_t *bs) { } else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET) && !(bs->flags & BFL_IDEALVIEWSET) && !BotMove_SuppressRoamView(bs)) { - attack_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1); + attack_skill = BotEnhanced_GetAttackSkill(bs); //if the bot is skilled anough and the enemy is visible if (attack_skill > 0.3) { //&& BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy) @@ -2786,9 +2718,8 @@ int AINode_Battle_NBG(bot_state_t *bs) { } } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { - bs->weaponnum = moveresult.weapon; - } + BotWpnSelect_ApplyMovementWeapon(bs, moveresult.weapon, + moveresult.flags & MOVERESULT_MOVEMENTWEAPON); BotMove_OnPostMoveToGoal(bs, &moveresult); //attack the enemy if possible BotCheckAttack(bs); diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index 02a9685..1138973 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -45,11 +45,12 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #include "ai_main.h" #include "ai_bot_enhanced.h" +#include "ai_bot_opponent.h" +#include "ai_bot_combat.h" #include "ai_dmq3.h" #include "ai_aim_harness.h" #include "ai_weapon_select.h" #include "ai_bot_tactics.h" -#include "ai_bot_combat.h" #include "ai_bot_move_harness.h" #include "ai_chat.h" #include "ai_cmd.h" @@ -1729,7 +1730,7 @@ void BotChooseWeapon(bot_state_t *bs) { trap_EA_SelectWeapon(bs->client, bs->weaponnum); } else { - if (BotEnhanced_WeaponsActive() && !BotWpnSelect_ShouldRunChooser(bs)) { + if (BotEnhanced_IsActive() && !BotWpnSelect_ShouldRunChooser(bs)) { trap_EA_SelectWeapon(bs->client, bs->weaponnum); return; } @@ -1740,7 +1741,7 @@ void BotChooseWeapon(bot_state_t *bs) { else { if (bs->enemy >= 0) { newweaponnum = BotWpnSelect_Choose(bs); - } else if (BotEnhanced_WeaponsActive()) { + } else if (BotEnhanced_IsActive()) { newweaponnum = BotWpnSelect_ChooseRoaming(bs); } else { newweaponnum = -1; @@ -2408,47 +2409,88 @@ int TeamPlayIsOn(void) { BotAggression ================== */ +static float BotAggression_WithOpponentBias(bot_state_t *bs, float agg) { + float bias; + + if (!BotOpponent_IsTracking(bs)) { + return agg; + } + bias = BotOpponent_GetEngageBias(bs); + if (bias < 0.0f) { + agg += bias * 55.0f; + if (agg < 0.0f) { + agg = 0.0f; + } + } else if (bias > 0.0f) { + agg += bias * 45.0f; + if (agg > 100.0f) { + agg = 100.0f; + } + } + return agg; +} + float BotAggression(bot_state_t *bs) { //if the bot has quad if (bs->inventory[INVENTORY_QUAD]) { //if the bot is not holding the gauntlet or the enemy is really nearby if (bs->weaponnum != WP_GAUNTLET || bs->inventory[ENEMY_HORIZONTAL_DIST] < 80) { - return 70; + return BotAggression_WithOpponentBias(bs, 70.0f); } } //if the enemy is located way higher than the bot - if (bs->inventory[ENEMY_HEIGHT] > 200) return 0; + if (bs->inventory[ENEMY_HEIGHT] > 200) { + return BotAggression_WithOpponentBias(bs, 0.0f); + } //if the bot is very low on health - if (bs->inventory[INVENTORY_HEALTH] < 60) return 0; + if (bs->inventory[INVENTORY_HEALTH] < 60) { + return BotAggression_WithOpponentBias(bs, 0.0f); + } //if the bot is low on health if (bs->inventory[INVENTORY_HEALTH] < 80) { //if the bot has insufficient armor - if (bs->inventory[INVENTORY_ARMOR] < 40) return 0; + if (bs->inventory[INVENTORY_ARMOR] < 40) { + return BotAggression_WithOpponentBias(bs, 0.0f); + } } //if the bot can use the bfg if (bs->inventory[INVENTORY_BFG10K] > 0 && - bs->inventory[INVENTORY_BFGAMMO] > 7) return 100; + bs->inventory[INVENTORY_BFGAMMO] > 7) { + return BotAggression_WithOpponentBias(bs, 100.0f); + } //if the bot can use the railgun if (bs->inventory[INVENTORY_RAILGUN] > 0 && - bs->inventory[INVENTORY_SLUGS] > 5) return 95; + bs->inventory[INVENTORY_SLUGS] > 5) { + return BotAggression_WithOpponentBias(bs, 95.0f); + } //if the bot can use the lightning gun if (bs->inventory[INVENTORY_LIGHTNING] > 0 && - bs->inventory[INVENTORY_LIGHTNINGAMMO] > 50) return 90; + bs->inventory[INVENTORY_LIGHTNINGAMMO] > 50) { + return BotAggression_WithOpponentBias(bs, 90.0f); + } //if the bot can use the rocketlauncher if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && - bs->inventory[INVENTORY_ROCKETS] > 5) return 90; + bs->inventory[INVENTORY_ROCKETS] > 5) { + return BotAggression_WithOpponentBias(bs, 90.0f); + } //if the bot can use the plasmagun if (bs->inventory[INVENTORY_PLASMAGUN] > 0 && - bs->inventory[INVENTORY_CELLS] > 40) return 85; + bs->inventory[INVENTORY_CELLS] > 40) { + return BotAggression_WithOpponentBias(bs, 85.0f); + } //if the bot can use the grenade launcher if (bs->inventory[INVENTORY_GRENADELAUNCHER] > 0 && - bs->inventory[INVENTORY_GRENADES] > 10) return 80; + bs->inventory[INVENTORY_GRENADES] > 10) { + return BotAggression_WithOpponentBias(bs, 80.0f); + } //if the bot can use the shotgun if (bs->inventory[INVENTORY_SHOTGUN] > 0 && - bs->inventory[INVENTORY_SHELLS] > 10) return 50; + bs->inventory[INVENTORY_SHELLS] > 10) { + return BotAggression_WithOpponentBias(bs, 50.0f); + } //otherwise the bot is not feeling too good - return 0; + return BotAggression_WithOpponentBias(bs, 0.0f); } /* @@ -2524,8 +2566,16 @@ int BotWantsToRetreat(bot_state_t *bs) { if (bs->ltgtype == LTG_GETFLAG) return qtrue; // - if (BotAggression(bs) < 50) + if (BotAggression(bs) < 50) { + if (BotOpponent_IsActive() && BotOpponent_WantsDuelCommit(bs) && + bs->enemy >= 0 && BotFeelingBad(bs) < 50) { + return qfalse; + } + return qtrue; + } + if (BotOpponent_IsActive() && BotOpponent_WantsAvoidEngagement(bs)) { return qtrue; + } return qfalse; } @@ -2578,6 +2628,12 @@ int BotWantsToChase(bot_state_t *bs) { if (bs->ltgtype == LTG_GETFLAG) return qfalse; // + if (BotOpponent_IsActive() && BotOpponent_WantsDuelCommit(bs)) { + return qtrue; + } + if (BotOpponent_IsActive() && BotOpponent_WantsAvoidEngagement(bs)) { + return qfalse; + } if (BotAggression(bs) > 50) return qtrue; return qfalse; @@ -2854,7 +2910,7 @@ BotAttackMove ================== */ bot_moveresult_t BotAttackMove(bot_state_t *bs, int tfl) { - int movetype, i, attackentity; + int movetype, i, attackentity, holdHighGround; float attack_skill, jumper, croucher, dist, strafechange_time; float attack_dist, attack_range; vec3_t forward, backward, sideward, hordir, up = {0, 0, 1}; @@ -2863,6 +2919,7 @@ bot_moveresult_t BotAttackMove(bot_state_t *bs, int tfl) { bot_goal_t goal; attackentity = bs->enemy; + holdHighGround = 0; // if (bs->attackchase_time > FloatTime()) { //create the chase goal @@ -2880,11 +2937,14 @@ bot_moveresult_t BotAttackMove(bot_state_t *bs, int tfl) { // memset(&moveresult, 0, sizeof(bot_moveresult_t)); // - attack_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1); + attack_skill = BotEnhanced_GetAttackSkill(bs); jumper = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_JUMPER, 0, 1); croucher = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CROUCHER, 0, 1); //if the bot is really stupid - if (attack_skill < 0.2) return moveresult; + if (!BotEnhanced_IsActive() && attack_skill < 0.2) return moveresult; + if (BotEnhanced_IsActive()) { + return BotCombat_AttackMove(bs, tfl); + } //initialize the movement state BotSetupForMovement(bs); //get the enemy entity info @@ -2926,22 +2986,10 @@ bot_moveresult_t BotAttackMove(bot_state_t *bs, int tfl) { attack_dist = IDEAL_ATTACKDIST; attack_range = 40; } - /* ENHANCED: rush opponent — charge in (gauntlet / voluntary SG / plasma) */ - if (BotCombat_IsRushOpponent(bs) && - bs->combat.move_policy == BOT_MOVE_CLOSE_MELEE) { - movetype = MOVE_WALK; - if (trap_BotMoveInDirection(bs->ms, forward, 400, movetype)) { - return moveresult; - } - if (trap_BotMoveInDirection(bs->ms, forward, 400, MOVE_RUN)) { - return moveresult; - } - return moveresult; - } //if the bot is stupid if (attack_skill <= 0.4) { //just walk to or away from the enemy - if (dist > attack_dist + attack_range) { + if (dist > attack_dist + attack_range && !holdHighGround) { if (trap_BotMoveInDirection(bs->ms, forward, 400, movetype)) return moveresult; } if (dist < attack_dist - attack_range) { @@ -2975,15 +3023,15 @@ bot_moveresult_t BotAttackMove(bot_state_t *bs, int tfl) { //reverse the vector depending on the strafe direction if (bs->flags & BFL_STRAFERIGHT) VectorNegate(sideward, sideward); //randomly go back a little - if (random() > 0.9) { + if (!holdHighGround && random() > 0.9) { VectorAdd(sideward, backward, sideward); } else { //walk forward or backward to get at the ideal attack distance - if (dist > attack_dist + attack_range) { + if (dist > attack_dist + attack_range && !holdHighGround) { VectorAdd(sideward, forward, sideward); } - else if (dist < attack_dist - attack_range) { + else if (dist < attack_dist - attack_range && !holdHighGround) { VectorAdd(sideward, backward, sideward); } } @@ -3163,6 +3211,19 @@ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int BotFindEnemy ================== */ +int BotAI_IsOriginNearRecentTeleport(const vec3_t origin) { + vec3_t dir; + + if (!origin) { + return 0; + } + if (lastteleport_time <= FloatTime() - 3) { + return 0; + } + VectorSubtract(origin, lastteleport_origin, dir); + return VectorLengthSquared(dir) < Square(70); +} + int BotFindEnemy(bot_state_t *bs, int curenemy) { int i, healthdecrease; float f, alertness, easyfragger, vis; @@ -3170,8 +3231,8 @@ int BotFindEnemy(bot_state_t *bs, int curenemy) { aas_entityinfo_t entinfo, curenemyinfo; vec3_t dir, angles; - alertness = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ALERTNESS, 0, 1); - easyfragger = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_EASY_FRAGGER, 0, 1); + alertness = BotEnhanced_GetAlertness(bs); + easyfragger = BotEnhanced_GetEasyFragger(bs); //check if the health decreased healthdecrease = bs->lasthealth > bs->inventory[INVENTORY_HEALTH]; //remember the current health value @@ -3189,6 +3250,11 @@ int BotFindEnemy(bot_state_t *bs, int curenemy) { if (curenemy >= 0) { BotEntityInfo(curenemy, &curenemyinfo); if (EntityCarriesFlag(&curenemyinfo)) return qfalse; + } + if (BotEnhanced_IsActive()) { + return BotCombat_FindEnemy(bs, curenemy); + } + if (curenemy >= 0) { VectorSubtract(curenemyinfo.origin, bs->origin, dir); cursquaredist = VectorLengthSquared(dir); } @@ -3561,12 +3627,12 @@ void BotAimAtEnemy(bot_state_t *bs) { // //BotAI_Print(PRT_MESSAGE, "client %d: aiming at client %d\n", bs->entitynum, bs->enemy); // - aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1); - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1); + aim_skill = BotEnhanced_GetAimSkill(bs); + aim_accuracy = BotEnhanced_GetAimAccuracy(bs); // - if (aim_skill > 0.95) { + if (!BotEnhanced_IsActive() && aim_skill > 0.95) { //don't aim too early - reactiontime = 0.5 * trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1); + reactiontime = 0.5f * BotEnhanced_GetReactionTime(bs); if (bs->enemysight_time > FloatTime() - reactiontime) return; if (bs->teleport_time > FloatTime() - reactiontime) return; } @@ -3574,40 +3640,42 @@ void BotAimAtEnemy(bot_state_t *bs) { //get the weapon information trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); //get the weapon specific aim accuracy and or aim skill - if (wi.number == WP_MACHINEGUN) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1); - } - else if (wi.number == WP_SHOTGUN) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1); - } - else if (wi.number == WP_GRENADE_LAUNCHER) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1); - aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_GRENADELAUNCHER, 0, 1); - } - else if (wi.number == WP_ROCKET_LAUNCHER) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_ROCKETLAUNCHER, 0, 1); - aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_ROCKETLAUNCHER, 0, 1); - } - else if (wi.number == WP_LIGHTNING) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_LIGHTNING, 0, 1); - } - else if (wi.number == WP_RAILGUN) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_RAILGUN, 0, 1); - } - else if (wi.number == WP_PLASMAGUN) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_PLASMAGUN, 0, 1); - aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_PLASMAGUN, 0, 1); - } - else if (wi.number == WP_BFG) { - aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_BFG10K, 0, 1); - aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_BFG10K, 0, 1); + if (!BotEnhanced_IsActive()) { + if (wi.number == WP_MACHINEGUN) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1); + } + else if (wi.number == WP_SHOTGUN) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1); + } + else if (wi.number == WP_GRENADE_LAUNCHER) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1); + aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_GRENADELAUNCHER, 0, 1); + } + else if (wi.number == WP_ROCKET_LAUNCHER) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_ROCKETLAUNCHER, 0, 1); + aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_ROCKETLAUNCHER, 0, 1); + } + else if (wi.number == WP_LIGHTNING) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_LIGHTNING, 0, 1); + } + else if (wi.number == WP_RAILGUN) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_RAILGUN, 0, 1); + } + else if (wi.number == WP_PLASMAGUN) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_PLASMAGUN, 0, 1); + aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_PLASMAGUN, 0, 1); + } + else if (wi.number == WP_BFG) { + aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_BFG10K, 0, 1); + aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_BFG10K, 0, 1); + } } // if (aim_accuracy <= 0) aim_accuracy = 0.0001f; //get the enemy entity information BotEntityInfo(bs->enemy, &entinfo); //if the enemy is invisible then shoot crappy most of the time - if (EntityIsInvisible(&entinfo)) { + if (!BotEnhanced_IsActive() && EntityIsInvisible(&entinfo)) { if (random() > 0.1) aim_accuracy *= 0.4f; } // @@ -3621,7 +3689,7 @@ void BotAimAtEnemy(bot_state_t *bs) { VectorCopy(entinfo.origin, bs->enemyorigin); } //if not extremely skilled - if (aim_skill < 0.9) { + if (!BotEnhanced_IsActive() && aim_skill < 0.9) { VectorSubtract(entinfo.origin, bs->enemyorigin, dir); //if the enemy moved a bit if (VectorLengthSquared(dir) > Square(48)) { @@ -3633,12 +3701,15 @@ void BotAimAtEnemy(bot_state_t *bs) { } } //check visibility of enemy + if (BotEnhanced_IsActive()) { + BotAimHarness_PreserveAimTargetSample(bs); + } enemyvisible = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy); //if the enemy is visible if (enemyvisible) { // VectorCopy(entinfo.origin, bestorigin); - if (BotEnhanced_AimActive() && wi.number == WP_PLASMAGUN) { + if (BotEnhanced_IsActive() && wi.number == WP_PLASMAGUN) { bestorigin[2] += 28; } else { bestorigin[2] += 8; @@ -3707,7 +3778,7 @@ void BotAimAtEnemy(bot_state_t *bs) { } //if the projectile does radial damage if (aim_skill > 0.6 && wi.proj.damagetype & DAMAGETYPE_RADIAL && - !(BotEnhanced_AimActive() && wi.number == WP_PLASMAGUN)) { + !(BotEnhanced_IsActive() && wi.number == WP_PLASMAGUN)) { //if the enemy isn't standing significantly higher than the bot if (entinfo.origin[2] < bs->origin[2] + 16) { //try to aim at the ground in front of the enemy @@ -3741,7 +3812,7 @@ void BotAimAtEnemy(bot_state_t *bs) { } } /* ENHANCED: aim — combat origin jitter when harness off */ - if (!BotEnhanced_AimActive()) { + if (!BotEnhanced_IsActive()) { bestorigin[0] += 20 * crandom() * (1 - aim_accuracy); bestorigin[1] += 20 * crandom() * (1 - aim_accuracy); bestorigin[2] += 10 * crandom() * (1 - aim_accuracy); @@ -3756,7 +3827,8 @@ void BotAimAtEnemy(bot_state_t *bs) { //do prediction shots around corners if (wi.number == WP_BFG || wi.number == WP_ROCKET_LAUNCHER || - wi.number == WP_GRENADE_LAUNCHER) { + wi.number == WP_GRENADE_LAUNCHER || + wi.number == WP_PLASMAGUN) { //create the chase goal goal.entitynum = bs->client; goal.areanum = bs->areanum; @@ -3783,17 +3855,20 @@ void BotAimAtEnemy(bot_state_t *bs) { else { VectorCopy(bestorigin, bs->aimtarget); } - if (BotEnhanced_AimActive() && wi.number == WP_ROCKET_LAUNCHER) { + if (BotEnhanced_IsActive() && wi.number == WP_ROCKET_LAUNCHER) { BotAimHarness_ApplyRocketFeetAim(bs, bs->aimtarget); } - if (BotEnhanced_AimActive() && wi.number == WP_PLASMAGUN) { + if (BotEnhanced_IsActive() && wi.number == WP_PLASMAGUN) { BotAimHarness_ApplyPlasmaCenterMassAim(bs, bs->aimtarget); } - if (BotEnhanced_AimActive() && wi.number == WP_RAILGUN) { + if (BotEnhanced_IsActive() && wi.number == WP_RAILGUN) { BotAimHarness_ApplyRailInterceptAim(bs, bs->aimtarget, aim_skill, aim_accuracy); - } else if (BotEnhanced_AimActive()) { + } else if (BotEnhanced_IsActive() && !BotAimHarness_UsingTrackingHitscan(bs)) { BotAimHarness_ApplyMovementLead(bs, bs->aimtarget, aim_skill); } + if (BotEnhanced_IsActive()) { + BotAimHarness_CommitAimTargetSample(bs); + } //get aim direction VectorSubtract(bs->aimtarget, bs->eye, dir); // @@ -3807,14 +3882,14 @@ void BotAimAtEnemy(bot_state_t *bs) { f = 0.6 + dist / 150 * 0.4; aim_accuracy *= f; } - if (!BotEnhanced_AimActive()) { + if (!BotEnhanced_IsActive()) { if (aim_accuracy < 0.8) { VectorNormalize(dir); for (i = 0; i < 3; i++) dir[i] += 0.3 * crandom() * (1 - aim_accuracy); } } vectoangles(dir, bs->ideal_viewangles); - if (!BotEnhanced_AimActive()) { + if (!BotEnhanced_IsActive()) { bs->ideal_viewangles[PITCH] += 6 * wi.vspread * crandom() * (1 - aim_accuracy); bs->ideal_viewangles[PITCH] = AngleMod(bs->ideal_viewangles[PITCH]); bs->ideal_viewangles[YAW] += 6 * wi.hspread * crandom() * (1 - aim_accuracy); @@ -3824,7 +3899,7 @@ void BotAimAtEnemy(bot_state_t *bs) { wi.vspread, wi.hspread); } /* Legacy challenge snap-aim; skipped when enhanced aim harness is active */ - if (bot_challenge.integer && !BotEnhanced_AimActive()) { + if (bot_challenge.integer && !BotEnhanced_IsActive()) { //if the bot is really accurate and has the enemy in view for some time if (aim_accuracy > 0.9 && bs->enemysight_time < FloatTime() - 1) { //set the view angles directly @@ -3841,7 +3916,7 @@ BotCheckAttack ================== */ void BotCheckAttack(bot_state_t *bs) { - float points, reactiontime, fov, firethrottle; + float points, reactiontime, fov; int attackentity; bsp_trace_t bsptrace; //float selfpreservation; @@ -3873,22 +3948,28 @@ void BotCheckAttack(bot_state_t *bs) { } } // - reactiontime = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1); + reactiontime = BotEnhanced_IsActive() ? + BotEnhanced_GetReactionTime(bs) : + trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1); if (bs->enemysight_time > FloatTime() - reactiontime) return; if (bs->teleport_time > FloatTime() - reactiontime) return; //if changing weapons if (bs->weaponchange_time > FloatTime() - 0.1) return; - //check fire throttle characteristic - if (bs->firethrottlewait_time > FloatTime()) return; - firethrottle = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1); - if (bs->firethrottleshoot_time < FloatTime()) { - if (random() > firethrottle) { - bs->firethrottlewait_time = FloatTime() + firethrottle; - bs->firethrottleshoot_time = 0; - } - else { - bs->firethrottleshoot_time = FloatTime() + 1 - firethrottle; - bs->firethrottlewait_time = 0; + //check fire throttle characteristic (legacy bots only; enhanced resets each think) + if (!BotEnhanced_IsActive()) { + float firethrottle; + + if (bs->firethrottlewait_time > FloatTime()) return; + firethrottle = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1); + if (bs->firethrottleshoot_time < FloatTime()) { + if (random() > firethrottle) { + bs->firethrottlewait_time = FloatTime() + firethrottle; + bs->firethrottleshoot_time = 0; + } + else { + bs->firethrottleshoot_time = FloatTime() + 1 - firethrottle; + bs->firethrottlewait_time = 0; + } } } // @@ -3909,7 +3990,7 @@ void BotCheckAttack(bot_state_t *bs) { } } /* ENHANCED: aim — suppressive fire (loose aim + per-frame hold for hitscan) */ - if (BotEnhanced_AimActive() && bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + if (BotEnhanced_IsActive() && bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { BotAimHarness_CheckAttack(bs); return; } @@ -4744,6 +4825,9 @@ void BotAIBlocked(bot_state_t *bs, bot_moveresult_t *moveresult, int activate) { bs->notblocked_time = FloatTime(); return; } + if (BotEnhanced_SuppressBlockedAvoid(bs)) { + return; + } // weapon jump uses fixed view/weapon; sidestep avoidance breaks alignment if (moveresult->flags & (MOVERESULT_MOVEMENTVIEWSET | MOVERESULT_MOVEMENTVIEW | MOVERESULT_MOVEMENTWEAPON | MOVERESULT_SWIMVIEW)) { @@ -5072,6 +5156,27 @@ void BotCheckForKamikazeBody(bot_state_t *bs, entityState_t *state) { bs->kamikazebody = state->number; } +/* +================== +BotAI_EventPickerClient +================== +*/ +int BotAI_EventPickerClient(const entityState_t *state) { + int picker; + + if (!state) { + return -1; + } + if (state->eType > ET_EVENTS) { + picker = state->otherEntityNum; + } else if (state->clientNum >= 0 && state->clientNum < MAX_CLIENTS) { + picker = state->clientNum; + } else { + picker = state->number; + } + return picker; +} + /* ================== BotCheckEvents @@ -5169,6 +5274,7 @@ void BotCheckEvents(bot_state_t *bs, entityState_t *state) { if (!strcmp(buf, "sound/items/poweruprespawn.wav")) { //powerup respawned... go get it BotGoForPowerups(bs); + BotEnhanced_OnPowerupRespawnSound(bs, state->origin); } break; } @@ -5290,6 +5396,9 @@ void BotCheckEvents(bot_state_t *bs, entityState_t *state) { case EV_WATER_CLEAR: case EV_ITEM_PICKUP: case EV_GLOBAL_ITEM_PICKUP: + case EV_ITEM_RESPAWN: + BotEnhanced_OnSnapshotClientEvent(bs, state, event); + break; case EV_NOAMMO: case EV_CHANGE_WEAPON: case EV_FIRE_WEAPON: @@ -5312,6 +5421,7 @@ void BotCheckEvents(bot_state_t *bs, entityState_t *state) { case EV_USE_ITEM14: break; } + BotOpponent_OnClientEvent(bs, state, event); } /* @@ -5344,12 +5454,22 @@ void BotCheckSnapshot(bot_state_t *bs) { BotCheckForKamikazeBody(bs, &state); } //check the player state for events - BotAI_GetEntityState(bs->client, &state); - //copy the player state events to the entity state - state.event = bs->cur_ps.externalEvent; - state.eventParm = bs->cur_ps.externalEventParm; - // - BotCheckEvents(bs, &state); + if (BotAI_GetEntityState(bs->client, &state)) { + /* externalEvent is only on playerState; stamp picker identity. */ + state.number = bs->client; + state.clientNum = bs->client; + state.event = bs->cur_ps.externalEvent; + state.eventParm = bs->cur_ps.externalEventParm; + BotCheckEvents(bs, &state); + } else if (bs->cur_ps.externalEvent) { + memset(&state, 0, sizeof(state)); + state.number = bs->client; + state.clientNum = bs->client; + VectorCopy(bs->origin, state.origin); + state.event = bs->cur_ps.externalEvent; + state.eventParm = bs->cur_ps.externalEventParm; + BotCheckEvents(bs, &state); + } } /* @@ -5560,11 +5680,9 @@ void BotDeathmatchAI(bot_state_t *bs, float thinktime) { //update some inventory values BotUpdateInventory(bs); BotEnhanced_OnThinkStart(bs); - if (BotEnhanced_WeaponsActive() && bs->enemy < 0 && !BotIsObserver(bs)) { - BotWpnSelect_TickRoaming(bs); - } //check out the snapshot BotCheckSnapshot(bs); + BotEnhanced_AfterCheckSnapshot(bs); //check for air BotCheckAir(bs); } diff --git a/ratoa_gamecode/code/game/ai_dmq3.h b/ratoa_gamecode/code/game/ai_dmq3.h index dfc03f1..c6a3fe3 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.h +++ b/ratoa_gamecode/code/game/ai_dmq3.h @@ -121,6 +121,10 @@ void BotVisibleTeamMatesAndEnemies(bot_state_t *bs, int *teammates, int *enemies qboolean InFieldOfVision(vec3_t viewangles, float fov, vec3_t angles); //returns true and sets the .enemy field when an enemy is found int BotFindEnemy(bot_state_t *bs, int curenemy); +int BotGetAirGoal(bot_state_t *bs, bot_goal_t *goal); +int BotAI_IsOriginNearRecentTeleport(const vec3_t origin); +/* Client that raised an EV_ITEM_PICKUP / noise event (snapshot-safe). */ +int BotAI_EventPickerClient(const entityState_t *state); //returns a roam goal void BotRoamGoal(bot_state_t *bs, vec3_t goal); //returns entity visibility in the range [0, 1] diff --git a/ratoa_gamecode/code/game/ai_main.c b/ratoa_gamecode/code/game/ai_main.c index 5f8a494..0f882fd 100644 --- a/ratoa_gamecode/code/game/ai_main.c +++ b/ratoa_gamecode/code/game/ai_main.c @@ -804,8 +804,8 @@ void BotChangeViewAngles(bot_state_t *bs, float thinktime) { if (bs->ideal_viewangles[PITCH] > 180) bs->ideal_viewangles[PITCH] -= 360; // if (bs->enemy >= 0) { - factor = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_FACTOR, 0.01f, 1); - maxchange = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_MAXCHANGE, 1, 1800); + factor = BotEnhanced_GetViewFactor(bs); + maxchange = BotEnhanced_GetViewMaxChange(bs); } else { factor = 0.05f; @@ -958,7 +958,7 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { thinktime = (float)elapsed_time / 1000.0f; /* ENHANCED: aim — input-frame motor path */ - if (BotEnhanced_AimActive()) { + if (BotEnhanced_IsActive()) { if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { return; } @@ -1111,7 +1111,7 @@ int BotAI(int client, float thinktime) { { /*ignore*/ } } /* ENHANCED: aim — legacy delta-angle rebase when harness off */ - if (!BotEnhanced_AimActive()) { + if (!BotEnhanced_IsActive()) { for (j = 0; j < 3; j++) { bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); @@ -1132,7 +1132,7 @@ int BotAI(int client, float thinktime) { BotDeathmatchAI(bs, thinktime); //set the weapon selection every AI frame trap_EA_SelectWeapon(bs->client, bs->weaponnum); - if (!BotEnhanced_AimActive()) { + if (!BotEnhanced_IsActive()) { for (j = 0; j < 3; j++) { bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index de2787d..1236c66 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -129,6 +129,9 @@ typedef struct bot_activategoal_s } bot_activategoal_t; #include "ai_bot_combat.h" +#include "ai_bot_item_timing.h" +#include "ai_bot_opponent.h" +#include "ai_bot_nav_guard.h" //bot state typedef struct bot_state_s @@ -299,14 +302,30 @@ typedef struct bot_state_s int evt_damage; int evt_mod; float enh_goal_last_push_time; + int enh_cached_active; + int enh_cached_debug; + int enh_travel_tfl; + qboolean enh_travel_tfl_valid; /* ---- end BOT ENHANCED ---- */ /* ---- BOT ITEMS: ai_bot_items.c — remove this block to revert ---- */ qboolean item_commit_active; + qboolean item_commit_timing; /* pre-spawn / timing pursuit commit */ + qboolean item_commit_detour; /* brief snag while primary suspended */ + qboolean item_commit_opportune; /* health/armor snag while primary suspended */ + qboolean item_commit_suspended; /* primary timing commit saved */ int item_commit_kind; /* BOT_ITEM_* while committed */ float item_commit_until; float item_next_scan_time; + float item_opportune_next_scan_time; + float item_opportune_block_until; bot_goal_t item_commit_goal; + bot_goal_t item_commit_suspended_goal; + int item_commit_suspended_kind; + float item_commit_suspended_until; + qboolean item_commit_suspended_timing; + float item_commit_suspended_progress_time; + vec3_t item_commit_suspended_progress_origin; int item_commit_snap_health; int item_commit_snap_armor; int item_commit_snap_quad; @@ -315,8 +334,24 @@ typedef struct bot_state_s int item_commit_snap_weapon; float item_commit_progress_time; vec3_t item_commit_progress_origin; + int item_lj_attempts; + float item_lj_lip_since; + float item_lj_jump_until; /* ---- end BOT ITEMS ---- */ + /* ---- BOT ITEM TIMING: ai_bot_item_timing.c — remove this block to revert ---- */ + timing_belief_t timing_track[BOT_TIMING_TRACK_COUNT]; + int timing_pursue_track; /* latched slot 0..2, else -1 */ + int timing_detour_track; /* opportunistic snag slot, else -1 */ + float timing_next_plan_time; /* next random track evaluation */ + float timing_next_detour_time;/* next on-route detour scan */ + float timing_detour_block_until; /* global detour cooldown */ + float timing_spawn_due_at; /* expected spawn while pursuing (0 = none) */ + float timing_far_pursue_since; /* 0 = at pad; else far-pursue stall clock */ + float timing_next_preempt_time; /* next higher-item preempt scan */ + float timing_preempt_block_until; /* global preempt cooldown */ + /* ---- end BOT ITEM TIMING ---- */ + /* ---- BOT AIM HARNESS (v1): ai_aim_harness.c — remove this block to revert ---- */ vec3_t aimh_goal; float aimh_vel[2]; @@ -347,8 +382,22 @@ typedef struct bot_state_s qboolean aimh_hold_fire; /* suppressive fire: +attack each input frame */ vec3_t aimh_rail_lead_point; /* lead-and-wait intercept aim (rail) */ qboolean aimh_rail_lead_valid; + vec3_t aimh_rail_smooth_vel; /* world horiz vel (smoothed, rail intercept) */ + vec3_t aimh_rail_last_origin; + float aimh_rail_vel_sample_time; + qboolean aimh_rail_vel_valid; float aimh_shot_press_since; /* slow weapons: engage without firing (shot urgency) */ int aimh_shot_press_weapon; + vec3_t aimh_prev_aimtarget; /* prior think aimtarget (MG/LG vel estimate) */ + vec3_t aimh_track_vel; /* blended travel vel (MG/LG lead) */ + vec3_t aimh_track_offset; /* aimtarget minus enemy origin at last think */ + float aimh_prev_aimtarget_time; + float aimh_aimtarget_sample_time; + qboolean aimh_prev_aimtarget_valid; + float aimh_recal_lead_scale; /* MG/LG hit-feedback lead trim (1.0 = nominal) */ + float aimh_recal_next_time; /* next observe/adjust window */ + float aimh_recal_fire_since; /* when current suppressive burst started */ + int aimh_recal_last_hits; /* PERS_HITS latch at last recal window */ /* ---- end BOT AIM HARNESS ---- */ /* ---- BOT MOVE HARNESS: ai_bot_move_harness.c ---- */ @@ -368,6 +417,10 @@ typedef struct bot_state_s vec3_t movej_rj_fire_view; /* down-aim latched at fire; held during prep */ float movej_rj_prep_view_until; /* keep fire view + attack until RL fires */ float movej_no_walkoff_until; /* strip TFL_WALKOFFLEDGE from routing */ + float movej_walkoff_allow_until; /* escape: temporarily allow walkoff routing */ + vec3_t movej_walkoff_abort_origin; /* last risky walkoff abort position */ + int movej_walkoff_abort_count; /* aborts near origin within window */ + float movej_walkoff_abort_window; /* start of abort count window */ float movej_urgent_health_until; /* prioritize health pickups */ /* ---- end BOT MOVE HARNESS ---- */ @@ -388,6 +441,34 @@ typedef struct bot_state_s int tact_evt_mod; float tact_last_hurt_time; /* ---- end BOT TACTICAL AI ---- */ + + /* ---- BOT OPPONENT: ai_bot_opponent.c — remove this block to revert ---- */ + opponent_belief_t opponent_belief; + /* ---- end BOT OPPONENT ---- */ + + /* ---- BOT POSITION: ai_bot_position.c — remove this block to revert ---- */ + float pos_enemy_z_delta; /* bot.z minus last-known enemy.z (0 = no enemy) */ + float pos_last_high_z; /* highest Z reached in recent BOTPOS_HIGH_DECAY_SEC */ + float pos_high_sampled_at; /* FloatTime() of last pos_last_high_z update */ + float pos_ledge_peek_until; /* next stand/crouch toggle while ledge-holding */ + qboolean pos_ledge_peek_crouch; /* current peek phase: crouched behind cover */ + qboolean pos_item_harass_active; /* timing pursuit suspended for overlook fight */ + float pos_route_audit_time; /* next mid-route elevation audit */ + qboolean pos_uplift_active; /* short uplift waypoint on goal stack */ + float pos_uplift_until; + bot_goal_t pos_uplift_goal; + /* ---- end BOT POSITION ---- */ + + /* ---- BOT NAV GUARD: ai_bot_nav_guard.c ---- */ + float nav_progress_time; + vec3_t nav_progress_origin; + int nav_ring_count; + int nav_ring_pos; + int nav_ring_areanum[BOTNAV_RING_SAMPLES]; + vec3_t nav_ring_origin[BOTNAV_RING_SAMPLES]; + float nav_next_ring_sample; + float nav_breakout_cooldown_until; + /* ---- end BOT NAV GUARD ---- */ } bot_state_t; //resets the whole bot state diff --git a/ratoa_gamecode/code/game/ai_weapon_select.c b/ratoa_gamecode/code/game/ai_weapon_select.c index 77a25d4..37a1759 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.c +++ b/ratoa_gamecode/code/game/ai_weapon_select.c @@ -20,11 +20,23 @@ BOT SMART WEAPON SELECT (v1) — see ai_weapon_select.h #include "ai_bot_combat.h" #include "ai_bot_tactics.h" -vmCvar_t bot_enhanced_weapons; - /* Forward — defined in ai_dmq3.c */ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int ent); +static float BotWpnSel_SkillCombat(bot_state_t *bs) { + if (BotEnhanced_IsActive()) { + return BotEnhanced_GetAimSkill(bs); + } + return trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL, 0.0f, 1.0f); +} + +static float BotWpnSel_ReactionTime(bot_state_t *bs) { + if (BotEnhanced_IsActive()) { + return BotEnhanced_GetReactionTime(bs); + } + return trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0.0f, 1.0f); +} + #define WPNSEL_LEGACY_BIAS 28.0f #define WPNSEL_HYSTERESIS_BASE 12.0f #define WPNSEL_HYSTERESIS_SKILL 22.0f @@ -60,6 +72,9 @@ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int #define WPNSEL_ROAM_MG_LASTRESORT_PEN 48.0f #define WPNSEL_ROAM_MG_ONLY_BONUS 22.0f #define WPNSEL_ROAM_NOISE_MAX 18.0f +#define WPNSEL_ROAM_RL_BONUS 30.0f +#define WPNSEL_GL_LASTRESORT_PEN 68.0f +#define WPNSEL_GL_ROAM_LASTRESORT_PEN 60.0f /* Enhanced fight select: min 1s between swaps; longer latch after close combat commit. */ #define WPNSEL_ENHANCED_MIN_SWITCH_INTERVAL 1.0f #define WPNSEL_ENHANCED_CLOSE_COMBAT_LATCH 3.5f @@ -67,10 +82,8 @@ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int #define WPNSEL_VOLUNTARY_CLOSE_COMBAT_CHANCE 0.25f #define WPNSEL_VOLUNTARY_CLOSE_COMBAT_BONUS 78.0f #define WPNSEL_VOLUNTARY_CLOSE_COMBAT_PENALTY 45.0f -#define WPNSEL_VOLUNTARY_SG_CLOSE_DIST 256.0f -#define WPNSEL_VOLUNTARY_PLASMA_CLOSE_DIST 700.0f -static int BotWpnSel_HasWeaponAndAmmo(bot_state_t *bs, int wp) { +int BotWpnSelect_HasWeaponAndAmmo(const bot_state_t *bs, int wp) { if (wp <= WP_NONE || wp >= WP_NUM_WEAPONS) { return 0; } @@ -141,11 +154,11 @@ static float BotWpnSel_RangeScore(int wp, float dist) { if (d < 1600.0f) return 10.0f; return 4.0f; case WP_GRENADE_LAUNCHER: - if (d < 96.0f) return 38.0f; - if (d < 400.0f) return 76.0f; - if (d < 640.0f) return 52.0f; - if (d < 900.0f) return 26.0f; - return 8.0f; + if (d < 96.0f) return 22.0f; + if (d < 400.0f) return 48.0f; + if (d < 640.0f) return 36.0f; + if (d < 900.0f) return 18.0f; + return 6.0f; case WP_PLASMAGUN: if (d < 200.0f) return 76.0f; if (d < 700.0f) return 80.0f; @@ -272,7 +285,7 @@ static float BotWpnSel_SwitchInCost(const weaponinfo_t *wi) { return t; } -static int BotWpnSel_RocketCombatSuitable(bot_state_t *bs) { +static int BotWpnSel_RocketCombatSuitable(const bot_state_t *bs) { if (bs->enemy < 0) { return 1; } @@ -294,11 +307,11 @@ static int BotWpnSel_EnemyHealth(bot_state_t *bs) { static int BotWpnSel_HasLongRangeHitscan(bot_state_t *bs, float dist) { (void)dist; - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_RAILGUN)) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_RAILGUN)) { return 1; } #ifdef MISSIONPACK - if (dist > 800.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { + if (dist > 800.0f && BotWpnSelect_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { return 1; } #endif @@ -308,43 +321,96 @@ static int BotWpnSel_HasLongRangeHitscan(bot_state_t *bs, float dist) { /* * Weapons clearly better than MG at this distance (for fallback / overshadow logic). */ -static int BotWpnSel_CountCombatAlternatives(bot_state_t *bs, float dist) { +int BotWpnSelect_CountCombatAlternatives(const bot_state_t *bs, float dist) { int n; n = 0; - if (dist < 200.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { + if (dist < 200.0f && BotWpnSelect_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { n++; } - if (dist < 650.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_LIGHTNING)) { + if (dist < 650.0f && BotWpnSelect_HasWeaponAndAmmo(bs, WP_LIGHTNING)) { n++; } - if (dist > 350.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_RAILGUN)) { + if (dist > 350.0f && BotWpnSelect_HasWeaponAndAmmo(bs, WP_RAILGUN)) { n++; } - if (dist > 200.0f && dist < 1100.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { + if (dist > 200.0f && dist < 1100.0f && BotWpnSelect_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { n++; } - if (dist > 96.0f && dist < 900.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER) && + if (dist > 96.0f && dist < 900.0f && BotWpnSelect_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER) && BotWpnSel_RocketCombatSuitable(bs)) { n++; } - if (dist > 96.0f && dist < 900.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_GRENADE_LAUNCHER)) { - n++; - } #ifdef MISSIONPACK - if (dist > 600.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { + if (dist > 600.0f && BotWpnSelect_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { n++; } #endif return n; } +/* + * Weapons that make a voluntary gauntlet charge unnecessary at this range. + */ +int BotWpnSelect_HasStrongCombatOption(const bot_state_t *bs, float dist) { + if (!bs) { + return 0; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_SHOTGUN) && dist <= 256.0f) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_LIGHTNING) && dist <= 650.0f) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_PLASMAGUN) && + dist >= (float)BOT_COMBAT_CLOSE_WEAPON_MIN_DIST && + dist <= 400.0f) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER) && + dist > 96.0f && dist < 900.0f && + BotWpnSel_RocketCombatSuitable(bs)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_RAILGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_BFG)) { + return 1; + } +#ifdef MISSIONPACK + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_CHAINGUN) && dist > 400.0f) { + return 1; + } +#endif + return 0; +} + +int BotWpnSelect_VoluntaryGauntletWarranted(bot_state_t *bs, float dist) { + if (!bs) { + return 0; + } + if (!BotWpnSelect_HasWeaponAndAmmo(bs, WP_GAUNTLET)) { + return 0; + } + if (BotTactics_IsGauntletOnly(bs)) { + return 1; + } + if (BotWpnSelect_HasStrongCombatOption(bs, dist)) { + return 0; + } + if (BotWpnSelect_CountCombatAlternatives(bs, dist) > 0) { + return 0; + } + return 1; +} + static float BotWpnSel_MachinegunModifier(bot_state_t *bs, float dist, float skillCombat) { int alternatives, enemyHealth; float mod, penScale, chipBonus, plinkBonus; - alternatives = BotWpnSel_CountCombatAlternatives(bs, dist); + alternatives = BotWpnSelect_CountCombatAlternatives(bs, dist); if (alternatives <= 0) { return WPNSEL_MG_FALLBACK_BONUS; } @@ -393,13 +459,10 @@ static float BotWpnSel_SwitchFatigue(bot_state_t *bs, float skillCombat) { } void BotWpnSelect_RegisterCvars(void) { - trap_Cvar_Register(&bot_enhanced_weapons, "bot_enhanced_weapons", "0", - CVAR_ARCHIVE); - trap_Cvar_Update(&bot_enhanced_weapons); } int BotWpnSelect_IsActive(void) { - return BotEnhanced_WeaponsActive(); + return BotEnhanced_IsActive(); } static float BotWpnSel_RoamStealth(bot_state_t *bs) { @@ -416,30 +479,67 @@ static float BotWpnSel_RoamStealth(bot_state_t *bs) { return stealth; } -static int BotWpnSel_HasBetterSilentRoamer(bot_state_t *bs) { - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER)) { +static int BotWpnSel_HasBetterThanGrenade(bot_state_t *bs) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { return 1; } - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { return 1; } - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_LIGHTNING)) { return 1; } - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_GRENADE_LAUNCHER)) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_RAILGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_MACHINEGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_BFG)) { return 1; } #ifdef MISSIONPACK - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_NAILGUN)) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { return 1; } - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_PROX_LAUNCHER)) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_NAILGUN)) { + return 1; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_PROX_LAUNCHER)) { return 1; } #endif return 0; } +static int BotWpnSel_BestSilentRoamer(bot_state_t *bs) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER)) { + return WP_ROCKET_LAUNCHER; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { + return WP_SHOTGUN; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { + return WP_PLASMAGUN; + } +#ifdef MISSIONPACK + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_NAILGUN)) { + return WP_NAILGUN; + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_PROX_LAUNCHER)) { + return WP_PROX_LAUNCHER; + } +#endif + return WP_NONE; +} + +static int BotWpnSel_HasBetterSilentRoamer(bot_state_t *bs) { + return BotWpnSel_BestSilentRoamer(bs) != WP_NONE; +} + static float BotWpnSel_RoamArsenalTier(int wp) { switch (wp) { case WP_RAILGUN: @@ -451,9 +551,9 @@ static float BotWpnSel_RoamArsenalTier(int wp) { case WP_PLASMAGUN: return 82.0f; case WP_ROCKET_LAUNCHER: - return 80.0f; + return 88.0f; case WP_GRENADE_LAUNCHER: - return 72.0f; + return 28.0f; case WP_SHOTGUN: return 68.0f; #ifdef MISSIONPACK @@ -488,7 +588,7 @@ static float BotWpnSel_RoamAudiblePenalty(int wp, float stealth, float skillComb } static float BotWpnSel_MachinegunRoamModifier(bot_state_t *bs) { - if (!BotWpnSel_HasWeaponAndAmmo(bs, WP_MACHINEGUN)) { + if (!BotWpnSelect_HasWeaponAndAmmo(bs, WP_MACHINEGUN)) { return 0.0f; } if (BotWpnSel_HasBetterSilentRoamer(bs)) { @@ -510,6 +610,9 @@ static qboolean BotWpnSel_VoluntaryCloseCombatEligible(bot_state_t *bs, float di if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { return qfalse; } + if (!BotWpnSelect_VoluntaryGauntletWarranted(bs, dist)) { + return qfalse; + } return dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST; } @@ -521,29 +624,18 @@ static qboolean BotWpnSel_RollVoluntaryCloseCombat(bot_state_t *bs, float dist) } static int BotWpnSel_PreferredVoluntaryCloseWeapon(bot_state_t *bs, float dist) { - if (!bs || dist > (float)BOT_COMBAT_GAUNTLET_RUSH_DIST) { + (void)dist; + if (!bs) { return WP_NONE; } - if (dist <= WPNSEL_VOLUNTARY_SG_CLOSE_DIST && - BotWpnSel_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { - return WP_SHOTGUN; - } - if (dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST && - BotWpnSel_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { - return WP_SHOTGUN; - } - if (dist <= WPNSEL_VOLUNTARY_PLASMA_CLOSE_DIST && - BotWpnSel_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { - return WP_PLASMAGUN; - } - if (BotWpnSel_HasWeaponAndAmmo(bs, WP_GAUNTLET)) { + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_GAUNTLET)) { return WP_GAUNTLET; } return WP_NONE; } static qboolean BotWpnSel_IsVoluntaryCloseCombatWeapon(int wp) { - return (wp == WP_GAUNTLET || wp == WP_SHOTGUN || wp == WP_PLASMAGUN); + return (wp == WP_GAUNTLET); } static qboolean BotWpnSel_IsCloseCombatCommit(bot_state_t *bs, int wp) { @@ -569,7 +661,7 @@ static qboolean BotWpnSel_IsCloseCombatCommit(bot_state_t *bs, int wp) { static void BotWpnSel_EnhancedApplyLatch(bot_state_t *bs, int prev_wp, int new_wp) { float now, latch; - if (!BotEnhanced_WeaponsActive() || prev_wp == new_wp) { + if (!BotEnhanced_IsActive() || prev_wp == new_wp) { return; } now = FloatTime(); @@ -584,7 +676,7 @@ static void BotWpnSel_EnhancedApplyLatch(bot_state_t *bs, int prev_wp, int new_w } int BotWpnSelect_ShouldRunChooser(bot_state_t *bs) { - if (!bs || !BotEnhanced_WeaponsActive()) { + if (!bs || !BotEnhanced_IsActive()) { return 1; } if (FloatTime() < bs->wps_enhanced_latch_until) { @@ -601,6 +693,19 @@ void BotWpnSelect_OnVoluntaryGauntletAborted(bot_state_t *bs) { bs->wps_next_eval_time = 0.0f; } +void BotWpnSelect_ApplyMovementWeapon(bot_state_t *bs, int weapon, int apply) { + if (!bs || !apply) { + return; + } + if (BotEnhanced_IsActive()) { + if (bs->activatestack && bs->activatestack->shoot) { + bs->weaponnum = weapon; + } + return; + } + bs->weaponnum = weapon; +} + void BotWpnSelect_Reset(bot_state_t *bs) { bs->wps_next_eval_time = 0.0f; bs->wps_next_roam_eval_time = 0.0f; @@ -646,14 +751,12 @@ int BotWpnSelect_Choose(bot_state_t *bs) { return -1; } - skillCombat = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL, - 0.0f, 1.0f); - react = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, - 0.0f, 1.0f); + skillCombat = BotWpnSel_SkillCombat(bs); + react = BotWpnSel_ReactionTime(bs); eval_dt = WPNSEL_EVAL_MIN + (WPNSEL_EVAL_MAX - WPNSEL_EVAL_MIN) * (0.35f + 0.4f * react + 0.25f * (1.0f - skillCombat)); - if (BotEnhanced_WeaponsActive()) { + if (BotEnhanced_IsActive()) { if (eval_dt < WPNSEL_ENHANCED_MIN_EVAL_INTERVAL) { eval_dt = WPNSEL_ENHANCED_MIN_EVAL_INTERVAL; } @@ -673,12 +776,18 @@ int BotWpnSelect_Choose(bot_state_t *bs) { } dist = BotWpnSel_EnemyDistance(bs); - vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy); - if (vis <= 0.0f) { - dist *= 1.15f; + if (BotEnhanced_IsActive()) { + if (!BotCombat_HasFightLOS(bs, bs->enemy)) { + dist *= 1.15f; + } + } else { + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy); + if (vis <= 0.0f) { + dist *= 1.15f; + } } - alternatives = BotWpnSel_CountCombatAlternatives(bs, dist); + alternatives = BotWpnSelect_CountCombatAlternatives(bs, dist); mgMod = BotWpnSel_MachinegunModifier(bs, dist, skillCombat); voluntaryCloseCombat = 0; @@ -714,7 +823,7 @@ int BotWpnSelect_Choose(bot_state_t *bs) { for (i = 0; i < n_weaps; i++) { wp = weap_list[i]; - if (!BotWpnSel_HasWeaponAndAmmo(bs, wp)) { + if (!BotWpnSelect_HasWeaponAndAmmo(bs, wp)) { continue; } if (wp == WP_ROCKET_LAUNCHER && !BotWpnSel_RocketCombatSuitable(bs)) { @@ -732,6 +841,9 @@ int BotWpnSelect_Choose(bot_state_t *bs) { if (wp == WP_MACHINEGUN) { score += mgMod; } + if (wp == WP_GRENADE_LAUNCHER && BotWpnSel_HasBetterThanGrenade(bs)) { + score -= WPNSEL_GL_LASTRESORT_PEN; + } if (dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST && BotWpnSel_IsVoluntaryCloseCombatWeapon(wp)) { @@ -739,6 +851,10 @@ int BotWpnSelect_Choose(bot_state_t *bs) { if (wp == WP_GAUNTLET) { score += 35.0f; } + } else if (!BotWpnSelect_VoluntaryGauntletWarranted(bs, dist)) { + if (wp == WP_GAUNTLET) { + score -= 120.0f; + } } else if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { if (wp == WP_GAUNTLET) { score -= 120.0f; @@ -847,7 +963,7 @@ int BotWpnSelect_Choose(bot_state_t *bs) { (best_miss_score - best_score) / 85.0f); } - if (BotEnhanced_WeaponsActive() && best_wp != bs->weaponnum) { + if (BotEnhanced_IsActive() && best_wp != bs->weaponnum) { if (FloatTime() - bs->wps_last_switch_time < WPNSEL_ENHANCED_MIN_SWITCH_INTERVAL) { best_wp = bs->weaponnum; } @@ -860,7 +976,7 @@ int BotWpnSelect_Choose(bot_state_t *bs) { } if (voluntaryCloseCombat > 0 && preferredCloseWp >= 0 && best_wp != preferredCloseWp && - BotWpnSel_HasWeaponAndAmmo(bs, preferredCloseWp)) { + BotWpnSelect_HasWeaponAndAmmo(bs, preferredCloseWp)) { best_wp = preferredCloseWp; } @@ -880,10 +996,8 @@ int BotWpnSelect_ChooseRoaming(bot_state_t *bs) { return -1; } - skillCombat = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL, - 0.0f, 1.0f); - react = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, - 0.0f, 1.0f); + skillCombat = BotWpnSel_SkillCombat(bs); + react = BotWpnSel_ReactionTime(bs); eval_dt = WPNSEL_ROAM_EVAL_MIN + (WPNSEL_ROAM_EVAL_MAX - WPNSEL_ROAM_EVAL_MIN) * (0.3f + 0.35f * react + 0.35f * (1.0f - skillCombat)); @@ -920,7 +1034,7 @@ int BotWpnSelect_ChooseRoaming(bot_state_t *bs) { if (wp == WP_GAUNTLET) { continue; } - if (!BotWpnSel_HasWeaponAndAmmo(bs, wp)) { + if (!BotWpnSelect_HasWeaponAndAmmo(bs, wp)) { continue; } trap_BotGetWeaponInfo(bs->ws, wp, &wi); @@ -934,6 +1048,12 @@ int BotWpnSelect_ChooseRoaming(bot_state_t *bs) { if (wp == WP_MACHINEGUN) { score += BotWpnSel_MachinegunRoamModifier(bs); } + if (wp == WP_ROCKET_LAUNCHER) { + score += WPNSEL_ROAM_RL_BONUS; + } + if (wp == WP_GRENADE_LAUNCHER && BotWpnSel_HasBetterThanGrenade(bs)) { + score -= WPNSEL_GL_ROAM_LASTRESORT_PEN; + } if (wp != bs->weaponnum) { score -= BotWpnSel_SwitchFatigue(bs, skillCombat) * 0.5f; @@ -962,22 +1082,12 @@ int BotWpnSelect_ChooseRoaming(bot_state_t *bs) { } } - if (best_wp == WP_MACHINEGUN && BotWpnSel_HasBetterSilentRoamer(bs) && - skillCombat > 0.4f) { - for (i = 0; i < n_weaps; i++) { - wp = weap_list[i]; - if (wp == WP_MACHINEGUN || wp == WP_GAUNTLET) { - continue; - } - if (!BotWpnSel_HasWeaponAndAmmo(bs, wp)) { - continue; - } - if (BotWpnSel_RoamArsenalTier(wp) + BotWpnSel_MachinegunRoamModifier(bs) > - best_score - 6.0f) { - best_wp = wp; - break; - } - } + if (best_wp == WP_MACHINEGUN && BotWpnSel_HasBetterSilentRoamer(bs)) { + best_wp = BotWpnSel_BestSilentRoamer(bs); + } + if (BotWpnSelect_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER) && + (best_wp == WP_GRENADE_LAUNCHER || best_wp == WP_MACHINEGUN)) { + best_wp = WP_ROCKET_LAUNCHER; } return best_wp; @@ -1009,4 +1119,5 @@ void BotWpnSelect_TickRoaming(bot_state_t *bs) { bs->weaponchange_time = FloatTime(); bs->weaponnum = new_wp; BotWpnSelect_NotifyWeaponCommitted(bs, prev_wp, new_wp); + trap_EA_SelectWeapon(bs->client, bs->weaponnum); } diff --git a/ratoa_gamecode/code/game/ai_weapon_select.h b/ratoa_gamecode/code/game/ai_weapon_select.h index fb1c69e..7999454 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.h +++ b/ratoa_gamecode/code/game/ai_weapon_select.h @@ -40,5 +40,17 @@ void BotWpnSelect_GetDesire(struct bot_state_s *bs, bot_weapon_desire_t *out); /* When false, BotChooseWeapon keeps current weaponnum (enhanced latch / min interval). */ int BotWpnSelect_ShouldRunChooser(struct bot_state_s *bs); void BotWpnSelect_OnVoluntaryGauntletAborted(struct bot_state_s *bs); +/* + * True when gauntlet rush/charge is appropriate: out of good weapons, spawn + * loadout, or tactics gauntlet-only — not merely because the enemy is close. + */ +int BotWpnSelect_VoluntaryGauntletWarranted(struct bot_state_s *bs, float dist); +/* Loadout / engage-range helpers (ai_bot_combat opponent gating). */ +int BotWpnSelect_HasWeaponAndAmmo(const struct bot_state_s *bs, int wp); +int BotWpnSelect_HasStrongCombatOption(const struct bot_state_s *bs, float dist); +int BotWpnSelect_CountCombatAlternatives(const struct bot_state_s *bs, float dist); +/* Botlib travel weapon — ignored when bot_enhanced_weapons is on. */ +void BotWpnSelect_ApplyMovementWeapon(struct bot_state_s *bs, int weapon, + int apply); #endif /* AI_WEAPON_SELECT_H */ diff --git a/ratoa_gamecode/code/game/g_client.c b/ratoa_gamecode/code/game/g_client.c index 0483e10..0262c18 100644 --- a/ratoa_gamecode/code/game/g_client.c +++ b/ratoa_gamecode/code/game/g_client.c @@ -3126,6 +3126,9 @@ void ClientBegin( int clientNum ) { if (ent->r.svFlags & SVF_BOT) { BotAimHarness_SyncClientDebug(clientNum); + if (client->sess.sessionTeam != TEAM_SPECTATOR) { + BotEnhanced_OnArenaEntry(clientNum); + } } if( ( client->sess.sessionTeam != TEAM_SPECTATOR ) && diff --git a/ratoa_gamecode/code/game/g_main.c b/ratoa_gamecode/code/game/g_main.c index 8e27a2f..bbe4587 100644 --- a/ratoa_gamecode/code/game/g_main.c +++ b/ratoa_gamecode/code/game/g_main.c @@ -22,6 +22,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #include "g_local.h" +#include "ai_bot_enhanced.h" level_locals_t level; @@ -760,6 +761,8 @@ void G_RegisterCvars( void ) { g_is_team_gt = BG_IsTeamGametype(g_gametype.integer); level.warmupModificationCount = g_warmup.modificationCount; + + BotEnhanced_RegisterCvars(); } qboolean G_IsTeamGametype(void) { diff --git a/ratoa_gamecode/windows_scripts/game.q3asm b/ratoa_gamecode/windows_scripts/game.q3asm index ec11ae6..083dc36 100644 --- a/ratoa_gamecode/windows_scripts/game.q3asm +++ b/ratoa_gamecode/windows_scripts/game.q3asm @@ -11,7 +11,6 @@ ai_dmnet ai_dmq3 ai_bot_enhanced ai_bot_combat -ai_bot_events ai_bot_move_harness ai_aim_harness ai_weapon_select diff --git a/ratoa_gamecode/windows_scripts/game_mp.q3asm b/ratoa_gamecode/windows_scripts/game_mp.q3asm index e9ea0ab..8b6ef49 100644 --- a/ratoa_gamecode/windows_scripts/game_mp.q3asm +++ b/ratoa_gamecode/windows_scripts/game_mp.q3asm @@ -11,7 +11,6 @@ ai_dmnet ai_dmq3 ai_bot_enhanced ai_bot_combat -ai_bot_events ai_bot_move_harness ai_aim_harness ai_weapon_select diff --git a/ratoa_gamecode/windows_scripts/windows_compile_game.bat b/ratoa_gamecode/windows_scripts/windows_compile_game.bat index b03e397..e651c50 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game.bat @@ -23,7 +23,6 @@ cd windows\build\game %cc% ../../../code/game/ai_dmq3.c %cc% ../../../code/game/ai_bot_enhanced.c %cc% ../../../code/game/ai_bot_combat.c -%cc% ../../../code/game/ai_bot_events.c %cc% ../../../code/game/ai_bot_move_harness.c %cc% ../../../code/game/ai_aim_harness.c %cc% ../../../code/game/ai_weapon_select.c diff --git a/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat b/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat index 076be9a..f6bd7af 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat @@ -23,7 +23,6 @@ cd windows\build\game %cc% ../../../code/game/ai_dmq3.c %cc% ../../../code/game/ai_bot_enhanced.c %cc% ../../../code/game/ai_bot_combat.c -%cc% ../../../code/game/ai_bot_events.c %cc% ../../../code/game/ai_bot_move_harness.c %cc% ../../../code/game/ai_aim_harness.c %cc% ../../../code/game/ai_weapon_select.c