diff --git a/.cursor/rules/build.mdc b/.cursor/rules/build.mdc new file mode 100644 index 0000000..52e41d9 --- /dev/null +++ b/.cursor/rules/build.mdc @@ -0,0 +1,16 @@ +--- +description: How to build and deploy Devotion on Windows +alwaysApply: true +--- + +# Build (Windows) + +After changing game/cgame/ui code or when verifying a compile: + +From repo root in PowerShell: + + .\build_windows.ps1 -Quiet -Deploy + +Do not use `ratoa_gamecode/windows_scripts/windows_compile_game.bat` unless explicitly asked — use the root script above. + +See BUILD_WINDOWS.md for flags and troubleshooting. diff --git a/docs/BOT-CVARS.md b/docs/BOT-CVARS.md index 49c9c11..c975278 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -16,6 +16,7 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | `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_forceclustering` | Vanilla | `0` | 0 or 1 | Forces the navigation system to rebuild area clusters for the current map (slow; map load / AAS build). | diff --git a/docs/BOT-ENHANCED-ARCHITECTURE.md b/docs/BOT-ENHANCED-ARCHITECTURE.md index d0a1154..eda009b 100644 --- a/docs/BOT-ENHANCED-ARCHITECTURE.md +++ b/docs/BOT-ENHANCED-ARCHITECTURE.md @@ -52,6 +52,7 @@ flowchart TB | 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` | Legacy `bot_humanizeaim` / `bot_smartWeaponChoice` / `bot_tacticalAI` are migrated once at init if new cvars are still default (see BOT-CVARS.md). @@ -62,16 +63,18 @@ Legacy `bot_humanizeaim` / `bot_smartWeaponChoice` / `bot_tacticalAI` are migrat | File | Role | |------|------| -| `ai_bot_enhanced.c/h` | Master cvar, facade gates, `OnThinkStart`, register/reset orchestration | +| `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` | Stub only (`BotMoveHarness_IsActive` always false) | -| `ai_aim_harness.c/h` | Humanized view motor + suppressive fire | -| `ai_weapon_select.c/h` | Range/ammo weapon picker + roam selection | -| `ai_bot_tactics.c/h` | Gauntlet flee, hurt-by-other, closer threat, finish wounded | -| `ai_main.h` | `combat`, `evt_*`, `aimh_*`, `wps_*`, `tact_*` blocks | -| `ai_dmq3.c` | `BotDeathmatchAI`, aim-at-enemy, `BotChooseWeapon` (facade at boundaries) | -| `ai_dmnet.c` | Battle/retreat node hooks into tactics | +| `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_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 | +| `ai_main.h` | `combat`, `evt_*`, `aimh_*`, `movej_*`, `wps_*`, `tact_*` blocks | +| `ai_dmq3.c` | `BotDeathmatchAI`, aim-at-enemy, `BotChooseWeapon`, `BotJumppad_Update` (thin `BotJumppad_EffectiveTfl` → harness) | +| `ai_dmnet.c` | Battle/retreat node hooks; calls `BotEnhanced_*` goal-stack helpers before nearby/LTG item choose | | `ai_main.c` | `BotUpdateInput` aim path | --- @@ -89,7 +92,24 @@ Legacy `bot_humanizeaim` / `bot_smartWeaponChoice` / `bot_tacticalAI` are migrat 1. Extend `bot_move_policy_t` in `ai_bot_combat.h`. 2. Set policy in `BotCombat_UpdateIntent()`. -3. Wire `ai_bot_move_harness.c` when ready; call from `BotAttackMove` / `BotUpdateInput` only after `BotMoveHarness_IsActive()` is implemented. +3. Implement actuation in `ai_bot_move_harness.c` (reuse `ai_bot_move_util.c` for walk/view/helpers); call `BotMove_OnPostMoveToGoal` after `trap_BotMoveToGoal` and `BotMove_OnInputFrame` from `BotUpdateInput` when `BotMove_SuppressesAimMotor()`. + +### Botlib movement + enhanced aim (rocket jump) + +When `bot_enhanced_aim` is on, botlib `MOVERESULT_MOVEMENT*` (rocket/BFG jump, swim, activate shoot) must not be overridden by the aim harness motor. + +| Hook | Where | Role | +|------|--------|------| +| `BotMove_OnPostMoveToGoal` | `ai_dmnet.c` after each `trap_BotMoveToGoal` | Cache moveresult flags; short latch after movement view | +| `BotMove_SuppressRoamView` | `ai_dmnet.c` roam-view branches | Skip roam ideal angles while botlib owns view | +| `BotMove_SuppressesAimMotor` | `ai_aim_harness.c`, `BotUpdateInput` | Skip harness motor / use legacy input path | +| `BotMove_OnInputFrame` | `BotUpdateInput` | Vanilla view smoothing + `trap_EA_GetInput` while suppressed | + +No rocket-jump logic in `ai_dmq3.c`; botlib handles jump/attack timing. + +**Enhanced RJ maneuver** (active prep/fire coordination) requires **both** `bot_enhanced_aim` (view-motor bypass) and `bot_enhanced_movement`. With only `bot_enhanced_aim` on, the aim motor still yields to botlib view during native RJ travel but the harness does not actively prepare or fire the jump. The standard `bot_rocketjump` botlib path is unaffected by either cvar. + +**Walk-off ledge:** after `trap_BotMoveToGoal`, if `traveltype == TRAVEL_WALKOFFLEDGE` and `bot_enhanced_movement` is on, `trap_AAS_PredictClientMovement` with `SE_HITGROUNDDAMAGE` estimates fall damage; if damage ≥ half current health, the reach is avoided (`TFL_WALKOFFLEDGE` stripped, `BotResetAvoidReach`) and `BotItems_RequestUrgentHealth` commits a visible health pickup when possible. ### New world event (next think) @@ -135,6 +155,7 @@ set bot_enhanced 0 set bot_enhanced_aim 0 set bot_enhanced_weapons 0 set bot_enhanced_tactics 0 +set bot_enhanced_movement 0 set bot_challenge 0 set bot_debugAim 0 ``` @@ -145,14 +166,17 @@ set bot_debugAim 0 |---|--------|----------|------| | 1 | All enhanced **off** (defaults) | Vanilla bot aim, weapon pick, combat decisions | Pass | | 2 | `bot_enhanced 1`; all sub-cvars **0** | Same as row 1 (master on, features off) | Pass | -| 3 | `bot_enhanced 1`; `bot_enhanced_aim 1` only | Smooth humanized aim; MG/LG hold fire; no snap-aim jitter | Pass | +| 3 | `bot_enhanced 1`; `bot_enhanced_aim 1` only | Humanized aim by menu skill 0–5 ladder; skill 5 ~0.94 harness accuracy; MG/LG suppressive fire wide cone; rail trace at skill 3+; view yields to botlib during native RJ travel | Pass | | 4 | `bot_enhanced 1`; `bot_enhanced_weapons 1` only | Range/ammo-aware switches; roaming silent weapon bias | Pass | | 5 | `bot_enhanced 1`; `bot_enhanced_tactics 1` only | Gauntlet flee/rush, third-party hurt switch, nearer threat, finish wounded | Pass | -| 6 | All enhanced **on** (`bot_enhanced 1` + all three sub-cvars `1`) | Same as pre-refactor with all three legacy features enabled together | Pass | +| 6 | `bot_enhanced 1` + aim + weapons + tactics (`1`) | Same as pre-refactor with all three legacy features enabled together | Pass | | 7 | Row 3 + `bot_challenge 1` | Same as row 3 (harness stays on; legacy challenge snap path skipped) | Pass | | 8 | Row 3 or 6 + `bot_debugAim 1` + client `cg_debugBotAim 1` | Debug lines unchanged (green wish, yellow crosshair) | Pass | +| 9 | Row 3 + maps with rocket jumps (`bot_rocketjump 1`) | Bots complete RJ via vanilla botlib; no stare-down / timeout retry loop; enhanced maneuver **off** | — | +| 10 | `bot_enhanced 1`; `bot_enhanced_aim 1`; `bot_enhanced_movement 1`; maps with RJ (`bot_rocketjump 1`) | Enhanced RJ maneuver active: bots approach spot, aim down, fire+jump; no retry stare-down | — | +| 11 | `bot_enhanced 1`; `bot_enhanced_movement 1` only (aim **off**); maps with ledge drops | Walk-off avoidance active (no enhanced RJ — requires aim too); bots reroute away from lethal drops; standard RJ via botlib unaffected | — | -**Row 1 trap (master gate):** `bot_enhanced 0` with all sub-cvars `1` must still behave as vanilla. +**Row 1 trap (master gate):** `bot_enhanced 0` with all sub-cvars `1` (including `bot_enhanced_movement`) must still behave as vanilla. **Legacy migration (optional):** `bot_enhanced 0`, `set bot_humanizeaim 1` only in `server.cfg`, restart map → server prints deprecation line; aim matches row 3 after migration. @@ -164,37 +188,56 @@ set bot_enhanced 0 set bot_enhanced_aim 0 set bot_enhanced_weapons 0 set bot_enhanced_tactics 0 +set bot_enhanced_movement 0 // Row 3 — aim only set bot_enhanced 1 set bot_enhanced_aim 1 set bot_enhanced_weapons 0 set bot_enhanced_tactics 0 +set bot_enhanced_movement 0 // Row 4 — weapons only set bot_enhanced 1 set bot_enhanced_aim 0 set bot_enhanced_weapons 1 set bot_enhanced_tactics 0 +set bot_enhanced_movement 0 // Row 5 — tactics only set bot_enhanced 1 set bot_enhanced_aim 0 set bot_enhanced_weapons 0 set bot_enhanced_tactics 1 +set bot_enhanced_movement 0 -// Row 6 — all on +// Row 6 — aim + weapons + tactics (no movement) set bot_enhanced 1 set bot_enhanced_aim 1 set bot_enhanced_weapons 1 set bot_enhanced_tactics 1 +set bot_enhanced_movement 0 + +// Row 10 — aim + movement (enhanced RJ maneuver + ledge avoidance) +set bot_enhanced 1 +set bot_enhanced_aim 1 +set bot_enhanced_weapons 0 +set bot_enhanced_tactics 0 +set bot_enhanced_movement 1 + +// Row 11 — movement only (ledge avoidance; no enhanced RJ without aim) +set bot_enhanced 1 +set bot_enhanced_aim 0 +set bot_enhanced_weapons 0 +set bot_enhanced_tactics 0 +set bot_enhanced_movement 1 ``` ### Regression focus While running rows 3–6, watch for regressions vs. known pre-refactor behavior: -- **Weapon switching cadence** — no rapid flip-flop; MG not primary at long range when rail/RL available. +- **Weapon switching cadence** — no rapid flip-flop; MG not primary at long range when rail/RL available; low combat-skill bots penalized more for audible rail/LG while roaming. - **Gauntlet-only survival** — bot flees when far with only gauntlet; rushes when close (tactics). - **MG + humanize aim** — sustained fire on target with smooth view tracking, not single-tap snap shots. - **Third-party damage** — bot fighting A switches toward B when B chips them (tactics, row 5/6). @@ -211,7 +254,7 @@ Do not add new stance/move gameplay until: - [x] `BotCombat_OnWeaponCommitted` called from weapon notify (stub) - [x] `BotEvents_*` ingress; drain only from `OnThinkStart` - [x] Legacy hooks use `BotEnhanced_*Active()` at boundaries -- [x] `ai_bot_move_harness` linked (no-op) +- [x] `ai_bot_move_harness` — botlib movement bypass for enhanced aim - [x] This document (architecture + parity table) - [x] **Parity table rows 1–8 passed** (manual sign-off) diff --git a/ratoa_assets/assets/music/sad_synthwave.ogg b/ratoa_assets/assets/music/sad_synthwave.ogg new file mode 100644 index 0000000..204c4b0 Binary files /dev/null and b/ratoa_assets/assets/music/sad_synthwave.ogg differ diff --git a/ratoa_assets/assets/sound/misc/Aries_Beats_-_Sad_Synthwave.ogg b/ratoa_assets/assets/sound/misc/Aries_Beats_-_Sad_Synthwave.ogg deleted file mode 100644 index 56a2e01..0000000 Binary files a/ratoa_assets/assets/sound/misc/Aries_Beats_-_Sad_Synthwave.ogg and /dev/null differ diff --git a/ratoa_gamecode/Makefile b/ratoa_gamecode/Makefile index e90c855..d8539f1 100644 --- a/ratoa_gamecode/Makefile +++ b/ratoa_gamecode/Makefile @@ -1795,6 +1795,7 @@ Q3GOBJ_ = \ $(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 \ @@ -1863,6 +1864,7 @@ MPGOBJ_ = \ $(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 \ diff --git a/ratoa_gamecode/code/cgame/cg_consolecmds.c b/ratoa_gamecode/code/cgame/cg_consolecmds.c index 03c894d..e20fbb5 100644 --- a/ratoa_gamecode/code/cgame/cg_consolecmds.c +++ b/ratoa_gamecode/code/cgame/cg_consolecmds.c @@ -267,6 +267,10 @@ static void CG_ScoresDown_f( void ) { if (cg.predictedPlayerState.pm_type == PM_INTERMISSION) { cg.showScoreboardNum++; } + + /* Load deferred player models when the scoreboard is opened (cg_deferPlayers 1). */ + cg.deferredPlayerLoading = 0; + CG_LoadDeferredPlayers(); } static void CG_ScoresUp_f( void ) { diff --git a/ratoa_gamecode/code/cgame/cg_draw.c b/ratoa_gamecode/code/cgame/cg_draw.c index 3455f77..25ae5d6 100644 --- a/ratoa_gamecode/code/cgame/cg_draw.c +++ b/ratoa_gamecode/code/cgame/cg_draw.c @@ -6391,30 +6391,6 @@ void CG_DrawEngineSupport(void) { 0); return; } - /* - if (!CG_SupportsOggVorbis()) { - static int drawEngineMessageTime = 0; - const char *s = "WARNING: Engine lacks Ogg Vorbis support, some sounds might not play!"; - float char_h = 12; - float char_w = CG_HeightToWidth(6); - float color[4] = { 1.0, 1.0, 0.0, 1.0 }; - - if (drawEngineMessageTime == 0) { - drawEngineMessageTime = cg.time; - } else if (drawEngineMessageTime + 60 * 1000 < cg.time) { - return; - } - CG_DrawStringExt( SCREEN_WIDTH * 0.5 - (CG_DrawStrlen(s) * char_w)/2.0, - 1, - s, - color, - qtrue, - qfalse, - char_w, - char_h, - 0); - } - */ } #define HELPMOTDOVERLAY_LINES CONSOLE_MAXHEIGHT diff --git a/ratoa_gamecode/code/cgame/cg_main.c b/ratoa_gamecode/code/cgame/cg_main.c index 3c9bb16..bc3a8fd 100644 --- a/ratoa_gamecode/code/cgame/cg_main.c +++ b/ratoa_gamecode/code/cgame/cg_main.c @@ -682,16 +682,23 @@ static void CG_RegisterItemSounds( int itemNum ) { } } +/* +=============== +CG_SupportsOggVorbis + +Probe whether the client engine is capable of loading OGG-format audio files. +Note: This function is duplicated in ui_menu.c as ui_SupportsOggVorbis + because they're built as separate QVMs without a suitable cross-linked header + to put the function in +=============== +*/ qboolean CG_SupportsOggVorbis(void) { static int supports_ogg = -1; + //We only ever test this once per game load, second attempt will just fall through this check and return if (supports_ogg == -1) { - qhandle_t ogg = trap_S_RegisterSound("sound/testoggvorbis.ogg", qtrue); - if (ogg) { - supports_ogg = 1; - } else { - supports_ogg = 0; - } + qhandle_t ogg = trap_S_RegisterSound("music/sad_synthwave.ogg", qtrue); + supports_ogg = (ogg != 0) ? 1 : 0; } return (qboolean)supports_ogg; diff --git a/ratoa_gamecode/code/cgame/cg_players.c b/ratoa_gamecode/code/cgame/cg_players.c index d179c48..4d6ee65 100644 --- a/ratoa_gamecode/code/cgame/cg_players.c +++ b/ratoa_gamecode/code/cgame/cg_players.c @@ -1001,10 +1001,10 @@ static void CG_SetDeferredClientInfo( int clientNum, clientInfo_t *ci ) { } } - // find the first valid clientinfo and grab its stuff + // find the first valid, fully-loaded clientinfo and grab its stuff for ( i = 0 ; i < cgs.maxclients ; i++ ) { match = &cgs.clientinfo[ i ]; - if ( !match->infoValid ) { + if ( !match->infoValid || match->deferred ) { continue; } @@ -1321,6 +1321,8 @@ clientInfo_t *ci; clientInfo_t newInfo; const char *configstring; const char *v; + const char *modelConfig; + const char *headModelConfig; char *slash; const char *local_config; int local_team; @@ -1436,18 +1438,20 @@ clientInfo_t *ci; Q_strncpyz(newInfo.blueTeam, v, MAX_TEAMNAME); // model - v = Info_ValueForKey( configstring, "model" ); - - CG_SetSkinAndModel( &newInfo, v, allowNativeModel, viewerTeam, clientNum, qtrue, - newInfo.modelName, sizeof( newInfo.modelName ), newInfo.skinName, sizeof( newInfo.skinName ) ); + modelConfig = Info_ValueForKey( configstring, "model" ); + + CG_SetSkinAndModel( &newInfo, modelConfig, allowNativeModel, viewerTeam, clientNum, qtrue, + newInfo.modelName, sizeof( newInfo.modelName ), newInfo.skinName, sizeof( newInfo.skinName ) ); if ( cg_teamColor.string[0] && team != TEAM_SPECTATOR ) { - v = CG_GetTeamColorsOSP( cg_teamColor.string ); - len = strlen( v ); + const char *ospColors; + + ospColors = CG_GetTeamColorsOSP( cg_teamColor.string ); + len = strlen( ospColors ); if ( len >= 4 ) - CG_ColorFromChar( v[3], newInfo.color1 ); + CG_ColorFromChar( ospColors[3], newInfo.color1 ); if ( len >= 5 ) - CG_ColorFromChar( v[4], newInfo.color2 ); + CG_ColorFromChar( ospColors[4], newInfo.color2 ); } if (CG_IsTeamGametype()) { @@ -1522,14 +1526,14 @@ clientInfo_t *ci; } if (CG_IsTeamGametype()) { - // keep skin name - slash = strchr( v, '/' ); + // keep skin name from this player's configstring model + slash = strchr( modelConfig, '/' ); if ( slash ) { Q_strncpyz( newInfo.skinName, slash + 1, sizeof( newInfo.skinName ) ); } } } else { - Q_strncpyz( newInfo.modelName, v, sizeof( newInfo.modelName ) ); + Q_strncpyz( newInfo.modelName, modelConfig, sizeof( newInfo.modelName ) ); slash = strchr( newInfo.modelName, '/' ); if ( !slash ) { @@ -1543,9 +1547,9 @@ clientInfo_t *ci; } // head model - v = Info_ValueForKey( configstring, "hmodel" ); + headModelConfig = Info_ValueForKey( configstring, "hmodel" ); - CG_SetSkinAndModel( &newInfo, v, allowNativeModel, viewerTeam, clientNum, qtrue, + CG_SetSkinAndModel( &newInfo, headModelConfig, allowNativeModel, viewerTeam, clientNum, qtrue, newInfo.headModelName, sizeof( newInfo.headModelName ), newInfo.headSkinName, sizeof( newInfo.headSkinName ) ); if (cgs.ratFlags & RAT_ALLOWFORCEDMODELS && @@ -1606,14 +1610,14 @@ clientInfo_t *ci; } if (CG_IsTeamGametype()) { - // keep skin name - slash = strchr( v, '/' ); + // keep skin name from this player's configstring head model + slash = strchr( headModelConfig, '/' ); if ( slash ) { Q_strncpyz( newInfo.headSkinName, slash + 1, sizeof( newInfo.headSkinName ) ); } } } else { - Q_strncpyz( newInfo.headModelName, v, sizeof( newInfo.headModelName ) ); + Q_strncpyz( newInfo.headModelName, headModelConfig, sizeof( newInfo.headModelName ) ); slash = strchr( newInfo.headModelName, '/' ); if ( !slash ) { diff --git a/ratoa_gamecode/code/cgame/cg_servercmds.c b/ratoa_gamecode/code/cgame/cg_servercmds.c index a6c7fce..d98ec0b 100644 --- a/ratoa_gamecode/code/cgame/cg_servercmds.c +++ b/ratoa_gamecode/code/cgame/cg_servercmds.c @@ -1540,15 +1540,17 @@ static void CG_ConfigStringModified( void ) { cgs.gameSounds[ num-CS_SOUNDS] = trap_S_RegisterSound( str, qfalse ); } } else if ( num >= CS_PLAYERS && num < CS_PLAYERS+MAX_CLIENTS ) { - CG_NewClientInfo( num - CS_PLAYERS ); - if (num - CS_PLAYERS == cg.clientNum) { - // make sure to update all other player models in case we switched teams + int clientNum; + + clientNum = num - CS_PLAYERS; + CG_NewClientInfo( clientNum ); + if ( clientNum == cg.clientNum ) { + /* Local team/userinfo change: refresh all clients (team/enemy model rules). */ CG_ForceModelChange(); CG_ParseForcedColors(); } + /* Other slots: CG_NewClientInfo only (defer 0 loads there; defer 1 waits for scoreboard). */ CG_BuildSpectatorString(); - CG_ForceModelChange(); //duffman91 - for bugs w/ quake 3 pm models. - CG_ParseForcedColors(); } else if ( num == CS_FLAGSTATUS ) { // if( cgs.gametype == GT_CTF || cgs.gametype == GT_CTF_ELIMINATION || cgs.gametype == GT_DOUBLE_D) { if( cgs.gametype == GT_CTF || cgs.gametype == GT_CTF_ELIMINATION ) { diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index e595e65..9eafbbd 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -16,8 +16,9 @@ BOT AIM HARNESS (v1) — see ai_aim_harness.h #include "ai_dmq3.h" #include "chars.h" #include "ai_aim_harness.h" -#include "ai_bot_enhanced.h" #include "ai_dmq3.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_move_harness.h" vmCvar_t bot_enhanced_aim; vmCvar_t bot_debugAim; @@ -41,7 +42,7 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_ROAM_MIN_VEL 90.0f #define AIMH_ROAM_MIN_ERR 3.0f #define AIMH_COMBAT_MIN_VEL 55.0f -#define AIMH_COMBAT_CATCHUP_GAIN 14.0f +#define AIMH_COMBAT_CATCHUP_GAIN 17.0f #define AIMH_COMBAT_VEL_SCALE 1.85f #define AIMH_MOTOR_NOISE_SCALE 2.0f #define AIMH_HITSCAN_LEAD_EXTRA 0.025f @@ -58,6 +59,26 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_YAW_CATCHUP_ERR 10.0f #define AIMH_YAW_CATCHUP_RATE 8.0f #define AIMH_ENEMY_Z_SPIKE 72.0f +/* Rocket splash: enemy feet = player center Z - 28 (56uu tall, origin at feet). */ +#define AIMH_RL_ENEMY_ABOVE_Z 16.0f +#define AIMH_RL_PLAYER_CENTER_Z 28.0f +#define AIMH_RL_FEET_HIT_Z_TOL 50.0f +#define AIMH_RL_FEET_HIT_XY_TOL 60.0f +#define AIMH_RL_FEET_MIN_DIST 32.0f +/* Universal shot lead: nudge final aim point along enemy travel (speed + distance). */ +#define AIMH_LEAD_SPEED_MIN 32.0f +#define AIMH_LEAD_SPEED_FULL 420.0f +#define AIMH_LEAD_DIST_MIN 96.0f +#define AIMH_LEAD_DIST_FULL 900.0f +#define AIMH_LEAD_MAX_DIST 36.0f +#define AIMH_LEAD_TOTAL_MAX 52.0f +#define AIMH_LEAD_THINK_AHEAD_BASE 0.75f +#define AIMH_LEAD_THINK_AHEAD_SKILL 0.55f +#define AIMH_LEAD_STRAFE_BASE 0.6f +#define AIMH_LEAD_STRAFE_SKILL 0.45f +#define AIMH_RL_SPLASH_NEAR_XY 96.0f +#define AIMH_RL_SPLASH_NEAR_3D 128.0f +#define AIMH_RL_CLOSE_SPLASH_DIST 800.0f #define AIMH_PITCH_RESET_ERR 14.0f #define AIMH_PITCH_CATCHUP_ERR 10.0f #define AIMH_PITCH_CATCHUP_RATE 7.0f @@ -101,12 +122,62 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_MOTOR_SUBSTEP_DT 0.010f #define AIMH_MOTOR_SUBSTEP_MAX 12 -/* Suppressive fire: loose aim cone (degrees, full width) and view-vs-intent slack */ -#define AIMH_FIRE_FOV_NEAR 72.0f -#define AIMH_FIRE_FOV_FAR 58.0f -#define AIMH_FIRE_FOV_DIST 100.0f -#define AIMH_FIRE_VIEW_SLACK 14.0f -#define AIMH_FIRE_MIN_VISIBILITY 0.12f +/* Suppressive fire: wide cone for all skills (low skill = spray, not picky trigger). */ +#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 +/* 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 +#define AIMH_PURSUIT_ERR_DIST_NEAR 256.0f +#define AIMH_PURSUIT_ERR_DIST_FAR 1200.0f +#define AIMH_PURSUIT_PITCH_SCALE 0.55f +/* End of think interval: converge on true aimtarget; underdamped band allows overshoot. */ +#define AIMH_SETTLE_THINK_FRAC 0.32f +#define AIMH_SETTLE_THINK_FRAC_ELITE 0.54f +#define AIMH_SETTLE_ERR_PHASE 4.0f +#define AIMH_SETTLE_ERR_SNAP 1.25f +#define AIMH_SETTLE_DAMP_NEAR 0.42f +#define AIMH_SETTLE_STIFF_MULT 1.30f +#define AIMH_SETTLE_SNAP_GAIN 10.0f +#define AIMH_CATCHUP_ERR_MIN 0.8f +#define AIMH_CATCHUP_ERR_MAX 26.0f +#define AIMH_OVERSHOOT_DAMP_ACC 0.12f +/* 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_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 +/* Slow weapons: build pressure to fire after reload + grace without a shot. */ +#define AIMH_URGENCY_GRACE 0.12f +#define AIMH_URGENCY_RAMP 0.40f +#define AIMH_URGENCY_TRACK_FOV 40.0f +#define AIMH_URGENCY_TRACK_SLACK 22.0f +#define AIMH_URGENCY_ANGLE_MAX 8.0f +#define AIMH_URGENCY_MIN_RELOAD 0.05f static int bot_enhanced_aim_last = -1; static int bot_debugAim_last = -1; @@ -116,6 +187,7 @@ void BotAimHarness_SyncAllBotsDebug(void); static float BotAimHarness_ClampPitch(float pitch); static float BotAimHarness_PitchDiff(float pitch, float goal); static float BotAimHarness_YawDiff(float yaw, float goal); +static void BotAimHarness_GetCombatAimAngles(bot_state_t *bs, vec3_t angles); static float BotAimHarness_Lerp(float a, float b, float t) { return a + (b - a) * t; @@ -136,6 +208,78 @@ 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. @@ -237,6 +381,63 @@ static void BotAimHarness_RefreshEye(bot_state_t *bs) { bs->eye[2] += bs->cur_ps.viewheight; } +static float BotAimHarness_GetHorizBotSpeed(bot_state_t *bs) { + vec3_t vel; + + if (!bs) { + return 0.0f; + } + VectorCopy(bs->velocity, vel); + vel[2] = 0.0f; + if (VectorLengthSquared(vel) >= Square(24.0f)) { + return VectorLength(vel); + } + if (BotAI_GetClientState(bs->client, &bs->cur_ps)) { + VectorCopy(bs->cur_ps.velocity, vel); + vel[2] = 0.0f; + return VectorLength(vel); + } + return 0.0f; +} + +/* + * Hitscan lead direction/speed in shooter frame (enemy horiz vel minus bot horiz vel). + */ +static qboolean BotAimHarness_GetRelativeLeadDir(bot_state_t *bs, aas_entityinfo_t *entinfo, + vec3_t relDir, float *relSpeed) { + vec3_t enemyVel, botVel; + + VectorSubtract(entinfo->origin, entinfo->lastvisorigin, enemyVel); + enemyVel[2] = 0.0f; + if (entinfo->update_time > 0.001f) { + VectorScale(enemyVel, 1.0f / entinfo->update_time, enemyVel); + } else { + VectorClear(enemyVel); + } + if (VectorLengthSquared(enemyVel) < Square(AIMH_LEAD_SPEED_MIN) && + VectorLengthSquared(bs->enemyvelocity) > Square(20.0f)) { + VectorCopy(bs->enemyvelocity, enemyVel); + enemyVel[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(enemyVel, botVel, relDir); + relDir[2] = 0.0f; + *relSpeed = VectorLength(relDir); + if (*relSpeed < 0.001f) { + return qfalse; + } + VectorScale(relDir, 1.0f / *relSpeed, relDir); + return *relSpeed >= AIMH_LEAD_SPEED_MIN; +} + int BotAimHarness_AimTargetValid(bot_state_t *bs) { if (VectorCompare(bs->aimtarget, vec3_origin)) { return 0; @@ -273,39 +474,625 @@ static void BotAimHarness_ApplyLead(vec3_t target, const vec3_t vel, float leadT target[2] += leadVel[2] * leadTime * vertScale; } -static float BotAimHarness_HitscanLeadTime(bot_state_t *bs) { - float thinkSec, motorLag, leadTime, aimSkill; +static float BotAimHarness_VertLeadScale(bot_state_t *bs) { + float aimSkill; + + aimSkill = bs->aimh_aim_skill; + if (aimSkill < 0.0f) { + aimSkill = 0.0f; + } + if (aimSkill > 1.0f) { + aimSkill = 1.0f; + } + return AIMH_VERT_LEAD_MIN + (AIMH_VERT_LEAD_MAX - AIMH_VERT_LEAD_MIN) * aimSkill; +} + +static float BotAimHarness_EnemyHorizDist(bot_state_t *bs, aas_entityinfo_t *entinfo) { + vec3_t delta; + + VectorSubtract(entinfo->origin, bs->origin, delta); + delta[2] = 0.0f; + return VectorLength(delta); +} + +static float BotAimHarness_GetCombatPursuitDist(bot_state_t *bs) { + vec3_t dir; + aas_entityinfo_t entinfo; + + if (VectorLengthSquared(bs->aimh_combat_target) > 1.0f) { + VectorSubtract(bs->aimh_combat_target, bs->eye, dir); + return VectorLength(dir); + } + if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + BotEntityInfo(bs->enemy, &entinfo); + if (entinfo.valid) { + VectorSubtract(entinfo.origin, bs->eye, dir); + return VectorLength(dir); + } + } + return AIMH_PURSUIT_ERR_DIST_FAR; +} + +/* + * 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_ClearRailLead(bot_state_t *bs) { + VectorClear(bs->aimh_rail_lead_point); + bs->aimh_rail_lead_valid = qfalse; +} + +static int BotAimHarness_UsingRailgun(bot_state_t *bs) { + if (bs->weaponnum == WP_RAILGUN) { + return qtrue; + } + if (bs->cur_ps.weapon == WP_RAILGUN) { + return qtrue; + } + return qfalse; +} + +static int BotAimHarness_UsingShotgun(bot_state_t *bs) { + if (bs->weaponnum == WP_SHOTGUN) { + return qtrue; + } + if (bs->cur_ps.weapon == WP_SHOTGUN) { + return qtrue; + } + return qfalse; +} + +static int BotAimHarness_IsSlowFireWeapon(bot_state_t *bs) { + return BotAimHarness_UsingRailgun(bs) || + BotAimHarness_UsingRocketLauncher(bs) || + BotAimHarness_UsingShotgun(bs); +} + +static void BotAimHarness_ResetShotUrgency(bot_state_t *bs) { + bs->aimh_shot_press_since = 0.0f; + bs->aimh_shot_press_weapon = -1; +} + +static float BotAimHarness_SlowWeaponReloadSec(bot_state_t *bs) { + weaponinfo_t wi; + float reload; + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + reload = wi.reload * 0.001f; + if (reload < AIMH_URGENCY_MIN_RELOAD) { + reload = AIMH_URGENCY_MIN_RELOAD; + } + return reload; +} + +/* + * 0 = patient (perfect-shot rules). Rises after reload+grace on target without firing. + */ +static float BotAimHarness_GetShotUrgency(bot_state_t *bs) { + float now, threshold, elapsed; + + if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim) { + BotAimHarness_ResetShotUrgency(bs); + return 0.0f; + } + if (!BotAimHarness_IsSlowFireWeapon(bs)) { + BotAimHarness_ResetShotUrgency(bs); + return 0.0f; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + BotAimHarness_ResetShotUrgency(bs); + return 0.0f; + } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + BotAimHarness_ResetShotUrgency(bs); + return 0.0f; + } + if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { + return 0.0f; + } + if (!BotAimHarness_WeaponReady(bs)) { + return 0.0f; + } + if (bs->cur_ps.weaponTime > 0) { + BotAimHarness_ResetShotUrgency(bs); + return 0.0f; + } + if (bs->aimh_shot_press_weapon != bs->weaponnum) { + BotAimHarness_ResetShotUrgency(bs); + } + now = FloatTime(); + if (bs->aimh_shot_press_since <= 0.0f) { + bs->aimh_shot_press_since = now; + bs->aimh_shot_press_weapon = bs->weaponnum; + return 0.0f; + } + threshold = BotAimHarness_SlowWeaponReloadSec(bs) + AIMH_URGENCY_GRACE; + elapsed = now - bs->aimh_shot_press_since; + if (elapsed < threshold) { + return 0.0f; + } + elapsed -= threshold; + if (elapsed >= AIMH_URGENCY_RAMP) { + return 1.0f; + } + return elapsed / AIMH_URGENCY_RAMP; +} + +static qboolean BotAimHarness_IsRailInterceptActive(bot_state_t *bs) { + if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim) { + return qfalse; + } + if (!BotAimHarness_UsingRailgun(bs)) { + return qfalse; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + return qtrue; +} + +static void BotAimHarness_RefreshThinkPursuitGoal(bot_state_t *bs) { + vec3_t trueAngles, dir; + float dist, closeFactor, maxErr, accScale, pitchOff, yawOff; + + if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim) { + return; + } + + if (BotAimHarness_IsRailInterceptActive(bs) && bs->aimh_rail_lead_valid) { + VectorSubtract(bs->aimh_rail_lead_point, bs->eye, dir); + if (VectorLengthSquared(dir) < 1.0f) { + BotAimHarness_GetCombatAimAngles(bs, trueAngles); + } else { + vectoangles(dir, trueAngles); + } + trueAngles[PITCH] = BotAimHarness_ClampPitch(trueAngles[PITCH]); + trueAngles[YAW] = AngleMod(trueAngles[YAW]); + bs->aimh_pursuit_pitch_off = 0.0f; + bs->aimh_pursuit_yaw_off = 0.0f; + 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 = trueAngles[PITCH]; + bs->aimh_smooth_goal_yaw = trueAngles[YAW]; + 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)); + + pitchOff = (crandom() + crandom()) * 0.5f * maxErr * AIMH_PURSUIT_PITCH_SCALE; + yawOff = (crandom() + crandom()) * 0.5f * maxErr; + + 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_GetCombatTrueAimAngles(bot_state_t *bs, vec3_t angles) { + BotAimHarness_GetCombatAimAngles(bs, angles); + angles[PITCH] = BotAimHarness_ClampPitch(angles[PITCH]); + angles[YAW] = AngleMod(angles[YAW]); + angles[ROLL] = 0.0f; +} + +/* Input frames: eye moves with bot; re-aim from live eye + think-sampled pursuit offset. */ +static void BotAimHarness_GetLiveCombatMotorGoal(bot_state_t *bs, vec3_t goal) { + vec3_t trueAngles, dir; + + 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); + return; + } + vectoangles(dir, goal); + goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); + goal[YAW] = AngleMod(goal[YAW]); + return; + } + + BotAimHarness_GetCombatTrueAimAngles(bs, trueAngles); + goal[PITCH] = BotAimHarness_ClampPitch(trueAngles[PITCH] + bs->aimh_pursuit_pitch_off); + goal[YAW] = AngleMod(trueAngles[YAW] + bs->aimh_pursuit_yaw_off); +} + +static float BotAimHarness_GetThinkIntervalSec(void) { + float thinkSec; + + trap_Cvar_Update(&bot_thinktime); + thinkSec = bot_thinktime.integer / 1000.0f; + if (thinkSec < 0.05f) { + thinkSec = 0.1f; + } + return thinkSec; +} + +/* + * Last fraction of each bot-think cycle: motor goal = true aimtarget (not biased pursuit). + */ +static qboolean BotAimHarness_InThinkSettleWindow(bot_state_t *bs) { + float thinkSec, elapsed; + + if (BotAimHarness_IsRailInterceptActive(bs)) { + return qfalse; + } + if (!bs->aimh_combat_aim || bs->aimh_pursuit_set_time <= 0.0f) { + return qfalse; + } + if (bs->aimh_acquire_until > FloatTime()) { + return qfalse; + } + 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)); + return elapsed >= thinkSec; +} + +static void BotAimHarness_GetMotorPursuitAngles(bot_state_t *bs, vec3_t angles) { + angles[PITCH] = BotAimHarness_ClampPitch(bs->aimh_smooth_goal_pitch); + angles[YAW] = AngleMod(bs->aimh_smooth_goal_yaw); + angles[ROLL] = 0.0f; +} + +static float BotAimHarness_EnemyHorizSpeed(aas_entityinfo_t *entinfo) { + vec3_t vel; + float speed; + + VectorSubtract(entinfo->origin, entinfo->lastvisorigin, vel); + vel[2] = 0.0f; + speed = VectorLength(vel); + if (entinfo->update_time > 0.001f) { + speed /= entinfo->update_time; + } + return speed; +} + +/* + * Splash Z at enemy feet: torso center (+28 from origin) minus 28 => origin[2]. + * Q3 players are 56uu tall with origin at the feet; no floor traces. + */ +static float BotAimHarness_GetEnemyFeetZ(aas_entityinfo_t *entinfo) { + float centerZ; + + centerZ = entinfo->origin[2] + AIMH_RL_PLAYER_CENTER_Z; + return centerZ - AIMH_RL_PLAYER_CENTER_Z; +} + +static float BotAimHarness_GetEnemyCenterMassZ(aas_entityinfo_t *entinfo) { + return entinfo->origin[2] + AIMH_RL_PLAYER_CENTER_Z; +} + +static void BotAimHarness_SetRocketSplashPoint(aas_entityinfo_t *entinfo, + const vec3_t refPoint, vec3_t splash) { + VectorCopy(refPoint, splash); + splash[2] = BotAimHarness_GetEnemyFeetZ(entinfo); +} + +static float BotAimHarness_Clamp01(float v) { + if (v < 0.0f) { + return 0.0f; + } + if (v > 1.0f) { + return 1.0f; + } + return v; +} + +/* + * Overpredict aimtarget along enemy travel (once per bot think). Extrapolates ~1.5–2.5 + * think intervals ahead; extra when the enemy strafes across the bot's line of sight. + */ +void BotAimHarness_ApplyMovementLead(bot_state_t *bs, vec3_t shotPoint, float aimSkill) { + aas_entityinfo_t entinfo; + vec3_t velDir, toEnemy; + float speed, speedT, dist, distT, t, leadDist, thinkSec, thinkAhead; + float along, lateral; + + if (!BotAimHarness_IsActive()) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return; + } + + if (!BotAimHarness_GetRelativeLeadDir(bs, &entinfo, velDir, &speed)) { + return; + } + + if (aimSkill < 0.0f) { + aimSkill = 0.0f; + } + if (aimSkill > 1.0f) { + aimSkill = 1.0f; + } + + trap_Cvar_Update(&bot_thinktime); + thinkSec = bot_thinktime.integer / 1000.0f; + if (thinkSec < 0.001f) { + thinkSec = 0.1f; + } + thinkAhead = AIMH_LEAD_THINK_AHEAD_BASE + AIMH_LEAD_THINK_AHEAD_SKILL * aimSkill; + leadDist = speed * thinkSec * thinkAhead; + + VectorSubtract(shotPoint, bs->eye, toEnemy); + dist = VectorLength(toEnemy); + speedT = BotAimHarness_Clamp01((speed - AIMH_LEAD_SPEED_MIN) / + (AIMH_LEAD_SPEED_FULL - AIMH_LEAD_SPEED_MIN)); + distT = BotAimHarness_Clamp01((dist - AIMH_LEAD_DIST_MIN) / + (AIMH_LEAD_DIST_FULL - AIMH_LEAD_DIST_MIN)); + t = speedT * distT * (0.45f + 0.55f * aimSkill); + leadDist += AIMH_LEAD_MAX_DIST * t; + + 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_LEAD_STRAFE_BASE + AIMH_LEAD_STRAFE_SKILL * aimSkill); + } + + if (leadDist > AIMH_LEAD_TOTAL_MAX) { + leadDist = AIMH_LEAD_TOTAL_MAX; + } + leadDist *= BotAimHarness_Lerp(1.0f, 0.42f, BotAimHarness_GetEliteMotorTier(bs)); + leadDist /= 1.0f + 0.0016f * BotAimHarness_GetHorizBotSpeed(bs); + if (leadDist < 1.0f) { + return; + } + + VectorMA(shotPoint, leadDist, velDir, shotPoint); +} + +static int BotAimHarness_SplashNearEnemy(vec3_t splash, aas_entityinfo_t *entinfo) { + vec3_t delta; + + VectorSubtract(splash, entinfo->origin, delta); + if (VectorLength(delta) <= AIMH_RL_SPLASH_NEAR_3D) { + return qtrue; + } + delta[2] = 0.0f; + return VectorLength(delta) <= AIMH_RL_SPLASH_NEAR_XY; +} + +static int BotAimHarness_ValidateRocketSplashShot(bot_state_t *bs, aas_entityinfo_t *entinfo, + vec3_t splash) { + bsp_trace_t trace; + vec3_t start, dir; + weaponinfo_t wi; + float horizDist; + + if (!BotAimHarness_SplashNearEnemy(splash, entinfo)) { + return qfalse; + } + + horizDist = BotAimHarness_EnemyHorizDist(bs, entinfo); + VectorCopy(bs->origin, start); + start[2] += bs->cur_ps.viewheight; + trap_BotGetWeaponInfo(bs->ws, WP_ROCKET_LAUNCHER, &wi); + if (wi.valid) { + start[2] += wi.offset[2]; + } + + BotAI_Trace(&trace, start, NULL, NULL, splash, bs->entitynum, MASK_SHOT); + VectorSubtract(trace.endpos, splash, dir); + if (VectorLength(dir) > 80.0f) { + return qfalse; + } + + VectorSubtract(trace.endpos, start, dir); + if (horizDist > 96.0f && VectorLengthSquared(dir) <= Square(AIMH_RL_FEET_MIN_DIST)) { + return qfalse; + } + + return qtrue; +} + +/* + * Close/medium fights: splash between bot and enemy at enemy feet height. + */ +static int BotAimHarness_TryRocketCloseSplashPoint(bot_state_t *bs, + aas_entityinfo_t *entinfo, vec3_t feetPoint) { + vec3_t dir; + float horizDist; + + horizDist = BotAimHarness_EnemyHorizDist(bs, entinfo); + if (horizDist > AIMH_RL_CLOSE_SPLASH_DIST || horizDist < 8.0f) { + return qfalse; + } + + VectorSubtract(entinfo->origin, bs->origin, dir); + dir[2] = 0.0f; + VectorNormalize(dir); + VectorMA(bs->origin, horizDist * 0.55f, dir, feetPoint); + feetPoint[2] = BotAimHarness_GetEnemyFeetZ(entinfo); + + return BotAimHarness_ValidateRocketSplashShot(bs, entinfo, feetPoint); +} + +/* + * Splash rockets: enemy feet Z, XY from refPoint (lead). + */ +static int BotAimHarness_TryRocketFeetPoint(bot_state_t *bs, aas_entityinfo_t *entinfo, + const vec3_t refPoint, vec3_t feetPoint) { + if (entinfo->origin[2] >= bs->origin[2] + AIMH_RL_ENEMY_ABOVE_Z) { + return qfalse; + } + + if (BotAimHarness_TryRocketCloseSplashPoint(bs, entinfo, feetPoint)) { + return qtrue; + } + + 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; +} + +void BotAimHarness_ApplyPlasmaCenterMassAim(bot_state_t *bs, vec3_t aimPoint) { + aas_entityinfo_t entinfo; + + if (!BotAimHarness_IsActive() || !BotAimHarness_UsingPlasmagun(bs)) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return; + } + 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; + + if (!BotAimHarness_IsActive() || !BotAimHarness_UsingRailgun(bs)) { + BotAimHarness_ClearRailLead(bs); + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + BotAimHarness_ClearRailLead(bs); + return; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + BotAimHarness_ClearRailLead(bs); + return; + } + + aimPoint[2] = entinfo.origin[2] + AIMH_RAIL_CENTER_Z; - aimSkill = bs->aimh_aim_skill; 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; + } + + if (!BotAimHarness_GetRelativeLeadDir(bs, &entinfo, velDir, &speed)) { + VectorCopy(aimPoint, bs->aimh_rail_lead_point); + bs->aimh_rail_lead_valid = qtrue; + return; + } + + VectorCopy(aimPoint, bs->aimh_rail_lead_point); + bs->aimh_rail_lead_valid = qtrue; + + if (speed < AIMH_RAIL_LEAD_SPEED_MIN || speed < 0.001f) { + VectorCopy(aimPoint, bs->aimh_rail_lead_point); + return; + } trap_Cvar_Update(&bot_thinktime); thinkSec = bot_thinktime.integer / 1000.0f; - motorLag = AIMH_MOTOR_LAG_BASE + AIMH_MOTOR_LAG_SKILL * (1.0f - aimSkill); - leadTime = (thinkSec * AIMH_HITSCAN_LEAD_SCALE) + AIMH_HITSCAN_LEAD_EXTRA + motorLag; - leadTime *= (0.55f + 0.55f * aimSkill); - if (leadTime > AIMH_HITSCAN_LEAD_MAX) { - leadTime = AIMH_HITSCAN_LEAD_MAX; + if (thinkSec < 0.001f) { + thinkSec = 0.1f; + } + + leadDist = AIMH_RAIL_LEAD_BASE + AIMH_RAIL_LEAD_SKILL * aimSkill; + leadDist *= 0.4f + 0.6f * aimAccuracy; + + 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; + + leadDist += speed * thinkSec * + (AIMH_RAIL_STRAFE_SEC_BASE + AIMH_RAIL_STRAFE_SEC_SKILL * aimSkill); + + 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); + } + + 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) { + return; } - return leadTime; + + VectorMA(aimPoint, leadDist, velDir, aimPoint); + VectorCopy(aimPoint, bs->aimh_rail_lead_point); } -static float BotAimHarness_VertLeadScale(bot_state_t *bs) { - float aimSkill; +void BotAimHarness_ApplyRocketFeetAim(bot_state_t *bs, vec3_t aimPoint) { + aas_entityinfo_t entinfo; - aimSkill = bs->aimh_aim_skill; - if (aimSkill < 0.0f) { - aimSkill = 0.0f; + if (!BotAimHarness_IsActive() || !BotAimHarness_UsingRocketLauncher(bs)) { + return; } - if (aimSkill > 1.0f) { - aimSkill = 1.0f; + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; } - return AIMH_VERT_LEAD_MIN + (AIMH_VERT_LEAD_MAX - AIMH_VERT_LEAD_MIN) * aimSkill; + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return; + } + BotAimHarness_TryRocketFeetPoint(bs, &entinfo, aimPoint, aimPoint); } /* @@ -313,9 +1100,6 @@ static float BotAimHarness_VertLeadScale(bot_state_t *bs) { */ static int BotAimHarness_GetCombatTarget(bot_state_t *bs, vec3_t target) { aas_entityinfo_t entinfo; - weaponinfo_t wi; - vec3_t vel, dir; - float leadTime, dist, flightTime, vertScale; if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return qfalse; @@ -333,73 +1117,37 @@ static int BotAimHarness_GetCombatTarget(bot_state_t *bs, vec3_t target) { bs->aimh_last_enemy_z = entinfo.origin[2]; VectorCopy(entinfo.origin, target); - target[2] += 8.0f; - if (BotAimHarness_AimTargetValid(bs)) { - target[2] = bs->aimtarget[2]; - } - - VectorSubtract(entinfo.origin, entinfo.lastvisorigin, vel); - if (entinfo.update_time > 0.001f) { - VectorScale(vel, 1.0f / entinfo.update_time, vel); + if (BotAimHarness_UsingPlasmagun(bs)) { + target[2] = BotAimHarness_GetEnemyCenterMassZ(&entinfo); } else { - VectorClear(vel); + target[2] += 8.0f; + if (!BotAimHarness_UsingRocketLauncher(bs) && BotAimHarness_AimTargetValid(bs)) { + target[2] = bs->aimtarget[2]; + } } - BotAimHarness_ClampVerticalLeadVel(vel); - trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); - vertScale = BotAimHarness_VertLeadScale(bs); + if (BotAimHarness_UsingRocketLauncher(bs)) { + vec3_t feetPoint; - if (wi.speed > 0.0f) { - VectorSubtract(target, bs->eye, dir); - dist = VectorLength(dir); - flightTime = dist / wi.speed; - if (flightTime > 2.0f) { - flightTime = 2.0f; + if (BotAimHarness_TryRocketFeetPoint(bs, &entinfo, target, feetPoint)) { + VectorCopy(feetPoint, target); } - BotAimHarness_ApplyLead(target, vel, flightTime, vertScale * 0.65f); - } else { - leadTime = BotAimHarness_HitscanLeadTime(bs); - BotAimHarness_ApplyLead(target, vel, leadTime, vertScale); } + BotAimHarness_ApplyMovementLead(bs, target, bs->aimh_aim_skill); + VectorCopy(target, bs->aimh_combat_target); return qtrue; } void BotAimHarness_ApplyThinkHitscanOrigin(bot_state_t *bs, vec3_t bestorigin, void *entinfoPtr, float aimSkill) { - aas_entityinfo_t *entinfo; - vec3_t dir; - float dist, speed, thinkSec, leadTime; + (void)entinfoPtr; - if (!BotAimHarness_IsActive()) { - return; - } - if (!entinfoPtr || aimSkill < 0.25f) { + if (!BotAimHarness_IsActive() || aimSkill < 0.25f) { return; } - entinfo = (aas_entityinfo_t *)entinfoPtr; - - VectorSubtract(entinfo->origin, bs->origin, dir); - dist = VectorLength(dir); - VectorSubtract(entinfo->origin, entinfo->lastvisorigin, dir); - dir[2] = 0.0f; - speed = VectorNormalize(dir); - if (entinfo->update_time > 0.001f) { - speed /= entinfo->update_time; - } else { - speed = 0.0f; - } - - trap_Cvar_Update(&bot_thinktime); - thinkSec = bot_thinktime.integer / 1000.0f; - leadTime = thinkSec * (0.45f + 0.85f * aimSkill); - if (leadTime > AIMH_HITSCAN_LEAD_MAX) { - leadTime = AIMH_HITSCAN_LEAD_MAX; - } - VectorMA(bestorigin, leadTime * speed, dir, bestorigin); - - (void)dist; + BotAimHarness_ApplyMovementLead(bs, bestorigin, aimSkill); } /* @@ -407,10 +1155,43 @@ 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; + vec3_t dir, aimPoint; + aas_entityinfo_t entinfo; + + if (BotAimHarness_UsingRocketLauncher(bs) && bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + BotEntityInfo(bs->enemy, &entinfo); + if (entinfo.valid) { + if (BotAimHarness_AimTargetValid(bs)) { + VectorCopy(bs->aimtarget, aimPoint); + } else if (VectorLengthSquared(bs->aimh_combat_target) > 1.0f) { + VectorCopy(bs->aimh_combat_target, aimPoint); + } else { + VectorCopy(entinfo.origin, aimPoint); + aimPoint[2] += 8.0f; + } + if (BotAimHarness_TryRocketFeetPoint(bs, &entinfo, aimPoint, 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) && 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_AimTargetValid(bs)) { - VectorSubtract(bs->aimtarget, bs->eye, dir); + 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) { VectorSubtract(bs->aimh_combat_target, bs->eye, dir); } else { @@ -423,20 +1204,42 @@ static void BotAimHarness_GetCombatAimAngles(bot_state_t *bs, vec3_t angles) { } static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { + if (bs->aimh_combat_aim && BotAimHarness_IsActive()) { + if (BotAimHarness_IsRailInterceptActive(bs)) { + VectorCopy(bs->viewangles, angles); + angles[PITCH] = BotAimHarness_ClampPitch(angles[PITCH]); + angles[YAW] = AngleMod(angles[YAW]); + angles[ROLL] = 0.0f; + return; + } + if (BotAimHarness_InThinkSettleWindow(bs)) { + BotAimHarness_GetCombatTrueAimAngles(bs, angles); + } else { + BotAimHarness_GetMotorPursuitAngles(bs, angles); + } + return; + } BotAimHarness_GetCombatAimAngles(bs, angles); } /* - * Body aim point for fire permission (no think-time lead; motor still uses lead). + * Aim point for fire permission (no think-time lead; motor still uses lead). + * Rockets use aimtarget (feet / splash point) so LOS and FOV match actual aim. */ static void BotAimHarness_GetEnemyFirePoint(bot_state_t *bs, vec3_t point) { aas_entityinfo_t entinfo; + vec3_t feetPoint; if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { VectorCopy(bs->eye, point); return; } + if (BotAimHarness_UsingRocketLauncher(bs) && BotAimHarness_AimTargetValid(bs)) { + VectorCopy(bs->aimtarget, point); + return; + } + BotEntityInfo(bs->enemy, &entinfo); if (!entinfo.valid) { if (BotAimHarness_AimTargetValid(bs)) { @@ -447,78 +1250,210 @@ static void BotAimHarness_GetEnemyFirePoint(bot_state_t *bs, vec3_t point) { return; } - VectorCopy(entinfo.origin, point); - point[2] += 24.0f; -} - -static float BotAimHarness_FireFov(bot_state_t *bs, vec3_t firePoint) { - vec3_t dir; - float dist; + if (BotAimHarness_UsingRocketLauncher(bs) && + BotAimHarness_TryRocketFeetPoint(bs, &entinfo, entinfo.origin, feetPoint)) { + VectorCopy(feetPoint, point); + return; + } - VectorSubtract(firePoint, bs->eye, dir); - dist = VectorLength(dir); - if (dist < AIMH_FIRE_FOV_DIST) { - return AIMH_FIRE_FOV_NEAR; + if (BotAimHarness_UsingPlasmagun(bs)) { + VectorCopy(entinfo.origin, point); + point[2] = BotAimHarness_GetEnemyCenterMassZ(&entinfo); + return; } - return AIMH_FIRE_FOV_FAR; + + VectorCopy(entinfo.origin, point); + point[2] += 24.0f; } -static qboolean BotAimHarness_FireLosToEnemy(bot_state_t *bs, vec3_t firePoint) { +/* + * True when a solid surface clearly blocks the fight (not merely off-aim). + */ +static qboolean BotAimHarness_ShotObviouslyBlocked(bot_state_t *bs) { + aas_entityinfo_t entinfo; bsp_trace_t trace; - int enemy; + vec3_t end; + float vis; - enemy = bs->enemy; - BotAI_Trace(&trace, bs->eye, NULL, NULL, firePoint, bs->client, MASK_SHOT); - if (trace.ent == enemy) { + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return qtrue; } - if (trace.fraction >= 0.97f) { + + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy); + if (vis > AIMH_FIRE_ANY_VISIBILITY) { + return qfalse; + } + + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { return qtrue; } - if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, enemy) > - AIMH_FIRE_MIN_VISIBILITY) { + + VectorCopy(entinfo.origin, end); + end[2] += 24.0f; + BotAI_Trace(&trace, bs->eye, NULL, NULL, end, bs->client, MASK_SHOT); + if (trace.fraction < AIMH_FIRE_BLOCKED_TRACE_FRAC && trace.ent != bs->enemy) { return qtrue; } + return qfalse; } /* - * Kinda-on-target: view roughly toward enemy body or intent; not a guaranteed hit trace. + * View is moving toward the fight — wide cone, no hit trace required. */ -static qboolean BotAimHarness_PassesLooseAimGate(bot_state_t *bs, const weaponinfo_t *wi, - vec3_t firePoint) { - vec3_t dir, toEnemy, attackAngles; - float fov, dist; +static qboolean BotAimHarness_IsTrackingTarget(bot_state_t *bs, vec3_t firePoint) { + aas_entityinfo_t entinfo; + vec3_t dir, toEnemy, toPoint, attackAngles; + float vis, horizDist, trackFov, trackSlack, urgency; - VectorSubtract(firePoint, bs->eye, dir); - dist = VectorLength(dir); - if (dist < 1.0f) { - return qtrue; + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return qfalse; } - vectoangles(dir, toEnemy); - fov = BotAimHarness_FireFov(bs, firePoint); + horizDist = BotAimHarness_EnemyHorizDist(bs, &entinfo); + if (BotAimHarness_UsingRocketLauncher(bs) && horizDist > AIMH_FIRE_RL_MAX_DIST) { + return qfalse; + } + + vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy); + if (vis <= AIMH_FIRE_ANY_VISIBILITY) { + return qfalse; + } - if (InFieldOfVision(bs->viewangles, fov, toEnemy)) { + trackFov = AIMH_FIRE_TRACK_FOV; + trackSlack = AIMH_FIRE_TRACK_SLACK; + if (BotAimHarness_IsSlowFireWeapon(bs)) { + urgency = BotAimHarness_GetShotUrgency(bs); + trackFov += urgency * AIMH_URGENCY_TRACK_FOV; + trackSlack += urgency * AIMH_URGENCY_TRACK_SLACK; + } + + VectorSubtract(entinfo.origin, bs->eye, dir); + dir[2] += 16.0f; + if (VectorLengthSquared(dir) < 1.0f) { + return qtrue; + } + vectoangles(dir, toEnemy); + if (InFieldOfVision(bs->viewangles, trackFov, toEnemy)) { return qtrue; } + VectorSubtract(firePoint, bs->eye, dir); + if (VectorLengthSquared(dir) > 1.0f) { + vectoangles(dir, toPoint); + if (InFieldOfVision(bs->viewangles, trackFov + trackSlack, toPoint)) { + return qtrue; + } + } + BotAimHarness_GetAttackAimAngles(bs, attackAngles); - if (InFieldOfVision(bs->viewangles, fov + AIMH_FIRE_VIEW_SLACK, attackAngles)) { + if (InFieldOfVision(bs->viewangles, trackFov + trackSlack, attackAngles)) { + return qtrue; + } + if (InFieldOfVision(attackAngles, trackSlack + 8.0f, toEnemy)) { return qtrue; } - if (InFieldOfVision(attackAngles, AIMH_FIRE_VIEW_SLACK + 6.0f, toEnemy)) { + + return qfalse; +} + +/* + * 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; + 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; + } + 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, 1000, forward, end); + VectorMA(start, -12, forward, start); + BotAI_Trace(&trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT); + if (trace.ent == bs->enemy) { return qtrue; } + if (trace.ent >= 0 && trace.ent < MAX_CLIENTS && BotSameTeam(bs, trace.ent)) { + return qfalse; + } - /* Projectiles: allow a bit more angle slack; still need plausible LOS */ - if (wi->speed > 0.0f) { - if (InFieldOfVision(bs->viewangles, fov + 10.0f, attackAngles)) { + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return qfalse; + } + VectorSubtract(entinfo.origin, bs->eye, toEnemy); + toEnemy[2] += AIMH_RAIL_CENTER_Z; + if (VectorLengthSquared(toEnemy) < 1.0f) { + 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; + } + return qfalse; + } + /* High menu skill: trace hit only (no angle slack). */ + if (BotAimHarness_GetMenuSkillTier(bs) >= AIMH_RAIL_FIRE_TRACE_TIER) { + return qfalse; } + return pitchErr <= tol && yawErr <= tol; +} - return qfalse; +static qboolean BotAimHarness_WantsRailFire(bot_state_t *bs) { + vec3_t firePoint; + + if (BotAimHarness_CrosshairHitsEnemy(bs)) { + return qtrue; + } + if (BotAimHarness_GetShotUrgency(bs) < 0.35f) { + return qfalse; + } + if (BotAimHarness_ShotObviouslyBlocked(bs)) { + return qfalse; + } + BotAimHarness_GetEnemyFirePoint(bs, firePoint); + return BotAimHarness_IsTrackingTarget(bs, firePoint); } static qboolean BotAimHarness_WeaponReady(bot_state_t *bs) { @@ -534,7 +1469,7 @@ static qboolean BotAimHarness_WeaponReady(bot_state_t *bs) { /* Reaction, throttle, weapon swap — evaluated on bot think only */ static qboolean BotAimHarness_PassesThinkFireGates(bot_state_t *bs) { - float reactiontime, firethrottle; + float reactiontime; if (bs->enemy < 0) { return qfalse; @@ -561,22 +1496,46 @@ static qboolean BotAimHarness_PassesThinkFireGates(bot_state_t *bs) { if (bs->firethrottlewait_time > FloatTime()) { return qfalse; } - 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.0f - firethrottle; - bs->firethrottlewait_time = 0; - } + + return qtrue; +} + +static qboolean BotAimHarness_TryRailFire(bot_state_t *bs) { + if (!BotAimHarness_IsRailInterceptActive(bs)) { + return qfalse; + } + if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { + return qfalse; + } + if (!BotAimHarness_WeaponReady(bs)) { + return qfalse; + } + if (bs->cur_ps.weaponstate != WEAPON_READY) { + return qfalse; + } + if (bs->cur_ps.weaponTime > 0) { + return qfalse; + } + if (!BotAimHarness_PassesThinkFireGates(bs)) { + return qfalse; + } + if (BotAimHarness_ShotObviouslyBlocked(bs)) { + return qfalse; + } + if (!BotAimHarness_WantsRailFire(bs)) { + return qfalse; } + trap_EA_Attack(bs->client); return qtrue; } -static qboolean BotAimHarness_IsContinuousFireWeapon(const weaponinfo_t *wi) { +static qboolean BotAimHarness_IsContinuousFireWeapon(bot_state_t *bs, + const weaponinfo_t *wi) { + if (BotAimHarness_UsingRocketLauncher(bs) && wi->speed > 0.0f && + (wi->proj.damagetype & DAMAGETYPE_RADIAL)) { + return qtrue; + } if (wi->flags & WFL_FIRERELEASED) { return qfalse; } @@ -587,6 +1546,8 @@ static qboolean BotAimHarness_WantsSuppressiveFire(bot_state_t *bs, const weaponinfo_t *wi, vec3_t firePoint) { vec3_t dir; + (void)wi; + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return qfalse; } @@ -601,10 +1562,10 @@ static qboolean BotAimHarness_WantsSuppressiveFire(bot_state_t *bs, } } - if (!BotAimHarness_FireLosToEnemy(bs, firePoint)) { + if (BotAimHarness_ShotObviouslyBlocked(bs)) { return qfalse; } - if (!BotAimHarness_PassesLooseAimGate(bs, wi, firePoint)) { + if (!BotAimHarness_IsTrackingTarget(bs, firePoint)) { return qfalse; } @@ -636,11 +1597,15 @@ void BotAimHarness_CheckAttack(bot_state_t *bs) { trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); BotAimHarness_GetEnemyFirePoint(bs, firePoint); + if (BotAimHarness_IsRailInterceptActive(bs)) { + return; + } + if (!BotAimHarness_WantsSuppressiveFire(bs, &wi, firePoint)) { return; } - if (BotAimHarness_IsContinuousFireWeapon(&wi)) { + if (BotAimHarness_IsContinuousFireWeapon(bs, &wi)) { bs->aimh_hold_fire = qtrue; trap_EA_Attack(bs->client); return; @@ -661,7 +1626,13 @@ void BotAimHarness_ApplyCombatFire(bot_state_t *bs) { weaponinfo_t wi; vec3_t firePoint; - if (!BotAimHarness_IsActive() || !bs->aimh_hold_fire) { + if (!BotAimHarness_IsActive()) { + return; + } + if (BotAimHarness_TryRailFire(bs)) { + return; + } + if (!bs->aimh_hold_fire) { return; } if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { @@ -677,12 +1648,11 @@ void BotAimHarness_ApplyCombatFire(bot_state_t *bs) { } trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); - if (!BotAimHarness_IsContinuousFireWeapon(&wi)) { + if (!BotAimHarness_IsContinuousFireWeapon(bs, &wi)) { return; } - BotAimHarness_GetEnemyFirePoint(bs, firePoint); - if (!BotAimHarness_WantsSuppressiveFire(bs, &wi, firePoint)) { + if (BotAimHarness_ShotObviouslyBlocked(bs)) { bs->aimh_hold_fire = qfalse; return; } @@ -802,10 +1772,17 @@ static void BotAimHarness_OnEnemyChange(bot_state_t *bs) { bs->aimh_vel[PITCH] *= 0.4f; bs->aimh_vel[YAW] *= 0.4f; bs->aimh_last_goal_time = 0.0f; + bs->aimh_pursuit_pitch_off = 0.0f; + bs->aimh_pursuit_yaw_off = 0.0f; + bs->aimh_pursuit_set_time = 0.0f; + bs->aimh_true_goal_pitch = bs->viewangles[PITCH]; + bs->aimh_true_goal_yaw = bs->viewangles[YAW]; bs->aimh_smooth_goal_pitch = bs->viewangles[PITCH]; bs->aimh_smooth_goal_yaw = bs->viewangles[YAW]; bs->aimh_tracked_ideal_pitch = bs->viewangles[PITCH]; bs->aimh_tracked_ideal_yaw = bs->viewangles[YAW]; + BotAimHarness_ClearRailLead(bs); + BotAimHarness_ResetShotUrgency(bs); } static void BotAimHarness_ClearEntityDebug(gentity_t *ent) { @@ -832,13 +1809,17 @@ static void BotAimHarness_AnglesToAimPoint(bot_state_t *bs, float pitch, float y } /* - * Debug aim point: combat = fire/motor intent (aimtarget); roam = navigation + * Debug aim point: combat = aimtarget (updated each bot think); roam = navigation * ideal (never stale aimtarget left over from the last fight). */ static int BotAimHarness_GetDebugAimPoint(bot_state_t *bs, vec3_t point) { vec3_t wishAngles; 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_AimTargetValid(bs)) { VectorCopy(bs->aimtarget, point); return qtrue; @@ -1014,6 +1995,7 @@ void BotAimHarness_SyncMotorToView(bot_state_t *bs) { return; } bs->aimh_combat_aim = qfalse; + BotAimHarness_ClearRailLead(bs); bs->aimh_vel[PITCH] = 0.0f; bs->aimh_vel[YAW] = 0.0f; bs->aimh_tracked_ideal_pitch = BotAimHarness_ClampPitch(bs->viewangles[PITCH]); @@ -1056,17 +2038,17 @@ void BotAimHarness_Reset(bot_state_t *bs) { bs->aimh_last_goal_time = 0.0f; bs->aimh_smooth_goal_pitch = bs->viewangles[PITCH]; bs->aimh_smooth_goal_yaw = bs->viewangles[YAW]; + bs->aimh_pursuit_pitch_off = 0.0f; + bs->aimh_pursuit_yaw_off = 0.0f; + bs->aimh_pursuit_set_time = 0.0f; + bs->aimh_true_goal_pitch = bs->viewangles[PITCH]; + bs->aimh_true_goal_yaw = bs->viewangles[YAW]; bs->aimh_tracked_ideal_pitch = bs->viewangles[PITCH]; bs->aimh_tracked_ideal_yaw = bs->viewangles[YAW]; VectorClear(bs->aimh_combat_target); bs->aimh_hold_fire = qfalse; - bs->aimh_weapon_jump_until = 0.0f; - VectorClear(bs->aimh_weapon_jump_angles); - VectorClear(bs->aimh_weapon_jump_spot); - VectorClear(bs->aimh_weapon_jump_dest); - VectorClear(bs->aimh_weapon_jump_air_dir); - bs->aimh_weapon_jump_weapon = 0; - bs->aimh_weapon_jump_fired = qfalse; + BotAimHarness_ClearRailLead(bs); + BotAimHarness_ResetShotUrgency(bs); } void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, @@ -1080,6 +2062,19 @@ void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, 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; } @@ -1096,7 +2091,8 @@ void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, bs->aimh_aim_accuracy = aimAccuracy; inaccuracy = 1.0f - aimAccuracy; - bs->aimh_motor_inaccuracy = inaccuracy * 0.7f; + bs->aimh_motor_inaccuracy = inaccuracy * + BotAimHarness_Lerp(0.62f, 0.06f, BotAimHarness_GetEliteMotorTier(bs)); bs->aimh_combat_aim = qtrue; VectorCopy(bs->aimh_goal, bs->ideal_viewangles); @@ -1105,21 +2101,15 @@ void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, } else if (bs->enemy >= 0) { BotAimHarness_GetCombatTarget(bs, bs->aimh_combat_target); } - if (VectorLengthSquared(bs->aimh_combat_target) > 1.0f) { - vec3_t ang; - - BotAimHarness_GetCombatAimAngles(bs, ang); - bs->aimh_smooth_goal_pitch = ang[PITCH]; - bs->aimh_smooth_goal_yaw = ang[YAW]; - } + BotAimHarness_RefreshThinkPursuitGoal(bs); } /* - * Roam: goal is movement ideal; spring + light noise humanize. Combat: live world target - * each motor frame; spring/catch-up humanize (no extra goal slew). + * Roam: goal is movement ideal; spring + light noise humanize. + * Combat: live eye->aimtarget each input frame (+ think pursuit offset); settle uses true aim. */ static void BotAimHarness_GetMotorGoal(bot_state_t *bs, vec3_t goal, float dt) { - vec3_t targetAngles, dir; + vec3_t targetAngles; float pitchJump, yawJump; if (dt <= 0.0f) { @@ -1127,9 +2117,12 @@ static void BotAimHarness_GetMotorGoal(bot_state_t *bs, vec3_t goal, float dt) { } if (bs->aimh_combat_aim) { - BotAimHarness_GetCombatAimAngles(bs, targetAngles); - bs->aimh_smooth_goal_pitch = targetAngles[PITCH]; - bs->aimh_smooth_goal_yaw = targetAngles[YAW]; + if (BotAimHarness_InThinkSettleWindow(bs)) { + BotAimHarness_GetCombatTrueAimAngles(bs, goal); + } else { + BotAimHarness_GetLiveCombatMotorGoal(bs, goal); + } + return; } else { VectorCopy(bs->ideal_viewangles, targetAngles); targetAngles[PITCH] = BotAimHarness_ClampPitch(targetAngles[PITCH]); @@ -1151,11 +2144,7 @@ static void BotAimHarness_GetMotorGoal(bot_state_t *bs, vec3_t goal, float dt) { goal[YAW] = targetAngles[YAW]; bs->aimh_smooth_goal_pitch = goal[PITCH]; bs->aimh_smooth_goal_yaw = goal[YAW]; - return; } - - goal[PITCH] = bs->aimh_smooth_goal_pitch; - goal[YAW] = bs->aimh_smooth_goal_yaw; } static void BotAimHarness_ResyncViewFromPSIfDesynced(bot_state_t *bs) { @@ -1190,7 +2179,7 @@ void BotAimHarness_BeginMotorFrame(bot_state_t *bs) { return; } - if (BotAI_WeaponJumpActive(bs)) { + if (BotMove_SuppressesAimMotor(bs)) { return; } @@ -1205,8 +2194,9 @@ static void BotAimHarness_SaveGoalHistory(bot_state_t *bs, const vec3_t goal) { static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, float dt, float stiffness, float damping, float maxVel, float motorNoise, - float minVel, float catchupGain) { - float err, accel, delta; + float minVel, float catchupGain, qboolean settleToTrue) { + float err, accel, delta, errAbs, dampScale, stiffScale, phaseT, snapBlend; + qboolean inSnapBand; if (axis == PITCH) { goalAngle = BotAimHarness_ClampPitch(goalAngle); @@ -1215,7 +2205,22 @@ static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, err = BotAimHarness_YawDiff(bs->viewangles[YAW], goalAngle); } - accel = stiffness * err - damping * bs->aimh_vel[axis]; + errAbs = fabs(err); + dampScale = 1.0f; + stiffScale = 1.0f; + if (settleToTrue) { + stiffScale = AIMH_SETTLE_STIFF_MULT; + } + if (errAbs < AIMH_SETTLE_ERR_PHASE) { + phaseT = errAbs / AIMH_SETTLE_ERR_PHASE; + dampScale = BotAimHarness_Lerp(AIMH_SETTLE_DAMP_NEAR, 1.0f, phaseT); + dampScale += bs->aimh_aim_accuracy * AIMH_OVERSHOOT_DAMP_ACC; + if (dampScale > 0.92f) { + dampScale = 0.92f; + } + } + + accel = (stiffness * stiffScale) * err - (damping * dampScale) * bs->aimh_vel[axis]; bs->aimh_vel[axis] += accel * dt; if (minVel > 0.0f && fabs(err) > AIMH_ROAM_MIN_ERR) { @@ -1225,7 +2230,7 @@ static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, bs->aimh_vel[axis] = -minVel; } } - if (catchupGain > 0.0f && fabs(err) > 2.5f && fabs(err) < 26.0f) { + if (catchupGain > 0.0f && errAbs > AIMH_CATCHUP_ERR_MIN && errAbs < AIMH_CATCHUP_ERR_MAX) { bs->aimh_vel[axis] += err * catchupGain * dt; } @@ -1236,15 +2241,21 @@ static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, } delta = bs->aimh_vel[axis] * dt; + inSnapBand = settleToTrue && errAbs < AIMH_SETTLE_ERR_SNAP; + if (inSnapBand) { + snapBlend = BotAimHarness_Clamp01((AIMH_SETTLE_ERR_SNAP - errAbs) / AIMH_SETTLE_ERR_SNAP); + delta += err * AIMH_SETTLE_SNAP_GAIN * snapBlend * dt; + } + if (axis == PITCH) { bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH] + delta); - if (motorNoise > 0.01f && fabs(err) > 2.5f && fabs(err) < 9.0f) { + if (!inSnapBand && motorNoise > 0.01f && errAbs > 2.5f && errAbs < 9.0f) { bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH] + crandom() * AIMH_MOTOR_NOISE_SCALE * motorNoise * dt * 60.0f * 0.35f); } } else { bs->viewangles[axis] = AngleMod(bs->viewangles[axis] + delta); - if (motorNoise > 0.01f && fabs(err) > 2.5f && fabs(err) < 9.0f) { + if (!inSnapBand && motorNoise > 0.01f && errAbs > 2.5f && errAbs < 9.0f) { bs->viewangles[axis] = AngleMod(bs->viewangles[axis] + crandom() * AIMH_MOTOR_NOISE_SCALE * motorNoise * dt * 60.0f * 0.4f); } @@ -1256,6 +2267,9 @@ static void BotAimHarness_IntegrateMotorSubsteps(bot_state_t *bs, vec3_t goal, float motorNoise, float minVel, float catchupGain, int refreshGoalEachStep) { float remaining, subDt; int steps; + qboolean settleToTrue; + + settleToTrue = bs->aimh_combat_aim && BotAimHarness_InThinkSettleWindow(bs); remaining = frameTime; steps = 0; @@ -1277,9 +2291,9 @@ static void BotAimHarness_IntegrateMotorSubsteps(bot_state_t *bs, vec3_t goal, } BotAimHarness_UpdateAxis(bs, PITCH, goal[PITCH], subDt, - stiffness, damping, maxVelPitch, motorNoise, minVel, catchupGain); + stiffness, damping, maxVelPitch, motorNoise, minVel, catchupGain, settleToTrue); BotAimHarness_UpdateAxis(bs, YAW, goal[YAW], subDt, - stiffness, damping, maxVel, motorNoise, minVel, catchupGain); + stiffness, damping, maxVel, motorNoise, minVel, catchupGain, settleToTrue); } } @@ -1309,26 +2323,19 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->aimh_last_sanity_enemy = -1; bs->aimh_acquire_until = 0.0f; bs->aimh_last_goal_time = 0.0f; - bs->aimh_weapon_jump_until = 0.0f; - bs->aimh_weapon_jump_fired = qfalse; trap_EA_View(bs->client, bs->viewangles); BotAimHarness_DebugSync(bs); return 1; } - if (BotAI_WeaponJumpActive(bs)) { - VectorCopy(bs->aimh_weapon_jump_angles, bs->ideal_viewangles); - VectorCopy(bs->aimh_weapon_jump_angles, bs->viewangles); - bs->viewangles[ROLL] = 0.0f; - trap_EA_View(bs->client, bs->viewangles); - BotAimHarness_SyncMotorToView(bs); - BotAimHarness_DebugSync(bs); - return 1; + if (BotMove_SuppressesAimMotor(bs)) { + return 0; } if (bs->enemy < 0) { bs->aimh_combat_aim = qfalse; bs->aimh_hold_fire = qfalse; + BotAimHarness_ClearRailLead(bs); bs->aimh_motor_inaccuracy = 0.0f; bs->aimh_last_enemy_z = 0.0f; bs->aimh_last_sanity_enemy = -1; @@ -1413,8 +2420,13 @@ 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); } if (bs->aimh_combat_aim) { @@ -1424,9 +2436,15 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { (void)catchupMult; (void)ffDummy; } + 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); + } if (bs->aimh_combat_aim && maxVel < 140.0f) { - maxVel = 140.0f + 80.0f * aimSkill; + maxVel = 140.0f + 80.0f * aimSkill + + 40.0f * BotAimHarness_GetEliteMotorTier(bs); } else if (maxVel < 90.0f) { maxVel = 90.0f; } @@ -1440,8 +2458,10 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } if (bs->aimh_combat_aim) { - minVel = AIMH_COMBAT_MIN_VEL + 45.0f * aimSkill; - catchupGain = AIMH_COMBAT_CATCHUP_GAIN * catchupMult; + 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); } 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 bf376cc..c20c634 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -12,8 +12,16 @@ Debug: bot_debugAim 1 (server, CVAR_CHEAT) publishes motor wish (ideal_viewangle when roaming, aimtarget when fighting) via ps.grapplePoint + EXTFL_BOT_AIM_DEBUG; cg_debugBotAim draws green = wish, yellow (bit 4) = crosshair. -Combat fire: loose FOV + eye LOS to enemy body (not lead-only); MG/LG hold +attack -each input frame while on target. Railgun-style (WFL_FIRERELEASED) uses think cadence. +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. + +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. 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, @@ -42,6 +50,16 @@ void BotAimHarness_SetCombatGoal(struct bot_state_s *bs, const float idealAngles float aimSkill, float aimAccuracy, float weaponVSpread, float weaponHSpread); void BotAimHarness_ApplyThinkHitscanOrigin(struct bot_state_s *bs, float bestorigin[3], void *entinfo, float aimSkill); +/* 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); +/* 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). */ +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); int BotAimHarness_AimTargetValid(struct bot_state_s *bs); /* diff --git a/ratoa_gamecode/code/game/ai_bot_combat.c b/ratoa_gamecode/code/game/ai_bot_combat.c index 88ff037..6311e33 100644 --- a/ratoa_gamecode/code/game/ai_bot_combat.c +++ b/ratoa_gamecode/code/game/ai_bot_combat.c @@ -29,14 +29,31 @@ static int BotCombat_HorizontalDistToEnemy(bot_state_t *bs) { dir[2] = 0; return (int)VectorLength(dir); } +static qboolean BotCombat_IsVoluntaryCloseCombatWeapon(int wp) { + return (wp == WP_GAUNTLET || wp == WP_SHOTGUN || wp == WP_PLASMAGUN); +} static qboolean BotCombat_GauntletChosen(bot_state_t *bs) { return (bs->weaponnum == WP_GAUNTLET || bs->cur_ps.weapon == WP_GAUNTLET); } +static qboolean BotCombat_VoluntaryCloseCombatChosen(bot_state_t *bs) { + int wp; + + wp = bs->weaponnum; + if (!BotCombat_IsVoluntaryCloseCombatWeapon(wp)) { + wp = bs->cur_ps.weapon; + } + return BotCombat_IsVoluntaryCloseCombatWeapon(wp); +} static qboolean BotCombat_HasGauntlet(bot_state_t *bs) { return (bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GAUNTLET)) != 0; } -static qboolean BotCombat_CloseVoluntaryGauntlet(bot_state_t *bs) { - if (!BotCombat_GauntletChosen(bs)) { +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; +} +static qboolean BotCombat_CloseVoluntaryCloseCombat(bot_state_t *bs) { + if (!BotCombat_VoluntaryCloseCombatChosen(bs)) { return qfalse; } if (BotTactics_IsGauntletOnly(bs)) { @@ -109,7 +126,7 @@ static qboolean BotCombat_UpdateVoluntaryGauntletAbort(bot_state_t *bs) { if (!BotCombat_VoluntaryGauntletPursuitActive(bs)) { return qfalse; } - if (!BotCombat_GauntletChosen(bs) || BotTactics_IsGauntletOnly(bs)) { + if (!BotCombat_VoluntaryCloseCombatChosen(bs) || BotTactics_IsGauntletOnly(bs)) { BotCombat_ClearVoluntaryPursuit(bs); return qfalse; } @@ -134,18 +151,18 @@ static qboolean BotCombat_UpdateVoluntaryGauntletAbort(bot_state_t *bs) { BotWpnSelect_OnVoluntaryGauntletAborted(bs); return qtrue; } -static qboolean BotCombat_GauntletRushAllowed(bot_state_t *bs) { +static qboolean BotCombat_CloseCombatRushAllowed(bot_state_t *bs) { int horiz; - if (!BotCombat_GauntletChosen(bs)) { - return qfalse; - } + horiz = BotCombat_HorizontalDistToEnemy(bs); /* Out of ammo: last-resort gauntlet — rush out to 384; flee only beyond (tactics). */ if (BotTactics_IsGauntletOnly(bs)) { - return horiz <= BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST; + return BotCombat_GauntletChosen(bs) && + horiz <= BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST; } - /* Voluntary close gauntlet (skill 4–5) overrides stale survival-flee. */ - if (horiz <= BOT_COMBAT_GAUNTLET_RUSH_DIST && + /* Voluntary close combat (skill 4–5): SG/plasma/gauntlet charge. */ + if (BotCombat_VoluntaryCloseCombatChosen(bs) && + horiz <= BOT_COMBAT_GAUNTLET_RUSH_DIST && BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { return qfalse; @@ -157,20 +174,20 @@ static qboolean BotCombat_GauntletRushAllowed(bot_state_t *bs) { } return qfalse; } -static void BotCombat_ApplyGauntletRush(bot_state_t *bs) { +static void BotCombat_ApplyCloseCombatRush(bot_state_t *bs) { bs->combat.stance = BOT_STANCE_RUSH_OPPONENT; bs->combat.move_policy = BOT_MOVE_CLOSE_MELEE; - if (BotCombat_CloseVoluntaryGauntlet(bs) || + if (BotCombat_CloseVoluntaryCloseCombat(bs) || (BotTactics_IsGauntletOnly(bs) && BotCombat_InGauntletEngageRange(bs))) { bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; } } -static void BotCombat_UpdateGauntletRush(bot_state_t *bs) { - if (!BotCombat_GauntletRushAllowed(bs)) { +static void BotCombat_UpdateCloseCombatRush(bot_state_t *bs) { + if (!BotCombat_CloseCombatRushAllowed(bs)) { return; } - BotCombat_ApplyGauntletRush(bs); + BotCombat_ApplyCloseCombatRush(bs); } static void BotCombat_ResetStance(bot_state_t *bs) { bs->combat.stance = BOT_STANCE_NORMAL; @@ -209,7 +226,7 @@ void BotCombat_UpdateIntent(bot_state_t *bs) { if (BotCombat_UpdateVoluntaryGauntletAbort(bs)) { return; } - BotCombat_UpdateGauntletRush(bs); + BotCombat_UpdateCloseCombatRush(bs); } int BotCombat_IsRushOpponent(const bot_state_t *bs) { if (!bs) { @@ -224,7 +241,7 @@ int BotCombat_ShouldEngageFromRetreat(bot_state_t *bs) { if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return 0; } - if (!BotCombat_HasGauntlet(bs)) { + if (!BotCombat_HasCloseCombatWeapon(bs)) { return 0; } if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { @@ -242,22 +259,22 @@ void BotCombat_OnWeaponCommitted(bot_state_t *bs, int prev_wp, int new_wp) { if (!bs || !BotEnhanced_IsActive()) { return; } - if (new_wp != WP_GAUNTLET) { - if (prev_wp == WP_GAUNTLET) { + if (!BotCombat_IsVoluntaryCloseCombatWeapon(new_wp)) { + if (BotCombat_IsVoluntaryCloseCombatWeapon(prev_wp)) { BotCombat_ClearVoluntaryPursuit(bs); } return; } - if (prev_wp == WP_GAUNTLET) { + if (prev_wp == new_wp) { return; } if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return; } BotCombat_StartVoluntaryPursuit(bs); - if (!BotCombat_GauntletRushAllowed(bs)) { + if (!BotCombat_CloseCombatRushAllowed(bs)) { return; } - BotCombat_ApplyGauntletRush(bs); + BotCombat_ApplyCloseCombatRush(bs); } diff --git a/ratoa_gamecode/code/game/ai_bot_combat.h b/ratoa_gamecode/code/game/ai_bot_combat.h index 5b7150c..4470ea3 100644 --- a/ratoa_gamecode/code/game/ai_bot_combat.h +++ b/ratoa_gamecode/code/game/ai_bot_combat.h @@ -17,15 +17,14 @@ typedef enum { typedef enum { BOT_MOVE_POLICY_LEGACY = 0, - BOT_MOVE_CLOSE_MELEE /* close to contact (gauntlet first consumer) */ - /* BOT_MOVE_CLOSE_TO_WEAPON_IDEAL — reserved (LG, shotgun, plasma) */ + BOT_MOVE_CLOSE_MELEE /* charge opponent (gauntlet / voluntary SG / plasma) */ } bot_move_policy_t; typedef enum { BOT_FIRE_POLICY_LEGACY = 0 } bot_fire_policy_t; -/* Close gauntlet rush (voluntary switch while other weapons may have ammo). */ +/* Close combat rush (voluntary SG/plasma/gauntlet at skill 4–5). */ #define BOT_COMBAT_GAUNTLET_RUSH_DIST 192 /* Gauntlet-only last resort: rush/fight out to this range (tactics flee beyond). */ #define BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST 384 diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.c b/ratoa_gamecode/code/game/ai_bot_enhanced.c index a054aff..451e878 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.c +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -28,11 +28,22 @@ vmCvar_t bot_enhanced; extern vmCvar_t bot_enhanced_aim; extern vmCvar_t bot_enhanced_weapons; extern vmCvar_t bot_enhanced_tactics; +extern vmCvar_t bot_enhanced_movement; #define BOT_ENHANCED_LEGACY_AIM "bot_humanizeaim" #define BOT_ENHANCED_LEGACY_WEAPONS "bot_smartWeaponChoice" #define BOT_ENHANCED_LEGACY_TACTICS "bot_tacticalAI" +#ifdef Q3_VM +void Botlib_RawPushGoal(int goalstate, bot_goal_t *goal) { + trap_BotPushGoal(goalstate, goal); +} + +void Botlib_RawPopGoal(int goalstate) { + trap_BotPopGoal(goalstate); +} +#endif + static int BotEnhanced_LegacyCvarActive(const char *name) { return trap_Cvar_VariableValue(name) != 0.0f; } @@ -109,6 +120,7 @@ void BotEnhanced_RegisterCvars(void) { BotWpnSelect_RegisterCvars(); BotTactics_RegisterCvars(); BotItems_RegisterCvars(); + BotMoveHarness_RegisterCvars(); BotEnhanced_MigrateLegacyCvars(); } @@ -141,6 +153,14 @@ int BotEnhanced_TacticsActive(void) { return bot_enhanced_tactics.integer != 0; } +int BotEnhanced_MovementActive(void) { + if (!BotEnhanced_IsActive()) { + return 0; + } + trap_Cvar_Update(&bot_enhanced_movement); + return bot_enhanced_movement.integer != 0; +} + int BotEnhanced_ItemsActive(void) { return BotItems_IsActive(); } @@ -206,6 +226,17 @@ static void BotEnhanced_DropChattingEnemy(bot_state_t *bs) { bs->enemydeath_time = 0; } +static void BotEnhanced_DropDeadEnemy(bot_state_t *bs) { + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + if (!EntityClientIsDead(bs->enemy)) { + return; + } + bs->enemy = -1; + bs->enemydeath_time = 0; +} + static void BotEnhanced_CancelCampLongTermGoal(bot_state_t *bs) { if (!bs) { return; @@ -217,9 +248,229 @@ static void BotEnhanced_CancelCampLongTermGoal(bot_state_t *bs) { bs->teamgoal_time = 0; } +#define BOTENH_GOAL_DUP_ORIGIN_DIST 48.0f +#define BOTENH_GOAL_PUSH_MIN_INTERVAL 0.08f +#define BOTENH_GOAL_STACK_SOFT_MAX 6 + +static int BotEnhanced_GoalEquivalent(const bot_goal_t *a, const bot_goal_t *b) { + vec3_t delta; + + if (!a || !b) { + return 0; + } + if (a->number == b->number && a->number >= 0) { + return 1; + } + VectorSubtract(a->origin, b->origin, delta); + if (VectorLengthSquared(delta) <= Square(BOTENH_GOAL_DUP_ORIGIN_DIST)) { + return 1; + } + return 0; +} + +static int BotEnhanced_GoalInList(const bot_goal_t *goal, const bot_goal_t *list, int count) { + int i; + + for (i = 0; i < count; i++) { + if (BotEnhanced_GoalEquivalent(goal, &list[i])) { + return 1; + } + } + return 0; +} + +/* +================== +BotEnhanced_GoalStackDepth +================== +*/ +int BotEnhanced_GoalStackDepth(bot_state_t *bs) { + bot_goal_t stack[MAX_GOALSTACK]; + int depth; + + if (!bs) { + return 0; + } + + depth = 0; + while (depth < MAX_GOALSTACK && trap_BotGetTopGoal(bs->gs, &stack[depth])) { + Botlib_RawPopGoal(bs->gs); + depth++; + } + while (depth > 0) { + depth--; + Botlib_RawPushGoal(bs->gs, &stack[depth]); + } + return depth; +} + +/* +================== +BotEnhanced_GoalStackContains +================== +*/ +int BotEnhanced_GoalStackContains(bot_state_t *bs, int goalNumber) { + bot_goal_t stack[MAX_GOALSTACK]; + int depth, i; + int found; + + if (!bs) { + return 0; + } + + found = 0; + depth = 0; + while (depth < MAX_GOALSTACK && trap_BotGetTopGoal(bs->gs, &stack[depth])) { + if (stack[depth].number == goalNumber) { + found = 1; + } + Botlib_RawPopGoal(bs->gs); + depth++; + } + for (i = depth - 1; i >= 0; i--) { + Botlib_RawPushGoal(bs->gs, &stack[i]); + } + return found; +} + +int BotEnhanced_GoalStackHasEquivalent(bot_state_t *bs, bot_goal_t *goal) { + bot_goal_t stack[MAX_GOALSTACK]; + int depth, i, found; + + if (!bs || !goal) { + return 0; + } + + found = 0; + depth = 0; + while (depth < MAX_GOALSTACK && trap_BotGetTopGoal(bs->gs, &stack[depth])) { + if (!found && BotEnhanced_GoalEquivalent(goal, &stack[depth])) { + found = 1; + } + Botlib_RawPopGoal(bs->gs); + depth++; + } + for (i = depth - 1; i >= 0; i--) { + Botlib_RawPushGoal(bs->gs, &stack[i]); + } + return found; +} + +/* +================== +BotEnhanced_PushGoalSafe + +Returns 1 if goal is on stack (already or newly pushed), 0 if stack full / rate limited. +================== +*/ +int BotEnhanced_PushGoalSafe(bot_state_t *bs, bot_goal_t *goal) { + float now; + + if (!bs || !goal) { + return 0; + } + if (BotEnhanced_GoalStackHasEquivalent(bs, goal)) { + return 1; + } + now = FloatTime(); + if (now - bs->enh_goal_last_push_time < BOTENH_GOAL_PUSH_MIN_INTERVAL) { + return 0; + } + if (BotEnhanced_GoalStackDepth(bs) >= MAX_GOALSTACK) { + return 0; + } + Botlib_RawPushGoal(bs->gs, goal); + bs->enh_goal_last_push_time = now; + return 1; +} + +/* +================== +BotEnhanced_ReserveGoalStackRoom + +Pop top goals until at least slotsNeeded slots are free (botlib may push). +================== +*/ +void BotEnhanced_ReserveGoalStackRoom(bot_state_t *bs, int slotsNeeded) { + int depth, limit; + + if (!bs || slotsNeeded < 1) { + return; + } + if (slotsNeeded > MAX_GOALSTACK - 1) { + slotsNeeded = MAX_GOALSTACK - 1; + } + limit = MAX_GOALSTACK - slotsNeeded; + while ((depth = BotEnhanced_GoalStackDepth(bs)) > limit) { + Botlib_RawPopGoal(bs->gs); + } +} + +/* +================== +BotEnhanced_DedupeGoalStack + +Remove duplicate goals (same number or same place); keep newest entries on top. +================== +*/ +void BotEnhanced_DedupeGoalStack(bot_state_t *bs) { + bot_goal_t stack[MAX_GOALSTACK]; + bot_goal_t unique[MAX_GOALSTACK]; + int depth, i, n, start; + + if (!bs) { + return; + } + + depth = 0; + while (depth < MAX_GOALSTACK && trap_BotGetTopGoal(bs->gs, &stack[depth])) { + Botlib_RawPopGoal(bs->gs); + depth++; + } + + n = 0; + for (i = depth - 1; i >= 0; i--) { + if (!BotEnhanced_GoalInList(&stack[i], unique, n)) { + unique[n++] = stack[i]; + } + } + + if (n > BOTENH_GOAL_STACK_SOFT_MAX) { + start = n - BOTENH_GOAL_STACK_SOFT_MAX; + } else { + start = 0; + } + + for (i = start; i < n; i++) { + Botlib_RawPushGoal(bs->gs, &unique[i]); + } +} + +void BotEnhanced_OnGoalChooseDone(bot_state_t *bs) { + if (!bs) { + return; + } + BotEnhanced_SanitizeGoalStack(bs); + BotEnhanced_DedupeGoalStack(bs); +} + +/* +================== +BotEnhanced_SanitizeGoalStack +================== +*/ +void BotEnhanced_SanitizeGoalStack(bot_state_t *bs) { + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + BotEnhanced_DedupeGoalStack(bs); +} + void BotEnhanced_OnThinkStart(bot_state_t *bs) { BotEvents_Drain(bs); + if (bs && bs->inuse) { + BotEnhanced_SanitizeGoalStack(bs); + } if (BotEnhanced_IsActive()) { + BotEnhanced_DropDeadEnemy(bs); BotEnhanced_DropChattingEnemy(bs); BotEnhanced_CancelCampLongTermGoal(bs); BotCombat_UpdateIntent(bs); @@ -248,6 +499,9 @@ int BotEnhanced_AllowsVoluntaryCloseGauntlet(bot_state_t *bs) { } void BotEnhanced_ResetBot(bot_state_t *bs) { + if (bs) { + bs->enh_goal_last_push_time = 0.0f; + } BotMoveHarness_Reset(bs); BotEvents_Reset(bs); BotCombat_Reset(bs); diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.h b/ratoa_gamecode/code/game/ai_bot_enhanced.h index a398066..62cedec 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.h +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.h @@ -17,6 +17,7 @@ bot_smartWeaponChoice, bot_tacticalAI — see BotEnhanced_RegisterCvars migratio #define AI_BOT_ENHANCED_H struct bot_state_s; +struct bot_goal_s; void BotEnhanced_RegisterCvars(void); void BotEnhanced_ResetBot(struct bot_state_s *bs); @@ -25,6 +26,7 @@ int BotEnhanced_IsActive(void); int BotEnhanced_AimActive(void); int BotEnhanced_WeaponsActive(void); int BotEnhanced_TacticsActive(void); +int BotEnhanced_MovementActive(void); void BotEnhanced_OnThinkStart(struct bot_state_s *bs); @@ -45,4 +47,20 @@ int BotEnhanced_CanEngageClient(struct bot_state_s *bs, int clientnum); /* Enhanced bots do not use info_camp / BotWantsToCamp roaming. */ int BotEnhanced_AllowsCamping(void); +/* + * Goal stack guards (botlib MAX_GOALSTACK = 8). Use before botlib choose/push + * paths that can overflow the heap (items, nearby goals, air goals). + */ +/* Headroom for botlib ChooseNBG/LTG internal pushes (heap size 8). */ +#define BOTENHANCED_GOAL_STACK_RESERVE 5 + +int BotEnhanced_GoalStackDepth(struct bot_state_s *bs); +int BotEnhanced_GoalStackContains(struct bot_state_s *bs, int goalNumber); +int BotEnhanced_GoalStackHasEquivalent(struct bot_state_s *bs, struct bot_goal_s *goal); +int BotEnhanced_PushGoalSafe(struct bot_state_s *bs, struct bot_goal_s *goal); +void BotEnhanced_ReserveGoalStackRoom(struct bot_state_s *bs, int slotsNeeded); +void BotEnhanced_DedupeGoalStack(struct bot_state_s *bs); +void BotEnhanced_SanitizeGoalStack(struct bot_state_s *bs); +void BotEnhanced_OnGoalChooseDone(struct bot_state_s *bs); + #endif /* AI_BOT_ENHANCED_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_items.c b/ratoa_gamecode/code/game/ai_bot_items.c index 4c764b2..fb8d795 100644 --- a/ratoa_gamecode/code/game/ai_bot_items.c +++ b/ratoa_gamecode/code/game/ai_bot_items.c @@ -8,392 +8,297 @@ BOT ITEMS — visible high-value pickup with committed goal persistence. */ - - #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_items.h" - #include "ai_bot_enhanced.h" - +#include "ai_bot_move_harness.h" #include "ai_dmq3.h" - #include "ai_team.h" - - vmCvar_t bot_enhanced_items; - vmCvar_t bot_enhanced_items_debug; - - -#define BOT_ITEMS_VISIBLE_RANGE 1200.0f - #define BOT_ITEMS_COMMIT_DURATION 12.0f - #define BOT_ITEMS_SCAN_INTERVAL 0.35f - #define BOT_ITEMS_GONE_AVOID_TIME 20.0f - - - +#define BOT_ITEMS_STUCK_DIST 48.0f +#define BOT_ITEMS_STUCK_TIME 1.75f #define BOT_ITEM_NONE 0 - #define BOT_ITEM_QUAD 1 - -#define BOT_ITEM_ENEMY_FLAG 2 - -#define BOT_ITEM_MEGA_HEALTH 3 - -#define BOT_ITEM_RED_ARMOR 4 - -#define BOT_ITEM_YELLOW_ARMOR 5 - -#define BOT_ITEM_WEAPON_SHOTGUN 6 - -#define BOT_ITEM_WEAPON_GRENADE 7 - -#define BOT_ITEM_WEAPON_ROCKET 8 - -#define BOT_ITEM_WEAPON_PLASMA 9 - -#define BOT_ITEM_WEAPON_RAIL 10 - -#define BOT_ITEM_WEAPON_LIGHTNING 11 - -#define BOT_ITEM_WEAPON_BFG 12 - -#define BOT_ITEM_WEAPON_MACHINEGUN 13 - -#define BOT_ITEM_WEAPON_NAILGUN 14 - -#define BOT_ITEM_WEAPON_PROX 15 - -#define BOT_ITEM_WEAPON_CHAINGUN 16 - -#define BOT_ITEM_WEAPON_GRAPPLE 17 - - - +#define BOT_ITEM_RED_FLAG 2 +#define BOT_ITEM_BLUE_FLAG 3 +#define BOT_ITEM_MEGA_HEALTH 4 +#define BOT_ITEM_RED_ARMOR 5 +#define BOT_ITEM_YELLOW_ARMOR 6 +#define BOT_ITEM_WEAPON_SHOTGUN 7 +#define BOT_ITEM_WEAPON_GRENADE 8 +#define BOT_ITEM_WEAPON_ROCKET 9 +#define BOT_ITEM_WEAPON_PLASMA 10 +#define BOT_ITEM_WEAPON_RAIL 11 +#define BOT_ITEM_WEAPON_LIGHTNING 12 +#define BOT_ITEM_WEAPON_BFG 13 +#define BOT_ITEM_WEAPON_MACHINEGUN 14 +#define BOT_ITEM_WEAPON_NAILGUN 15 +#define BOT_ITEM_WEAPON_PROX 16 +#define BOT_ITEM_WEAPON_CHAINGUN 17 +#define BOT_ITEM_HEALTH_SMALL 18 +#define BOT_ITEM_HEALTH 19 +#define BOT_ITEM_HEALTH_LARGE 20 +#define BOT_ITEM_HEALTH_SEEK_MAX 80 #define BOT_ITEMS_DBG_GOT 1 - #define BOT_ITEMS_DBG_TIMEOUT 2 - #define BOT_ITEMS_DBG_GONE 3 - #define BOT_ITEMS_DBG_RESET 4 +#define BOT_ITEMS_DBG_STUCK 5 static const int botItemsScanKinds[] = { - BOT_ITEM_QUAD, - - BOT_ITEM_ENEMY_FLAG, - + BOT_ITEM_RED_FLAG, + BOT_ITEM_BLUE_FLAG, BOT_ITEM_MEGA_HEALTH, - BOT_ITEM_RED_ARMOR, - BOT_ITEM_YELLOW_ARMOR, - + BOT_ITEM_HEALTH_SMALL, + BOT_ITEM_HEALTH, + BOT_ITEM_HEALTH_LARGE, BOT_ITEM_WEAPON_SHOTGUN, - BOT_ITEM_WEAPON_GRENADE, - BOT_ITEM_WEAPON_ROCKET, - BOT_ITEM_WEAPON_PLASMA, - BOT_ITEM_WEAPON_RAIL, - BOT_ITEM_WEAPON_LIGHTNING, - BOT_ITEM_WEAPON_BFG, - BOT_ITEM_WEAPON_MACHINEGUN, - BOT_ITEM_WEAPON_NAILGUN, - BOT_ITEM_WEAPON_PROX, - - BOT_ITEM_WEAPON_CHAINGUN, - - BOT_ITEM_WEAPON_GRAPPLE - + BOT_ITEM_WEAPON_CHAINGUN }; - - #define BOT_ITEMS_SCAN_KIND_COUNT (sizeof(botItemsScanKinds) / sizeof(botItemsScanKinds[0])) #define BOT_ITEM_WEAPON_DEF_COUNT (sizeof(botItemWeaponDefs) / sizeof(botItemWeaponDefs[0])) - - typedef struct { - int inventoryIndex; - const char *goalname; - const char *label; - float priorityScale; - } 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_GRAPPLINGHOOK, "Grappling Hook", "Grappling Hook", 1.0f } - + { INVENTORY_CHAINGUN, "Chaingun", "Chaingun", 1.0f } }; - - static int BotItems_DebugEnabled(void) { - trap_Cvar_Update(&bot_enhanced_items_debug); - return bot_enhanced_items_debug.integer != 0; - } - - static const botItemWeaponDef_t *BotItems_WeaponDef(int kind) { - int index; - - - - if (kind < BOT_ITEM_WEAPON_SHOTGUN || kind > BOT_ITEM_WEAPON_GRAPPLE) { - + if (kind < BOT_ITEM_WEAPON_SHOTGUN || kind > BOT_ITEM_WEAPON_CHAINGUN) { return NULL; - } - index = kind - BOT_ITEM_WEAPON_SHOTGUN; - if (index < 0 || index >= (int)BOT_ITEM_WEAPON_DEF_COUNT) { - return NULL; - } - return &botItemWeaponDefs[index]; - } - - static void BotItems_KindLabel(int kind, char *buf, int bufsize) { - const botItemWeaponDef_t *wdef; - - - if (!buf || bufsize < 2) { - return; - } - switch (kind) { - case BOT_ITEM_QUAD: - Q_strncpyz(buf, "Quad Damage", bufsize); - break; - - case BOT_ITEM_ENEMY_FLAG: - - Q_strncpyz(buf, "Enemy Flag", bufsize); - + case BOT_ITEM_RED_FLAG: + Q_strncpyz(buf, "Red Flag", bufsize); + break; + case BOT_ITEM_BLUE_FLAG: + Q_strncpyz(buf, "Blue Flag", bufsize); break; - case BOT_ITEM_MEGA_HEALTH: - Q_strncpyz(buf, "Mega Health", bufsize); - break; - case BOT_ITEM_RED_ARMOR: - Q_strncpyz(buf, "Heavy Armor", bufsize); - break; - case BOT_ITEM_YELLOW_ARMOR: - Q_strncpyz(buf, "Armor", bufsize); - break; - + case BOT_ITEM_HEALTH_SMALL: + Q_strncpyz(buf, "5 Health", bufsize); + break; + case BOT_ITEM_HEALTH: + Q_strncpyz(buf, "25 Health", bufsize); + break; + case BOT_ITEM_HEALTH_LARGE: + Q_strncpyz(buf, "50 Health", bufsize); + break; default: - wdef = BotItems_WeaponDef(kind); - if (wdef) { - Q_strncpyz(buf, wdef->label, bufsize); - } else { - Q_strncpyz(buf, "item", bufsize); - } - break; + } +} + +static qboolean BotItems_FlagCaptureGametype(void) { + return gametype == GT_CTF || gametype == GT_CTF_ELIMINATION; +} + +static qboolean BotItems_IsFlagKind(int kind) { + return kind == BOT_ITEM_RED_FLAG || kind == BOT_ITEM_BLUE_FLAG; +} +/* session team first; cur_ps fallback for the think frame. */ +static int BotItems_ClientTeam(bot_state_t *bs) { + int team; + if (!bs || bs->client < 0 || bs->client >= MAX_CLIENTS) { + return TEAM_FREE; + } + team = level.clients[bs->client].sess.sessionTeam; + if (team == TEAM_RED || team == TEAM_BLUE) { + return team; } + team = bs->cur_ps.persistant[PERS_TEAM]; + if (team == TEAM_RED || team == TEAM_BLUE) { + return team; + } + return TEAM_FREE; +} +static qboolean BotItems_FlagIsEnemy(bot_state_t *bs, int kind) { + int team; + team = BotItems_ClientTeam(bs); + if (team == TEAM_RED) { + return kind == BOT_ITEM_BLUE_FLAG; + } + if (team == TEAM_BLUE) { + return kind == BOT_ITEM_RED_FLAG; + } + return qfalse; } +static qboolean BotItems_FlagAtBase(bot_state_t *bs, int kind) { + if (!bs) { + return qfalse; + } + if (kind == BOT_ITEM_RED_FLAG) { + return bs->redflagstatus == 0; + } + if (kind == BOT_ITEM_BLUE_FLAG) { + return bs->blueflagstatus == 0; + } + return qfalse; +} +static const char *BotItems_TeamLabel(int team) { + if (team == TEAM_RED) { + return "red"; + } + if (team == TEAM_BLUE) { + return "blue"; + } + return "free"; +} static void BotItems_DebugLine(bot_state_t *bs, int kind, const char *event) { - char botName[64]; - char itemName[32]; - - - + int team; if (!BotItems_DebugEnabled() || !bs || !event) { - return; - } - ClientName(bs->client, botName, sizeof(botName)); - 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", + botName, event, itemName, BotItems_TeamLabel(team)); + return; + } G_Printf("BotItems: %s %s the %s\n", botName, event, itemName); - } - - /* BotGetLevelItemGoal matches items.c "name", not entity classname. */ - static void BotItems_GoalName(bot_state_t *bs, int kind, char *buf, int bufsize) { - const botItemWeaponDef_t *wdef; - - - if (!buf || bufsize < 2) { - return; - } - buf[0] = '\0'; - switch (kind) { - case BOT_ITEM_QUAD: - Q_strncpyz(buf, "Quad Damage", bufsize); - break; - - case BOT_ITEM_ENEMY_FLAG: - - if (bs && BotTeam(bs) == TEAM_RED) { - - Q_strncpyz(buf, "Blue Flag", bufsize); - - } else if (bs && BotTeam(bs) == TEAM_BLUE) { - - Q_strncpyz(buf, "Red Flag", bufsize); - - } - + case BOT_ITEM_RED_FLAG: + Q_strncpyz(buf, "Red Flag", bufsize); + break; + case BOT_ITEM_BLUE_FLAG: + Q_strncpyz(buf, "Blue Flag", bufsize); break; - case BOT_ITEM_MEGA_HEALTH: - Q_strncpyz(buf, "Mega Health", bufsize); - break; - case BOT_ITEM_RED_ARMOR: - Q_strncpyz(buf, "Heavy Armor", bufsize); - break; - case BOT_ITEM_YELLOW_ARMOR: - Q_strncpyz(buf, "Armor", bufsize); - break; - + case BOT_ITEM_HEALTH_SMALL: + Q_strncpyz(buf, "5 Health", bufsize); + break; + case BOT_ITEM_HEALTH: + Q_strncpyz(buf, "25 Health", bufsize); + break; + case BOT_ITEM_HEALTH_LARGE: + Q_strncpyz(buf, "50 Health", bufsize); + break; default: - wdef = BotItems_WeaponDef(kind); - if (wdef) { - Q_strncpyz(buf, wdef->goalname, bufsize); - } - break; - } - } - +static qboolean BotItems_NeedsHealthPickup(bot_state_t *bs) { + if (!bs) { + return qfalse; + } + return bs->inventory[INVENTORY_HEALTH] < BOT_ITEM_HEALTH_SEEK_MAX; +} static float BotItems_PriorityScale(int kind) { @@ -407,8 +312,8 @@ static float BotItems_PriorityScale(int kind) { return 0.45f; - case BOT_ITEM_ENEMY_FLAG: - + case BOT_ITEM_RED_FLAG: + case BOT_ITEM_BLUE_FLAG: return 0.55f; case BOT_ITEM_MEGA_HEALTH: @@ -423,6 +328,18 @@ static float BotItems_PriorityScale(int kind) { return 0.90f; + case BOT_ITEM_HEALTH_LARGE: + + return 0.92f; + + case BOT_ITEM_HEALTH: + + return 0.98f; + + case BOT_ITEM_HEALTH_SMALL: + + return 1.05f; + default: wdef = BotItems_WeaponDef(kind); @@ -501,6 +418,12 @@ void BotItems_Reset(bot_state_t *bs) { bs->item_commit_snap_weapon = 0; + bs->item_commit_progress_time = 0.0f; + + VectorClear(bs->item_commit_progress_origin); + + bs->item_next_scan_time = 0.0f; + } @@ -581,7 +504,7 @@ static void BotItems_ClearCommit(bot_state_t *bs, int endEvent) { case BOT_ITEMS_DBG_TIMEOUT: - (bs, kind, "abandoned (timeout)"); + BotItems_DebugLine(bs, kind, "abandoned (timeout)"); break; @@ -597,6 +520,12 @@ static void BotItems_ClearCommit(bot_state_t *bs, int endEvent) { break; + case BOT_ITEMS_DBG_STUCK: + + BotItems_DebugLine(bs, kind, "abandoned (stuck)"); + + break; + default: break; @@ -647,49 +576,15 @@ static qboolean BotItems_DenialPickupGametype(void) { -static qboolean BotItems_FlagCaptureGametype(void) { +static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { - return gametype == GT_CTF || gametype == GT_CTF_ELIMINATION; + const botItemWeaponDef_t *wdef; -} + if (!bs) { -static qboolean BotItems_EnemyFlagAtBase(bot_state_t *bs) { - - if (!BotItems_FlagCaptureGametype() || !bs) { - - return qfalse; - - } - - if (BotTeam(bs) == TEAM_RED) { - - return bs->blueflagstatus == 0; - - } - - if (BotTeam(bs) == TEAM_BLUE) { - - return bs->redflagstatus == 0; - - } - - return qfalse; - -} - - - -static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { - - const botItemWeaponDef_t *wdef; - - - - if (!bs) { - - return qfalse; + return qfalse; } @@ -718,11 +613,13 @@ static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { return qtrue; case BOT_ITEM_QUAD: - - case BOT_ITEM_ENEMY_FLAG: - return qtrue; + case BOT_ITEM_HEALTH_SMALL: + case BOT_ITEM_HEALTH: + case BOT_ITEM_HEALTH_LARGE: + return BotItems_NeedsHealthPickup(bs); + default: return qfalse; @@ -739,9 +636,15 @@ static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { return qtrue; - case BOT_ITEM_ENEMY_FLAG: - - return BotItems_EnemyFlagAtBase(bs); + 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: @@ -755,6 +658,11 @@ static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { 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: return qfalse; @@ -819,10 +727,16 @@ static qboolean BotItems_CommitAchieved(bot_state_t *bs) { } - if (!BotItems_NeedsKind(bs, kind)) { + /* 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; - } wdef = BotItems_WeaponDef(kind); @@ -839,13 +753,10 @@ static qboolean BotItems_CommitAchieved(bot_state_t *bs) { return bs->inventory[INVENTORY_QUAD] && !bs->item_commit_snap_quad; - case BOT_ITEM_ENEMY_FLAG: - - return (bs->inventory[INVENTORY_REDFLAG] > bs->item_commit_snap_redflag) || - - (bs->inventory[INVENTORY_BLUEFLAG] > bs->item_commit_snap_blueflag); - 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; @@ -865,6 +776,32 @@ static qboolean BotItems_CommitAchieved(bot_state_t *bs) { +/* Level item goals often use entitynum -1 when no spawned pickup is linked in AAS. */ + +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) { @@ -911,6 +848,23 @@ static qboolean BotItems_PickupEntityActive(int entnum) { +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; +} + + + static qboolean BotItems_RefreshGoalByNumber(bot_state_t *bs, bot_goal_t *goal, int kind) { char goalname[64]; @@ -959,10 +913,6 @@ static qboolean BotItems_RefreshGoalByNumber(bot_state_t *bs, bot_goal_t *goal, static qboolean BotItems_GoalVisibleToBot(bot_state_t *bs, bot_goal_t *goal) { - vec3_t dir; - - float dist; - bsp_trace_t trace; @@ -975,18 +925,6 @@ static qboolean BotItems_GoalVisibleToBot(bot_state_t *bs, bot_goal_t *goal) { - VectorSubtract(goal->origin, bs->origin, dir); - - dist = VectorLength(dir); - - if (dist > BOT_ITEMS_VISIBLE_RANGE) { - - return qfalse; - - } - - - BotAI_Trace(&trace, bs->eye, NULL, NULL, goal->origin, bs->client, CONTENTS_SOLID | CONTENTS_PLAYERCLIP); @@ -1011,7 +949,7 @@ static qboolean BotItems_GoalIsPresent(bot_state_t *bs, bot_goal_t *goal) { } - if (trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) { + if (BotItems_ItemGoalInVisButNotVisible(bs, goal)) { return qfalse; @@ -1023,6 +961,38 @@ static qboolean BotItems_GoalIsPresent(bot_state_t *bs, bot_goal_t *goal) { +/* + * 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; + + 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; +} + /* Acquire-time AAS reachability (func_button pattern in ai_dmq3.c). */ static qboolean BotItems_GoalReachable(bot_state_t *bs, bot_goal_t *goal) { @@ -1043,7 +1013,8 @@ static qboolean BotItems_GoalReachable(bot_state_t *bs, bot_goal_t *goal) { } - t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, bs->tfl); + t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, + BotItems_TravelFlags(bs)); return t > 0; @@ -1059,6 +1030,12 @@ static void BotItems_BeginCommit(bot_state_t *bs, bot_goal_t *goal, int kind) { } + if (!BotEnhanced_PushGoalSafe(bs, goal)) { + + return; + + } + memcpy(&bs->item_commit_goal, goal, sizeof(bot_goal_t)); @@ -1075,10 +1052,12 @@ static void BotItems_BeginCommit(bot_state_t *bs, bot_goal_t *goal, int kind) { trap_BotRemoveFromAvoidGoals(bs->gs, goal->number); - trap_BotPushGoal(bs->gs, goal); - bs->nbg_time = bs->item_commit_until; + bs->item_commit_progress_time = FloatTime(); + + VectorCopy(bs->origin, bs->item_commit_progress_origin); + BotItems_DebugLine(bs, kind, "going for"); } @@ -1087,7 +1066,33 @@ static void BotItems_BeginCommit(bot_state_t *bs, bot_goal_t *goal, int kind) { static void BotItems_EnsureGoalOnStack(bot_state_t *bs) { - bot_goal_t top; + 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; @@ -1097,15 +1102,38 @@ static void BotItems_EnsureGoalOnStack(bot_state_t *bs) { } - if (!trap_BotGetTopGoal(bs->gs, &top) || top.number != bs->item_commit_goal.number) { + /* + * 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; + + } + + VectorSubtract(bs->origin, bs->item_commit_progress_origin, delta); + + dist = VectorLength(delta); + + if (dist >= BOT_ITEMS_STUCK_DIST) { + + bs->item_commit_progress_time = FloatTime(); - trap_BotPushGoal(bs->gs, &bs->item_commit_goal); + VectorCopy(bs->origin, bs->item_commit_progress_origin); + + return; } - if (bs->nbg_time < FloatTime() + 1.0f) { + if (FloatTime() - bs->item_commit_progress_time >= BOT_ITEMS_STUCK_TIME) { - bs->nbg_time = bs->item_commit_until; + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_STUCK); } @@ -1153,7 +1181,7 @@ static qboolean BotItems_TryAcquireVisible(bot_state_t *bs) { bestKind = BOT_ITEM_NONE; - bestDist = BOT_ITEMS_VISIBLE_RANGE; + bestDist = 999999.0f; @@ -1213,6 +1241,12 @@ static qboolean BotItems_TryAcquireVisible(bot_state_t *bs) { } + if (!BotItems_GoalEntityMatchesFlagKind(&goal, kind)) { + + continue; + + } + bestDist = effDist; @@ -1233,6 +1267,12 @@ static qboolean BotItems_TryAcquireVisible(bot_state_t *bs) { } + if (!BotItems_GoalHasPickupEntity(&bestGoal)) { + + return qfalse; + + } + BotItems_BeginCommit(bs, &bestGoal, bestKind); @@ -1275,11 +1315,91 @@ static void BotItems_DebugConfigOnce(void) { } - G_Printf("BotItems: debug active (quad, flag, armor, mega, weapons)\n"); + G_Printf("BotItems: debug active (quad, red/blue flag, armor, mega, weapons)\n"); + +} + + + +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)); + } + } + 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) { @@ -1301,6 +1421,21 @@ void BotItems_Tick(bot_state_t *bs) { + 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 (FloatTime() >= bs->item_commit_until) { @@ -1335,6 +1470,8 @@ void BotItems_Tick(bot_state_t *bs) { } + BotItems_CheckStuck(bs); + BotItems_EnsureGoalOnStack(bs); return; @@ -1441,7 +1578,7 @@ int BotItems_HandleReachedGoal(bot_state_t *bs, bot_goal_t *goal) { if (!BotItems_PickupEntityActive(goal->entitynum) || - trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) { + BotItems_ItemGoalInVisButNotVisible(bs, goal)) { BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); @@ -1470,3 +1607,322 @@ int BotItems_ShouldPreserveGoalStack(bot_state_t *bs) { } + +void BotItems_AbortCommit(bot_state_t *bs) { + + if (!BotItems_HasActiveCommit(bs)) { + + return; + + } + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); + +} + + + +void BotItems_OnMoveFailure(bot_state_t *bs) { + + 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; + + } + + 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_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; + +} + + diff --git a/ratoa_gamecode/code/game/ai_bot_items.h b/ratoa_gamecode/code/game/ai_bot_items.h index 647cf84..1affcfa 100644 --- a/ratoa_gamecode/code/game/ai_bot_items.h +++ b/ratoa_gamecode/code/game/ai_bot_items.h @@ -2,8 +2,9 @@ =========================================================================== BOT ITEMS — visible high-value pickup + committed goal (bot_enhanced). -Visible pickups: quad, enemy CTF flag (at base), mega/armor, weapons missing -from inventory (not gauntlet). Commit to level item goal with AAS acquire check. +Visible pickups: quad, enemy CTF flag at base (red/blue by team), mega/armor, health +(5/25/50 when health < 80), weapons missing from inventory (not gauntlet or grapple). +Commit to level item goal with AAS acquire check. =========================================================================== */ @@ -27,5 +28,24 @@ float BotItems_CommitNbgTime(struct bot_state_s *bs); int BotItems_HandleReachedGoal(struct bot_state_s *bs, struct bot_goal_s *goal); int BotItems_ShouldPreserveGoalStack(struct bot_state_s *bs); +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); +/* 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_ChooseLTGItem(struct bot_state_s *bs, int tfl); + +/* + * Safe wrapper for trap_BotItemGoalInVisButNotVisible — botlib calls AAS_EntityInfo + * on goal->entitynum and fatals on -1. Item goals without a live pickup use -1. + */ +int BotItems_ItemGoalInVisButNotVisible(struct bot_state_s *bs, + struct bot_goal_s *goal); #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 9cea37a..44275d3 100644 --- a/ratoa_gamecode/code/game/ai_bot_move_harness.c +++ b/ratoa_gamecode/code/game/ai_bot_move_harness.c @@ -1,6 +1,12 @@ /* =========================================================================== -BOT MOVE HARNESS — no-op stubs (Phase 6 scaffold). +BOT MOVE HARNESS — botlib movement bypass for enhanced aim; rocket jump maneuver; +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. =========================================================================== */ @@ -12,13 +18,766 @@ BOT MOVE HARNESS — no-op stubs (Phase 6 scaffold). #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 "inv.h" +#include "bg_public.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_move_util.h" #include "ai_bot_move_harness.h" +#include "ai_bot_items.h" + +vmCvar_t bot_enhanced_movement; + +#define BOTMOVE_BYPASS_LATCH_SEC 2.5f +#define BOTMOVE_WALKOFF_BLOCK_SEC 10.0f +#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 + +/* ---- Rocket jump tuning ---- */ +#define BOTMOVE_RJ_VIEWTARGET_DIST 300.0f +#define BOTMOVE_RJ_FIRE_DIST 36.0f +#define BOTMOVE_RJ_HOLD_DIST 28.0f +#define BOTMOVE_RJ_SPOT_LATCH_DIST 40.0f +#define BOTMOVE_RJ_STOPPED_HVEL 14.0f +#define BOTMOVE_RJ_FIRE_STALL_SEC 0.35f +#define BOTMOVE_RJ_SLOW_RADIUS 96.0f +#define BOTMOVE_RJ_BRAKE_DIST 48.0f +#define BOTMOVE_RJ_APPROACH_CAP 100.0f +#define BOTMOVE_RJ_APPROACH_MIN 0.0f +#define BOTMOVE_RJ_MAX_HVEL 56.0f +#define BOTMOVE_RJ_BRAKE_HVEL 80.0f +#define BOTMOVE_RJ_FIRE_FOV 20.0f +#define BOTMOVE_RJ_AIR_WALK_SPEED 400.0f +#define BOTMOVE_RJ_PREP_VIEW_SEC 0.22f + +static void BotMove_ClearRJ(bot_state_t *bs) { + bs->movej_rj_fired = qfalse; + bs->movej_rj_was_airborne = qfalse; + bs->movej_rj_active = qfalse; + bs->movej_on_rj_travel = qfalse; + bs->movej_bypass_until = 0.0f; + bs->movej_rj_prep_view_until = 0.0f; + VectorClear(bs->movej_air_steer); + VectorClear(bs->movej_rj_fire_view); +} + +/* After RJ landing: drop all latched views so think/input do not fight (yaw spin). */ +static void BotMove_RJ_Finish(bot_state_t *bs) { + if (!bs) { + return; + } + BotMove_ClearRJ(bs); + bs->movej_moveresult_flags = 0; + VectorClear(bs->movej_move_viewangles); + VectorClear(bs->movej_rj_spot); + VectorClear(bs->movej_air_dest); + VectorClear(bs->movej_movedir); + bs->movej_travel_type = 0; + bs->movej_rj_weapon = 0; + bs->viewanglespeed[0] = 0.0f; + bs->viewanglespeed[1] = 0.0f; +} + +static int BotMove_RJ_HoldingFireView(bot_state_t *bs) { + if (!bs || !bs->movej_rj_fired) { + return 0; + } + if (bs->movej_rj_prep_view_until > FloatTime()) { + return 1; + } + return !bs->movej_rj_was_airborne && + VectorLengthSquared(bs->movej_rj_fire_view) > 0.01f; +} + +static int BotMove_SuppressEnhancedView(bot_state_t *bs) { + if (!bs || !BotMoveHarness_IsActive() || bs->enemy >= 0) { + return 0; + } + /* + * Keep BotMove_OnInputFrame for the whole maneuver until landing after a jump. + * At the RJ spot botlib often clears on_rj_travel / VIEWSET; aim motor path + * would run and TryFire would never execute (stare at floor until timeout). + */ + if (bs->movej_rj_fired) { + return 1; + } + if (bs->movej_rj_active && + (bs->movej_on_rj_travel || BotMoveUtil_BypassActive(bs))) { + return 1; + } + return BotMoveUtil_HasMovementView(bs->movej_moveresult_flags) || + BotMoveUtil_BypassActive(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) { - (void)bs; + if (!bs) { + return; + } + bs->movej_no_walkoff_until = 0.0f; + bs->movej_urgent_health_until = 0.0f; + bs->movej_moveresult_flags = 0; + bs->movej_bypass_until = 0.0f; + VectorClear(bs->movej_move_viewangles); + VectorClear(bs->movej_rj_spot); + VectorClear(bs->movej_air_dest); + VectorClear(bs->movej_air_steer); + VectorClear(bs->movej_movedir); + bs->movej_travel_type = 0; + bs->movej_rj_weapon = 0; + BotMove_ClearRJ(bs); } +/* Aim-motor bypass gate: follows bot_enhanced_aim so botlib can own view. */ int BotMoveHarness_IsActive(void) { + return BotEnhanced_AimActive(); +} + +/* Movement-enhancement gate: rocket-jump maneuver and walk-off avoidance. */ +static int BotMoveHarness_MovementActive(void) { + return BotEnhanced_MovementActive(); +} + +void BotMove_CancelBypass(bot_state_t *bs) { + if (!bs) { + return; + } + BotMove_RJ_Finish(bs); +} + +int BotMove_SuppressesAimMotor(bot_state_t *bs) { + return BotMove_SuppressEnhancedView(bs); +} + +int BotMove_SuppressRoamView(bot_state_t *bs) { + return BotMove_SuppressEnhancedView(bs); +} + +int BotMove_ShouldSkipAvoidReachReset(bot_state_t *bs, bot_moveresult_t *moveresult) { + if (!bs || !moveresult || !BotMoveHarness_IsActive() || !BotMoveHarness_MovementActive()) { + return 0; + } + if (!bs->movej_rj_active || bs->movej_rj_fired) { + return 0; + } + return BotMoveUtil_IsWeaponJumpTravel(moveresult->traveltype & TRAVELTYPE_MASK); +} + +/* ======================== Rocket jump ======================== */ + +static int BotMove_RJ_InFireZone(bot_state_t *bs) { + float dist; + + if (!bs || VectorLengthSquared(bs->movej_rj_spot) < 0.01f) { + return 0; + } + dist = BotMoveUtil_HorizDist(bs->cur_ps.origin, bs->movej_rj_spot); + if (dist <= BOTMOVE_RJ_FIRE_DIST) { + return 1; + } + /* Stopped just short of the latched spot — still commit the jump. */ + return dist <= BOTMOVE_RJ_FIRE_DIST + 16.0f && + BotMoveUtil_HorizSpeed(bs) <= BOTMOVE_RJ_STOPPED_HVEL; +} + +static void BotMove_RJ_RefreshAnchors(bot_state_t *bs) { + vec3_t target; + float spotDist; + + /* Latch spot once close — stop chasing a moving view target (endless creep). */ + spotDist = VectorLengthSquared(bs->movej_rj_spot) > 0.01f ? + BotMoveUtil_HorizDist(bs->cur_ps.origin, bs->movej_rj_spot) : 9999.0f; + if (spotDist > BOTMOVE_RJ_SPOT_LATCH_DIST && + BotMoveUtil_MovementViewTarget(bs, BOTMOVE_RJ_VIEWTARGET_DIST, target)) { + VectorCopy(target, bs->movej_rj_spot); + } + BotMoveUtil_GetTopGoalOrigin(bs, bs->movej_air_dest); +} + +static int BotMove_RJ_HasAim(bot_state_t *bs) { + vec3_t aim; + + if (!bs) { + return 0; + } + if (VectorLengthSquared(bs->movej_move_viewangles) > 0.01f) { + return 1; + } + if (VectorLengthSquared(bs->movej_rj_fire_view) > 0.01f) { + return 1; + } + /* Botlib down-aim: steep negative pitch in stored view. */ + VectorCopy(bs->viewangles, aim); + return aim[PITCH] < -25.0f; +} + +static int BotMove_RJ_FireWeapon(bot_state_t *bs) { + if (bs && bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0) { + return WP_ROCKET_LAUNCHER; + } + if (bs && bs->movej_rj_weapon > 0) { + return bs->movej_rj_weapon; + } + return bs ? bs->weaponnum : 0; +} + +static int BotMove_RJ_ManeuverActive(bot_state_t *bs) { + if (!bs || !bs->movej_rj_active) { + return 0; + } + if (BotMoveUtil_IsWeaponJumpTravel(bs->movej_travel_type) || bs->movej_on_rj_travel) { + return 1; + } + return BotMove_RJ_InFireZone(bs); +} + +/* Spot for prep; fall back to goal origin when view target not ready yet. */ +static int BotMove_RJ_GetPrepTarget(bot_state_t *bs, vec3_t target) { + if (VectorLengthSquared(bs->movej_rj_spot) > 0.01f) { + VectorCopy(bs->movej_rj_spot, target); + return 1; + } + if (VectorLengthSquared(bs->movej_air_dest) > 0.01f) { + VectorCopy(bs->movej_air_dest, target); + return 1; + } return 0; } + +static int BotMove_RJ_GetAirDir(bot_state_t *bs, vec3_t hordir) { + if (BotMoveUtil_HorizDir(bs->cur_ps.origin, bs->movej_air_dest, hordir)) { + return 1; + } + if (VectorLengthSquared(bs->movej_movedir) > 0.01f) { + VectorScale(bs->movej_movedir, -1.0f, hordir); + return VectorNormalize(hordir) > 0.1f; + } + return 0; +} + +static void BotMove_RJ_LatchAirDir(bot_state_t *bs) { + vec3_t hordir; + + if (VectorLengthSquared(bs->movej_air_steer) > 0.01f) { + return; + } + if (BotMove_RJ_GetAirDir(bs, hordir)) { + VectorCopy(hordir, bs->movej_air_steer); + } +} + +static void BotMove_RJ_TickAirborne(bot_state_t *bs) { + if (!bs->movej_rj_fired) { + bs->movej_rj_was_airborne = qfalse; + return; + } + if (bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + bs->movej_rj_was_airborne = qtrue; + BotMoveUtil_LatchBypass(bs, BOTMOVE_BYPASS_LATCH_SEC); + return; + } + if (bs->movej_rj_was_airborne) { + BotMove_RJ_Finish(bs); + } +} + +static void BotMove_RJ_ApplyPrep(bot_state_t *bs, bot_input_t *bi) { + vec3_t prepTarget, toSpot, vel; + float dist, speed, hvel; + + if (bs->movej_rj_fired || bs->movej_rj_was_airborne || + bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { + return; + } + if (!bs->movej_on_rj_travel && !BotMove_RJ_ManeuverActive(bs)) { + return; + } + if (!BotMove_RJ_GetPrepTarget(bs, prepTarget)) { + return; + } + + dist = BotMoveUtil_HorizDist(bs->cur_ps.origin, prepTarget); + if (dist < 0.01f) { + BotMoveUtil_BiStopWalk(bi); + return; + } + if (!BotMoveUtil_HorizDir(bs->cur_ps.origin, prepTarget, toSpot)) { + return; + } + + hvel = BotMoveUtil_HorizSpeed(bs); + + if (BotMove_RJ_InFireZone(bs) && hvel <= BOTMOVE_RJ_MAX_HVEL) { + BotMoveUtil_BiStopWalk(bi); + return; + } + + /* Still carrying approach speed: tap reverse walk instead of forward. */ + if (hvel > BOTMOVE_RJ_BRAKE_HVEL && dist < BOTMOVE_RJ_BRAKE_DIST) { + VectorCopy(bs->cur_ps.velocity, vel); + vel[2] = 0.0f; + if (VectorNormalize(vel) > 0.1f && DotProduct(vel, toSpot) > 0.35f) { + VectorScale(vel, -1.0f, toSpot); + speed = hvel * 0.5f; + if (speed > 120.0f) { + speed = 120.0f; + } + if (speed > 16.0f) { + BotMoveUtil_BiWalk(bi, toSpot, speed); + } else { + BotMoveUtil_BiStopWalk(bi); + } + return; + } + } + + speed = BotMoveUtil_ApproachSpeedVel(dist, BOTMOVE_RJ_HOLD_DIST, BOTMOVE_RJ_SLOW_RADIUS, + BOTMOVE_RJ_APPROACH_CAP, BOTMOVE_RJ_APPROACH_MIN, hvel, BOTMOVE_RJ_MAX_HVEL); + if (speed <= 0.0f) { + BotMoveUtil_BiStopWalk(bi); + return; + } + BotMoveUtil_BiWalk(bi, toSpot, speed); +} + +static void BotMove_RJ_ApplyAir(bot_state_t *bs, bot_input_t *bi, vec3_t worldView) { + vec3_t hordir, faceAngles; + + if (!bs->movej_rj_fired || !bs->movej_rj_was_airborne || + bs->movej_rj_prep_view_until > FloatTime() || + bs->cur_ps.groundEntityNum != ENTITYNUM_NONE) { + return; + } + + BotMoveUtil_LatchBypass(bs, BOTMOVE_BYPASS_LATCH_SEC); + bs->movej_rj_active = qtrue; + BotMove_RJ_LatchAirDir(bs); + + if (VectorLengthSquared(bs->movej_air_steer) > 0.01f) { + VectorCopy(bs->movej_air_steer, hordir); + } else if (!BotMove_RJ_GetAirDir(bs, hordir)) { + return; + } + + vectoangles(hordir, faceAngles); + faceAngles[PITCH] = 0.0f; + BotMoveView_SetWorld(bs, faceAngles); + BotMoveView_StoredToWorld(bs, bs->viewangles, worldView); + BotMoveUtil_BiWalk(bi, hordir, BOTMOVE_RJ_AIR_WALK_SPEED); +} + +static void BotMove_RJ_TryFire(bot_state_t *bs, bot_input_t *bi) { + vec3_t aim; + int weapon; + float now; + + if (bs->movej_rj_fired || !BotMove_RJ_ManeuverActive(bs) || !BotMove_RJ_HasAim(bs)) { + return; + } + if (!BotMove_RJ_InFireZone(bs)) { + bs->movej_rj_prep_view_until = 0.0f; + return; + } + if (BotMoveUtil_HorizSpeed(bs) > BOTMOVE_RJ_MAX_HVEL) { + return; + } + + now = FloatTime(); + if (bs->movej_rj_prep_view_until <= 0.0f) { + bs->movej_rj_prep_view_until = now; + } + BotMoveUtil_LatchBypass(bs, BOTMOVE_BYPASS_LATCH_SEC); + + weapon = BotMove_RJ_FireWeapon(bs); + if (weapon <= 0) { + return; + } + if (bs->cur_ps.weapon != weapon) { + trap_EA_SelectWeapon(bs->client, weapon); + if (bi) { + bi->weapon = weapon; + } + if (now - bs->movej_rj_prep_view_until < BOTMOVE_RJ_FIRE_STALL_SEC) { + return; + } + } + if (bs->cur_ps.weaponstate == WEAPON_RAISING || + bs->cur_ps.weaponstate == WEAPON_DROPPING) { + if (now - bs->movej_rj_prep_view_until < BOTMOVE_RJ_FIRE_STALL_SEC) { + return; + } + } + + if (VectorLengthSquared(bs->movej_move_viewangles) > 0.01f) { + VectorCopy(bs->movej_move_viewangles, aim); + } else { + VectorCopy(bs->viewangles, aim); + } + BotMoveView_SetWorld(bs, aim); + + VectorCopy(aim, bs->movej_rj_fire_view); + bs->movej_rj_fire_view[PITCH] = AngleMod(bs->movej_rj_fire_view[PITCH]); + bs->movej_rj_fire_view[ROLL] = 0.0f; + VectorCopy(aim, bs->movej_move_viewangles); + + trap_EA_SelectWeapon(bs->client, weapon); + trap_EA_Jump(bs->client); + trap_EA_Attack(bs->client); + + if (bi) { + bi->weapon = weapon; + bi->actionflags |= ACTION_ATTACK | ACTION_JUMP; + BotMoveView_StoredToWorld(bs, aim, bi->viewangles); + } + + bs->movej_rj_fired = qtrue; + bs->movej_rj_was_airborne = qfalse; + bs->movej_rj_prep_view_until = now + BOTMOVE_RJ_PREP_VIEW_SEC; + BotMove_RJ_RefreshAnchors(bs); + VectorClear(bs->movej_air_steer); +} + +static void BotMove_RJ_ApplyFireView(bot_state_t *bs, vec3_t worldView) { + if (VectorLengthSquared(bs->movej_rj_fire_view) < 0.01f) { + return; + } + BotMoveView_SetWorld(bs, bs->movej_rj_fire_view); + BotMoveView_StoredToWorld(bs, bs->viewangles, worldView); +} + +static void BotMove_RJ_UpdateView(bot_state_t *bs, vec3_t worldView) { + if (BotMove_RJ_HoldingFireView(bs)) { + BotMove_RJ_ApplyFireView(bs, worldView); + return; + } + if (!bs->movej_rj_fired) { + if (bs->movej_rj_active && + VectorLengthSquared(bs->movej_move_viewangles) > 0.01f) { + BotMoveView_SetWorld(bs, bs->movej_move_viewangles); + } + BotMove_RJ_TryFire(bs, NULL); + if (BotMove_RJ_HoldingFireView(bs)) { + BotMove_RJ_ApplyFireView(bs, worldView); + } else { + BotMoveView_StoredToWorld(bs, bs->viewangles, worldView); + } + return; + } + BotMoveView_StoredToWorld(bs, bs->viewangles, worldView); +} + +static void BotMove_RJ_OnPostMove(bot_state_t *bs, bot_moveresult_t *mr, int travel) { + qboolean weaponJump = BotMoveUtil_IsWeaponJumpTravel(travel); + + if (bs->movej_rj_fired) { + bs->movej_rj_active = qtrue; + BotMove_RJ_RefreshAnchors(bs); + if (BotMove_RJ_HoldingFireView(bs)) { + if (VectorLengthSquared(bs->movej_rj_fire_view) > 0.01f) { + VectorCopy(bs->movej_rj_fire_view, bs->movej_move_viewangles); + VectorCopy(bs->movej_rj_fire_view, bs->ideal_viewangles); + } + return; + } + if (mr->flags & MOVERESULT_MOVEMENTWEAPON) { + bs->movej_rj_weapon = mr->weapon; + bs->weaponnum = mr->weapon; + } + return; + } + + if (weaponJump || bs->movej_on_rj_travel) { + bs->movej_rj_active = qtrue; + BotMove_RJ_RefreshAnchors(bs); + if (BotMove_RJ_InFireZone(bs)) { + BotMoveUtil_LatchBypass(bs, BOTMOVE_BYPASS_LATCH_SEC); + } + if (mr->flags & MOVERESULT_MOVEMENTVIEWSET) { + VectorCopy(mr->ideal_viewangles, bs->movej_move_viewangles); + BotMoveUtil_LatchBypass(bs, BOTMOVE_BYPASS_LATCH_SEC); + BotMoveView_ApplyIdeal(bs, mr->ideal_viewangles); + } else if (BotMoveUtil_HasMovementView(mr->flags)) { + VectorCopy(mr->ideal_viewangles, bs->movej_move_viewangles); + VectorCopy(mr->ideal_viewangles, bs->ideal_viewangles); + } + if (mr->flags & MOVERESULT_MOVEMENTWEAPON) { + bs->movej_rj_weapon = mr->weapon; + bs->weaponnum = mr->weapon; + } + return; + } + + if (BotMove_RJ_InFireZone(bs) && BotMoveUtil_BypassActive(bs)) { + bs->movej_rj_active = qtrue; + BotMove_RJ_RefreshAnchors(bs); + if (mr->flags & MOVERESULT_MOVEMENTVIEWSET) { + VectorCopy(mr->ideal_viewangles, bs->movej_move_viewangles); + BotMoveView_ApplyIdeal(bs, mr->ideal_viewangles); + } else if (BotMoveUtil_HasMovementView(mr->flags)) { + VectorCopy(mr->ideal_viewangles, bs->movej_move_viewangles); + VectorCopy(mr->ideal_viewangles, bs->ideal_viewangles); + } + if (mr->flags & MOVERESULT_MOVEMENTWEAPON) { + bs->movej_rj_weapon = mr->weapon; + bs->weaponnum = mr->weapon; + } + return; + } + + /* Landed on ledge above latched ground spot — do not stay in RJ harness. */ + if (!BotMoveUtil_BypassActive(bs)) { + BotMove_RJ_Finish(bs); + } +} + +/* + * Match botlib AAS landing check (be_aas_move.c) and game fall damage tiers. + */ +static float BotMove_FallDeltaFromImpactSpeed(float impactSpeed) { + float delta; + + delta = fabs(impactSpeed) * 10.0f; + return delta * delta * 0.0001f; +} + +static int BotMove_FallDamageFromDelta(float delta) { + if (delta <= 40.0f) { + return 0; + } + if (delta > 60.0f) { + return 10; + } + return 5; +} + +static int BotMove_EstimatePredictedFallDamage(const aas_clientmove_t *move, + const vec3_t startOrigin) { + float delta, drop, impactSpeed; + + if (!move || !(move->stopevent & SE_HITGROUNDDAMAGE)) { + return 0; + } + impactSpeed = move->velocity[2]; + delta = BotMove_FallDeltaFromImpactSpeed(impactSpeed); + if (delta > 40.0f) { + return BotMove_FallDamageFromDelta(delta); + } + drop = startOrigin[2] - move->endpos[2]; + if (drop <= 0.0f) { + return 5; + } + impactSpeed = sqrt(drop * 800.0f); + delta = BotMove_FallDeltaFromImpactSpeed(impactSpeed); + return BotMove_FallDamageFromDelta(delta); +} + +/* + * Run the AAS walkoff prediction. Fills *moveOut and returns 1 on success. + * Separated so TryAbortRiskyWalkoff can inspect endpos without a second call. + */ +static int BotMove_RunWalkoffPredict(bot_state_t *bs, const vec3_t movedir, + aas_clientmove_t *moveOut) { + vec3_t origin, velocity, cmdmove, hordir; + int presencetype, onground; + + if (!bs || !moveOut || VectorLengthSquared(movedir) < 0.01f) { + return 0; + } + if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { + return 0; + } + + VectorCopy(bs->origin, origin); + origin[2] += 1.0f; + VectorCopy(bs->cur_ps.velocity, velocity); + onground = bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ? qtrue : qfalse; + presencetype = (bs->cur_ps.pm_flags & PMF_DUCKED) ? PRESENCE_CROUCH : PRESENCE_NORMAL; + + hordir[0] = movedir[0]; + hordir[1] = movedir[1]; + hordir[2] = 0.0f; + if (VectorNormalize(hordir) < 0.1f) { + return 0; + } + VectorScale(hordir, 400.0f, cmdmove); + cmdmove[2] = 0.0f; + + memset(moveOut, 0, sizeof(*moveOut)); + trap_AAS_PredictClientMovement(moveOut, bs->entitynum, origin, presencetype, onground, + velocity, cmdmove, 2, BOTMOVE_WALKOFF_PRED_FRAMES, BOTMOVE_WALKOFF_PRED_FRAMETIME, + SE_HITGROUNDDAMAGE | SE_GAP | SE_ENTERWATER | SE_ENTERSLIME | SE_ENTERLAVA, + 0, qfalse); + return 1; +} + +static void BotMove_CancelWalkoffMoveresult(bot_state_t *bs, bot_moveresult_t *mr) { + if (!bs || !mr) { + return; + } + mr->failure = qtrue; + mr->blocked = qfalse; + VectorClear(mr->movedir); + mr->flags &= ~(MOVERESULT_MOVEMENTVIEW | MOVERESULT_MOVEMENTVIEWSET | + MOVERESULT_MOVEMENTWEAPON | MOVERESULT_SWIMVIEW); + trap_BotResetAvoidReach(bs->ms); +} + +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; + + if (!BotMoveHarness_MovementActive() || !bs || !mr) { + return qfalse; + } + if ((g_dmflags.integer & DF_NO_FALLING) || + (G_IsElimGT() && !g_elimination_selfdamage.integer)) { + return qfalse; + } + if (travel != TRAVEL_WALKOFFLEDGE) { + return qfalse; + } + + if (!BotMove_RunWalkoffPredict(bs, mr->movedir, &move)) { + return qfalse; + } + + 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. */ + 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)) { + itemGoalZ = BotItems_GetCommitGoalOriginZ(bs); + abortForItemZ = (itemGoalZ > move.endpos[2] + BOTMOVE_COMMIT_ABOVE_THRESHOLD); + } + + if (!abortForDamage && !abortForItemZ) { + return qfalse; + } + + 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->movej_urgent_health_until = FloatTime() + BOTMOVE_URGENT_HEALTH_SEC; + BotItems_RequestUrgentHealth(bs); + } + return qtrue; +} + +static int BotMove_ShouldAvoidWalkoffLedges(bot_state_t *bs) { + if (!bs || !BotMoveHarness_MovementActive()) { + return 0; + } + return bs->movej_no_walkoff_until > FloatTime(); +} + +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) { + int tfl; + + if (!bs) { + return TFL_DEFAULT; + } + tfl = bs->tfl; + if (!tfl) { + tfl = TFL_DEFAULT; + } + if (bs->jumppad_avoid_until > FloatTime()) { + tfl &= ~TFL_JUMPPAD; + } + if (BotMove_ShouldAvoidWalkoffLedges(bs)) { + tfl &= ~TFL_WALKOFFLEDGE; + } + return tfl; +} + +/* ======================== Think / input hooks ======================== */ + +void BotMove_OnPostMoveToGoal(bot_state_t *bs, bot_moveresult_t *mr) { + int travel; + + if (!bs || !mr) { + return; + } + + travel = mr->traveltype & TRAVELTYPE_MASK; + if (BotMove_TryAbortRiskyWalkoff(bs, mr, travel)) { + travel = 0; + } + + bs->movej_moveresult_flags = mr->flags; + bs->movej_travel_type = travel; + bs->movej_on_rj_travel = BotMoveUtil_IsWeaponJumpTravel(travel); + BotMoveUtil_CacheHorizMovedir(bs, mr); + + if (!BotMoveHarness_IsActive() || !BotMoveHarness_MovementActive()) { + return; + } + BotMove_RJ_OnPostMove(bs, mr, travel); +} + +void BotMove_OnInputFrame(bot_state_t *bs, int time, float thinktime) { + bot_input_t bi; + vec3_t worldView; + + (void)thinktime; + if (!bs) { + return; + } + + BotMove_RJ_TickAirborne(bs); + + trap_EA_GetInput(bs->client, (float)time / 1000.0f, &bi); + if (bi.actionflags & ACTION_RESPAWN) { + if (bs->lastucmd.buttons & BUTTON_ATTACK) { + bi.actionflags &= ~(ACTION_RESPAWN | ACTION_ATTACK); + } + } + + BotMove_RJ_ApplyPrep(bs, &bi); + BotMove_RJ_UpdateView(bs, worldView); + BotMove_RJ_TryFire(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; + } else if (bs->movej_rj_fired) { + BotMove_RJ_ApplyAir(bs, &bi, worldView); + } + + VectorCopy(worldView, bi.viewangles); + BotInputToUserCommand(&bi, &bs->lastucmd, bs->cur_ps.delta_angles, time); +} diff --git a/ratoa_gamecode/code/game/ai_bot_move_harness.h b/ratoa_gamecode/code/game/ai_bot_move_harness.h index 4265b6e..ae57f67 100644 --- a/ratoa_gamecode/code/game/ai_bot_move_harness.h +++ b/ratoa_gamecode/code/game/ai_bot_move_harness.h @@ -1,9 +1,17 @@ /* =========================================================================== -BOT MOVE HARNESS — stub for future movement actuation (not wired yet). +BOT MOVE HARNESS — botlib movement view/weapon compatibility for enhanced aim; +enhanced rocket-jump maneuver; walk-off ledge fall-damage avoidance. -Reset from BotEnhanced_ResetBot; BotMoveHarness_IsActive always false until -combat.move_policy is read from BotAttackMove / BotUpdateInput. +BotMoveHarness_IsActive() follows bot_enhanced_aim: 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. =========================================================================== */ @@ -11,8 +19,25 @@ combat.move_policy is read from BotAttackMove / BotUpdateInput. #define AI_BOT_MOVE_HARNESS_H struct bot_state_s; +struct bot_moveresult_s; +void BotMoveHarness_RegisterCvars(void); void BotMoveHarness_Reset(struct bot_state_s *bs); int BotMoveHarness_IsActive(void); +void BotMove_OnPostMoveToGoal(struct bot_state_s *bs, struct bot_moveresult_s *moveresult); +void BotMove_CancelBypass(struct bot_state_s *bs); + +/* Travel flags for routing/move (jumppad avoid, walk-off block). */ +int BotMove_EffectiveTfl(struct bot_state_s *bs); +int BotMove_WantsUrgentHealth(struct bot_state_s *bs); + +int BotMove_SuppressesAimMotor(struct bot_state_s *bs); +int BotMove_SuppressRoamView(struct bot_state_s *bs); + +/* True while on an RJ reach pre-fire: do not BotResetAvoidReach (blacklists RJ). */ +int BotMove_ShouldSkipAvoidReachReset(struct bot_state_s *bs, struct bot_moveresult_s *moveresult); + +void BotMove_OnInputFrame(struct bot_state_s *bs, int time, float thinktime); + #endif /* AI_BOT_MOVE_HARNESS_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_move_util.c b/ratoa_gamecode/code/game/ai_bot_move_util.c new file mode 100644 index 0000000..f76f635 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_move_util.c @@ -0,0 +1,197 @@ +/* +=========================================================================== +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_move_util.h b/ratoa_gamecode/code/game/ai_bot_move_util.h new file mode 100644 index 0000000..690740b --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_move_util.h @@ -0,0 +1,47 @@ +/* +=========================================================================== +BOT MOVE UTIL — shared helpers for movement harness maneuvers (RJ, future). +=========================================================================== +*/ + +#ifndef AI_BOT_MOVE_UTIL_H +#define AI_BOT_MOVE_UTIL_H + +struct bot_state_s; +struct bot_input_s; +struct bot_moveresult_s; + +#define BOTMOVE_VIEW_FLAGS (MOVERESULT_MOVEMENTVIEWSET | MOVERESULT_MOVEMENTVIEW | \ + MOVERESULT_MOVEMENTWEAPON | MOVERESULT_SWIMVIEW) + +float BotMoveUtil_HorizDist(const vec3_t a, const vec3_t b); +float BotMoveUtil_HorizSpeed(struct bot_state_s *bs); +/* Unit horiz dir from -> to; returns 0 if too short. */ +int BotMoveUtil_HorizDir(const vec3_t from, const vec3_t to, vec3_t dir); + +/* Speed 0 inside hold ring; ramps to cap between hold and slowRadius. */ +float BotMoveUtil_ApproachSpeed(float dist, float hold, float slowRadius, float cap, + float minSpd); +/* Caps command speed when hvel is high near the spot (braking / no overshoot). */ +float BotMoveUtil_ApproachSpeedVel(float dist, float hold, float slowRadius, float cap, + float minSpd, float hvel, float maxHvelNear); + +void BotMoveUtil_BiWalk(struct bot_input_s *bi, const vec3_t dir, float speed); +void BotMoveUtil_BiStopWalk(struct bot_input_s *bi); + +void BotMoveView_SetWorld(struct bot_state_s *bs, const vec3_t worldView); +void BotMoveView_StoredToWorld(struct bot_state_s *bs, const vec3_t stored, vec3_t world); +void BotMoveView_WorldToStored(struct bot_state_s *bs, const vec3_t world, vec3_t stored); +void BotMoveView_ApplyIdeal(struct bot_state_s *bs, const vec3_t ideal); + +void BotMoveUtil_LatchBypass(struct bot_state_s *bs, float seconds); +int BotMoveUtil_BypassActive(struct bot_state_s *bs); + +int BotMoveUtil_HasMovementView(int flags); +int BotMoveUtil_IsWeaponJumpTravel(int travel); + +void BotMoveUtil_CacheHorizMovedir(struct bot_state_s *bs, struct bot_moveresult_s *mr); +int BotMoveUtil_GetTopGoalOrigin(struct bot_state_s *bs, vec3_t origin); +int BotMoveUtil_MovementViewTarget(struct bot_state_s *bs, float maxDist, vec3_t target); + +#endif /* AI_BOT_MOVE_UTIL_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.c b/ratoa_gamecode/code/game/ai_bot_tactics.c index b5b795e..6c61fcd 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.c +++ b/ratoa_gamecode/code/game/ai_bot_tactics.c @@ -26,8 +26,11 @@ qboolean EntityCarriesFlag(aas_entityinfo_t *entinfo); vmCvar_t bot_enhanced_tactics; -#define BOT_TACTICS_FAR_ENGAGE_DIST 512 #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 @@ -105,6 +108,87 @@ 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; @@ -395,8 +479,8 @@ int BotTactics_SkipAimAtEnemy(bot_state_t *bs) { } void BotTactics_PreferCloserEnemy(bot_state_t *bs) { - int i, curenemy, bestenemy, curhoriz, candhoriz, besthoriz; - float vis; + int i, curenemy, bestenemy, curhoriz, candhoriz, besthoriz, minMargin; + float vis, swapChance; vec3_t dir; aas_entityinfo_t entinfo, cureinfo; @@ -411,8 +495,8 @@ void BotTactics_PreferCloserEnemy(bot_state_t *bs) { return; } curhoriz = bs->inventory[ENEMY_HORIZONTAL_DIST]; - if (curhoriz <= BOT_TACTICS_FAR_ENGAGE_DIST) { - return; + if (curhoriz <= 0) { + curhoriz = BotTactics_HorizontalDist(bs, curenemy); } if (g_entities[curenemy].health > 0 && g_entities[curenemy].health <= BOT_TACTICS_FINISH_HEALTH) { @@ -423,6 +507,9 @@ void BotTactics_PreferCloserEnemy(bot_state_t *bs) { 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++) { @@ -445,10 +532,13 @@ void BotTactics_PreferCloserEnemy(bot_state_t *bs) { 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 + BOT_TACTICS_CLOSER_MARGIN >= curhoriz) { + if (candhoriz + minMargin >= curhoriz) { continue; } vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360, i); @@ -461,12 +551,15 @@ void BotTactics_PreferCloserEnemy(bot_state_t *bs) { } } - if (bestenemy < 0) { + if (bestenemy < 0 || bestenemy == curenemy) { return; } - if (bestenemy == curenemy) { + + swapChance = BotTactics_CloserSwapChance(bs, curhoriz, besthoriz); + if (swapChance <= 0.0f || random() >= swapChance) { return; } + BotTactics_AssignEnemy(bs, bestenemy); BotUpdateBattleInventory(bs, bestenemy); } diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.h b/ratoa_gamecode/code/game/ai_bot_tactics.h index 2bcc007..7f06073 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.h +++ b/ratoa_gamecode/code/game/ai_bot_tactics.h @@ -37,7 +37,7 @@ 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 while engaging someone far away */ +/* 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 1a30139..ddfb643 100644 --- a/ratoa_gamecode/code/game/ai_dmnet.c +++ b/ratoa_gamecode/code/game/ai_dmnet.c @@ -47,6 +47,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "ai_bot_items.h" #include "ai_bot_tactics.h" #include "ai_dmq3.h" +#include "ai_bot_move_harness.h" #include "ai_chat.h" #include "ai_cmd.h" #include "ai_dmnet.h" @@ -170,12 +171,14 @@ int BotGoForAir(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { #endif //DEBUG //if we can find an air goal if (BotGetAirGoal(bs, &goal)) { - trap_BotPushGoal(bs->gs, &goal); + BotEnhanced_PushGoalSafe(bs, &goal); return qtrue; } else { + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); //get a nearby goal outside the water - while(trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range)) { + 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))) { @@ -213,7 +216,8 @@ int BotNearbyGoal(bot_state_t *bs, int tfl, bot_goal_t *ltg, float range) { } } // - ret = trap_BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range); + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + ret = BotItems_ChooseNBGItem(bs, BotJumppad_EffectiveTfl(bs), ltg, range); /* if (ret) { @@ -250,7 +254,7 @@ int BotReachedGoal(bot_state_t *bs, bot_goal_t *goal) { return qtrue; } //if the goal isn't there - if (trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) { + if (BotItems_ItemGoalInVisButNotVisible(bs, goal)) { /* float avoidtime; int t; @@ -311,7 +315,7 @@ int BotGetItemLongTermGoal(bot_state_t *bs, int tfl, bot_goal_t *goal) { //BotAI_Print(PRT_MESSAGE, "%s: choosing new ltg\n", ClientName(bs->client, netname, sizeof(netname))); //choose a new goal //BotAI_Print(PRT_MESSAGE, "%6.1f client %d: BotChooseLTGItem\n", FloatTime(), bs->client); - if (trap_BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl)) { + if (BotItems_ChooseLTGItem(bs, BotJumppad_EffectiveTfl(bs))) { /* char buf[128]; //get the goal at the top of the stack @@ -703,7 +707,7 @@ int BotGetLongTermGoal(bot_state_t *bs, int tfl, int retreat, bot_goal_t *goal) bs->ltgtype = 0; } // - if (trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) { + if (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); @@ -1665,7 +1669,7 @@ int AINode_Seek_ActivateEntity(bot_state_t *bs) { //initialize the movement state BotSetupForMovement(bs); //move towards the goal - trap_BotMoveToGoal(&moveresult, bs->ms, goal, bs->tfl); + trap_BotMoveToGoal(&moveresult, bs->ms, goal, BotJumppad_EffectiveTfl(bs)); //if the movement failed if (moveresult.failure) { //reset the avoid reach, otherwise bot is stuck in current area @@ -1711,8 +1715,8 @@ int AINode_Seek_ActivateEntity(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } } - else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { - if (trap_BotMovementViewTarget(bs->ms, goal, bs->tfl, 300, target)) { + else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotMove_SuppressRoamView(bs)) { + if (trap_BotMovementViewTarget(bs->ms, goal, BotJumppad_EffectiveTfl(bs), 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); } @@ -1725,7 +1729,7 @@ int AINode_Seek_ActivateEntity(bot_state_t *bs) { if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { bs->weaponnum = moveresult.weapon; } - BotAI_HandleWeaponJumpMove(bs, goal, &moveresult); + BotMove_OnPostMoveToGoal(bs, &moveresult); // if there is an enemy if (BotFindEnemy(bs, -1)) { if (BotWantsToRetreat(bs)) { @@ -1824,11 +1828,14 @@ int AINode_Seek_NBG(bot_state_t *bs) { //initialize the movement state BotSetupForMovement(bs); //move towards the goal - trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl); + trap_BotMoveToGoal(&moveresult, bs->ms, &goal, BotJumppad_EffectiveTfl(bs)); //if the movement failed if (moveresult.failure) { - //reset the avoid reach, otherwise bot is stuck in current area - trap_BotResetAvoidReach(bs->ms); + if (!BotMove_ShouldSkipAvoidReachReset(bs, &moveresult)) { + //reset the avoid reach, otherwise bot is stuck in current area + trap_BotResetAvoidReach(bs->ms); + } + BotItems_OnMoveFailure(bs); bs->nbg_time = 0; } //check if the bot is blocked @@ -1848,11 +1855,14 @@ int AINode_Seek_NBG(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } } - else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { + 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, bs->tfl, 300, target)) { + if (trap_BotMovementViewTarget(bs->ms, &goal, BotJumppad_EffectiveTfl(bs), 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); + if (goal.origin[2] < bs->origin[2] - 64.0f) { + bs->ideal_viewangles[PITCH] = 0.0f; + } } //FIXME: look at cluster portals? else vectoangles(moveresult.movedir, bs->ideal_viewangles); @@ -1862,14 +1872,16 @@ int AINode_Seek_NBG(bot_state_t *bs) { if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { bs->weaponnum = moveresult.weapon; } - BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); + 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 if (!BotItems_ShouldPreserveGoalStack(bs)) { + else { + BotItems_AbortCommit(bs); trap_BotResetLastAvoidReach(bs->ms); //empty the goal stack trap_BotEmptyGoalStack(bs->gs); @@ -2026,12 +2038,13 @@ int AINode_Seek_LTG(bot_state_t *bs) //initialize the movement state BotSetupForMovement(bs); //move towards the goal - trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl); + trap_BotMoveToGoal(&moveresult, bs->ms, &goal, BotJumppad_EffectiveTfl(bs)); //if the movement failed if (moveresult.failure) { //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); bs->ltg_time = 0; } // @@ -2051,10 +2064,13 @@ int AINode_Seek_LTG(bot_state_t *bs) bs->ideal_viewangles[2] *= 0.5; } } - else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { - if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) { + else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotMove_SuppressRoamView(bs)) { + if (trap_BotMovementViewTarget(bs->ms, &goal, BotJumppad_EffectiveTfl(bs), 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); + if (goal.origin[2] < bs->origin[2] - 64.0f) { + bs->ideal_viewangles[PITCH] = 0.0f; + } } //FIXME: look at cluster portals? else if (VectorLengthSquared(moveresult.movedir)) { @@ -2072,7 +2088,7 @@ int AINode_Seek_LTG(bot_state_t *bs) if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { bs->weaponnum = moveresult.weapon; } - BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); + BotMove_OnPostMoveToGoal(bs, &moveresult); // return qtrue; } @@ -2141,6 +2157,12 @@ 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; + AIEnter_Seek_LTG(bs, "battle fight: enemy dead"); + return qfalse; + } if (bs->enemydeath_time) { if (bs->enemydeath_time < FloatTime() - 1.0) { bs->enemydeath_time = 0; @@ -2381,7 +2403,7 @@ int AINode_Battle_Chase(bot_state_t *bs) //initialize the movement state BotSetupForMovement(bs); //move towards the goal - trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl); + trap_BotMoveToGoal(&moveresult, bs->ms, &goal, BotJumppad_EffectiveTfl(bs)); //if the movement failed if (moveresult.failure) { //reset the avoid reach, otherwise bot is stuck in current area @@ -2395,12 +2417,12 @@ int AINode_Battle_Chase(bot_state_t *bs) if (moveresult.flags & (MOVERESULT_MOVEMENTVIEWSET|MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) { VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles); } - else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { + else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotMove_SuppressRoamView(bs)) { if (bs->chase_time > FloatTime() - 2) { BotAimAtEnemy(bs); } else { - if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) { + if (trap_BotMovementViewTarget(bs->ms, &goal, BotJumppad_EffectiveTfl(bs), 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); } @@ -2414,7 +2436,7 @@ int AINode_Battle_Chase(bot_state_t *bs) if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { bs->weaponnum = moveresult.weapon; } - BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); + BotMove_OnPostMoveToGoal(bs, &moveresult); //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) @@ -2491,7 +2513,7 @@ int AINode_Battle_Retreat(bot_state_t *bs) { BotTactics_RetreatAfterInventory(bs); if (BotCombat_ShouldEngageFromRetreat(bs)) { bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; - AIEnter_Battle_Fight(bs, "enhanced: close gauntlet charge"); + AIEnter_Battle_Fight(bs, "enhanced: close combat charge"); return qfalse; } //if the bot doesn't want to retreat anymore... probably picked up some nice items @@ -2581,7 +2603,7 @@ int AINode_Battle_Retreat(bot_state_t *bs) { //initialize the movement state BotSetupForMovement(bs); //move towards the goal - trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl); + trap_BotMoveToGoal(&moveresult, bs->ms, &goal, BotJumppad_EffectiveTfl(bs)); //if the movement failed if (moveresult.failure) { //reset the avoid reach, otherwise bot is stuck in current area @@ -2598,14 +2620,14 @@ int AINode_Battle_Retreat(bot_state_t *bs) { VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles); } else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET) - && !(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs) ) { + && !(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) { BotAimAtEnemy(bs); } else { - if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) { + if (trap_BotMovementViewTarget(bs->ms, &goal, BotJumppad_EffectiveTfl(bs), 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); } @@ -2619,7 +2641,7 @@ int AINode_Battle_Retreat(bot_state_t *bs) { if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { bs->weaponnum = moveresult.weapon; } - BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); + BotMove_OnPostMoveToGoal(bs, &moveresult); //attack the enemy if possible BotCheckAttack(bs); // @@ -2726,7 +2748,7 @@ int AINode_Battle_NBG(bot_state_t *bs) { //initialize the movement state BotSetupForMovement(bs); //move towards the goal - trap_BotMoveToGoal(&moveresult, bs->ms, &goal, bs->tfl); + trap_BotMoveToGoal(&moveresult, bs->ms, &goal, BotJumppad_EffectiveTfl(bs)); //if the movement failed if (moveresult.failure) { //reset the avoid reach, otherwise bot is stuck in current area @@ -2745,7 +2767,7 @@ int AINode_Battle_NBG(bot_state_t *bs) { VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles); } else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET) - && !(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { + && !(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 and the enemy is visible if (attack_skill > 0.3) { @@ -2753,7 +2775,7 @@ int AINode_Battle_NBG(bot_state_t *bs) { BotAimAtEnemy(bs); } else { - if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) { + if (trap_BotMovementViewTarget(bs->ms, &goal, BotJumppad_EffectiveTfl(bs), 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); } @@ -2767,7 +2789,7 @@ int AINode_Battle_NBG(bot_state_t *bs) { if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { bs->weaponnum = moveresult.weapon; } - BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); + 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 3630513..02a9685 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -44,12 +44,13 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "../botlib/be_ai_weap.h" // #include "ai_main.h" +#include "ai_bot_enhanced.h" #include "ai_dmq3.h" #include "ai_aim_harness.h" #include "ai_weapon_select.h" #include "ai_bot_tactics.h" -#include "ai_bot_enhanced.h" #include "ai_bot_combat.h" +#include "ai_bot_move_harness.h" #include "ai_chat.h" #include "ai_cmd.h" #include "ai_dmnet.h" @@ -240,21 +241,16 @@ EntityIsDead ================== */ qboolean EntityClientIsDead(int clientNum) { - playerState_t ps; + gentity_t *ent; if (clientNum < 0 || clientNum >= MAX_CLIENTS) { return qfalse; } - if (!BotAI_GetClientState(clientNum, &ps)) { - return qtrue; - } - if (ps.pm_type != PM_NORMAL) { - return qtrue; - } - if (ps.stats[STAT_HEALTH] <= 0) { + ent = &g_entities[clientNum]; + if (!ent->inuse || !ent->client) { return qtrue; } - return qfalse; + return (ent->health < 1) ? qtrue : qfalse; } /* @@ -1796,6 +1792,42 @@ void BotSetupForMovement(bot_state_t *bs) { VectorCopy(bs->viewangles, initmove.viewangles); // trap_BotInitMoveState(bs->ms, &initmove); + BotJumppad_Update(bs); +} + +#define BOT_JUMPPAD_AVOID_TIME 12.0f + +/* +================== +BotJumppad_Update + +Record trigger_push use via playerState and avoid re-routing through jumppads +for a while. BotResetLastAvoidReach clears the botlib avoid timer (wrong for +loop prevention); stripping TFL_JUMPPAD forces a walk-off or alternate path. +================== +*/ +void BotJumppad_Update(bot_state_t *bs) { + int entnum; + + if (!bs) { + return; + } + + entnum = bs->cur_ps.jumppad_ent; + if (!entnum) { + return; + } + + if (bs->jumppad_last_ent == entnum && bs->jumppad_avoid_until > FloatTime()) { + bs->jumppad_avoid_until = FloatTime() + BOT_JUMPPAD_AVOID_TIME; + } else { + bs->jumppad_last_ent = entnum; + bs->jumppad_avoid_until = FloatTime() + BOT_JUMPPAD_AVOID_TIME; + } +} + +int BotJumppad_EffectiveTfl(bot_state_t *bs) { + return BotMove_EffectiveTfl(bs); } /* @@ -2866,7 +2898,8 @@ bot_moveresult_t BotAttackMove(bot_state_t *bs, int tfl) { movetype = MOVE_WALK; // if (bs->attackcrouch_time < FloatTime() - 1) { - if (random() < jumper) { + /* Enhanced bots: no random attack jumps (reduces strafe+hop off ledges). */ + if (!BotEnhanced_IsActive() && random() < jumper) { movetype = MOVE_JUMP; } //wait at least one second before crouching again @@ -2893,7 +2926,7 @@ bot_moveresult_t BotAttackMove(bot_state_t *bs, int tfl) { attack_dist = IDEAL_ATTACKDIST; attack_range = 40; } - /* ENHANCED: rush opponent — drive into gauntlet range, skip strafe dance */ + /* ENHANCED: rush opponent — charge in (gauntlet / voluntary SG / plasma) */ if (BotCombat_IsRushOpponent(bs) && bs->combat.move_policy == BOT_MOVE_CLOSE_MELEE) { movetype = MOVE_WALK; @@ -2989,330 +3022,6 @@ int BotSameTeam(bot_state_t *bs, int entnum) { return qfalse; } -#define BOT_WEAPONJUMP_MAX_DIST 4.0f -#define BOT_WEAPONJUMP_MAX_HVEL 48.0f -#define BOT_WEAPONJUMP_FOV 5.0f -#define BOT_WEAPONJUMP_MIN_PITCH 45.0f -/* 5 degrees above straight down (90); face away from dest so blast pushes toward ledge */ -#define BOT_WEAPONJUMP_AIM_PITCH 85.0f - -/* -================== -BotAI_WeaponJumpActive -================== -*/ -int BotAI_WeaponJumpActive(bot_state_t *bs) { - if (!bs) { - return 0; - } - return bs->aimh_weapon_jump_until > FloatTime(); -} - -/* -================== -BotAI_WeaponJumpHorizSpeed -================== -*/ -static float BotAI_WeaponJumpHorizSpeed(bot_state_t *bs) { - vec3_t vel; - - VectorCopy(bs->cur_ps.velocity, vel); - vel[2] = 0.0f; - return VectorLength(vel); -} - -/* -================== -BotAI_WeaponJumpUpdateSpot -================== -*/ -static void BotAI_WeaponJumpUpdateSpot(bot_state_t *bs, bot_goal_t *goal) { - vec3_t target; - - if (!goal || !trap_BotMovementViewTarget(bs->ms, goal, bs->tfl, 80, target)) { - return; - } - VectorCopy(target, bs->aimh_weapon_jump_spot); -} - -/* -================== -BotAI_WeaponJumpUpdateDest -================== -*/ -static void BotAI_WeaponJumpUpdateDest(bot_state_t *bs, bot_goal_t *goal) { - if (!goal) { - return; - } - VectorCopy(goal->origin, bs->aimh_weapon_jump_dest); -} - -/* -================== -BotAI_WeaponJumpSetAimAngles - -Pitch slightly above nadir; yaw 180 from goal so the rocket kicks toward dest. -================== -*/ -static void BotAI_WeaponJumpSetAimAngles(bot_state_t *bs) { - vec3_t toDest, faceAngles; - - VectorSubtract(bs->aimh_weapon_jump_dest, bs->cur_ps.origin, toDest); - toDest[2] = 0.0f; - if (VectorLength(toDest) < 1.0f) { - bs->aimh_weapon_jump_angles[PITCH] = AngleMod(BOT_WEAPONJUMP_AIM_PITCH); - bs->aimh_weapon_jump_angles[ROLL] = 0.0f; - return; - } - VectorNormalize(toDest); - vectoangles(toDest, faceAngles); - bs->aimh_weapon_jump_angles[PITCH] = AngleMod(BOT_WEAPONJUMP_AIM_PITCH); - bs->aimh_weapon_jump_angles[YAW] = AngleMod(faceAngles[YAW] + 180.0f); - bs->aimh_weapon_jump_angles[ROLL] = 0.0f; -} - -/* -================== -BotAI_WeaponJumpHorizDirToDest -================== -*/ -static qboolean BotAI_WeaponJumpHorizDirToDest(bot_state_t *bs, vec3_t dir) { - vec3_t toDest; - - VectorSubtract(bs->aimh_weapon_jump_dest, bs->cur_ps.origin, toDest); - toDest[2] = 0.0f; - if (VectorNormalize(toDest) < 0.1f) { - return qfalse; - } - VectorCopy(toDest, dir); - return qtrue; -} - -/* -================== -BotAI_WeaponJumpAirMove -================== -*/ -static void BotAI_WeaponJumpAirMove(bot_state_t *bs) { - vec3_t hordir; - - if (!bs->aimh_weapon_jump_fired) { - return; - } - if (bs->cur_ps.groundEntityNum != ENTITYNUM_NONE) { - return; - } - - bs->aimh_weapon_jump_until = FloatTime() + 2.0f; - - VectorSubtract(bs->aimh_weapon_jump_dest, bs->cur_ps.origin, hordir); - hordir[2] = 0.0f; - if (VectorNormalize(hordir) < 0.1f) { - VectorCopy(bs->aimh_weapon_jump_air_dir, hordir); - if (VectorNormalize(hordir) < 0.1f) { - return; - } - } - trap_EA_Move(bs->client, hordir, 800); -} - -/* -================== -BotAI_WeaponJumpDistToSpot -================== -*/ -static float BotAI_WeaponJumpDistToSpot(bot_state_t *bs) { - vec3_t delta; - - VectorSubtract(bs->aimh_weapon_jump_spot, bs->cur_ps.origin, delta); - delta[2] = 0.0f; - return VectorLength(delta); -} - -/* -================== -BotAI_WeaponJumpReadyToFire -================== -*/ -qboolean BotAI_WeaponJumpReadyToFire(bot_state_t *bs) { - float pitch, dist; - - if (bs->aimh_weapon_jump_fired) { - return qfalse; - } - - pitch = bs->aimh_weapon_jump_angles[PITCH]; - if (pitch > 180.0f) { - pitch -= 360.0f; - } - if (pitch < BOT_WEAPONJUMP_MIN_PITCH) { - return qfalse; - } - - dist = BotAI_WeaponJumpDistToSpot(bs); - if (dist > BOT_WEAPONJUMP_MAX_DIST) { - return qfalse; - } - - if (BotAI_WeaponJumpHorizSpeed(bs) > BOT_WEAPONJUMP_MAX_HVEL) { - return qfalse; - } - - if (!InFieldOfVision(bs->viewangles, BOT_WEAPONJUMP_FOV, bs->aimh_weapon_jump_angles)) { - return qfalse; - } - - if (bs->aimh_weapon_jump_weapon > 0) { - if (bs->cur_ps.weapon != bs->aimh_weapon_jump_weapon) { - return qfalse; - } - if (bs->cur_ps.weaponstate == WEAPON_RAISING || - bs->cur_ps.weaponstate == WEAPON_DROPPING) { - return qfalse; - } - } - - return qtrue; -} - -/* -================== -BotAI_WeaponJumpFire -================== -*/ -static void BotAI_WeaponJumpFire(bot_state_t *bs) { - vec3_t moveDir; - - bs->aimh_weapon_jump_fired = qtrue; - - BotAI_WeaponJumpSetAimAngles(bs); - trap_EA_View(bs->client, bs->aimh_weapon_jump_angles); - - trap_EA_Jump(bs->client); - trap_EA_Attack(bs->client); - - if (BotAI_WeaponJumpHorizDirToDest(bs, moveDir)) { - VectorCopy(moveDir, bs->aimh_weapon_jump_air_dir); - trap_EA_Move(bs->client, moveDir, 800); - } -} - -/* -================== -BotAI_WeaponJumpInput - -Approach reach start between thinks (botlib speed curve). No jump until -BotAI_HandleWeaponJumpMove confirms stopped at the spot. -================== -*/ -void BotAI_WeaponJumpInput(bot_state_t *bs) { - vec3_t toSpot; - float dist, speed; - - if (!bs) { - return; - } - - if (bs->aimh_weapon_jump_fired) { - BotAI_WeaponJumpAirMove(bs); - return; - } - - if (BotAI_WeaponJumpReadyToFire(bs)) { - BotAI_WeaponJumpFire(bs); - BotAI_WeaponJumpAirMove(bs); - return; - } - - VectorSubtract(bs->aimh_weapon_jump_spot, bs->cur_ps.origin, toSpot); - toSpot[2] = 0.0f; - dist = VectorLength(toSpot); - if (dist <= BOT_WEAPONJUMP_MAX_DIST) { - return; - } - - if (dist > 80.0f) { - dist = 80.0f; - } - speed = 400.0f - (400.0f - 5.0f * dist); - VectorNormalize(toSpot); - trap_EA_Move(bs->client, toSpot, speed); -} - -/* -================== -BotAI_HandleWeaponJumpMove -================== -*/ -void BotAI_HandleWeaponJumpMove(bot_state_t *bs, bot_goal_t *goal, bot_moveresult_t *moveresult) { - int travel; - vec3_t movedir; - - if (!bs || !moveresult || !goal) { - return; - } - - if (BotAI_WeaponJumpActive(bs) || bs->aimh_weapon_jump_fired) { - BotAI_WeaponJumpUpdateDest(bs, goal); - if (!bs->aimh_weapon_jump_fired) { - BotAI_WeaponJumpSetAimAngles(bs); - } - } - - if (bs->aimh_weapon_jump_fired) { - if (VectorLengthSquared(moveresult->movedir) > 0.01f) { - VectorCopy(moveresult->movedir, movedir); - movedir[2] = 0.0f; - if (VectorNormalize(movedir) > 0.1f) { - VectorCopy(movedir, bs->aimh_weapon_jump_air_dir); - } - } - BotAI_WeaponJumpAirMove(bs); - } - - travel = moveresult->traveltype & TRAVELTYPE_MASK; - if (travel != TRAVEL_ROCKETJUMP && travel != TRAVEL_BFGJUMP) { - if (BotAI_WeaponJumpActive(bs)) { - if (bs->aimh_weapon_jump_fired && - bs->cur_ps.groundEntityNum == ENTITYNUM_NONE) { - bs->aimh_weapon_jump_until = FloatTime() + 2.0f; - } else { - bs->aimh_weapon_jump_until = 0.0f; - bs->aimh_weapon_jump_fired = qfalse; - bs->aimh_weapon_jump_weapon = 0; - } - } - return; - } - - if (moveresult->flags & MOVERESULT_MOVEMENTVIEWSET) { - bs->aimh_weapon_jump_until = FloatTime() + 5.0f; - BotAI_WeaponJumpUpdateSpot(bs, goal); - BotAI_WeaponJumpUpdateDest(bs, goal); - BotAI_WeaponJumpSetAimAngles(bs); - if (moveresult->flags & MOVERESULT_MOVEMENTWEAPON) { - bs->aimh_weapon_jump_weapon = moveresult->weapon; - bs->weaponnum = moveresult->weapon; - trap_EA_SelectWeapon(bs->client, bs->weaponnum); - } - } else if (!BotAI_WeaponJumpActive(bs)) { - return; - } else if (!bs->aimh_weapon_jump_fired) { - BotAI_WeaponJumpSetAimAngles(bs); - } - - VectorCopy(bs->aimh_weapon_jump_angles, bs->ideal_viewangles); - VectorCopy(bs->aimh_weapon_jump_angles, bs->viewangles); - trap_EA_View(bs->client, bs->viewangles); - if (BotEnhanced_AimActive()) { - BotAimHarness_SyncMotorToView(bs); - } - - if (!bs->aimh_weapon_jump_fired && BotAI_WeaponJumpReadyToFire(bs)) { - BotAI_WeaponJumpFire(bs); - } -} - /* ================== InFieldOfVision @@ -3929,7 +3638,11 @@ void BotAimAtEnemy(bot_state_t *bs) { if (enemyvisible) { // VectorCopy(entinfo.origin, bestorigin); - bestorigin[2] += 8; + if (BotEnhanced_AimActive() && wi.number == WP_PLASMAGUN) { + bestorigin[2] += 28; + } else { + bestorigin[2] += 8; + } //get the start point shooting from //NOTE: the x and y projectile start offsets are ignored VectorCopy(bs->origin, start); @@ -3993,7 +3706,8 @@ void BotAimAtEnemy(bot_state_t *bs) { } } //if the projectile does radial damage - if (aim_skill > 0.6 && wi.proj.damagetype & DAMAGETYPE_RADIAL) { + if (aim_skill > 0.6 && wi.proj.damagetype & DAMAGETYPE_RADIAL && + !(BotEnhanced_AimActive() && 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 @@ -4031,8 +3745,6 @@ void BotAimAtEnemy(bot_state_t *bs) { bestorigin[0] += 20 * crandom() * (1 - aim_accuracy); bestorigin[1] += 20 * crandom() * (1 - aim_accuracy); bestorigin[2] += 10 * crandom() * (1 - aim_accuracy); - } else if (!wi.speed) { - BotAimHarness_ApplyThinkHitscanOrigin(bs, bestorigin, &entinfo, aim_skill); } } else { @@ -4071,8 +3783,19 @@ void BotAimAtEnemy(bot_state_t *bs) { else { VectorCopy(bestorigin, bs->aimtarget); } + if (BotEnhanced_AimActive() && wi.number == WP_ROCKET_LAUNCHER) { + BotAimHarness_ApplyRocketFeetAim(bs, bs->aimtarget); + } + if (BotEnhanced_AimActive() && wi.number == WP_PLASMAGUN) { + BotAimHarness_ApplyPlasmaCenterMassAim(bs, bs->aimtarget); + } + if (BotEnhanced_AimActive() && wi.number == WP_RAILGUN) { + BotAimHarness_ApplyRailInterceptAim(bs, bs->aimtarget, aim_skill, aim_accuracy); + } else if (BotEnhanced_AimActive()) { + BotAimHarness_ApplyMovementLead(bs, bs->aimtarget, aim_skill); + } //get aim direction - VectorSubtract(bestorigin, bs->eye, dir); + VectorSubtract(bs->aimtarget, bs->eye, dir); // if (wi.number == WP_MACHINEGUN || wi.number == WP_SHOTGUN || diff --git a/ratoa_gamecode/code/game/ai_dmq3.h b/ratoa_gamecode/code/game/ai_dmq3.h index 8763a40..dfc03f1 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.h +++ b/ratoa_gamecode/code/game/ai_dmq3.h @@ -42,6 +42,9 @@ void BotFreeWaypoints(bot_waypoint_t *wp); void BotChooseWeapon(bot_state_t *bs); //setup movement stuff void BotSetupForMovement(bot_state_t *bs); +//after touching a jumppad, temporarily drop TFL_JUMPPAD from routing +void BotJumppad_Update(bot_state_t *bs); +int BotJumppad_EffectiveTfl(bot_state_t *bs); //update the inventory void BotUpdateInventory(bot_state_t *bs); //update the inventory during battle @@ -56,7 +59,7 @@ qboolean BotIsObserver(bot_state_t *bs); qboolean BotIntermission(bot_state_t *bs); //returns true if the bot is in lava or slime qboolean BotInLavaOrSlime(bot_state_t *bs); -//returns true if the client is dead (use clientNum, not entinfo fields alone) +//returns true if the client is dead (g_entities[clientNum].health < 1) qboolean EntityClientIsDead(int clientNum); //returns true if the entity is dead qboolean EntityIsDead(aas_entityinfo_t *entinfo); @@ -128,11 +131,6 @@ void BotAimAtEnemy(bot_state_t *bs); void BotCheckAttack(bot_state_t *bs); //AI when the bot is blocked void BotAIBlocked(bot_state_t *bs, bot_moveresult_t *moveresult, int activate); -/* Rocket/BFG jump: snap view + EA jump/attack when aligned (enhanced aim path). */ -int BotAI_WeaponJumpActive(bot_state_t *bs); -qboolean BotAI_WeaponJumpReadyToFire(bot_state_t *bs); -void BotAI_WeaponJumpInput(bot_state_t *bs); -void BotAI_HandleWeaponJumpMove(bot_state_t *bs, bot_goal_t *goal, bot_moveresult_t *moveresult); //AI to predict obstacles int BotAIPredictObstacles(bot_state_t *bs, bot_goal_t *goal); //enable or disable the areas the blocking entity is in diff --git a/ratoa_gamecode/code/game/ai_main.c b/ratoa_gamecode/code/game/ai_main.c index 47f1fde..5f8a494 100644 --- a/ratoa_gamecode/code/game/ai_main.c +++ b/ratoa_gamecode/code/game/ai_main.c @@ -51,6 +51,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "ai_vcmd.h" #include "ai_bot_enhanced.h" #include "ai_aim_harness.h" +#include "ai_bot_move_harness.h" #include "ai_weapon_select.h" #include "ai_bot_tactics.h" @@ -707,6 +708,14 @@ BotEntityInfo ============== */ void BotEntityInfo(int entnum, aas_entityinfo_t *info) { + if (!info) { + return; + } + if (entnum < 0) { + memset(info, 0, sizeof(*info)); + info->valid = qfalse; + return; + } trap_AAS_EntityInfo(entnum, info); } @@ -957,11 +966,13 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); } + /* Botlib rocket jump / movement view: legacy input, no aim motor */ + if (BotMove_SuppressesAimMotor(bs)) { + BotMove_OnInputFrame(bs, time, thinktime); + return; + } BotAimHarness_BeginMotorFrame(bs); BotChangeViewAngles(bs, thinktime); - if (BotAI_WeaponJumpActive(bs)) { - BotAI_WeaponJumpInput(bs); - } BotAimHarness_ApplyCombatFire(bs); trap_EA_GetInput(bs->client, (float)time / 1000, &bi); if (bi.actionflags & ACTION_RESPAWN) { @@ -969,11 +980,6 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { bi.actionflags &= ~(ACTION_RESPAWN | ACTION_ATTACK); } } - if (BotAI_WeaponJumpActive(bs) && !bs->aimh_weapon_jump_fired) { - if (!BotAI_WeaponJumpReadyToFire(bs)) { - bi.actionflags &= ~(ACTION_ATTACK | ACTION_JUMP); - } - } BotInputToUserCommand(&bi, &bs->lastucmd, bs->cur_ps.delta_angles, time); for (j = 0; j < 3; j++) { bs->viewangles[j] = AngleMod(bs->viewangles[j] - diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index fc966aa..de2787d 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -202,6 +202,8 @@ typedef struct bot_state_s float killedenemy_time; //time the bot killed the enemy float arrive_time; //time arrived (at companion) float lastair_time; //last time the bot had air + float jumppad_avoid_until; /* do not path via jumppads until this time */ + int jumppad_last_ent; /* last trigger_push entity touched */ float teleport_time; //last time the bot teleported float camp_time; //last time camped float camp_range; //camp range @@ -296,6 +298,7 @@ typedef struct bot_state_s int evt_attacker; int evt_damage; int evt_mod; + float enh_goal_last_push_time; /* ---- end BOT ENHANCED ---- */ /* ---- BOT ITEMS: ai_bot_items.c — remove this block to revert ---- */ @@ -310,6 +313,8 @@ typedef struct bot_state_s int item_commit_snap_redflag; int item_commit_snap_blueflag; int item_commit_snap_weapon; + float item_commit_progress_time; + vec3_t item_commit_progress_origin; /* ---- end BOT ITEMS ---- */ /* ---- BOT AIM HARNESS (v1): ai_aim_harness.c — remove this block to revert ---- */ @@ -331,19 +336,41 @@ typedef struct bot_state_s float aimh_last_goal_time; float aimh_smooth_goal_pitch; float aimh_smooth_goal_yaw; + float aimh_pursuit_pitch_off; + float aimh_pursuit_yaw_off; + float aimh_pursuit_set_time; + float aimh_true_goal_pitch; + float aimh_true_goal_yaw; float aimh_tracked_ideal_pitch; float aimh_tracked_ideal_yaw; vec3_t aimh_combat_target; qboolean aimh_hold_fire; /* suppressive fire: +attack each input frame */ - float aimh_weapon_jump_until; /* hold down-aim; bypass harness spring/PS resync */ - vec3_t aimh_weapon_jump_angles; - vec3_t aimh_weapon_jump_spot; /* reach start (MovementViewTarget) */ - vec3_t aimh_weapon_jump_dest; /* LTG / reach end while airborne */ - vec3_t aimh_weapon_jump_air_dir; - int aimh_weapon_jump_weapon; - qboolean aimh_weapon_jump_fired; + vec3_t aimh_rail_lead_point; /* lead-and-wait intercept aim (rail) */ + qboolean aimh_rail_lead_valid; + float aimh_shot_press_since; /* slow weapons: engage without firing (shot urgency) */ + int aimh_shot_press_weapon; /* ---- end BOT AIM HARNESS ---- */ + /* ---- BOT MOVE HARNESS: ai_bot_move_harness.c ---- */ + int movej_moveresult_flags; /* last think: botlib MOVERESULT_* */ + float movej_bypass_until; /* brief latch after MOVEMENTVIEWSET (air) */ + vec3_t movej_move_viewangles; /* last botlib movement ideal_viewangles */ + vec3_t movej_rj_spot; /* rocket-jump reach start */ + vec3_t movej_air_dest; /* goal origin / reach end while airborne */ + vec3_t movej_air_steer; /* horizontal steer dir latched at fire */ + vec3_t movej_movedir; /* last botlib horizontal movedir */ + int movej_travel_type; /* TRAVEL_ROCKETJUMP / BFGJUMP while active */ + int movej_rj_weapon; /* weapon botlib selected for movement */ + qboolean movej_rj_active; + qboolean movej_on_rj_travel; /* last think moveresult was RJ/BFG travel */ + qboolean movej_rj_fired; + qboolean movej_rj_was_airborne; /* true after leaving ground post-fire */ + 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_urgent_health_until; /* prioritize health pickups */ + /* ---- end BOT MOVE HARNESS ---- */ + /* ---- BOT SMART WEAPON SELECT (v1): ai_weapon_select.c — remove to revert ---- */ float wps_next_eval_time; float wps_next_roam_eval_time; diff --git a/ratoa_gamecode/code/game/ai_weapon_select.c b/ratoa_gamecode/code/game/ai_weapon_select.c index 72bd4c3..77a25d4 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.c +++ b/ratoa_gamecode/code/game/ai_weapon_select.c @@ -48,6 +48,8 @@ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int #define WPNSEL_MG_DOWNGRADE_HYSTERESIS 18.0f #define WPNSEL_MG_OBVIOUS_GAP_BASE 8.0f #define WPNSEL_MG_OBVIOUS_GAP_SKILL 10.0f +/* Rockets arc up; skip when enemy is clearly above the bot (same Z gate as splash ground aim). */ +#define WPNSEL_RL_ENEMY_ABOVE_Z 16 #define WPNSEL_MG_LEGACY_BIAS_SCALE 0.12f /* Roaming: throttled ready-weapon selection, prefer silent over audible. */ #define WPNSEL_ROAM_EVAL_MIN 0.85f @@ -58,10 +60,15 @@ 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 -/* Enhanced fight select: min 1s between swaps; longer latch after close gauntlet commit. */ +/* Enhanced fight select: min 1s between swaps; longer latch after close combat commit. */ #define WPNSEL_ENHANCED_MIN_SWITCH_INTERVAL 1.0f -#define WPNSEL_ENHANCED_GAUNTLET_LATCH 3.5f +#define WPNSEL_ENHANCED_CLOSE_COMBAT_LATCH 3.5f #define WPNSEL_ENHANCED_MIN_EVAL_INTERVAL 1.0f +#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) { if (wp <= WP_NONE || wp >= WP_NUM_WEAPONS) { @@ -265,6 +272,16 @@ static float BotWpnSel_SwitchInCost(const weaponinfo_t *wi) { return t; } +static int BotWpnSel_RocketCombatSuitable(bot_state_t *bs) { + if (bs->enemy < 0) { + return 1; + } + if (bs->inventory[ENEMY_HEIGHT] > WPNSEL_RL_ENEMY_ABOVE_Z) { + return 0; + } + return 1; +} + static int BotWpnSel_EnemyHealth(bot_state_t *bs) { if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return 999; @@ -307,7 +324,8 @@ static int BotWpnSel_CountCombatAlternatives(bot_state_t *bs, float dist) { if (dist > 200.0f && dist < 1100.0f && BotWpnSel_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 && BotWpnSel_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER) && + BotWpnSel_RocketCombatSuitable(bs)) { n++; } if (dist > 96.0f && dist < 900.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_GRENADE_LAUNCHER)) { @@ -466,7 +484,7 @@ static float BotWpnSel_RoamAudiblePenalty(int wp, float stealth, float skillComb } else { return 0.0f; } - return pen * stealth * (0.4f + 0.6f * skillCombat); + return pen * stealth * (0.4f + 0.6f * (1.0f - skillCombat)); } static float BotWpnSel_MachinegunRoamModifier(bot_state_t *bs) { @@ -479,10 +497,59 @@ static float BotWpnSel_MachinegunRoamModifier(bot_state_t *bs) { return WPNSEL_ROAM_MG_ONLY_BONUS; } -static qboolean BotWpnSel_IsCloseGauntletCommit(bot_state_t *bs, int wp) { +static qboolean BotWpnSel_VoluntaryCloseCombatEligible(bot_state_t *bs, float dist) { + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + if (BotTactics_IsGauntletOnly(bs)) { + return qfalse; + } + if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + return qfalse; + } + if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { + return qfalse; + } + return dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST; +} + +static qboolean BotWpnSel_RollVoluntaryCloseCombat(bot_state_t *bs, float dist) { + if (!BotWpnSel_VoluntaryCloseCombatEligible(bs, dist)) { + return qfalse; + } + return random() < WPNSEL_VOLUNTARY_CLOSE_COMBAT_CHANCE; +} + +static int BotWpnSel_PreferredVoluntaryCloseWeapon(bot_state_t *bs, float dist) { + if (!bs || dist > (float)BOT_COMBAT_GAUNTLET_RUSH_DIST) { + 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)) { + return WP_GAUNTLET; + } + return WP_NONE; +} + +static qboolean BotWpnSel_IsVoluntaryCloseCombatWeapon(int wp) { + return (wp == WP_GAUNTLET || wp == WP_SHOTGUN || wp == WP_PLASMAGUN); +} + +static qboolean BotWpnSel_IsCloseCombatCommit(bot_state_t *bs, int wp) { float dist; - if (!bs || wp != WP_GAUNTLET) { + if (!bs || !BotWpnSel_IsVoluntaryCloseCombatWeapon(wp)) { return qfalse; } if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { @@ -490,12 +557,13 @@ static qboolean BotWpnSel_IsCloseGauntletCommit(bot_state_t *bs, int wp) { } dist = BotWpnSel_EnemyDistance(bs); if (BotTactics_IsGauntletOnly(bs)) { - return dist <= (float)BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST; + return (wp == WP_GAUNTLET && + dist <= (float)BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST); } - if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + if (!BotWpnSel_VoluntaryCloseCombatEligible(bs, dist)) { return qfalse; } - return dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST; + return wp == BotWpnSel_PreferredVoluntaryCloseWeapon(bs, dist); } static void BotWpnSel_EnhancedApplyLatch(bot_state_t *bs, int prev_wp, int new_wp) { @@ -506,8 +574,8 @@ static void BotWpnSel_EnhancedApplyLatch(bot_state_t *bs, int prev_wp, int new_w } now = FloatTime(); latch = now + WPNSEL_ENHANCED_MIN_SWITCH_INTERVAL; - if (BotWpnSel_IsCloseGauntletCommit(bs, new_wp)) { - latch = now + WPNSEL_ENHANCED_GAUNTLET_LATCH; + if (BotWpnSel_IsCloseCombatCommit(bs, new_wp)) { + latch = now + WPNSEL_ENHANCED_CLOSE_COMBAT_LATCH; } if (latch > bs->wps_enhanced_latch_until) { bs->wps_enhanced_latch_until = latch; @@ -562,7 +630,7 @@ void BotWpnSelect_GetDesire(bot_state_t *bs, bot_weapon_desire_t *out) { int BotWpnSelect_Choose(bot_state_t *bs) { int wp, best_wp, legacy_best, weap_list[16], n_weaps, i; - int alternatives, best_non_mg_wp; + int alternatives, best_non_mg_wp, voluntaryCloseCombat, preferredCloseWp; float dist, score, best_score, cur_score, skillCombat, react, eval_dt; float hysteresis, noiseAmp, vis, mgMod, legacyBias; float miss_score, best_miss_score, best_non_mg_score, obviousGap; @@ -613,6 +681,15 @@ int BotWpnSelect_Choose(bot_state_t *bs) { alternatives = BotWpnSel_CountCombatAlternatives(bs, dist); mgMod = BotWpnSel_MachinegunModifier(bs, dist, skillCombat); + voluntaryCloseCombat = 0; + preferredCloseWp = WP_NONE; + if (BotWpnSel_VoluntaryCloseCombatEligible(bs, dist)) { + voluntaryCloseCombat = BotWpnSel_RollVoluntaryCloseCombat(bs, dist) ? 1 : -1; + if (voluntaryCloseCombat > 0) { + preferredCloseWp = BotWpnSel_PreferredVoluntaryCloseWeapon(bs, dist); + } + } + n_weaps = 0; weap_list[n_weaps++] = WP_GAUNTLET; weap_list[n_weaps++] = WP_MACHINEGUN; @@ -640,6 +717,9 @@ int BotWpnSelect_Choose(bot_state_t *bs) { if (!BotWpnSel_HasWeaponAndAmmo(bs, wp)) { continue; } + if (wp == WP_ROCKET_LAUNCHER && !BotWpnSel_RocketCombatSuitable(bs)) { + continue; + } trap_BotGetWeaponInfo(bs->ws, wp, &wi); if (!wi.valid) { continue; @@ -653,14 +733,27 @@ int BotWpnSelect_Choose(bot_state_t *bs) { score += mgMod; } - if (wp == WP_GAUNTLET && dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST) { + if (dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST && + BotWpnSel_IsVoluntaryCloseCombatWeapon(wp)) { if (BotTactics_IsGauntletOnly(bs)) { - score += 35.0f; + if (wp == WP_GAUNTLET) { + score += 35.0f; + } } else if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { - score -= 120.0f; - } else if (BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { - score += 78.0f; - } else { + if (wp == WP_GAUNTLET) { + score -= 120.0f; + } + } else if (voluntaryCloseCombat > 0 && preferredCloseWp >= 0) { + if (wp == preferredCloseWp) { + score += WPNSEL_VOLUNTARY_CLOSE_COMBAT_BONUS; + } else if (wp == WP_GAUNTLET) { + score -= WPNSEL_VOLUNTARY_CLOSE_COMBAT_PENALTY; + } + } else if (voluntaryCloseCombat < 0) { + if (wp == WP_GAUNTLET) { + score -= WPNSEL_VOLUNTARY_CLOSE_COMBAT_PENALTY; + } + } else if (wp == WP_GAUNTLET) { score -= 120.0f; } } @@ -760,12 +853,15 @@ int BotWpnSelect_Choose(bot_state_t *bs) { } } - if (best_wp == WP_GAUNTLET && !BotTactics_IsGauntletOnly(bs) && + if (voluntaryCloseCombat < 0 && best_wp == WP_GAUNTLET && + !BotTactics_IsGauntletOnly(bs) && dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST) { - if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until || - !BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { - best_wp = bs->weaponnum; - } + best_wp = bs->weaponnum; + } + if (voluntaryCloseCombat > 0 && preferredCloseWp >= 0 && + best_wp != preferredCloseWp && + BotWpnSel_HasWeaponAndAmmo(bs, preferredCloseWp)) { + best_wp = preferredCloseWp; } return best_wp; diff --git a/ratoa_gamecode/code/game/g_client.c b/ratoa_gamecode/code/game/g_client.c index 03b7386..0483e10 100644 --- a/ratoa_gamecode/code/game/g_client.c +++ b/ratoa_gamecode/code/game/g_client.c @@ -1087,7 +1087,7 @@ void G_CreateFrozenPlayer( gentity_t *player ) { body->takedamage = qtrue; body->health = 1; - body->think = NULL; + body->think = 0; body->nextthink = 0; body->die = frozenplayer_die; diff --git a/ratoa_gamecode/code/game/g_local.h b/ratoa_gamecode/code/game/g_local.h index 91b97a2..65a1db9 100644 --- a/ratoa_gamecode/code/game/g_local.h +++ b/ratoa_gamecode/code/game/g_local.h @@ -1660,6 +1660,10 @@ void trap_BotResetGoalState(int goalstate); void trap_BotRemoveFromAvoidGoals(int goalstate, int number); void trap_BotResetAvoidGoals(int goalstate); void trap_BotPushGoal(int goalstate, void /* struct bot_goal_s */ *goal); +#ifndef Q3_VM +void Botlib_RawPushGoal(int goalstate, void /* struct bot_goal_s */ *goal); +void Botlib_RawPopGoal(int goalstate); +#endif void trap_BotPopGoal(int goalstate); void trap_BotEmptyGoalStack(int goalstate); void trap_BotDumpAvoidGoals(int goalstate); diff --git a/ratoa_gamecode/code/game/g_syscalls.c b/ratoa_gamecode/code/game/g_syscalls.c index dafe391..7eb1e68 100644 --- a/ratoa_gamecode/code/game/g_syscalls.c +++ b/ratoa_gamecode/code/game/g_syscalls.c @@ -21,6 +21,10 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ // #include "g_local.h" +#include "ai_main.h" +#include "ai_bot_enhanced.h" + +extern bot_state_t *botstates[MAX_CLIENTS]; // this file is only included when building a dll // g_syscalls.asm is included instead when building a qvm @@ -592,12 +596,47 @@ void trap_BotRemoveFromAvoidGoals(int goalstate, int number) { syscall( BOTLIB_AI_REMOVE_FROM_AVOID_GOALS, goalstate, number); } +static bot_state_t *BotEnhanced_FindBotByGoalState(int goalstate) { + int i; + + for (i = 0; i < MAX_CLIENTS; i++) { + if (botstates[i] && botstates[i]->inuse && botstates[i]->gs == goalstate) { + return botstates[i]; + } + } + return NULL; +} + +#ifndef Q3_VM +void Botlib_RawPushGoal(int goalstate, void /* struct bot_goal_s */ *goal) { + syscall( BOTLIB_AI_PUSH_GOAL, goalstate, goal ); +} + +void Botlib_RawPopGoal(int goalstate) { + syscall( BOTLIB_AI_POP_GOAL, goalstate ); +} +#endif + void trap_BotPushGoal(int goalstate, void /* struct bot_goal_s */ *goal) { + bot_state_t *bs; + + bs = BotEnhanced_FindBotByGoalState(goalstate); + if (bs) { + if (BotEnhanced_PushGoalSafe(bs, (bot_goal_t *)goal)) { + return; + } + /* Stack full — drop push instead of overflowing botlib heap. */ + return; + } syscall( BOTLIB_AI_PUSH_GOAL, goalstate, goal ); } void trap_BotPopGoal(int goalstate) { +#ifndef Q3_VM + Botlib_RawPopGoal(goalstate); +#else syscall( BOTLIB_AI_POP_GOAL, goalstate ); +#endif } void trap_BotEmptyGoalStack(int goalstate) { @@ -625,11 +664,34 @@ int trap_BotGetSecondGoal(int goalstate, void /* struct bot_goal_s */ *goal) { } int trap_BotChooseLTGItem(int goalstate, vec3_t origin, int *inventory, int travelflags) { - return syscall( BOTLIB_AI_CHOOSE_LTG_ITEM, goalstate, origin, inventory, travelflags ); + bot_state_t *bs; + int ret; + + bs = BotEnhanced_FindBotByGoalState(goalstate); + if (bs) { + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + } + ret = syscall( BOTLIB_AI_CHOOSE_LTG_ITEM, goalstate, origin, inventory, travelflags ); + if (bs) { + BotEnhanced_OnGoalChooseDone(bs); + } + return ret; } int trap_BotChooseNBGItem(int goalstate, vec3_t origin, int *inventory, int travelflags, void /* struct bot_goal_s */ *ltg, float maxtime) { - return syscall( BOTLIB_AI_CHOOSE_NBG_ITEM, goalstate, origin, inventory, travelflags, ltg, PASSFLOAT(maxtime) ); + bot_state_t *bs; + int ret; + + bs = BotEnhanced_FindBotByGoalState(goalstate); + if (bs) { + BotEnhanced_ReserveGoalStackRoom(bs, BOTENHANCED_GOAL_STACK_RESERVE); + } + ret = syscall( BOTLIB_AI_CHOOSE_NBG_ITEM, goalstate, origin, inventory, travelflags, ltg, + PASSFLOAT(maxtime) ); + if (bs) { + BotEnhanced_OnGoalChooseDone(bs); + } + return ret; } int trap_BotTouchingGoal(vec3_t origin, void /* struct bot_goal_s */ *goal) { diff --git a/ratoa_gamecode/code/q3_ui/ui_menu.c b/ratoa_gamecode/code/q3_ui/ui_menu.c index 7892438..f2b734f 100644 --- a/ratoa_gamecode/code/q3_ui/ui_menu.c +++ b/ratoa_gamecode/code/q3_ui/ui_menu.c @@ -286,6 +286,27 @@ static void Main_MenuDraw( void ) { //} +/* +=============== +UI_SupportsOggVorbis + +Probe whether the client engine is capable of loading OGG-format audio files. +Note: This function is duplicated in cg_main.c as cg_SupportsOggVorbis + because they're built as separate QVMs without a suitable cross-linked header + to put the function in +=============== +*/ +static qboolean UI_SupportsOggVorbis( void ) { + static int supports_ogg = -1; + + if ( supports_ogg == -1 ) { + qhandle_t ogg = trap_S_RegisterSound( "music/sad_synthwave.ogg", qtrue ); + supports_ogg = ( ogg != 0 ) ? 1 : 0; + } + + return (qboolean)supports_ogg; +} + /* =============== UI_MainMenu @@ -464,6 +485,8 @@ void UI_MainMenu( void ) { trap_Key_SetCatcher( KEYCATCH_UI ); uis.menusp = 0; UI_PushMenu ( &s_main.menu ); - trap_S_StartBackgroundTrack("sound/misc/Aries_Beats_-_Sad_Synthwave.ogg", NULL); + if ( UI_SupportsOggVorbis() ) { + trap_S_StartBackgroundTrack( "music/sad_synthwave.ogg", NULL ); + } }