From c717a0eeaf631f0617e0d8b8983d0206637a4f02 Mon Sep 17 00:00:00 2001 From: Foo <2673475+br33zy59@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:23:35 +1200 Subject: [PATCH] Bot enhancements - Flattened enhanced bot skill. All enhanced bots are equally capable regardless of designated skill (0-5) level. This simplifies development, and re-introducing skill spread later should be trivial AFTER new bot behaviour is determined. - Bot's aim target now lerps predictively between think intervals. Prevents saw-tooth aiming issues and draws hitscan shots onto target better. - Generalized the gauntlet charge abandonment to other close-quarters situations, so bots don't get stuck in a death spiral with one another. - Generalized combat break-off so bots don't keep aim on enemy last-seen-position for long periods. - Fixed some weapon selection behaviour so bots don't continuously favour the machinegun in so many situations - Improved railgun leading and movement compensation/anticipation Item preferences & positioning Split item tracking targets into hierarchical preference instead of random selection of goal. Bots will now prioritize the more important item respawn (e.g. MH over RA) if they're coming up at similar times). Bots will also split off primary goal to reach a secondary item they track if they happen to be walking near it and it's available. They'll also divert briefly during the primary trip if they need health/shards/ammo. Bot positioning - Framework for module and initial behaviour tweak - Bots will prefer high pathing towards goals over low ground Enhanced Bots - Item tracking Item tracking/seeking and detours Bots will track to major items when they believe they will respawn soon. They will make small detours to pick up other items if they are passing close by and the items are spawned. - Nerfed flick rails to be less accurate and less instantaneous. - Bots now re-evaluate their tracked items each time they spawn (initial join, death etc) and not at game start/connect. So bots in queue don't hold goals and playing bots will re-assess their choices as the match progresses. - Bots hold at more sensible distances when weilding shotgun/plasma (not try and close to zero like gauntlet) - Bots favour RL more when roaming, heavily disfavour GL - Bots should now spam last-seen locations for opponents (doorways etc) for a while after losing sight of target - Various gates and guards against looping and frozen movements - Bots more reactive to opponent item pickups in 1v1 situations - Moderate refactor to rip enhanced functions out of dmq3 and into their own more appropriate files. Enhanced bot improvements - Bots now factor weapon loadout into assessment of their own stack. Disincentivises engagement when poorly equipped - Implemented slow shot adjustment for rapid-fire weapons. If bot's MG/LG leading is ahead of behind target, bot will periodically notice this and adjust - Loosened the 'don't walk off a ledge' logic so it doesn't lock the bot up so often when they're somewhere they can only exit by ledge fall - Upped initial combat reaction time of bot so they pre-fire less severely when they first spot an enemy - Bot re-evaluates stack on itself and enemy mid-fight, and can decide to break off if it's losing hard. Will also evaluate if it loses sight of enemy for a moment, and take the opportunity to bail if it makes sense. Refactor bot_enhanced Phased refactor of bot_enhanced code: Phase 1: - Fixed up elements of enhanced bot code that were not properly gated behind bot_enhanced 1 (vanilla bots were still running some enhanced routines pointlessly) - Cached cvar gates per think tick to avoid lots of unnecessary reads - Some expensive routines are now gated by context e.g. route elevation checks only run if bot has an active goal commit, not during combat or open roaming - Reduced duplicate AAS travel time queries by caching results Phase 2: - Merged ai_bot_events.c into ai_bot_tactics.c - Merged ai_bot_move_util.c into ai_bot_move_harness.c - Slimmed down ai_bot_dmnet.c by collapsing passthrough wrappers Phase 3 - Readability. Merged items and item timing into ai_bot_items.c Phase 4 - Flattened over-functionization - Collapsed nine near-identical functions (BotEnhanced_Get*) into one internal helper - Removed BotEnhanced_*Active() chains and collapsed them all into BotEnhanced_IsActive() - Consolidated ledge/hold into combat intent Phase 5 - Flatted feature cvars into bot_enhanced and bot_enhanced_debug - All sub-cvars are now gone/irrelevant --- docs/BOT-CVARS.md | 53 +- docs/BOT-ENHANCED-ARCHITECTURE.md | 29 +- ratoa_gamecode/Makefile | 13 +- ratoa_gamecode/code/cgame/cg_unlagged.c | 4 +- ratoa_gamecode/code/game/ai_aim_harness.c | 1277 +++- ratoa_gamecode/code/game/ai_aim_harness.h | 23 +- ratoa_gamecode/code/game/ai_bot_combat.c | 635 +- ratoa_gamecode/code/game/ai_bot_combat.h | 57 +- ratoa_gamecode/code/game/ai_bot_dmnet.c | 404 ++ ratoa_gamecode/code/game/ai_bot_dmnet.h | 79 + ratoa_gamecode/code/game/ai_bot_enhanced.c | 404 +- ratoa_gamecode/code/game/ai_bot_enhanced.h | 62 +- ratoa_gamecode/code/game/ai_bot_events.c | 73 - ratoa_gamecode/code/game/ai_bot_events.h | 1 + ratoa_gamecode/code/game/ai_bot_item_timing.h | 81 + ratoa_gamecode/code/game/ai_bot_items.c | 5861 ++++++++++++++--- ratoa_gamecode/code/game/ai_bot_items.h | 32 +- .../code/game/ai_bot_move_harness.c | 516 +- .../code/game/ai_bot_move_harness.h | 22 +- ratoa_gamecode/code/game/ai_bot_move_util.c | 197 - ratoa_gamecode/code/game/ai_bot_nav_guard.c | 473 ++ ratoa_gamecode/code/game/ai_bot_nav_guard.h | 24 + ratoa_gamecode/code/game/ai_bot_opponent.c | 1850 ++++++ ratoa_gamecode/code/game/ai_bot_opponent.h | 115 + ratoa_gamecode/code/game/ai_bot_position.c | 720 ++ ratoa_gamecode/code/game/ai_bot_position.h | 82 + ratoa_gamecode/code/game/ai_bot_tactics.c | 214 +- ratoa_gamecode/code/game/ai_bot_tactics.h | 3 - ratoa_gamecode/code/game/ai_dmnet.c | 293 +- ratoa_gamecode/code/game/ai_dmq3.c | 326 +- ratoa_gamecode/code/game/ai_dmq3.h | 4 + ratoa_gamecode/code/game/ai_main.c | 10 +- ratoa_gamecode/code/game/ai_main.h | 81 + ratoa_gamecode/code/game/ai_weapon_select.c | 287 +- ratoa_gamecode/code/game/ai_weapon_select.h | 12 + ratoa_gamecode/code/game/g_client.c | 3 + ratoa_gamecode/code/game/g_main.c | 3 + ratoa_gamecode/windows_scripts/game.q3asm | 1 - ratoa_gamecode/windows_scripts/game_mp.q3asm | 1 - .../windows_scripts/windows_compile_game.bat | 1 - .../windows_compile_game_missionpack.bat | 1 - 41 files changed, 12163 insertions(+), 2164 deletions(-) create mode 100644 ratoa_gamecode/code/game/ai_bot_dmnet.c create mode 100644 ratoa_gamecode/code/game/ai_bot_dmnet.h delete mode 100644 ratoa_gamecode/code/game/ai_bot_events.c create mode 100644 ratoa_gamecode/code/game/ai_bot_item_timing.h delete mode 100644 ratoa_gamecode/code/game/ai_bot_move_util.c create mode 100644 ratoa_gamecode/code/game/ai_bot_nav_guard.c create mode 100644 ratoa_gamecode/code/game/ai_bot_nav_guard.h create mode 100644 ratoa_gamecode/code/game/ai_bot_opponent.c create mode 100644 ratoa_gamecode/code/game/ai_bot_opponent.h create mode 100644 ratoa_gamecode/code/game/ai_bot_position.c create mode 100644 ratoa_gamecode/code/game/ai_bot_position.h 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