diff --git a/.gitignore b/.gitignore index 567609b..cded043 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build/ +test/ \ No newline at end of file diff --git a/BUILD_WINDOWS.md b/BUILD_WINDOWS.md index bf80794..8747827 100644 --- a/BUILD_WINDOWS.md +++ b/BUILD_WINDOWS.md @@ -1,6 +1,14 @@ +# Building Devotion on Windows (with WSL) + +1. Install Windows Subsystem for Linux +2. Launch the command line or powershell +3. Type 'wsl' to launch the WSL default shell +4. Within the shell, navigate to the repo directory +5. 'make' + # Building Devotion on Windows (without WSL) -This repo is built with GNU Make and Unix tooling. On Windows you do **not** need the Windows Subsystem for Linux (WSL). Use [**MSYS2**](https://www.msys2.org/) instead—a minimal POSIX environment alongside MinGW-w64—not a Linux distro. +This repo is built with GNU Make and Unix tooling. On Windows you could use the Windows Subsystem for Linux (WSL) to build it but alternatively you can use [**MSYS2**](https://www.msys2.org/). It's a minimal POSIX, not a full-blown distro, and has the advantage that you don't need windows hypervisor features installed to run it, unlike WSL. ## 1. Install MSYS2 @@ -45,6 +53,21 @@ Artifacts: - Staged pak contents: `build/pk3/` - Packaged PK3: `build/devotion-.pk3` (basename from the root Makefile) +## 4. Build and local test install + +From the repo root in **PowerShell** (with MSYS2 on `PATH`, or `msys2_shell.cmd` available): + +```powershell +.\build_windows.ps1 # build only +.\build_windows.ps1 -Deploy # build, then copy PK3 to test\devotion\ +.\build_windows.ps1 -NoBuild -Deploy # deploy only (PK3 already built) +.\build_windows.ps1 -Quiet # suppress per-file compile lines and config banner (warnings/errors still print) +``` + +From MSYS2 MINGW64 you can pass the same flag to GNU Make: `make QUIET=1` (also applies to `make clean QUIET=1`). This uses the ioquake3-style `QUIET=1` variable in [`ratoa_gamecode/Makefile`](ratoa_gamecode/Makefile); do not confuse it with `V=1`, which prints full compiler command lines. + +The `test/` tree is gitignored so you'll have to build a test environment yourself in that folder: You need a minimal local Quake III install (`baseq3/`, `devotion/`, Quake3e binaries). After `-Deploy`, run the test install yourself (e.g. `test\quake3e-vulkan.x64 +set fs_game devotion`). + ## 5. Troubleshooting **`cp: cannot stat '.../vm/*.qvm': No such file or directory`** diff --git a/Makefile b/Makefile index b05a49e..3956ed0 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,11 @@ ASSETS_DIR := ratoa_assets GAMECODE_OPTS := WITH_MULTITOURNAMENT=0 +# QUIET=1: suppress make chatter and per-file compile lines (see ratoa_gamecode/Makefile). +ifeq ($(QUIET),1) +MAKEFLAGS += -s --no-print-directory +endif + OUTPUT_DIR := build PK3_DIR := $(OUTPUT_DIR)/pk3 @@ -44,21 +49,21 @@ release: qvm $(OUTPUT_DIR) mkdir $(PK3_DIR)/vm cp $(GAMECODE_QVM_DIR)/*.qvm $(PK3_DIR)/vm/ #cd $(PK3_DIR) && zip -r ../$(RATMOD_PK3) -- . - cd $(PK3_DIR) && $(CURDIR)/caca_deterministic_zip.sh \ + cd $(PK3_DIR) && QUIET=$(QUIET) $(CURDIR)/caca_deterministic_zip.sh \ $(TIMESTAMP) ../$(RATMOD_PK3) . qvm: $(MAKE) -C $(GAMECODE_DIR) $(GAMECODE_OPTS) \ - BUILD_GAME_SO=0 BUILD_GAME_QVM=1 + BUILD_GAME_SO=0 BUILD_GAME_QVM=1 QUIET=$(QUIET) $(OUTPUT_DIR): mkdir -p $(OUTPUT_DIR) clean_assets: - $(MAKE) -C $(ASSETS_DIR) clean + $(MAKE) -C $(ASSETS_DIR) clean QUIET=$(QUIET) clean_gamecode: - $(MAKE) -C $(GAMECODE_DIR) clean + $(MAKE) -C $(GAMECODE_DIR) clean QUIET=$(QUIET) clean_output: rm -rf $(OUTPUT_DIR) diff --git a/build_windows.ps1 b/build_windows.ps1 new file mode 100644 index 0000000..1a991ca --- /dev/null +++ b/build_windows.ps1 @@ -0,0 +1,78 @@ +# Build Devotion on Windows via MSYS2 MINGW64. +# Important: mingw64 must be on the PATH. +# Optional: copy the built PK3 into the local test install (test\devotion\). +param( + [switch]$Deploy, + [switch]$NoBuild, + [switch]$Quiet +) + +$ErrorActionPreference = "Stop" +$RepoRoot = $PSScriptRoot + +function Deploy-TestPk3 { + $BuildDir = Join-Path $RepoRoot "build" + $ModDir = Join-Path $RepoRoot "test\devotion" + + if (-not (Test-Path $ModDir)) { + Write-Error "Test install not found at $ModDir" + } + + $pk3 = Get-ChildItem -Path $BuildDir -Filter "devotion-*.pk3" -File | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + + if (-not $pk3) { + Write-Error "No devotion-*.pk3 in $BuildDir - run build first (omit -NoBuild)." + } + + Get-ChildItem -Path $ModDir -Filter "devotion-*.pk3" -File | ForEach-Object { + if ($_.FullName -ne (Join-Path $ModDir $pk3.Name)) { + Remove-Item -LiteralPath $_.FullName -Force + Write-Host "Removed old $($_.Name)" + } + } + + $dest = Join-Path $ModDir $pk3.Name + Copy-Item -LiteralPath $pk3.FullName -Destination $dest -Force + Write-Host ('Deployed {0} to {1}' -f $pk3.Name, $dest) +} + +Push-Location $RepoRoot +try { + if (-not $NoBuild) { + $quietFlag = if ($Quiet) { "QUIET=1" } else { "" } + $makeArgs = if ($quietFlag) { + "make clean $quietFlag;make $quietFlag" + } else { + "make clean;make" + } + # Compiler warnings go to stderr; merge streams without NativeCommandError noise. + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + msys2_shell.cmd -mingw64 -defterm -no-start -here -c $makeArgs 2>&1 | + ForEach-Object { + if ($_ -is [System.Management.Automation.ErrorRecord]) { + $_.ToString() + } else { + $_ + } + } + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + finally { + $ErrorActionPreference = $prevEap + } + if ($Quiet) { + Write-Host "Build succeeded." + } + } + + if ($Deploy) { + Deploy-TestPk3 + } +} +finally { + Pop-Location +} diff --git a/caca_deterministic_zip.sh b/caca_deterministic_zip.sh index d2854bd..63503d8 100755 --- a/caca_deterministic_zip.sh +++ b/caca_deterministic_zip.sh @@ -17,7 +17,11 @@ shift 2 find "$@" -type d -print0 | xargs -0r chmod 755 find "$@" -type f -print0 | xargs -0r chmod 644 find "$@" -print0 -print0 | xargs -0r touch -find "$@" | sort | zip -X -@ "$OUTFILE" +if [ "$QUIET" = "1" ]; then + find "$@" | sort | zip -q -X -@ "$OUTFILE" +else + find "$@" | sort | zip -X -@ "$OUTFILE" +fi diff --git a/docs/BOT-CVARS.md b/docs/BOT-CVARS.md index ebb074b..49c9c11 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -7,15 +7,21 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | Name | Origin | Default | Valid values | Description | |------|--------|---------|--------------|-------------| | `bot_aasoptimize` | Vanilla | `0` | 0 or 1 | When `1`, allows the bot navigation library to optimize the map’s AAS data when it is built or loaded. | -| `bot_challenge` | Vanilla | `0` | 0 or 1 | Harder bots: steadier aim when locked on and more precise view tracking. Disables `bot_humanizeaim` while enabled. | +| `bot_challenge` | Vanilla | `0` | 0 or 1 | Harder bots on **legacy** aim: steadier tracking and snap-to-target when locked on. No effect on `bot_enhanced_aim` (enhanced harness ignores this cvar). | | `bot_developer` | Vanilla | `0` | 0 or 1 | Extra bot-library debug output. Requires `sv_cheats 1`. | +| `bot_debugAim` | Devotion | `0` | 0 or 1 | Publishes bot aim on `ps.grapplePoint`, `STAT_EXTFLAGS` (`EXTFL_BOT_AIM_DEBUG`), and `ent->s.origin2` for `cg_debugBotAim`. Works with or without enhanced aim. Requires `sv_cheats 1`. | | `bot_enable` | Vanilla | `0` | 0 or 1 | Master switch: bots only load and play when `1`. Usually set in `server.cfg` (provided by the engine, not the game module). | +| `bot_enhanced` | Devotion | `0` | 0 or 1 | Master switch for Devotion bot AI upgrades (aim harness, smart weapons, tactics). When `0`, sub-cvars have no effect. Saved to config. | +| `bot_enhanced_aim` | Devotion | `0` | 0 or 1 | Smoother, more human-like bot aiming when `1` and `bot_enhanced` is `1` (`0` = classic snap aim). Independent of `bot_challenge`. Saved to config. | +| `bot_enhanced_tactics` | Devotion | `0` | 0 or 1 | Extra combat decisions when `1` and `bot_enhanced` is `1`: gauntlet rush/flee, react to third-party damage, finish wounded targets, prefer nearer threats. Saved to config. | +| `bot_enhanced_items` | Devotion | `1` | 0 or 1 | Visible mega/red/yellow armor pickup with committed goals when `1` and `bot_enhanced` is `1`. Saved to config. | +| `bot_enhanced_items_debug` | Devotion | `0` | 0 or 1 | Server console lines when bots commit to or abandon major item pickups. | +| `bot_enhanced_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). | | `bot_forcereachability` | Vanilla | `0` | 0 or 1 | Forces reachability between areas to be recalculated (slow; map load / AAS build). | | `bot_forcewrite` | Vanilla | `0` | 0 or 1 | Forces the navigation `.aas` file to be written to disk when the map is processed. | | `bot_grapple` | Vanilla | `0` | 0 or 1 | When `1`, bots may use the off-hand grapple for movement where the mod supports it. | -| `bot_humanizeaim` | Devotion | `0` | 0 or 1 | Smoother, more human-like bot aiming when `1` (`0` = classic snap aim). No effect while `bot_challenge` is `1`. Saved to config. | | `bot_interbreedbots` | Vanilla | `10` | integer ≥ 1 | Number of bots spawned when a bot “interbreeding” run starts. | | `bot_interbreedchar` | Vanilla | `` | bot name string | Bot character file to use for interbreeding; setting this starts a tournament-style breeding session. Cleared after spawn. | | `bot_interbreedcycle` | Vanilla | `20` | integer ≥ 1 | How many matches to run before the best bot’s AI is saved and a new generation is spawned. | @@ -29,8 +35,6 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | `bot_report` | Vanilla | `0` | 0 or 1 | Updates internal bot status info shown in server config strings. Requires `sv_cheats 1`. | | `bot_rocketjump` | Vanilla | `1` | 0 or 1 | When `1`, bots may rocket-jump for vertical movement; `0` disables it. | | `bot_saveroutingcache` | Vanilla | `0` | 0 or 1 | One-shot: saves the routing cache for the current map, then resets to `0`. Requires `sv_cheats 1`. | -| `bot_smartWeaponChoice` | Devotion | `0` | 0 or 1 | Smarter weapon picks by range and ammo when `1` (e.g. rail/MG at distance, rocket mid-range, shotgun up close). `0` = original picker. Saved to config. | -| `bot_tacticalAI` | Devotion | `0` | 0 or 1 | Extra combat decisions when `1`: gauntlet rush/flee, react to third-party damage, finish wounded targets, prefer nearer threats. `0` = legacy behavior. Saved to config. | | `bot_testclusters` | Vanilla | `0` | 0 or 1 | Debug: print AAS area/cluster under the bot’s view. Requires `sv_cheats 1`. | | `bot_testichat` | Vanilla | `0` | 0 or 1 | Test mode: new bots fire their “enter game” chat line immediately. | | `bot_testrchat` | Vanilla | `0` | 0 or 1 | Test mode: triggers random chat handling in the bot library. | @@ -38,3 +42,25 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | `bot_thinktime` | Vanilla | `100` | integer (ms), max 200 | Milliseconds between bot AI updates; lower = more reactive bots, higher = less CPU. Capped at 200. | | `bot_visualizejumppads` | Vanilla | `0` | 0 or 1 | Debug: draw jump-pad reachability in the navigation mesh. | | `bot_wigglefactor` | Vanilla | `0.0` | 0.0–1.0 | How often bots change strafe direction in combat; higher = more side-to-side movement. | + +## Deprecated enhanced-bot cvars + +These names are **not registered** by the game module anymore. On first load, if a new `bot_enhanced_*` cvar is still at its default and the old name is set in `server.cfg`, the value is copied once and `bot_enhanced` is turned on when needed. A one-line message is printed to the server console when any deprecated name is detected. + +| Deprecated | Replaced by | +|------------|-------------| +| `bot_humanizeaim` | `bot_enhanced` + `bot_enhanced_aim` | +| `bot_smartWeaponChoice` | `bot_enhanced` + `bot_enhanced_weapons` | +| `bot_tacticalAI` | `bot_enhanced` + `bot_enhanced_tactics` | + +Example (equivalent to the old `set bot_humanizeaim 1`): + +```text +set bot_enhanced 1 +set bot_enhanced_aim 1 +set bot_enhanced_items 1 +``` + +Sub-cvars alone (`bot_enhanced_aim 1` without `bot_enhanced 1`) have no effect unless the master is enabled or legacy migration runs at init. + +For architecture, extension points, and the **Phase 8 parity test checklist**, see [BOT-ENHANCED-ARCHITECTURE.md](BOT-ENHANCED-ARCHITECTURE.md). diff --git a/docs/BOT-ENHANCED-ARCHITECTURE.md b/docs/BOT-ENHANCED-ARCHITECTURE.md new file mode 100644 index 0000000..d0a1154 --- /dev/null +++ b/docs/BOT-ENHANCED-ARCHITECTURE.md @@ -0,0 +1,228 @@ +# Bot enhanced AI — architecture & acceptance + +Refactor foundation for Devotion bot upgrades (aim harness, smart weapons, tactical AI). Gameplay behavior should match the pre-refactor feature set when the parity table below passes. New stances, move harness logic, and gauntlet fixes are **follow-up work** on this scaffold. + +See also: [BOT-CVARS.md](BOT-CVARS.md) (full cvar list and legacy names). + +--- + +## Think vs input + +Two layers run on different cadences: + +| Layer | When | Entry | Purpose | +|-------|------|--------|---------| +| **Think** | `bot_thinktime` (default 100 ms) | `BotDeathmatchAI` → `BotEnhanced_OnThinkStart` | Decisions: inventory, events, combat intent, weapon roam, AI nodes | +| **Input** | Every client frame | `BotUpdateInput` | Actuation: view motor, `+attack` hold, usercmd to engine | + +```mermaid +flowchart TB + subgraph think [Think tick] + INV[BotUpdateInventory] + ENH[BotEnhanced_OnThinkStart] + DRAIN[BotEvents_Drain] + INTENT[BotCombat_UpdateIntent] + ROAM[BotWpnSelect_TickRoaming] + NODE[AI nodes / dmnet tactics hooks] + INV --> ENH + ENH --> DRAIN + ENH --> INTENT + ENH --> ROAM + ROAM --> NODE + DRAIN --> NODE + end + subgraph input [Input frame] + AIM[BotEnhanced_AimActive path in BotUpdateInput] + FIRE[BotAimHarness_ApplyCombatFire] + CMD[trap_EA / usercmd] + AIM --> FIRE --> CMD + end + think -.->|sets ideal_viewangles aimh_hold_fire| input +``` + +**North-facing include for hooks:** `ai_bot_enhanced.h` (`BotEnhanced_*Active`, `BotEnhanced_OnThinkStart`, register/reset). + +--- + +## Cvar matrix + +| Active when | Cvar | Notes | +|-------------|------|--------| +| Master | `bot_enhanced` | Default `0`. Sub-cvars ignored if off. | +| Aim harness | `bot_enhanced` + `bot_enhanced_aim` | Facade: `BotEnhanced_AimActive()` (`bot_challenge` ignored) | +| Smart weapons | `bot_enhanced` + `bot_enhanced_weapons` | Facade: `BotEnhanced_WeaponsActive()` | +| Tactical AI | `bot_enhanced` + `bot_enhanced_tactics` | Facade: `BotEnhanced_TacticsActive()` | +| 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). + +--- + +## File ownership + +| File | Role | +|------|------| +| `ai_bot_enhanced.c/h` | Master cvar, facade gates, `OnThinkStart`, register/reset orchestration | +| `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_main.c` | `BotUpdateInput` aim path | + +--- + +## Extension cookbook + +### New combat stance + +1. Add enum value in `ai_bot_combat.h` (`bot_stance_t`). +2. Implement logic in `BotCombat_UpdateIntent()` (called every think when `bot_enhanced` is on). +3. Read `bs->combat.stance` / `move_policy` from `BotAttackMove` or weapon nodes as needed. +4. No new cvar required if gated by existing features. + +### New move policy + +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. + +### New world event (next think) + +1. Add `BOT_EVT_*` bit in `ai_bot_events.h` (keep in sync with tactics handler bits if delegated). +2. `BotEvents_Push(bs, bits, ent, parm)` from producer (damage, pickups, etc.). +3. Handle in `BotEvents_Drain` or forward into `ai_bot_tactics.c`. +4. **Do not** call drain outside `BotEnhanced_OnThinkStart`. + +### Weapon committed (same tick) + +1. Implement `BotCombat_OnWeaponCommitted()` in `ai_bot_combat.c` (already called from `BotWpnSelect_NotifyWeaponCommitted`). +2. Use for burst/sticky weapon state—not the `evt_*` queue (that is think-aligned). + +### Ingress queue vs combat intent + +| Mechanism | Timing | API | Use for | +|-----------|--------|-----|---------| +| **Ingress queue** | Drained once per **think** | `BotEvents_Push` / `BotEvents_Drain` | World → bot (hurt by third party, future signals) | +| **Combat intent** | Reset/updated each **think** | `bs->combat` | Stance, move/fire policy for this think frame | +| **Same-tick** | Immediate | `BotCombat_OnWeaponCommitted` | Weapon just changed | + +--- + +## Testing & acceptance (Phase 8) + +Manual FFA smoke tests on a dedicated server or local listen. Record pass/fail in the checklist when cutting a release or after bot AI changes. + +### Test environment + +| Setting | Suggested value | +|---------|-----------------| +| Gametype | FFA (`g_gametype 0`) | +| Map | Any medium DM with bots (e.g. `q3dm6`) | +| Bots | 2–4 | +| `bot_thinktime` | `100` (default) | +| `bot_enable` | `1` | +| `sv_cheats` | `1` only for row 8 (`bot_debugAim`) | + +**Reset cvars between rows** (or `map_restart` after changing archived cvars): + +```text +set bot_enhanced 0 +set bot_enhanced_aim 0 +set bot_enhanced_weapons 0 +set bot_enhanced_tactics 0 +set bot_challenge 0 +set bot_debugAim 0 +``` + +### Parity matrix + +| # | Config | Expected | Pass | +|---|--------|----------|------| +| 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 | +| 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 | +| 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 | + +**Row 1 trap (master gate):** `bot_enhanced 0` with all sub-cvars `1` 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. + +### Cvar presets (copy-paste) + +```text +// Row 1 — vanilla +set bot_enhanced 0 +set bot_enhanced_aim 0 +set bot_enhanced_weapons 0 +set bot_enhanced_tactics 0 + +// Row 3 — aim only +set bot_enhanced 1 +set bot_enhanced_aim 1 +set bot_enhanced_weapons 0 +set bot_enhanced_tactics 0 + +// Row 4 — weapons only +set bot_enhanced 1 +set bot_enhanced_aim 0 +set bot_enhanced_weapons 1 +set bot_enhanced_tactics 0 + +// Row 5 — tactics only +set bot_enhanced 1 +set bot_enhanced_aim 0 +set bot_enhanced_weapons 0 +set bot_enhanced_tactics 1 + +// Row 6 — all on +set bot_enhanced 1 +set bot_enhanced_aim 1 +set bot_enhanced_weapons 1 +set bot_enhanced_tactics 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. +- **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). + +Known **gauntlet quirks** from before the refactor are acceptable; this pass does not fix them. + +### Ready-for-enhancements gate + +Do not add new stance/move gameplay until: + +- [x] Single `bot_enhanced` gate; feature cvars renamed and documented +- [x] `BotEnhanced_OnThinkStart` runs every think before combat nodes +- [x] `bot_combat_intent_t` + `BotCombat_UpdateIntent` +- [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] This document (architecture + parity table) +- [x] **Parity table rows 1–8 passed** (manual sign-off) + +--- + +## Rush opponent (implemented) + +- **`BOT_STANCE_RUSH_OPPONENT`** + **`BOT_MOVE_CLOSE_MELEE`** (same movement/attack path for both): + - **Close gauntlet** (≤ 192): gauntlet chosen/out, not gauntlet-only, not in tactics survival flee. + - **Last resort** (≤ 384): gauntlet-only (no other weapon ammo); rush instead of flee; tactics flee/retreat only beyond 384. + - Rush arms on `weaponnum` commit, not only after raise/drop finishes (`BotCombat_OnWeaponCommitted`). +- **`BotAttackMove`**: closes on enemy (forward `MOVE_WALK`/`MOVE_RUN`), skips legacy strafe loop. +- **`BotCheckAttack`**: rush + gauntlet attacks within 72 units without waiting on full FOV/trace path. +- Later: same stance with `BOT_MOVE_CLOSE_TO_WEAPON_IDEAL` for LG / shotgun / plasma range bands. diff --git a/docs/CLIENT-CVARS.md b/docs/CLIENT-CVARS.md index 460eaf6..ddbaf91 100644 --- a/docs/CLIENT-CVARS.md +++ b/docs/CLIENT-CVARS.md @@ -117,6 +117,7 @@ Client-side variables registered by the **cgame** module (`cg_*`, plus related n | `cg_drawAmmoWarning` | Vanilla | `1` | 0 or 1 | Console variable `cg_drawAmmoWarning`. Default `1`. | | `cg_drawAttacker` | Vanilla | `1` | 0 or 1 | Console variable `cg_drawAttacker`. Default `1`. | | `cg_drawBBox` | RatMod | `0` | 0 or 1 | Console variable `cg_drawBBox`. Default `0`. | +| `cg_debugBotAim` | Devotion | `0` | 0–7 | Bot aim debug: green = motor wish (roam: `ideal_viewangles`, fight: aim point), yellow (bit `4`) = crosshair. `1` = followed bot, `2` = all bots, `5` = `1+4`, `6` = `2+4` in free spec. Needs `bot_debugAim 1` on server. `CVAR_CHEAT`. | | `cg_drawCrosshair` | Vanilla | `3` | integer ≥ 0 (typical) | Console variable `cg_drawCrosshair`. Default `3`. | | `cg_drawCrosshairNames` | Vanilla | `1` | 0 or 1 | Console variable `cg_drawCrosshairNames`. Default `1`. | | `cg_drawFPS` | Vanilla | `0` | 0 or 1 | Console variable `cg_drawFPS`. Default `0`. | diff --git a/docs/WORKPLAN-BOT-ENHANCEMENTS.md b/docs/WORKPLAN-BOT-ENHANCEMENTS.md new file mode 100644 index 0000000..e8b61a8 --- /dev/null +++ b/docs/WORKPLAN-BOT-ENHANCEMENTS.md @@ -0,0 +1,316 @@ +Implementation plan: Enhanced bot AI foundation +Goal: Refactor from three independent feature modules + scattered hooks to a single enhanced-bot entry point, renamed feature cvars, a combat intent scaffold, and a world-event ingress scaffold—without changing in-game behavior. + +Non-goals (this pass): Gauntlet fixes, move harness logic, new stances, bus subscribers beyond scaffolding, default bot_enhanced 1. + +Current state (baseline) +Piece Files Cvar Integration +Aim harness +ai_aim_harness.c/h +bot_humanizeaim +ai_main.c (input/view), ai_dmq3.c (aim/attack), g_active.c, g_client.c +Weapon select +ai_weapon_select.c/h +bot_smartWeaponChoice +BotChooseWeapon in ai_dmq3.c, roam tick in BotDeathmatchAI +Tactical AI +ai_bot_tactics.c/h +bot_tacticalAI +BotTactics_OnThink in BotDeathmatchAI; 5 calls in ai_dmnet.c +Debug aim +(harness) +bot_debugAim +Stays separate (cheat/debug; not part of enhanced bundle) +Build: Makefile, game.q3asm, game_mp.q3asm, windows_compile_game.bat list all three .c files. + +State on bot_state_t: Three marked blocks in ai_main.h (aimh_*, wps_*, tact_*). + +Target state (“ready for enhancements”) +bot_enhanced (master, CVAR_ARCHIVE, default 0) + ├─ bot_enhanced_aim (was bot_humanizeaim) + ├─ bot_enhanced_weapons (was bot_smartWeaponChoice) + └─ bot_enhanced_tactics (was bot_tacticalAI) +bot_debugAim — unchanged; bot_challenge does not gate enhanced aim (legacy path only) +Per think (combat path): + BotEnhanced_OnThinkStart(bs) — drain world events, reset/update intent scaffold + … legacy AINode / BotDeathmatchAI … + Feature modules (unchanged logic, new gates) +Per frame: + BotUpdateInput → aim harness path unchanged, gated by BotEnhanced_AimActive() +New modules (scaffolding only): + +File Responsibility +ai_bot_enhanced.c/h +Master cvar, feature gates, registration, single public include for legacy code +ai_bot_combat.c/h +bot_combat_intent_t on bot_state_t, BotCombat_Reset, BotCombat_UpdateIntent (no-op body) +ai_bot_events.c/h +Per-bot ingress queue API; migrate tact_pending pattern later; drain called from BotEnhanced_OnThinkStart +Existing modules: Keep filenames (ai_aim_harness, etc.) to limit build churn; only cvars and call sites move behind the facade. + +Empty stubs (for later): ai_bot_move_harness.c/h with BotMoveHarness_Reset, BotMoveHarness_IsActive (always false), no calls from BotAttackMove yet. + +Cvar plan +New name Replaces Active when +bot_enhanced +(new) +Master; 0 = all enhanced features off regardless of sub-cvars +bot_enhanced_aim +bot_humanizeaim +bot_enhanced && bot_enhanced_aim +bot_enhanced_weapons +bot_smartWeaponChoice +bot_enhanced && bot_enhanced_weapons +bot_enhanced_tactics +bot_tacticalAI +bot_enhanced && bot_enhanced_tactics +Compatibility (one release): In BotEnhanced_RegisterCvars, if new cvars are at default, copy from old names once at startup (read old via trap_Cvar_VariableStringBuffer). Register old names as deprecated (same variable struct aliasing is not possible—instead document removal in BOT-CVARS.md and optionally print one-time G_Printf if old cvars are non-zero). Remove old registrations after one milestone if desired. + +Facade macros/functions: + +int BotEnhanced_IsActive(void); +int BotEnhanced_AimActive(void); +int BotEnhanced_WeaponsActive(void); +int BotEnhanced_TacticsActive(void); +Replace direct BotAimHarness_IsActive() / BotWpnSelect_IsActive() / BotTactics_IsActive() at legacy hook sites only; internal module code can call facade or keep local check that includes master gate. + +bot_state_t layout (consolidation) +Add one marked block in ai_main.h: + +/* ---- BOT ENHANCED (foundation): ai_bot_combat.c, ai_bot_events.c ---- */ +bot_combat_intent_t combat; /* stance, move_policy, fire_policy — defaults = legacy */ +int evt_pending; +int evt_attacker; +... +/* ---- end BOT ENHANCED ---- */ +Keep existing aimh_*, wps_*, tact_* blocks for this pass (avoid big struct moves). Phase 2 optional: move tact_* fields into evt_* namespace in events module only. + +Intent struct (initial values = legacy behavior): + +typedef enum { + BOT_STANCE_NORMAL = 0, + /* BOT_STANCE_MELEE_COMMIT, BOT_STANCE_SURVIVAL_FLEE — reserved, unused */ +} bot_stance_t; +typedef enum { + BOT_MOVE_POLICY_LEGACY = 0, +} bot_move_policy_t; +typedef enum { + BOT_FIRE_POLICY_LEGACY = 0, +} bot_fire_policy_t; +typedef struct { + bot_stance_t stance; + bot_move_policy_t move_policy; + bot_fire_policy_t fire_policy; + float stance_until; /* 0 = no timer */ +} bot_combat_intent_t; +BotCombat_UpdateIntent(bs) — empty except set defaults; called from BotEnhanced_OnThinkStart when enhanced is on. + +Event ingress (formalize, no new gameplay) +Purpose A only (world → bot, next think): not same-tick planner signals yet. + +API: + +void BotEvents_Reset(bot_state_t *bs); +void BotEvents_Push(bot_state_t *bs, int evt_bits, int ent, int parm); +void BotEvents_Drain(bot_state_t *bs); /* clears pending; handlers no-op or delegate to tactics */ +Phase 1 implementation: BotEvents_Drain calls existing BotTactics_ScanEvents + BotTactics_ProcessPending when tactics active—logic stays in ai_bot_tactics.c, ingress API is a thin forwarder. Later, damage scan moves into ai_bot_events.c. + +Do not add WEAPON_COMMITTED synchronous bus yet; document hook point: call BotCombat_OnWeaponCommitted(bs, prev, new) from BotWpnSelect_NotifyWeaponCommitted with empty body. + +Hook consolidation map +North (callers should use ai_bot_enhanced.h only): + +Location Today After +BotInitLibrary / cvar register +3× RegisterCvars +BotEnhanced_RegisterCvars (+ feature registers inside) +BotAISetupClient / reset +3× Reset +BotEnhanced_ResetBot(bs) → calls all resets +BotDeathmatchAI start +BotTactics_OnThink, roam tick +BotEnhanced_OnThinkStart(bs) first +BotAimHarness_IsActive sites +direct +BotEnhanced_AimActive() +BotWpnSelect_IsActive sites +direct +BotEnhanced_WeaponsActive() +BotTactics_* in ai_dmnet.c +direct +Keep calls; gate inside tactics via BotEnhanced_TacticsActive() +BotChooseWeapon +NotifyWeaponCommitted +add BotCombat_OnWeaponCommitted (stub) +South (unchanged this pass): trap_EA_*, trap_BotUserCommand, BotInputToUserCommand, g_active debug sync. + +Think vs input: Document in docs/BOT-ENHANCED-ARCHITECTURE.md (new): think = decisions; input = aim/move harness actuation. No move harness wiring yet. + +Phased tasks +Phase 0 — Baseline & parity checklist (0 code behavior change) + + Record test matrix: FFA, bot_enhanced off, each sub-cvar on alone (old names), all on, with bot_humanizeaim 1 + challenge. + + Note bot_thinktime, map, 2–4 bots for manual smoke tests. + + Optional: short demo recording or log flags for weapon/ainode (cheat/debug only). +Exit: Checklist doc in PR description or docs/BOT-ENHANCED-ARCHITECTURE.md § Testing. + +Phase 1 — ai_bot_enhanced facade + master cvar +Add: ai_bot_enhanced.c/h. + +Tasks: + +Register bot_enhanced (default 0, CVAR_ARCHIVE). +Implement BotEnhanced_IsActive, BotEnhanced_*Active() compositing master + feature cvars (aim: no bot_challenge gate). +BotEnhanced_RegisterCvars() — call existing BotAimHarness_RegisterCvars, etc., after registering master/feature names. +BotEnhanced_ResetBot(bs) — call three existing resets. +Replace registrations in feature modules: feature cvars renamed to bot_enhanced_*; remove duplicate Register from ai_main.c if centralized. +Touch: ai_main.c (init/reset), ai_aim_harness.c, ai_weapon_select.c, ai_bot_tactics.c (cvar names + IsActive uses master gate). + +Exit: Build succeeds; with bot_enhanced 0, behavior identical to today with all sub-cvars 1 (sub-cvars ignored). + +Phase 2 — Cvar rename + docs + compat +Tasks: + +Rename cvars as in table; compat read from old names once at init. +Update docs/BOT-CVARS.md: add bot_enhanced, rename rows, note deprecation. +Update header comments in ai_*_harness.h, ai_weapon_select.h, ai_bot_tactics.h. +Grep repo for bot_humanizeaim, bot_smartWeaponChoice, bot_tacticalAI (configs, docs, scripts). +Exit: set bot_enhanced 1; set bot_enhanced_aim 1 reproduces old bot_humanizeaim 1 behavior. + +Phase 3 — ai_bot_combat intent scaffold +Add: ai_bot_combat.c/h. + +Tasks: + +Add bot_combat_intent_t combat to bot_state_t. +BotCombat_Reset / BotCombat_UpdateIntent (defaults only). +BotCombat_OnWeaponCommitted stub. +BotEnhanced_OnThinkStart: BotEvents_Drain (phase 4), BotCombat_UpdateIntent. +Call BotEnhanced_OnThinkStart from BotDeathmatchAI immediately after BotUpdateInventory (before roam/tactics ordering: inventory → enhanced start → tactics). +Touch: ai_main.h, ai_dmq3.c, ai_weapon_select.c (NotifyWeaponCommitted → stub). + +Exit: No gameplay change; intent fields visible in debugger, always legacy enums. + +Phase 4 — ai_bot_events ingress facade +Add: ai_bot_events.c/h. + +Tasks: + +Define BOT_EVT_HURT_BY_OTHER (same bit as BOT_TACT_EVT_HURT_BY_OTHER or alias). +BotEvents_Drain forwards to tactics scan/process when tactics active. +Deprecate direct BotTactics_OnThink from ai_dmq3.c; only BotEnhanced_OnThinkStart calls drain. +Document queue contract: producers push, drain once per think, bounded fields (no malloc). +Exit: Third-party hurt reaction unchanged when bot_enhanced_tactics 1; tactics file still owns logic. + +Phase 5 — Legacy hook pass (facade at boundaries) +Tasks: + +ai_dmq3.c, ai_dmnet.c, ai_main.c, g_client.c, g_active.c: #include "ai_bot_enhanced.h"; replace BotAimHarness_IsActive / BotWpnSelect_IsActive with facade at call sites (≥15 sites). +Leave feature-module internals as-is. +Add comment markers: /* ENHANCED: aim */ at clusters for future readers. +Exit: No direct feature IsActive from dmnet/main/dmq3 except inside feature .c files. + +Phase 6 — Move harness stub + build wiring +Add: ai_bot_move_harness.c/h (stubs only). + +Tasks: + +Add to Makefile, game.q3asm, game_mp.q3asm, windows_compile_game.bat. +BotMoveHarness_Reset from BotEnhanced_ResetBot. +No call from BotAttackMove or BotUpdateInput. +Exit: Links; zero runtime effect. + +Phase 7 — Architecture doc & “extension cookbook” +Add: docs/BOT-ENHANCED-ARCHITECTURE.md (concise): + +Think vs input diagram +Cvar matrix +File ownership table +Where to add: new stance, move policy, world event, weapon commit handler +Explicit: ingress queue vs same-tick intent (two layers) +Exit: New contributor can add melee commit without reading entire ai_dmq3.c. + +Phase 8 — Acceptance (behavior parity) +# Config Expected +1 +All enhanced off +Vanilla bots +2 +bot_enhanced 1, all sub off +Vanilla +3 +Enhanced + aim only +Same as old humanize aim +4 +Enhanced + weapons only +Same as old smart weapon +5 +Enhanced + tactics only +Same as old tactical +6 +All enhanced on +Same as old all three on +7 +bot_challenge 1 + enhanced aim on +Harness on (challenge ignored on enhanced path) +8 +bot_debugAim 1 +Debug unchanged +Regression focus: Weapon switching cadence, retreat/flee on gauntlet-only, MG fire rate with humanize aim—should match pre-refactor, including known gauntlet quirks. + +Suggested PR split (optional) +PR1: Phase 1–2 (facade + cvars + docs) +PR2: Phase 3–4 (combat + events scaffold) +PR3: Phase 5–7 (hook pass + move stub + doc) +Keeps reviewable diffs; each PR builds and passes parity table for its scope. + +File tree after refactor +code/game/ + ai_bot_enhanced.c/h ← master, facade, OnThinkStart + ai_bot_combat.c/h ← intent + weapon-commit stub + ai_bot_events.c/h ← ingress API (drain → tactics) + ai_bot_move_harness.c/h ← stubs only + ai_aim_harness.c/h ← implementation (gated) + ai_weapon_select.c/h + ai_bot_tactics.c/h + ai_main.h ← + combat block; keep aimh/wps/tact for now + ai_dmq3.c / ai_dmnet.c ← thinner includes, facade calls +docs/ + BOT-CVARS.md ← updated + BOT-ENHANCED-ARCHITECTURE.md ← new +Ready-for-enhancements checklist +Before adding gauntlet stance or move harness logic, all should be true: + + + Single bot_enhanced gate; feature cvars renamed and documented + + BotEnhanced_OnThinkStart runs every think before combat nodes + + bot_combat_intent_t exists; BotCombat_UpdateIntent is the one place to add stance logic + + BotCombat_OnWeaponCommitted stub called from weapon notify + + BotEvents_* ingress exists; world damage path documented + + Legacy files call BotEnhanced_*Active() at boundaries + + ai_bot_move_harness linked with no-op API + + Architecture doc describes think vs input and extension points + + Parity table passed +First enhancement after this (separate task): BOT_STANCE_MELEE_COMMIT + BotAttackMove reading combat.move_policy / weaponnum—no new cvars required. + +Risks & mitigations +Risk Mitigation +Cvar rename breaks server.cfg +One-time compat read; changelog +Double drain of tactics events +Only BotEnhanced_OnThinkStart drains +bot_enhanced 0 but sub-cvars 1 confuses admins +Doc: sub-cvars require master; optional G_Printf warning once +QVM size +New files are small; no heavy new logic +This plan is refactor-only through Phase 8; gameplay fixes (gauntlet, etc.) land on the scaffold in follow-up work. Switch to Agent mode when you want this implemented in the repo. \ No newline at end of file diff --git a/ratoa_assets/Makefile b/ratoa_assets/Makefile index fa53aea..b64de04 100644 --- a/ratoa_assets/Makefile +++ b/ratoa_assets/Makefile @@ -1,6 +1,16 @@ .PHONY: clean +ifeq ($(QUIET),1) +MAKEFLAGS += -s --no-print-directory +endif + clean: +ifeq ($(QUIET),1) + @find . -type f -iname '*~' -print0 | xargs -0r rm -f + @find . -type f -iname '*.swp' -print0 | xargs -0r rm -f + @find . -type f -iname '*.swo' -print0 | xargs -0r rm -f +else find . -type f -iname '*~' -print0 | xargs -0 rm -fv find . -type f -iname '*.swp' -print0 | xargs -0 rm -fv find . -type f -iname '*.swo' -print0 | xargs -0 rm -fv +endif diff --git a/ratoa_gamecode/Makefile b/ratoa_gamecode/Makefile index e6168f2..e90c855 100644 --- a/ratoa_gamecode/Makefile +++ b/ratoa_gamecode/Makefile @@ -907,7 +907,16 @@ BASE_CFLAGS += -DPRODUCT_VERSION=\\\"$(VERSION)\\\" BASE_CFLAGS += -DRATMOD_VERSION=\\\"$(RATMOD_VERSION)\\\" QVM_CFLAGS += -DRATMOD_VERSION=\"$(RATMOD_VERSION)\" -ifeq ($(V),1) +ifeq ($(QUIET),1) +MAKEFLAGS += -s --no-print-directory +endif + +# QUIET=1 — no per-file recipe labels or config banner (warnings/errors still print). +# V=1 — ioquake3 "verbose": full compiler command lines (not compatible with quiet). +ifeq ($(QUIET),1) +echo_cmd=@: +Q=@ +else ifeq ($(V),1) echo_cmd=@: Q= else @@ -1007,15 +1016,16 @@ all: debug release debug: @$(MAKE) targets B=$(BD) CFLAGS="$(CFLAGS) $(DEPEND_CFLAGS) \ - $(DEBUG_CFLAGS)" V=$(V) + $(DEBUG_CFLAGS)" V=$(V) QUIET=$(QUIET) release: @$(MAKE) targets B=$(BR) CFLAGS="$(CFLAGS) $(DEPEND_CFLAGS) \ - $(RELEASE_CFLAGS)" V=$(V) + $(RELEASE_CFLAGS)" V=$(V) QUIET=$(QUIET) # Create the build directories, check libraries and print out # an informational message, then start building targets: makedirs +ifneq ($(QUIET),1) @echo "" @echo "Building ioquake3 in $(B):" @echo " PLATFORM: $(PLATFORM)" @@ -1055,8 +1065,11 @@ targets: makedirs echo " $$i"; \ done @echo "" +else + @echo "Building $(B) (quiet)..." +endif ifneq ($(TARGETS),) - @$(MAKE) $(TARGETS) V=$(V) + @$(MAKE) $(TARGETS) V=$(V) QUIET=$(QUIET) endif makedirs: @@ -1779,9 +1792,14 @@ Q3GOBJ_ = \ $(B)/baseq3/game/ai_cmd.o \ $(B)/baseq3/game/ai_dmnet.o \ $(B)/baseq3/game/ai_dmq3.o \ + $(B)/baseq3/game/ai_bot_enhanced.o \ + $(B)/baseq3/game/ai_bot_combat.o \ + $(B)/baseq3/game/ai_bot_events.o \ + $(B)/baseq3/game/ai_bot_move_harness.o \ $(B)/baseq3/game/ai_aim_harness.o \ $(B)/baseq3/game/ai_weapon_select.o \ $(B)/baseq3/game/ai_bot_tactics.o \ + $(B)/baseq3/game/ai_bot_items.o \ $(B)/baseq3/game/ai_main.o \ $(B)/baseq3/game/ai_team.o \ $(B)/baseq3/game/ai_vcmd.o \ @@ -1842,6 +1860,10 @@ MPGOBJ_ = \ $(B)/missionpack/game/ai_cmd.o \ $(B)/missionpack/game/ai_dmnet.o \ $(B)/missionpack/game/ai_dmq3.o \ + $(B)/missionpack/game/ai_bot_enhanced.o \ + $(B)/missionpack/game/ai_bot_combat.o \ + $(B)/missionpack/game/ai_bot_events.o \ + $(B)/missionpack/game/ai_bot_move_harness.o \ $(B)/missionpack/game/ai_aim_harness.o \ $(B)/missionpack/game/ai_weapon_select.o \ $(B)/missionpack/game/ai_bot_tactics.o \ @@ -2126,7 +2148,9 @@ $(B)/baseq3/ui/%.asm: $(Q3UIDIR)/%.c $(Q3LCC) $(Q3UIDIR)/compile_version.h # Generated: #define COMPILE_VERSION "..." from $(COMPILE_VERSION) (Makefile expansion). $(Q3UIDIR)/compile_version.h: FORCE +ifneq ($(QUIET),1) @echo "GEN $@" +endif @echo '/* Generated by Makefile; do not edit. Set COMPILE_VERSION or override on the make command line. */' > $@ @echo '#ifndef COMPILE_VERSION_H' >> $@ @echo '#define COMPILE_VERSION_H' >> $@ @@ -2219,13 +2243,15 @@ clean: clean-debug clean-release #endif clean-debug: - @$(MAKE) clean2 B=$(BD) + @$(MAKE) clean2 B=$(BD) QUIET=$(QUIET) clean-release: - @$(MAKE) clean2 B=$(BR) + @$(MAKE) clean2 B=$(BR) QUIET=$(QUIET) clean2: +ifneq ($(QUIET),1) @echo "CLEAN $(B)" +endif @rm -f $(OBJ) @rm -f $(OBJ_D_FILES) @rm -f $(TARGETS) @@ -2234,13 +2260,15 @@ clean2: toolsclean: toolsclean-debug toolsclean-release toolsclean-debug: - @$(MAKE) toolsclean2 B=$(BD) + @$(MAKE) toolsclean2 B=$(BD) QUIET=$(QUIET) toolsclean-release: - @$(MAKE) toolsclean2 B=$(BR) + @$(MAKE) toolsclean2 B=$(BR) QUIET=$(QUIET) toolsclean2: +ifneq ($(QUIET),1) @echo "TOOLS_CLEAN $(B)" +endif @rm -f $(TOOLSOBJ) @rm -f $(TOOLSOBJ_D_FILES) @rm -f $(LBURG) $(DAGCHECK_C) $(Q3RCC) $(Q3CPP) $(Q3LCC) $(Q3ASM) diff --git a/ratoa_gamecode/code/cgame/cg_cvar.h b/ratoa_gamecode/code/cgame/cg_cvar.h index b600c6d..e926f8a 100644 --- a/ratoa_gamecode/code/cgame/cg_cvar.h +++ b/ratoa_gamecode/code/cgame/cg_cvar.h @@ -73,6 +73,7 @@ CG_CVAR( cg_debugAnim, "cg_debuganim", "0", CVAR_CHEAT ) CG_CVAR( cg_debugPosition, "cg_debugposition", "0", CVAR_CHEAT ) CG_CVAR( cg_debugEvents, "cg_debugevents", "0", CVAR_CHEAT ) CG_CVAR( cg_drawBBox, "cg_drawBBox", "0", CVAR_CHEAT ) +CG_CVAR( cg_debugBotAim, "cg_debugBotAim", "0", CVAR_CHEAT ) CG_CVAR( cg_errorDecay, "cg_errordecay", "100", 0 ) CG_CVAR( cg_nopredict, "cg_nopredict", "0", 0 ) CG_CVAR( cg_checkChangedEvents, "cg_checkChangedEvents", "1", CVAR_ARCHIVE ) diff --git a/ratoa_gamecode/code/cgame/cg_local.h b/ratoa_gamecode/code/cgame/cg_local.h index 509f6da..3323f63 100644 --- a/ratoa_gamecode/code/cgame/cg_local.h +++ b/ratoa_gamecode/code/cgame/cg_local.h @@ -1713,6 +1713,8 @@ int CG_ReliablePingFromSnaps(snapshot_t *snap, snapshot_t *nextsnap); void CG_RefreshDemoPovDisplayPing( void ); int CG_ScoreboardDisplayPing( int clientNum, int storedPing ); void CG_AddBoundingBox( centity_t *cent ); +void CG_AddBotAimDebug( centity_t *cent ); +void CG_DrawBotAimFollowFirstPerson( void ); qboolean CG_Cvar_ClampInt( const char *name, vmCvar_t *vmCvar, int min, int max ); qboolean CG_IsOwnMissile(centity_t *missile); diff --git a/ratoa_gamecode/code/cgame/cg_players.c b/ratoa_gamecode/code/cgame/cg_players.c index 4798343..d179c48 100644 --- a/ratoa_gamecode/code/cgame/cg_players.c +++ b/ratoa_gamecode/code/cgame/cg_players.c @@ -4109,6 +4109,7 @@ void CG_Player( centity_t *cent ) { // add powerups floating behind the player CG_PlayerPowerups( cent, &torso ); CG_AddBoundingBox( cent ); + CG_AddBotAimDebug( cent ); } diff --git a/ratoa_gamecode/code/cgame/cg_unlagged.c b/ratoa_gamecode/code/cgame/cg_unlagged.c index 0ab9891..943187a 100644 --- a/ratoa_gamecode/code/cgame/cg_unlagged.c +++ b/ratoa_gamecode/code/cgame/cg_unlagged.c @@ -1572,6 +1572,390 @@ void CG_AddBoundingBox( centity_t *cent ) { trap_R_AddPolyToScene( bboxShader_nocull, 4, verts ); } +#define CG_BOTAIMDBG_FOLLOW 1 +#define CG_BOTAIMDBG_ALL 2 +#define CG_BOTAIMDBG_VIEW 4 + +#define CG_BOTAIMDBG_RAIL_TIME 1 +#define CG_BOTAIMDBG_BODY_HEIGHT 0.45f /* fraction of viewheight for rail start (chest) */ + +static qboolean CG_ClientIsBot( int clientNum ) { + const char *configstring; + const char *skill; + + if (clientNum < 0 || clientNum >= MAX_CLIENTS) { + return qfalse; + } + if (cgs.clientinfo[clientNum].botSkill > 0) { + return qtrue; + } + /* Late-join bots: clientinfo can lag CS_PLAYERS by a frame. */ + configstring = CG_ConfigString(clientNum + CS_PLAYERS); + if (!configstring[0]) { + return qfalse; + } + skill = Info_ValueForKey(configstring, "skill"); + return (skill && skill[0]); +} + +/* +================= +CG_BotAimDebugHasData + +Server aim debug is signaled via STAT_EXTFLAGS (reliable), with legacy +eFlags / grapplePoint / origin2 fallbacks. +================= +*/ +static qboolean CG_BotAimDebugHasData( centity_t *cent, qboolean usePsAim ) { + if (usePsAim) { + if (cg.snap->ps.stats[STAT_EXTFLAGS] & EXTFL_BOT_AIM_DEBUG) { + return qtrue; + } + if (cg.snap->ps.eFlags & EF_BOT_AIM_DEBUG) { + return qtrue; + } + if (VectorLengthSquared(cg.snap->ps.grapplePoint) > 64.0f) { + return qtrue; + } + return qfalse; + } + + if (cent->currentState.eFlags & EF_BOT_AIM_DEBUG) { + return qtrue; + } + if (VectorLengthSquared(cent->currentState.origin2) > 64.0f) { + return qtrue; + } + return qfalse; +} + +/* +================= +CG_BotAimDebugRail + +Rail-core segment (same path as weapon rails — always visible). +================= +*/ +static void CG_BotAimDebugRail( const vec3_t start, const vec3_t end, int r, int g, int b ) { + localEntity_t *le; + refEntity_t *re; + vec3_t delta; + float len; + + VectorSubtract(end, start, delta); + len = VectorLength(delta); + if (len < 8.0f) { + return; + } + + le = CG_AllocLocalEntity(); + re = &le->refEntity; + + le->leType = LE_FADE_RGB; + le->startTime = cg.time; + le->endTime = cg.time + CG_BOTAIMDBG_RAIL_TIME; + le->lifeRate = 1.0f / (float)CG_BOTAIMDBG_RAIL_TIME; + + re->reType = RT_RAIL_CORE; + re->customShader = cgs.media.railCoreShader; + re->shaderTime = cg.time / 1000.0f; + VectorCopy(start, re->origin); + VectorCopy(end, re->oldorigin); + re->shaderRGBA[0] = r; + re->shaderRGBA[1] = g; + re->shaderRGBA[2] = b; + re->shaderRGBA[3] = 255; + le->color[0] = r / 255.0f; + le->color[1] = g / 255.0f; + le->color[2] = b / 255.0f; + le->color[3] = 1.0f; + AxisClear(re->axis); +} + +/* +================= +CG_BotAimDebugSprite + +Small oriented sprite at a trace impact (first-person friendly). +================= +*/ +static void CG_BotAimDebugSprite( const vec3_t origin, int r, int g, int b ) { + localEntity_t *le; + refEntity_t *re; + + le = CG_AllocLocalEntity(); + re = &le->refEntity; + + le->leType = LE_FADE_RGB; + le->startTime = cg.time; + le->endTime = cg.time + CG_BOTAIMDBG_RAIL_TIME; + le->lifeRate = 1.0f / (float)CG_BOTAIMDBG_RAIL_TIME; + + re->reType = RT_SPRITE; + re->customShader = cgs.media.energyMarkShader; + if (!re->customShader) { + re->customShader = cgs.media.bulletMarkShader; + } + re->shaderTime = cg.time / 1000.0f; + re->radius = 14; + VectorCopy(origin, re->origin); + re->shaderRGBA[0] = r; + re->shaderRGBA[1] = g; + re->shaderRGBA[2] = b; + re->shaderRGBA[3] = 255; + le->color[0] = r / 255.0f; + le->color[1] = g / 255.0f; + le->color[2] = b / 255.0f; + le->color[3] = 1.0f; + AxisClear(re->axis); +} + +/* +================= +CG_BotAimDebugMark + +Decal on the first surface a debug trace hits (skipped when cg_addMarks is 0). +================= +*/ +static void CG_BotAimDebugMark( const vec3_t origin, const vec3_t normal, + int r, int g, int b ) { + qhandle_t mark; + + if (!cg_addMarks.integer) { + return; + } + + mark = cgs.media.bulletMarkShader; + if (!mark) { + mark = cgs.media.energyMarkShader; + } + if (!mark) { + return; + } + + CG_ImpactMark( mark, origin, normal, random() * 360.0f, + r / 255.0f, g / 255.0f, b / 255.0f, 1.0f, qfalse, 14, qtrue ); +} + +/* +================= +CG_BotAimDebugDrawLine + +Always draw the full aim/view segment, then optionally mark the first +world hit along that line (trace starts slightly forward to avoid self-hits). +================= +*/ +static void CG_BotAimDebugDrawLine( const vec3_t start, const vec3_t endPos, + int skipClientNum, int r, int g, int b, int a ) { + trace_t tr; + vec3_t dir, traceStart, traceEnd; + float dist; + + VectorSubtract( endPos, start, dir ); + dist = VectorLength( dir ); + if (dist < 8.0f) { + return; + } + + CG_BotAimDebugRail( start, endPos, r, g, b ); + + VectorNormalize( dir ); + VectorMA( start, dist, dir, traceEnd ); + VectorMA( start, 12.0f, dir, traceStart ); + + if (dist <= 24.0f) { + return; + } + + CG_Trace( &tr, traceStart, NULL, NULL, traceEnd, skipClientNum, MASK_SHOT ); + if (tr.fraction < 1.0f) { + CG_BotAimDebugSprite( tr.endpos, r, g, b ); + CG_BotAimDebugMark( tr.endpos, tr.plane.normal, r, g, b ); + } +} + +/* + * Player vertical offset from lerpOrigin. heightScale 1 = eye, ~0.45 = chest. + */ +static void CG_BotAimDebugEye( centity_t *cent, vec3_t out, float heightScale ); + +/* +================= +CG_DrawBotAimFollowFirstPerson + +Follow spectator: rails from bot chest toward harness aim / view axes. +================= +*/ +void CG_DrawBotAimFollowFirstPerson( void ) { + int mode; + centity_t *cent; + vec3_t bodyStart, aimPoint, viewEnd, forward; + qboolean drawAim; + qboolean drawView; + + if (!cg_debugBotAim.integer || !cg.snap) { + return; + } + if (!(cg.snap->ps.pm_flags & PMF_FOLLOW) && !cg.demoPlayback) { + return; + } + if (!CG_ClientIsBot(cg.snap->ps.clientNum)) { + return; + } + if (!CG_BotAimDebugHasData(NULL, qtrue)) { + return; + } + + mode = cg_debugBotAim.integer; + drawAim = qfalse; + drawView = qfalse; + if (mode & CG_BOTAIMDBG_VIEW) { + drawView = qtrue; + } + if ((mode & CG_BOTAIMDBG_ALL) || + (mode & CG_BOTAIMDBG_FOLLOW)) { + drawAim = qtrue; + } + if (!drawAim && !drawView) { + return; + } + + cent = &cg_entities[cg.snap->ps.clientNum]; + CG_BotAimDebugEye( cent, bodyStart, CG_BOTAIMDBG_BODY_HEIGHT ); + + VectorCopy( cg.snap->ps.grapplePoint, aimPoint ); + if (VectorLengthSquared( aimPoint ) < 64.0f ) { + AngleVectors( cent->lerpAngles, forward, NULL, NULL ); + VectorMA( bodyStart, 2048.0f, forward, aimPoint ); + } + + if (drawAim) { + CG_BotAimDebugDrawLine( bodyStart, aimPoint, + cg.snap->ps.clientNum, 80, 255, 120, 220 ); + } + + if (drawView) { + AngleVectors( cg.refdefViewAngles, forward, NULL, NULL ); + VectorMA( bodyStart, 4096.0f, forward, viewEnd ); + CG_BotAimDebugDrawLine( bodyStart, viewEnd, + cg.snap->ps.clientNum, 255, 220, 80, 180 ); + } +} + +/* +================= +CG_AddBotAimDebug + +Draw harness aim (eye -> aim point) when server sets EF_BOT_AIM_DEBUG. +Followed player: aim point is in snap->ps.grapplePoint; others use origin2. +cg_debugBotAim: 1 = followed bot, 2 = all bots, 4 = also draw viewangles ray. +================= +*/ +/* + * Player vertical offset from lerpOrigin. heightScale 1 = eye, ~0.45 = chest. + */ +static void CG_BotAimDebugEye( centity_t *cent, vec3_t out, float heightScale ) { + int viewHeight; + int x, zd, zu; + + viewHeight = 32; + if (cent->currentState.number == cg.predictedPlayerState.clientNum) { + viewHeight = cg.predictedPlayerState.viewheight; + } else { + x = (cent->currentState.solid & 255); + zd = ((cent->currentState.solid >> 8) & 255); + zu = ((cent->currentState.solid >> 16) & 255) - 32; + if (zu > 8) { + viewHeight = zu; + } + (void)zd; + (void)x; + } + + VectorCopy(cent->lerpOrigin, out); + out[2] += (int)(viewHeight * heightScale); +} + +static void CG_BotAimDebugResolveAimPoint( centity_t *cent, qboolean usePsAim, + vec3_t aimPoint ) { + vec3_t forward; + + if (usePsAim) { + VectorCopy(cg.snap->ps.grapplePoint, aimPoint); + } else { + VectorCopy(cent->currentState.origin2, aimPoint); + } + + if (VectorLengthSquared(aimPoint) > 64.0f) { + return; + } + + AngleVectors(cent->lerpAngles, forward, NULL, NULL); + VectorMA(cent->lerpOrigin, 2048.0f, forward, aimPoint); +} + +void CG_AddBotAimDebug( centity_t *cent ) { + int mode; + vec3_t eye, viewEnd, forward, aimPoint; + qboolean usePsAim; + + if (!cg_debugBotAim.integer) { + return; + } + if (!cg.snap) { + return; + } + if (cent->currentState.eType != ET_PLAYER) { + return; + } + if (cent->currentState.eFlags & EF_DEAD) { + return; + } + + mode = cg_debugBotAim.integer; + + if (!CG_ClientIsBot(cent->currentState.clientNum)) { + return; + } + + usePsAim = qfalse; + if (cent->currentState.clientNum == cg.snap->ps.clientNum) { + usePsAim = qtrue; + } + + if (!CG_BotAimDebugHasData(cent, usePsAim)) { + return; + } + + if (!(mode & CG_BOTAIMDBG_ALL)) { + if (!(cg.snap->ps.pm_flags & PMF_FOLLOW) && !cg.demoPlayback) { + return; + } + if (cent->currentState.clientNum != cg.snap->ps.clientNum) { + return; + } + } + + /* Follow spectator view draws from cg.refdef in CG_DrawBotAimFollowFirstPerson. */ + if (cent->currentState.clientNum == cg.snap->ps.clientNum && + (cg.snap->ps.pm_flags & PMF_FOLLOW || cg.demoPlayback)) { + return; + } + + CG_BotAimDebugEye( cent, eye, 1.0f ); + CG_BotAimDebugResolveAimPoint(cent, usePsAim, aimPoint); + + CG_BotAimDebugDrawLine( eye, aimPoint, cent->currentState.number, + 80, 255, 120, 220 ); + + if (mode & CG_BOTAIMDBG_VIEW) { + AngleVectors(cent->lerpAngles, forward, NULL, NULL); + VectorMA(eye, 4096.0f, forward, viewEnd); + CG_BotAimDebugDrawLine( eye, viewEnd, cent->currentState.number, + 255, 220, 80, 180 ); + } +} + /* ================ CG_Cvar_ClampInt diff --git a/ratoa_gamecode/code/cgame/cg_view.c b/ratoa_gamecode/code/cgame/cg_view.c index 54adbb3..dc7c080 100644 --- a/ratoa_gamecode/code/cgame/cg_view.c +++ b/ratoa_gamecode/code/cgame/cg_view.c @@ -1055,6 +1055,7 @@ void CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demo // build the render lists if ( !cg.hyperspace ) { CG_AddPacketEntities(); // adter calcViewValues, so predicted player state is correct + CG_DrawBotAimFollowFirstPerson(); CG_AddPredictedMissiles(); CG_AddMarks(); CG_AddParticles (); diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index 539c2e8..e595e65 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -13,16 +13,21 @@ BOT AIM HARNESS (v1) — see ai_aim_harness.h #include "../botlib/be_ai_move.h" #include "../botlib/be_ai_weap.h" #include "ai_main.h" +#include "ai_dmq3.h" #include "chars.h" #include "ai_aim_harness.h" +#include "ai_bot_enhanced.h" +#include "ai_dmq3.h" -vmCvar_t bot_humanizeaim; +vmCvar_t bot_enhanced_aim; +vmCvar_t bot_debugAim; -/* Forward — defined in ai_dmq3.c */ +/* Forward — defined in ai_dmq3.c / ai_main.c */ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int ent); qboolean BotIsDead(bot_state_t *bs); +void BotAI_Trace(bsp_trace_t *bsptrace, vec3_t start, vec3_t mins, vec3_t maxs, + vec3_t end, int passent, int contentmask); -extern vmCvar_t bot_challenge; extern vmCvar_t bot_thinktime; extern bot_state_t *botstates[MAX_CLIENTS]; @@ -31,15 +36,27 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_STIFFNESS_FLICK 500.0f #define AIMH_DAMPING_TRACK 36.0f #define AIMH_DAMPING_FLICK 22.0f -#define AIMH_ROAM_STIFFNESS 80.0f -#define AIMH_ROAM_DAMPING 40.0f +#define AIMH_ROAM_STIFFNESS 300.0f +#define AIMH_ROAM_DAMPING 34.0f +#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_VEL_SCALE 1.85f #define AIMH_MOTOR_NOISE_SCALE 2.0f #define AIMH_HITSCAN_LEAD_EXTRA 0.025f #define AIMH_HITSCAN_LEAD_SCALE 0.9f +#define AIMH_MOTOR_LAG_BASE 0.055f +#define AIMH_MOTOR_LAG_SKILL 0.055f +#define AIMH_HITSCAN_LEAD_MAX 0.35f +#define AIMH_VERT_LEAD_MIN 0.25f +#define AIMH_VERT_LEAD_MAX 0.95f #define AIMH_AIM_HEIGHT 32.0f #define AIMH_MAX_VERT_LEAD_VEL 480.0f -#define AIMH_VERT_LEAD_SCALE 0.2f +#define AIMH_FEEDFORWARD_BASE 0.45f +#define AIMH_FEEDFORWARD_SKILL 0.55f +#define AIMH_YAW_CATCHUP_ERR 10.0f +#define AIMH_YAW_CATCHUP_RATE 8.0f #define AIMH_ENEMY_Z_SPIKE 72.0f #define AIMH_PITCH_RESET_ERR 14.0f #define AIMH_PITCH_CATCHUP_ERR 10.0f @@ -56,8 +73,119 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_RECOVER_CATCHUP_MULT 1.45f #define AIMH_ACQUIRE_DURATION 0.4f #define AIMH_ACQUIRE_FLICK_ANGLE 44.0f +/* Engage profile: 0 = close/urgent (jerky), 1 = far/calm (smooth). Option 2: angular goal rate. */ +#define AIMH_ENGAGE_DIST_NEAR 256.0f +#define AIMH_ENGAGE_DIST_FAR 1200.0f +#define AIMH_ENGAGE_REF_ANGVEL 140.0f +#define AIMH_ENGAGE_CLOSE_MAXVEL 1.35f +#define AIMH_ENGAGE_FAR_MAXVEL 0.78f +#define AIMH_ENGAGE_CLOSE_STIFF 1.15f +#define AIMH_ENGAGE_FAR_STIFF 0.70f +#define AIMH_ENGAGE_CLOSE_DAMP 0.88f +#define AIMH_ENGAGE_FAR_DAMP 1.12f +#define AIMH_ENGAGE_CLOSE_NOISE 1.45f +#define AIMH_ENGAGE_FAR_NOISE 0.55f +#define AIMH_ENGAGE_CLOSE_FF 1.00f +#define AIMH_ENGAGE_FAR_FF 0.40f +#define AIMH_ENGAGE_CLOSE_CATCHUP 1.20f +#define AIMH_ENGAGE_FAR_CATCHUP 0.65f +#define AIMH_ENGAGE_CLOSE_FLICK 0.72f +#define AIMH_ENGAGE_FAR_FLICK 1.12f +#define AIMH_ROAM_IDEAL_SNAP_PITCH 8.0f +#define AIMH_ROAM_IDEAL_SNAP_YAW 12.0f +/* Resync motor state to playerState only on large desync (avoids per-frame drag). */ +#define AIMH_PS_DESYNC_PITCH 14.0f +#define AIMH_PS_DESYNC_YAW 18.0f +#define AIMH_PS_DESYNC_BLEND 0.65f +/* Fixed motor sub-steps so low sv_fps (large frame dt) stays stable on dedicated. */ +#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 + +static int bot_enhanced_aim_last = -1; +static int bot_debugAim_last = -1; + +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 float BotAimHarness_Lerp(float a, float b, float t) { + return a + (b - a) * t; +} + +static float BotAimHarness_Smoothstep(float edge0, float edge1, float x) { + float t; + + if (edge1 <= edge0) { + return x >= edge1 ? 1.0f : 0.0f; + } + t = (x - edge0) / (edge1 - edge0); + if (t < 0.0f) { + t = 0.0f; + } else if (t > 1.0f) { + t = 1.0f; + } + return t * t * (3.0f - 2.0f * t); +} + +/* + * 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. + */ +static float BotAimHarness_GetEngageFar(bot_state_t *bs, const vec3_t goal, float goalDt) { + vec3_t dir; + float dist, distanceFar, urgency, angularRate, pitchRate, yawRate; + + dist = AIMH_ENGAGE_DIST_FAR; + if (VectorLengthSquared(bs->aimh_combat_target) > 1.0f) { + VectorSubtract(bs->aimh_combat_target, bs->eye, dir); + dist = VectorLength(dir); + } else if (bs->enemy >= 0) { + aas_entityinfo_t entinfo; + + BotEntityInfo(bs->enemy, &entinfo); + if (entinfo.valid) { + VectorSubtract(entinfo.origin, bs->eye, dir); + dist = VectorLength(dir); + } + } + distanceFar = BotAimHarness_Smoothstep(AIMH_ENGAGE_DIST_NEAR, AIMH_ENGAGE_DIST_FAR, dist); -static int bot_humanizeaim_last = -1; + if (bs->aimh_last_goal_time <= 0.0f || goalDt <= 0.001f || goalDt > 0.5f) { + return distanceFar; + } + + pitchRate = BotAimHarness_PitchDiff(goal[PITCH], bs->aimh_last_goal_pitch) / goalDt; + yawRate = BotAimHarness_YawDiff(goal[YAW], bs->aimh_last_goal_yaw) / goalDt; + angularRate = sqrt(pitchRate * pitchRate + yawRate * yawRate); + urgency = angularRate / AIMH_ENGAGE_REF_ANGVEL; + if (urgency < 0.0f) { + urgency = 0.0f; + } else if (urgency > 1.0f) { + urgency = 1.0f; + } + + return distanceFar < (1.0f - urgency) ? distanceFar : (1.0f - urgency); +} + +static void BotAimHarness_ApplyEngageProfile(float engageFar, float *stiffness, + float *damping, float *maxVel, float *motorNoise, float *ffGain, + float *catchupMult) { + *stiffness *= BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_STIFF, AIMH_ENGAGE_FAR_STIFF, engageFar); + *damping *= BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_DAMP, AIMH_ENGAGE_FAR_DAMP, engageFar); + *maxVel *= BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_MAXVEL, AIMH_ENGAGE_FAR_MAXVEL, engageFar); + *motorNoise *= BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_NOISE, AIMH_ENGAGE_FAR_NOISE, engageFar); + *ffGain = BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_FF, AIMH_ENGAGE_FAR_FF, engageFar); + *catchupMult = BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_CATCHUP, AIMH_ENGAGE_FAR_CATCHUP, engageFar); +} static float BotAimHarness_AngleDiff(float ang1, float ang2) { float diff; @@ -109,7 +237,7 @@ static void BotAimHarness_RefreshEye(bot_state_t *bs) { bs->eye[2] += bs->cur_ps.viewheight; } -static int BotAimHarness_AimTargetValid(bot_state_t *bs) { +int BotAimHarness_AimTargetValid(bot_state_t *bs) { if (VectorCompare(bs->aimtarget, vec3_origin)) { return 0; } @@ -134,37 +262,157 @@ static void BotAimHarness_ClampVerticalLeadVel(float *vel) { } static void BotAimHarness_ApplyLead(vec3_t target, const vec3_t vel, float leadTime, - qboolean reducedVertical) { + float vertScale) { vec3_t leadVel; - float vertScale; VectorCopy(vel, leadVel); BotAimHarness_ClampVerticalLeadVel(leadVel); - vertScale = reducedVertical ? AIMH_VERT_LEAD_SCALE : 1.0f; target[0] += leadVel[0] * leadTime; target[1] += leadVel[1] * leadTime; target[2] += leadVel[2] * leadTime * vertScale; } +static float BotAimHarness_HitscanLeadTime(bot_state_t *bs) { + float thinkSec, motorLag, leadTime, aimSkill; + + aimSkill = bs->aimh_aim_skill; + 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; + 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; + } + return leadTime; +} + +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; +} + /* - * Same reference BotCheckAttack uses for "in field of view" (aimtarget, not live torso). + * Live combat aim point: entity origin (with aimtarget Z when available) + skill-scaled lead. */ -static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { - vec3_t dir; +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; + } + + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + return qfalse; + } + + if (bs->aimh_last_enemy_z != 0.0f && + fabs(entinfo.origin[2] - bs->aimh_last_enemy_z) > AIMH_ENEMY_Z_SPIKE) { + bs->aimh_vel[PITCH] = 0.0f; + } + bs->aimh_last_enemy_z = entinfo.origin[2]; + VectorCopy(entinfo.origin, target); + target[2] += 8.0f; if (BotAimHarness_AimTargetValid(bs)) { - VectorSubtract(bs->aimtarget, bs->eye, dir); - } else if (bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { - aas_entityinfo_t entinfo; + target[2] = bs->aimtarget[2]; + } - BotEntityInfo(bs->enemy, &entinfo); - if (!entinfo.valid) { - VectorCopy(bs->aimh_goal, angles); - return; + VectorSubtract(entinfo.origin, entinfo.lastvisorigin, vel); + if (entinfo.update_time > 0.001f) { + VectorScale(vel, 1.0f / entinfo.update_time, vel); + } else { + VectorClear(vel); + } + BotAimHarness_ClampVerticalLeadVel(vel); + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + vertScale = BotAimHarness_VertLeadScale(bs); + + if (wi.speed > 0.0f) { + VectorSubtract(target, bs->eye, dir); + dist = VectorLength(dir); + flightTime = dist / wi.speed; + if (flightTime > 2.0f) { + flightTime = 2.0f; } - VectorSubtract(entinfo.origin, bs->eye, dir); - dir[2] += AIMH_AIM_HEIGHT; + BotAimHarness_ApplyLead(target, vel, flightTime, vertScale * 0.65f); + } else { + leadTime = BotAimHarness_HitscanLeadTime(bs); + BotAimHarness_ApplyLead(target, vel, leadTime, vertScale); + } + + 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; + + if (!BotAimHarness_IsActive()) { + return; + } + if (!entinfoPtr || 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; +} + +/* + * Fire and motor intent: aim at aimtarget (BotAimAtEnemy hittable point). Fall back to + * 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; + + if (BotAimHarness_AimTargetValid(bs)) { + VectorSubtract(bs->aimtarget, 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 { VectorCopy(bs->aimh_goal, angles); return; @@ -174,6 +422,279 @@ static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { angles[YAW] = AngleMod(angles[YAW]); } +static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { + BotAimHarness_GetCombatAimAngles(bs, angles); +} + +/* + * Body aim point for fire permission (no think-time lead; motor still uses lead). + */ +static void BotAimHarness_GetEnemyFirePoint(bot_state_t *bs, vec3_t point) { + aas_entityinfo_t entinfo; + + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + VectorCopy(bs->eye, point); + return; + } + + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid) { + if (BotAimHarness_AimTargetValid(bs)) { + VectorCopy(bs->aimtarget, point); + } else { + VectorCopy(bs->eye, 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; + + VectorSubtract(firePoint, bs->eye, dir); + dist = VectorLength(dir); + if (dist < AIMH_FIRE_FOV_DIST) { + return AIMH_FIRE_FOV_NEAR; + } + return AIMH_FIRE_FOV_FAR; +} + +static qboolean BotAimHarness_FireLosToEnemy(bot_state_t *bs, vec3_t firePoint) { + bsp_trace_t trace; + int enemy; + + enemy = bs->enemy; + BotAI_Trace(&trace, bs->eye, NULL, NULL, firePoint, bs->client, MASK_SHOT); + if (trace.ent == enemy) { + return qtrue; + } + if (trace.fraction >= 0.97f) { + return qtrue; + } + if (BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, enemy) > + AIMH_FIRE_MIN_VISIBILITY) { + return qtrue; + } + return qfalse; +} + +/* + * Kinda-on-target: view roughly toward enemy body or intent; not a guaranteed hit trace. + */ +static qboolean BotAimHarness_PassesLooseAimGate(bot_state_t *bs, const weaponinfo_t *wi, + vec3_t firePoint) { + vec3_t dir, toEnemy, attackAngles; + float fov, dist; + + VectorSubtract(firePoint, bs->eye, dir); + dist = VectorLength(dir); + if (dist < 1.0f) { + return qtrue; + } + + vectoangles(dir, toEnemy); + fov = BotAimHarness_FireFov(bs, firePoint); + + if (InFieldOfVision(bs->viewangles, fov, toEnemy)) { + return qtrue; + } + + BotAimHarness_GetAttackAimAngles(bs, attackAngles); + if (InFieldOfVision(bs->viewangles, fov + AIMH_FIRE_VIEW_SLACK, attackAngles)) { + return qtrue; + } + if (InFieldOfVision(attackAngles, AIMH_FIRE_VIEW_SLACK + 6.0f, toEnemy)) { + return qtrue; + } + + /* Projectiles: allow a bit more angle slack; still need plausible LOS */ + if (wi->speed > 0.0f) { + if (InFieldOfVision(bs->viewangles, fov + 10.0f, attackAngles)) { + return qtrue; + } + } + + return qfalse; +} + +static qboolean BotAimHarness_WeaponReady(bot_state_t *bs) { + if (bs->cur_ps.weaponstate == WEAPON_RAISING || + bs->cur_ps.weaponstate == WEAPON_DROPPING) { + return qfalse; + } + if (bs->cur_ps.weapon != bs->weaponnum) { + return qfalse; + } + return qtrue; +} + +/* Reaction, throttle, weapon swap — evaluated on bot think only */ +static qboolean BotAimHarness_PassesThinkFireGates(bot_state_t *bs) { + float reactiontime, firethrottle; + + if (bs->enemy < 0) { + return qfalse; + } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + return qfalse; + } + if (BotTargetPlayerIsDead(bs)) { + return qfalse; + } + + reactiontime = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, + 0, 1); + if (bs->enemysight_time > FloatTime() - reactiontime) { + return qfalse; + } + if (bs->teleport_time > FloatTime() - reactiontime) { + return qfalse; + } + if (bs->weaponchange_time > FloatTime() - 0.1f) { + return qfalse; + } + + 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_IsContinuousFireWeapon(const weaponinfo_t *wi) { + if (wi->flags & WFL_FIRERELEASED) { + return qfalse; + } + return qtrue; +} + +static qboolean BotAimHarness_WantsSuppressiveFire(bot_state_t *bs, + const weaponinfo_t *wi, vec3_t firePoint) { + vec3_t dir; + + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + if (!BotAimHarness_WeaponReady(bs)) { + return qfalse; + } + + if (bs->weaponnum == WP_GAUNTLET) { + VectorSubtract(firePoint, bs->eye, dir); + if (VectorLengthSquared(dir) > Square(60)) { + return qfalse; + } + } + + if (!BotAimHarness_FireLosToEnemy(bs, firePoint)) { + return qfalse; + } + if (!BotAimHarness_PassesLooseAimGate(bs, wi, firePoint)) { + return qfalse; + } + + return qtrue; +} + +/* + * Think frame: personality gates + set hold-fire for MG/LG/plasma-style weapons. + */ +void BotAimHarness_CheckAttack(bot_state_t *bs) { + weaponinfo_t wi; + vec3_t firePoint; + + bs->aimh_hold_fire = qfalse; + + if (!BotAimHarness_IsActive()) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + return; + } + if (!BotAimHarness_PassesThinkFireGates(bs)) { + return; + } + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + BotAimHarness_GetEnemyFirePoint(bs, firePoint); + + if (!BotAimHarness_WantsSuppressiveFire(bs, &wi, firePoint)) { + return; + } + + if (BotAimHarness_IsContinuousFireWeapon(&wi)) { + bs->aimh_hold_fire = qtrue; + trap_EA_Attack(bs->client); + return; + } + + if (wi.flags & WFL_FIRERELEASED) { + if (bs->flags & BFL_ATTACKED) { + trap_EA_Attack(bs->client); + } + bs->flags ^= BFL_ATTACKED; + } +} + +/* + * Every input frame: hold +attack while aimh_hold_fire (MG/LG etc). + */ +void BotAimHarness_ApplyCombatFire(bot_state_t *bs) { + weaponinfo_t wi; + vec3_t firePoint; + + if (!BotAimHarness_IsActive() || !bs->aimh_hold_fire) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + bs->aimh_hold_fire = qfalse; + return; + } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + bs->aimh_hold_fire = qfalse; + return; + } + if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { + return; + } + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + if (!BotAimHarness_IsContinuousFireWeapon(&wi)) { + return; + } + + BotAimHarness_GetEnemyFirePoint(bs, firePoint); + if (!BotAimHarness_WantsSuppressiveFire(bs, &wi, firePoint)) { + bs->aimh_hold_fire = qfalse; + return; + } + + trap_EA_Attack(bs->client); +} + +int BotAimHarness_TryAttack(bot_state_t *bs) { + BotAimHarness_CheckAttack(bs); + return bs->aimh_hold_fire; +} + /* * 0 = ok, 1 = soft (motor boost only), 2 = hard (also damp motor). */ @@ -280,46 +801,226 @@ static void BotAimHarness_OnEnemyChange(bot_state_t *bs) { bs->aimh_acquire_until = FloatTime() + AIMH_ACQUIRE_DURATION; bs->aimh_vel[PITCH] *= 0.4f; bs->aimh_vel[YAW] *= 0.4f; + 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_tracked_ideal_pitch = bs->viewangles[PITCH]; + bs->aimh_tracked_ideal_yaw = bs->viewangles[YAW]; +} + +static void BotAimHarness_ClearEntityDebug(gentity_t *ent) { + if (!ent || !ent->client) { + return; + } + ent->s.eFlags &= ~EF_BOT_AIM_DEBUG; + VectorClear(ent->s.origin2); + ent->client->ps.eFlags &= ~EF_BOT_AIM_DEBUG; + ent->client->ps.stats[STAT_EXTFLAGS] &= ~EXTFL_BOT_AIM_DEBUG; + VectorClear(ent->client->ps.grapplePoint); +} + +static void BotAimHarness_AnglesToAimPoint(bot_state_t *bs, float pitch, float yaw, + vec3_t point) { + vec3_t dir; + vec3_t angles; + + angles[PITCH] = BotAimHarness_ClampPitch(pitch); + angles[YAW] = AngleMod(yaw); + angles[ROLL] = 0; + AngleVectors(angles, dir, NULL, NULL); + VectorMA(bs->eye, 2048.0f, dir, point); +} + +/* + * Debug aim point: combat = fire/motor intent (aimtarget); 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_AimTargetValid(bs)) { + VectorCopy(bs->aimtarget, point); + return qtrue; + } + if (VectorLengthSquared(bs->aimh_combat_target) > 1.0f) { + VectorCopy(bs->aimh_combat_target, point); + return qtrue; + } + BotAimHarness_GetCombatAimAngles(bs, wishAngles); + BotAimHarness_AnglesToAimPoint(bs, wishAngles[PITCH], wishAngles[YAW], point); + return qtrue; + } + + if (BotAimHarness_IsActive()) { + BotAimHarness_AnglesToAimPoint(bs, bs->ideal_viewangles[PITCH], + bs->ideal_viewangles[YAW], point); + } else { + BotAimHarness_AnglesToAimPoint(bs, bs->viewangles[PITCH], + bs->viewangles[YAW], point); + } + return qtrue; +} + +static void BotAimHarness_ApplyEntityDebug(gentity_t *ent, const vec3_t point) { + vec3_t snapped; + + VectorCopy(point, snapped); + SnapVector(snapped); + VectorCopy(snapped, ent->s.origin2); + VectorCopy(snapped, ent->client->ps.grapplePoint); + ent->s.eFlags |= EF_BOT_AIM_DEBUG; + ent->client->ps.eFlags |= EF_BOT_AIM_DEBUG; + ent->client->ps.stats[STAT_EXTFLAGS] |= EXTFL_BOT_AIM_DEBUG; +} + +static void BotAimHarness_DebugSync(bot_state_t *bs) { + gentity_t *ent; + vec3_t point; + + trap_Cvar_Update(&bot_debugAim); + ent = &g_entities[bs->entitynum]; + if (!ent->client) { + return; + } + + BotAI_GetClientState(bs->client, &bs->cur_ps); + + if (!bot_debugAim.integer) { + BotAimHarness_ClearEntityDebug(ent); + return; + } + + BotAimHarness_RefreshEye(bs); + if (!BotAimHarness_GetDebugAimPoint(bs, point)) { + BotAimHarness_ClearEntityDebug(ent); + return; + } + + BotAimHarness_ApplyEntityDebug(ent, point); +} + +void BotAimHarness_SyncEntityFromPlayerState(gentity_t *ent) { + if (!ent || !ent->client) { + return; + } + if (!(ent->client->ps.stats[STAT_EXTFLAGS] & EXTFL_BOT_AIM_DEBUG)) { + ent->s.eFlags &= ~EF_BOT_AIM_DEBUG; + return; + } + ent->s.eFlags |= EF_BOT_AIM_DEBUG; + VectorCopy(ent->client->ps.grapplePoint, ent->s.origin2); +} + +void BotAimHarness_PostInputSync(bot_state_t *bs) { + if (!bs || !bs->inuse) { + return; + } + BotAimHarness_DebugSync(bs); + BotAimHarness_SyncEntityFromPlayerState(&g_entities[bs->entitynum]); +} + +void BotAimHarness_SyncClientDebug(int clientNum) { + bot_state_t *bs; + + if (clientNum < 0 || clientNum >= MAX_CLIENTS) { + return; + } + bs = botstates[clientNum]; + if (!bs || !bs->inuse) { + return; + } + if (!g_entities[clientNum].client) { + return; + } + if (g_entities[clientNum].client->pers.connected != CON_CONNECTED) { + return; + } + BotAimHarness_PostInputSync(bs); +} + +void BotAimHarness_SyncAllBotsDebug(void) { + int i; + + trap_Cvar_Update(&bot_debugAim); + if (!bot_debugAim.integer) { + return; + } + + for (i = 0; i < MAX_CLIENTS; i++) { + if (!botstates[i] || !botstates[i]->inuse) { + continue; + } + if (!g_entities[i].client || + g_entities[i].client->pers.connected != CON_CONNECTED) { + continue; + } + BotAimHarness_PostInputSync(botstates[i]); + } } void BotAimHarness_RegisterCvars(void) { - trap_Cvar_Register(&bot_humanizeaim, "bot_humanizeaim", "0", CVAR_ARCHIVE); - trap_Cvar_Update(&bot_humanizeaim); + trap_Cvar_Register(&bot_enhanced_aim, "bot_enhanced_aim", "0", CVAR_ARCHIVE); + trap_Cvar_Register(&bot_debugAim, "bot_debugAim", "0", CVAR_CHEAT); + trap_Cvar_Update(&bot_enhanced_aim); + trap_Cvar_Update(&bot_debugAim); } void BotAimHarness_ResetCvarLatch(void) { - bot_humanizeaim_last = -1; + bot_enhanced_aim_last = -1; + bot_debugAim_last = -1; } void BotAimHarness_UpdateCvar(void) { int i; - trap_Cvar_Update(&bot_humanizeaim); - if (bot_humanizeaim_last == bot_humanizeaim.integer) { + trap_Cvar_Update(&bot_enhanced_aim); + trap_Cvar_Update(&bot_debugAim); + + if (bot_debugAim_last != bot_debugAim.integer) { + bot_debugAim_last = bot_debugAim.integer; + if (!bot_debugAim.integer) { + for (i = 0; i < level.maxclients; i++) { + BotAimHarness_ClearEntityDebug(&g_entities[i]); + } + } else { + BotAimHarness_SyncAllBotsDebug(); + } + } + + if (bot_enhanced_aim_last == bot_enhanced_aim.integer) { return; } - bot_humanizeaim_last = bot_humanizeaim.integer; + bot_enhanced_aim_last = bot_enhanced_aim.integer; for (i = 0; i < MAX_CLIENTS; i++) { if (botstates[i] && botstates[i]->inuse) { BotAimHarness_Reset(botstates[i]); - if (!bot_humanizeaim.integer) { + if (!bot_enhanced_aim.integer) { botstates[i]->viewanglespeed[0] = 0; botstates[i]->viewanglespeed[1] = 0; + BotAimHarness_ClearEntityDebug(&g_entities[i]); } } } } int BotAimHarness_IsActive(void) { - trap_Cvar_Update(&bot_humanizeaim); - if (!bot_humanizeaim.integer) { - return 0; - } - if (bot_challenge.integer) { - return 0; + return BotEnhanced_AimActive(); +} + +void BotAimHarness_SyncMotorToView(bot_state_t *bs) { + if (!bs) { + return; } - return 1; + bs->aimh_combat_aim = qfalse; + bs->aimh_vel[PITCH] = 0.0f; + bs->aimh_vel[YAW] = 0.0f; + bs->aimh_tracked_ideal_pitch = BotAimHarness_ClampPitch(bs->viewangles[PITCH]); + bs->aimh_tracked_ideal_yaw = AngleMod(bs->viewangles[YAW]); + bs->aimh_smooth_goal_pitch = bs->aimh_tracked_ideal_pitch; + bs->aimh_smooth_goal_yaw = bs->aimh_tracked_ideal_yaw; + VectorCopy(bs->viewangles, bs->aimh_goal); } static void BotAimHarness_SyncViewAngles(bot_state_t *bs) { @@ -348,10 +1049,28 @@ void BotAimHarness_Reset(bot_state_t *bs) { bs->aimh_acquire_until = 0.0f; bs->aimh_last_sanity_enemy = -1; bs->aimh_sanity_miss_streak = 0; + bs->aimh_aim_skill = 0.5f; + bs->aimh_aim_accuracy = 0.5f; + bs->aimh_last_goal_pitch = 0.0f; + bs->aimh_last_goal_yaw = 0.0f; + 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_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; } void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, - float aimAccuracy, float weaponVSpread, float weaponHSpread) { + float aimSkill, float aimAccuracy, float weaponVSpread, float weaponHSpread) { float inaccuracy; (void)weaponVSpread; @@ -361,90 +1080,132 @@ 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]); - inaccuracy = 1.0f - aimAccuracy; - if (inaccuracy < 0.0f) { - inaccuracy = 0.0f; + if (aimSkill < 0.0f) { + aimSkill = 0.0f; + } + if (aimSkill > 1.0f) { + aimSkill = 1.0f; + } + if (aimAccuracy < 0.0f) { + aimAccuracy = 0.0f; } - if (inaccuracy > 1.0f) { - inaccuracy = 1.0f; + if (aimAccuracy > 1.0f) { + aimAccuracy = 1.0f; } + bs->aimh_aim_skill = aimSkill; + bs->aimh_aim_accuracy = aimAccuracy; + + inaccuracy = 1.0f - aimAccuracy; bs->aimh_motor_inaccuracy = inaccuracy * 0.7f; bs->aimh_combat_aim = qtrue; VectorCopy(bs->aimh_goal, bs->ideal_viewangles); + if (BotAimHarness_AimTargetValid(bs)) { + VectorCopy(bs->aimtarget, bs->aimh_combat_target); + } 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]; + } } -static void BotAimHarness_GetCombatGoal(bot_state_t *bs, vec3_t goal) { - aas_entityinfo_t entinfo; - weaponinfo_t wi; - vec3_t dir, target, vel; - float leadTime, dist, flightTime; +/* + * Roam: goal is movement ideal; spring + light noise humanize. Combat: live world target + * each motor frame; spring/catch-up humanize (no extra goal slew). + */ +static void BotAimHarness_GetMotorGoal(bot_state_t *bs, vec3_t goal, float dt) { + vec3_t targetAngles, dir; + float pitchJump, yawJump; - if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { - VectorCopy(bs->aimh_goal, goal); - return; + if (dt <= 0.0f) { + dt = 0.001f; } - BotEntityInfo(bs->enemy, &entinfo); - if (!entinfo.valid) { - VectorCopy(bs->aimh_goal, goal); + if (bs->aimh_combat_aim) { + BotAimHarness_GetCombatAimAngles(bs, targetAngles); + bs->aimh_smooth_goal_pitch = targetAngles[PITCH]; + bs->aimh_smooth_goal_yaw = targetAngles[YAW]; + } else { + VectorCopy(bs->ideal_viewangles, targetAngles); + targetAngles[PITCH] = BotAimHarness_ClampPitch(targetAngles[PITCH]); + targetAngles[YAW] = AngleMod(targetAngles[YAW]); + + pitchJump = fabs(BotAimHarness_PitchDiff(bs->aimh_tracked_ideal_pitch, + targetAngles[PITCH])); + yawJump = fabs(BotAimHarness_YawDiff(bs->aimh_tracked_ideal_yaw, targetAngles[YAW])); + if (pitchJump > AIMH_ROAM_IDEAL_SNAP_PITCH || yawJump > AIMH_ROAM_IDEAL_SNAP_YAW) { + bs->aimh_smooth_goal_pitch = targetAngles[PITCH]; + bs->aimh_smooth_goal_yaw = targetAngles[YAW]; + bs->aimh_vel[PITCH] *= 0.35f; + bs->aimh_vel[YAW] *= 0.35f; + } + bs->aimh_tracked_ideal_pitch = targetAngles[PITCH]; + bs->aimh_tracked_ideal_yaw = targetAngles[YAW]; + + goal[PITCH] = targetAngles[PITCH]; + goal[YAW] = targetAngles[YAW]; + bs->aimh_smooth_goal_pitch = goal[PITCH]; + bs->aimh_smooth_goal_yaw = goal[YAW]; return; } - if (bs->aimh_last_enemy_z != 0.0f && - fabs(entinfo.origin[2] - bs->aimh_last_enemy_z) > AIMH_ENEMY_Z_SPIKE) { - bs->aimh_vel[PITCH] = 0.0f; + goal[PITCH] = bs->aimh_smooth_goal_pitch; + goal[YAW] = bs->aimh_smooth_goal_yaw; +} + +static void BotAimHarness_ResyncViewFromPSIfDesynced(bot_state_t *bs) { + float psPitch, psYaw; + float pErr, yErr; + + psPitch = BotAimHarness_ClampPitch(bs->cur_ps.viewangles[PITCH]); + psYaw = AngleMod(bs->cur_ps.viewangles[YAW]); + + pErr = BotAimHarness_PitchDiff(bs->viewangles[PITCH], psPitch); + yErr = BotAimHarness_YawDiff(bs->viewangles[YAW], psYaw); + + if (fabs(pErr) < AIMH_PS_DESYNC_PITCH && fabs(yErr) < AIMH_PS_DESYNC_YAW) { + return; } - bs->aimh_last_enemy_z = entinfo.origin[2]; - VectorSubtract(entinfo.origin, entinfo.lastvisorigin, vel); - if (entinfo.update_time > 0.001f) { - VectorScale(vel, 1.0f / entinfo.update_time, vel); + if (fabs(pErr) > 45.0f || fabs(yErr) > 50.0f) { + bs->viewangles[PITCH] = psPitch; + bs->viewangles[YAW] = psYaw; + bs->aimh_vel[PITCH] *= 0.35f; + bs->aimh_vel[YAW] *= 0.35f; } else { - VectorClear(vel); + bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH] + + pErr * AIMH_PS_DESYNC_BLEND); + bs->viewangles[YAW] = AngleMod(bs->viewangles[YAW] + yErr * AIMH_PS_DESYNC_BLEND); } - BotAimHarness_ClampVerticalLeadVel(vel); + bs->viewangles[ROLL] = 0.0f; +} - trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); +void BotAimHarness_BeginMotorFrame(bot_state_t *bs) { + if (!BotAimHarness_IsActive()) { + return; + } - if (wi.speed > 0.0f) { - if (BotAimHarness_AimTargetValid(bs)) { - VectorCopy(bs->aimtarget, target); - } else { - VectorCopy(entinfo.origin, target); - target[2] += AIMH_AIM_HEIGHT; - VectorSubtract(target, bs->eye, dir); - dist = VectorLength(dir); - flightTime = dist / wi.speed; - if (flightTime > 2.0f) { - flightTime = 2.0f; - } - BotAimHarness_ApplyLead(target, vel, flightTime, qtrue); - } - } else { - if (BotAimHarness_AimTargetValid(bs)) { - VectorCopy(bs->aimtarget, target); - } else { - VectorCopy(entinfo.origin, target); - target[2] += AIMH_AIM_HEIGHT; - } - trap_Cvar_Update(&bot_thinktime); - leadTime = (bot_thinktime.integer / 1000.0f) * AIMH_HITSCAN_LEAD_SCALE; - leadTime += AIMH_HITSCAN_LEAD_EXTRA; - if (leadTime > 0.2f) { - leadTime = 0.2f; - } - BotAimHarness_ApplyLead(target, vel, leadTime, qtrue); + if (BotAI_WeaponJumpActive(bs)) { + return; } - VectorSubtract(target, bs->eye, dir); - vectoangles(dir, goal); - goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); - goal[YAW] = AngleMod(goal[YAW]); + BotAimHarness_ResyncViewFromPSIfDesynced(bs); +} + +static void BotAimHarness_SaveGoalHistory(bot_state_t *bs, const vec3_t goal) { + bs->aimh_last_goal_pitch = goal[PITCH]; + bs->aimh_last_goal_yaw = goal[YAW]; + bs->aimh_last_goal_time = FloatTime(); } static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, - float dt, float stiffness, float damping, float maxVel, float motorNoise) { + float dt, float stiffness, float damping, float maxVel, float motorNoise, + float minVel, float catchupGain) { float err, accel, delta; if (axis == PITCH) { @@ -457,6 +1218,17 @@ static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, accel = stiffness * err - damping * bs->aimh_vel[axis]; bs->aimh_vel[axis] += accel * dt; + if (minVel > 0.0f && fabs(err) > AIMH_ROAM_MIN_ERR) { + if (err > 0.0f && bs->aimh_vel[axis] < minVel) { + bs->aimh_vel[axis] = minVel; + } else if (err < 0.0f && bs->aimh_vel[axis] > -minVel) { + bs->aimh_vel[axis] = -minVel; + } + } + if (catchupGain > 0.0f && fabs(err) > 2.5f && fabs(err) < 26.0f) { + bs->aimh_vel[axis] += err * catchupGain * dt; + } + if (bs->aimh_vel[axis] > maxVel) { bs->aimh_vel[axis] = maxVel; } else if (bs->aimh_vel[axis] < -maxVel) { @@ -466,27 +1238,61 @@ static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, delta = bs->aimh_vel[axis] * dt; if (axis == PITCH) { bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH] + delta); - if (motorNoise > 0.01f && fabs(err) < 12.0f) { + if (motorNoise > 0.01f && fabs(err) > 2.5f && fabs(err) < 9.0f) { bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH] + - crandom() * AIMH_MOTOR_NOISE_SCALE * motorNoise * dt * 60.0f * 0.5f); + 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) < 12.0f) { + if (motorNoise > 0.01f && fabs(err) > 2.5f && fabs(err) < 9.0f) { bs->viewangles[axis] = AngleMod(bs->viewangles[axis] + - crandom() * AIMH_MOTOR_NOISE_SCALE * motorNoise * dt * 60.0f); + crandom() * AIMH_MOTOR_NOISE_SCALE * motorNoise * dt * 60.0f * 0.4f); } } } +static void BotAimHarness_IntegrateMotorSubsteps(bot_state_t *bs, vec3_t goal, + float frameTime, float stiffness, float damping, float maxVel, float maxVelPitch, + float motorNoise, float minVel, float catchupGain, int refreshGoalEachStep) { + float remaining, subDt; + int steps; + + remaining = frameTime; + steps = 0; + while (remaining > 0.0001f) { + if (steps >= AIMH_MOTOR_SUBSTEP_MAX) { + subDt = remaining; + } else { + subDt = remaining; + if (subDt > AIMH_MOTOR_SUBSTEP_DT) { + subDt = AIMH_MOTOR_SUBSTEP_DT; + } + } + remaining -= subDt; + steps++; + + if (refreshGoalEachStep) { + BotAimHarness_GetMotorGoal(bs, goal, subDt); + goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); + } + + BotAimHarness_UpdateAxis(bs, PITCH, goal[PITCH], subDt, + stiffness, damping, maxVelPitch, motorNoise, minVel, catchupGain); + BotAimHarness_UpdateAxis(bs, YAW, goal[YAW], subDt, + stiffness, damping, maxVel, motorNoise, minVel, catchupGain); + } +} + int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { vec3_t goal; - float skill, maxChange, maxTrack, maxFlick, maxVelPitch; - float stiffness, damping, maxVel, motorNoise; - float magErr, pitchErr, yawErr, catchup, catchupRate; - float flickAngle; + float skill, aimSkill, maxChange, maxTrack, maxFlick, maxVelPitch; + float stiffness, damping, maxVel, motorNoise, motorScale; + float magErr, pitchErr, yawErr; + float flickAngle, goalDt, engageFar, ffDummy, catchupMult; + float minVel, catchupGain; if (!BotAimHarness_IsActive()) { + BotAimHarness_DebugSync(bs); return 0; } @@ -502,24 +1308,44 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->aimh_last_enemy_z = 0.0f; 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 (bs->enemy < 0) { bs->aimh_combat_aim = qfalse; + bs->aimh_hold_fire = qfalse; bs->aimh_motor_inaccuracy = 0.0f; bs->aimh_last_enemy_z = 0.0f; bs->aimh_last_sanity_enemy = -1; BotAimHarness_ClearRecoveryState(bs); bs->aimh_acquire_until = 0.0f; + bs->aimh_last_goal_time = 0.0f; + VectorClear(bs->aimtarget); + bs->aimh_tracked_ideal_pitch = BotAimHarness_ClampPitch(bs->ideal_viewangles[PITCH]); + bs->aimh_tracked_ideal_yaw = AngleMod(bs->ideal_viewangles[YAW]); } else { BotAimHarness_OnEnemyChange(bs); } if (bs->aimh_combat_aim) { - BotAimHarness_GetCombatGoal(bs, goal); + BotAimHarness_GetMotorGoal(bs, goal, thinktime); motorNoise = bs->aimh_motor_inaccuracy; + aimSkill = bs->aimh_aim_skill; skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_FACTOR, 0.01f, 1.0f); maxChange = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_MAXCHANGE, 1.0f, 1800.0f); if (maxChange < 240.0f) { @@ -527,9 +1353,13 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } maxTrack = maxChange * skill * AIMH_COMBAT_VEL_SCALE; maxFlick = maxChange * 1.35f * skill * AIMH_COMBAT_VEL_SCALE; + motorScale = 0.82f + 0.38f * aimSkill; + maxTrack *= motorScale; + maxFlick *= motorScale; } else { - VectorCopy(bs->ideal_viewangles, goal); + BotAimHarness_GetMotorGoal(bs, goal, thinktime); motorNoise = 0.0f; + aimSkill = 0.05f; skill = 0.05f; maxTrack = 360.0f; maxFlick = 360.0f; @@ -538,67 +1368,92 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH]); + engageFar = 1.0f; + catchupMult = 1.0f; + goalDt = thinktime; + if (bs->aimh_combat_aim) { + engageFar = BotAimHarness_GetEngageFar(bs, goal, goalDt); + catchupMult = BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_CATCHUP, AIMH_ENGAGE_FAR_CATCHUP, + engageFar); + } + pitchErr = BotAimHarness_PitchDiff(bs->viewangles[PITCH], goal[PITCH]); yawErr = BotAimHarness_YawDiff(bs->viewangles[YAW], goal[YAW]); magErr = sqrt(pitchErr * pitchErr + yawErr * yawErr); if (bs->aimh_combat_aim && bs->aimh_acquire_until <= FloatTime() && fabs(pitchErr) > AIMH_PITCH_RESET_ERR) { - bs->aimh_vel[PITCH] = 0.0f; + bs->aimh_vel[PITCH] *= 0.55f; } flickAngle = AIMH_FLICK_ANGLE; + if (bs->aimh_combat_aim) { + flickAngle *= BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_FLICK, AIMH_ENGAGE_FAR_FLICK, + engageFar); + } if (bs->aimh_acquire_until > FloatTime()) { flickAngle = AIMH_ACQUIRE_FLICK_ANGLE; maxTrack *= 0.82f; maxFlick *= 0.82f; } - if (bs->aimh_combat_aim && magErr > flickAngle) { - stiffness = AIMH_STIFFNESS_FLICK; - damping = AIMH_DAMPING_FLICK; - maxVel = maxFlick; - } else if (bs->aimh_combat_aim) { + if (bs->aimh_combat_aim) { + /* Single track mode in combat — flick mode caused dedicated tick oscillation. */ stiffness = AIMH_STIFFNESS_TRACK; damping = AIMH_DAMPING_TRACK; maxVel = maxTrack; + if (magErr > flickAngle * 1.35f) { + maxVel = maxFlick; + stiffness = AIMH_STIFFNESS_FLICK; + damping = AIMH_DAMPING_FLICK; + } } else { stiffness = AIMH_ROAM_STIFFNESS; damping = AIMH_ROAM_DAMPING; maxVel = maxFlick; } + if (bs->aimh_combat_aim) { + stiffness *= (0.78f + 0.52f * aimSkill); + damping *= (0.88f + 0.32f * aimSkill); + } + + if (bs->aimh_combat_aim) { + ffDummy = 1.0f; + BotAimHarness_ApplyEngageProfile(engageFar, &stiffness, &damping, &maxVel, + &motorNoise, &ffDummy, &catchupMult); + (void)catchupMult; + (void)ffDummy; + } + if (bs->aimh_combat_aim && maxVel < 140.0f) { - maxVel = 140.0f; + maxVel = 140.0f + 80.0f * aimSkill; } else if (maxVel < 90.0f) { maxVel = 90.0f; } maxVelPitch = maxVel * 1.55f; if (bs->aimh_combat_aim && maxVelPitch < AIMH_PITCH_VEL_MIN) { - maxVelPitch = AIMH_PITCH_VEL_MIN; + maxVelPitch = AIMH_PITCH_VEL_MIN + 60.0f * aimSkill; } if (bs->aimh_recover_until > FloatTime() && bs->aimh_acquire_until <= FloatTime()) { maxVelPitch *= 1.12f; } - BotAimHarness_UpdateAxis(bs, PITCH, goal[PITCH], thinktime, - stiffness, damping, maxVelPitch, motorNoise); - BotAimHarness_UpdateAxis(bs, YAW, goal[YAW], thinktime, - stiffness, damping, maxVel, motorNoise); - - catchupRate = AIMH_PITCH_CATCHUP_RATE; - if (bs->aimh_recover_until > FloatTime() && bs->aimh_acquire_until <= FloatTime()) { - catchupRate *= AIMH_RECOVER_CATCHUP_MULT; + if (bs->aimh_combat_aim) { + minVel = AIMH_COMBAT_MIN_VEL + 45.0f * aimSkill; + catchupGain = AIMH_COMBAT_CATCHUP_GAIN * catchupMult; + } else { + minVel = AIMH_ROAM_MIN_VEL; + catchupGain = 0.0f; } - if (bs->aimh_combat_aim && bs->aimh_acquire_until <= FloatTime() && - fabs(pitchErr) > AIMH_PITCH_CATCHUP_ERR) { - catchup = pitchErr * thinktime * catchupRate; - if (catchup > pitchErr) { - catchup = pitchErr; - } else if (catchup < -pitchErr) { - catchup = -pitchErr; - } - bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH] + catchup); + + BotAimHarness_IntegrateMotorSubsteps(bs, goal, thinktime, + stiffness, damping, maxVel, maxVelPitch, motorNoise, minVel, catchupGain, + bs->aimh_combat_aim); + + if (bs->aimh_combat_aim) { + BotAimHarness_GetMotorGoal(bs, goal, thinktime); + BotAimHarness_SaveGoalHistory(bs, goal); } if (bs->aimh_combat_aim && bs->enemy >= 0) { @@ -608,5 +1463,6 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH]); trap_EA_View(bs->client, bs->viewangles); + BotAimHarness_DebugSync(bs); return 1; } diff --git a/ratoa_gamecode/code/game/ai_aim_harness.h b/ratoa_gamecode/code/game/ai_aim_harness.h index b850dfb..bf376cc 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -4,9 +4,20 @@ BOT AIM HARNESS (v1) — humanized view actuation for bots. Ringfenced module: all harness logic lives in ai_aim_harness.c / this header. To remove: delete these files, drop from Makefile/q3asm, revert marked hooks in -ai_main.h, ai_main.c, and ai_dmq3.c, and unset bot_humanizeaim. +ai_main.h, ai_main.c, and ai_dmq3.c, and unset bot_enhanced / bot_enhanced_aim. -Toggle at runtime: bot_humanizeaim 1 (default 0 = legacy aim motor) +Toggle at runtime: bot_enhanced 1 and bot_enhanced_aim 1 (default 0 = legacy aim motor). +Was bot_humanizeaim; that name is migrated at init if bot_enhanced_aim is unset. +Debug: bot_debugAim 1 (server, CVAR_CHEAT) publishes motor wish (ideal_viewangles +when roaming, aimtarget when fighting) via ps.grapplePoint + EXTFL_BOT_AIM_DEBUG; +cg_debugBotAim draws green = wish, yellow (bit 4) = crosshair. + +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. + +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, +and spring/catch-up toward live goals (humanized, not snap-aim). Include after g_local.h and ai_main.h in .c files (ai_main.h has no guards). =========================================================================== @@ -21,11 +32,31 @@ void BotAimHarness_RegisterCvars(void); void BotAimHarness_ResetCvarLatch(void); void BotAimHarness_UpdateCvar(void); int BotAimHarness_IsActive(void); +void BotAimHarness_SyncMotorToView(struct bot_state_s *bs); float BotAimHarness_ClampPitchAngle(float pitch); +/* Sync motor state from authoritative playerState before each bot input frame. */ +void BotAimHarness_BeginMotorFrame(struct bot_state_s *bs); void BotAimHarness_Reset(struct bot_state_s *bs); void BotAimHarness_SetCombatGoal(struct bot_state_s *bs, const float idealAngles[3], - float aimAccuracy, float weaponVSpread, float weaponHSpread); + float aimSkill, float aimAccuracy, float weaponVSpread, float weaponHSpread); +void BotAimHarness_ApplyThinkHitscanOrigin(struct bot_state_s *bs, float bestorigin[3], + void *entinfo, float aimSkill); int BotAimHarness_ChangeViewAngles(struct bot_state_s *bs, float thinktime); +int BotAimHarness_AimTargetValid(struct bot_state_s *bs); +/* + * Think-time attack (BotCheckAttack): sets aimh_hold_fire for suppressive weapons. + * Per-frame +attack while hold is set: BotAimHarness_ApplyCombatFire (BotUpdateInput). + */ +void BotAimHarness_CheckAttack(struct bot_state_s *bs); +void BotAimHarness_ApplyCombatFire(struct bot_state_s *bs); +int BotAimHarness_TryAttack(struct bot_state_s *bs); + +/* After bot usercmds: copy ps aim debug onto ent->s for entity snapshots. */ +void BotAimHarness_SyncEntityFromPlayerState(struct gentity_s *ent); +void BotAimHarness_PostInputSync(struct bot_state_s *bs); +/* Publish debug for one bot or every connected bot (late join / cvar on). */ +void BotAimHarness_SyncClientDebug(int clientNum); +void BotAimHarness_SyncAllBotsDebug(void); #endif /* AI_AIM_HARNESS_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_combat.c b/ratoa_gamecode/code/game/ai_bot_combat.c new file mode 100644 index 0000000..88ff037 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_combat.c @@ -0,0 +1,263 @@ +/* +=========================================================================== +BOT COMBAT — intent reset/update and weapon-commit hook. +=========================================================================== +*/ +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_combat.h" +#include "ai_bot_tactics.h" +#include "ai_weapon_select.h" +qboolean BotIsDead(bot_state_t *bs); +qboolean BotIsObserver(bot_state_t *bs); +static int BotCombat_HorizontalDistToEnemy(bot_state_t *bs) { + vec3_t dir; + aas_entityinfo_t entinfo; + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 99999; + } + BotEntityInfo(bs->enemy, &entinfo); + VectorSubtract(entinfo.origin, bs->origin, dir); + dir[2] = 0; + return (int)VectorLength(dir); +} +static qboolean BotCombat_GauntletChosen(bot_state_t *bs) { + return (bs->weaponnum == WP_GAUNTLET || bs->cur_ps.weapon == WP_GAUNTLET); +} +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)) { + return qfalse; + } + if (BotTactics_IsGauntletOnly(bs)) { + return qfalse; + } + if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + return qfalse; + } + return BotCombat_HorizontalDistToEnemy(bs) <= BOT_COMBAT_GAUNTLET_RUSH_DIST; +} +static qboolean BotCombat_InGauntletEngageRange(bot_state_t *bs) { + int horiz; + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + horiz = BotCombat_HorizontalDistToEnemy(bs); + if (BotTactics_IsGauntletOnly(bs)) { + return horiz <= BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST; + } + return horiz <= BOT_COMBAT_GAUNTLET_RUSH_DIST; +} +static void BotCombat_ClearVoluntaryPursuit(bot_state_t *bs) { + bs->combat.gauntlet_voluntary_since = 0.0f; + bs->combat.gauntlet_voluntary_best_dist = 0; +} +static void BotCombat_StartVoluntaryPursuit(bot_state_t *bs) { + int horiz; + if (BotTactics_IsGauntletOnly(bs)) { + return; + } + if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + return; + } + horiz = BotCombat_HorizontalDistToEnemy(bs); + if (horiz > BOT_COMBAT_GAUNTLET_RUSH_DIST) { + return; + } + bs->combat.gauntlet_voluntary_since = FloatTime(); + bs->combat.gauntlet_voluntary_best_dist = horiz; +} +static qboolean BotCombat_EnemyBackingAway(bot_state_t *bs) { + aas_entityinfo_t entinfo; + vec3_t toBot, vel; + float speedAway; + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + BotEntityInfo(bs->enemy, &entinfo); + if (!entinfo.valid || entinfo.update_time <= 0.001f) { + return qfalse; + } + VectorSubtract(bs->origin, entinfo.origin, toBot); + toBot[2] = 0.0f; + if (VectorLengthSquared(toBot) < Square(16.0f)) { + return qfalse; + } + VectorNormalize(toBot); + VectorSubtract(entinfo.origin, entinfo.lastvisorigin, vel); + VectorScale(vel, 1.0f / entinfo.update_time, vel); + vel[2] = 0.0f; + speedAway = -DotProduct(vel, toBot); + return speedAway > BOT_COMBAT_ENEMY_BACKING_AWAY_SPEED; +} +static qboolean BotCombat_VoluntaryGauntletPursuitActive(bot_state_t *bs) { + return bs->combat.gauntlet_voluntary_since > 0.0f; +} +static qboolean BotCombat_UpdateVoluntaryGauntletAbort(bot_state_t *bs) { + float elapsed; + int horiz; + if (!BotCombat_VoluntaryGauntletPursuitActive(bs)) { + return qfalse; + } + if (!BotCombat_GauntletChosen(bs) || BotTactics_IsGauntletOnly(bs)) { + BotCombat_ClearVoluntaryPursuit(bs); + return qfalse; + } + horiz = BotCombat_HorizontalDistToEnemy(bs); + if (horiz < bs->combat.gauntlet_voluntary_best_dist) { + bs->combat.gauntlet_voluntary_best_dist = horiz; + } + if (horiz <= BOT_COMBAT_GAUNTLET_ATTACK_DIST) { + BotCombat_ClearVoluntaryPursuit(bs); + return qfalse; + } + elapsed = FloatTime() - bs->combat.gauntlet_voluntary_since; + if (elapsed < BOT_COMBAT_VOLUNTARY_GAUNTLET_TIMEOUT) { + return qfalse; + } + if (!BotCombat_EnemyBackingAway(bs)) { + return qfalse; + } + bs->combat.gauntlet_voluntary_abandon_until = + FloatTime() + BOT_COMBAT_VOLUNTARY_GAUNTLET_ABANDON_COOLDOWN; + BotCombat_ClearVoluntaryPursuit(bs); + BotWpnSelect_OnVoluntaryGauntletAborted(bs); + return qtrue; +} +static qboolean BotCombat_GauntletRushAllowed(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; + } + /* Voluntary close gauntlet (skill 4–5) overrides stale survival-flee. */ + if (horiz <= BOT_COMBAT_GAUNTLET_RUSH_DIST && + BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { + return qfalse; + } + return qtrue; + } + if (bs->flags & BFL_TACTICS_SURVIVAL_FLEE) { + return qfalse; + } + return qfalse; +} +static void BotCombat_ApplyGauntletRush(bot_state_t *bs) { + bs->combat.stance = BOT_STANCE_RUSH_OPPONENT; + bs->combat.move_policy = BOT_MOVE_CLOSE_MELEE; + if (BotCombat_CloseVoluntaryGauntlet(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)) { + return; + } + BotCombat_ApplyGauntletRush(bs); +} +static void BotCombat_ResetStance(bot_state_t *bs) { + bs->combat.stance = BOT_STANCE_NORMAL; + bs->combat.move_policy = BOT_MOVE_POLICY_LEGACY; + bs->combat.fire_policy = BOT_FIRE_POLICY_LEGACY; + bs->combat.stance_until = 0.0f; +} +void BotCombat_Reset(bot_state_t *bs) { + if (!bs) { + return; + } + BotCombat_ResetStance(bs); + BotCombat_ClearVoluntaryPursuit(bs); + bs->combat.gauntlet_voluntary_abandon_until = 0.0f; +} +void BotCombat_UpdateIntent(bot_state_t *bs) { + if (!bs) { + return; + } + BotCombat_ResetStance(bs); + if (!BotEnhanced_IsActive()) { + return; + } + if (!bs->inuse || BotIsDead(bs) || BotIsObserver(bs)) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + BotCombat_ClearVoluntaryPursuit(bs); + return; + } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + bs->enemy = -1; + BotCombat_ClearVoluntaryPursuit(bs); + return; + } + if (BotCombat_UpdateVoluntaryGauntletAbort(bs)) { + return; + } + BotCombat_UpdateGauntletRush(bs); +} +int BotCombat_IsRushOpponent(const bot_state_t *bs) { + if (!bs) { + return 0; + } + return bs->combat.stance == BOT_STANCE_RUSH_OPPONENT; +} +int BotCombat_ShouldEngageFromRetreat(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return 0; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 0; + } + if (!BotCombat_HasGauntlet(bs)) { + return 0; + } + if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { + return 0; + } + if (BotTactics_IsGauntletOnly(bs)) { + return BotCombat_InGauntletEngageRange(bs); + } + if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + return 0; + } + return BotCombat_InGauntletEngageRange(bs); +} +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) { + BotCombat_ClearVoluntaryPursuit(bs); + } + return; + } + if (prev_wp == WP_GAUNTLET) { + return; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + BotCombat_StartVoluntaryPursuit(bs); + if (!BotCombat_GauntletRushAllowed(bs)) { + return; + } + BotCombat_ApplyGauntletRush(bs); +} + diff --git a/ratoa_gamecode/code/game/ai_bot_combat.h b/ratoa_gamecode/code/game/ai_bot_combat.h new file mode 100644 index 0000000..5b7150c --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_combat.h @@ -0,0 +1,60 @@ +/* +=========================================================================== +BOT COMBAT — per-think intent scaffold (stance, move/fire policy). + +Defaults match legacy behavior. Stance/policy logic in BotCombat_UpdateIntent. +=========================================================================== +*/ + +#ifndef AI_BOT_COMBAT_H +#define AI_BOT_COMBAT_H + +typedef enum { + BOT_STANCE_NORMAL = 0, + BOT_STANCE_RUSH_OPPONENT + /* BOT_STANCE_SURVIVAL_FLEE — reserved; tactics retreat for now */ +} bot_stance_t; + +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_policy_t; + +typedef enum { + BOT_FIRE_POLICY_LEGACY = 0 +} bot_fire_policy_t; + +/* Close gauntlet rush (voluntary switch while other weapons may have ammo). */ +#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 +/* Gauntlet hit range in BotCheckAttack is 60; slight margin for intent */ +#define BOT_COMBAT_GAUNTLET_ATTACK_DIST 72 +/* Voluntary gauntlet: abandon rush if enemy kites for this long without closing. */ +#define BOT_COMBAT_VOLUNTARY_GAUNTLET_TIMEOUT 2.0f +#define BOT_COMBAT_VOLUNTARY_GAUNTLET_ABANDON_COOLDOWN 4.0f +#define BOT_COMBAT_ENEMY_BACKING_AWAY_SPEED 80.0f + +typedef struct { + bot_stance_t stance; + bot_move_policy_t move_policy; + bot_fire_policy_t fire_policy; + float stance_until; /* 0 = no timer */ + float gauntlet_voluntary_since; /* 0 = not in voluntary pursuit */ + int gauntlet_voluntary_best_dist; + float gauntlet_voluntary_abandon_until; /* no voluntary gauntlet until */ +} bot_combat_intent_t; + +struct bot_state_s; + +void BotCombat_Reset(struct bot_state_s *bs); +void BotCombat_UpdateIntent(struct bot_state_s *bs); +void BotCombat_OnWeaponCommitted(struct bot_state_s *bs, int prev_wp, int new_wp); + +int BotCombat_IsRushOpponent(const struct bot_state_s *bs); + +/* Close enough to charge with gauntlet; pulls bot out of battle retreat. */ +int BotCombat_ShouldEngageFromRetreat(struct bot_state_s *bs); + +#endif /* AI_BOT_COMBAT_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.c b/ratoa_gamecode/code/game/ai_bot_enhanced.c new file mode 100644 index 0000000..a054aff --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -0,0 +1,258 @@ +/* +=========================================================================== +BOT ENHANCED — master cvar, feature gates, centralized register/reset. +=========================================================================== +*/ + +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "ai_dmq3.h" +#include "ai_bot_enhanced.h" +#include "ai_aim_harness.h" +#include "ai_weapon_select.h" +#include "ai_bot_tactics.h" +#include "ai_bot_combat.h" +#include "ai_bot_events.h" +#include "ai_bot_move_harness.h" +#include "ai_bot_items.h" + +vmCvar_t bot_enhanced; + +extern vmCvar_t bot_enhanced_aim; +extern vmCvar_t bot_enhanced_weapons; +extern vmCvar_t bot_enhanced_tactics; + +#define BOT_ENHANCED_LEGACY_AIM "bot_humanizeaim" +#define BOT_ENHANCED_LEGACY_WEAPONS "bot_smartWeaponChoice" +#define BOT_ENHANCED_LEGACY_TACTICS "bot_tacticalAI" + +static int BotEnhanced_LegacyCvarActive(const char *name) { + return trap_Cvar_VariableValue(name) != 0.0f; +} + +/* + * One-time at init: if a new sub-cvar is still at default, copy from the old name + * (server.cfg may still set the deprecated cvars). Enables bot_enhanced when any + * sub-cvar is migrated so pre-refactor configs keep working. + */ +static void BotEnhanced_MigrateLegacyCvars(void) { + int migrated; + int legacy_used; + + migrated = 0; + legacy_used = 0; + + trap_Cvar_Update(&bot_enhanced_aim); + if (!bot_enhanced_aim.integer) { + if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_AIM)) { + trap_Cvar_Set("bot_enhanced_aim", "1"); + migrated = 1; + legacy_used = 1; + } + } else if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_AIM)) { + legacy_used = 1; + } + + trap_Cvar_Update(&bot_enhanced_weapons); + if (!bot_enhanced_weapons.integer) { + if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_WEAPONS)) { + trap_Cvar_Set("bot_enhanced_weapons", "1"); + migrated = 1; + legacy_used = 1; + } + } else if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_WEAPONS)) { + legacy_used = 1; + } + + trap_Cvar_Update(&bot_enhanced_tactics); + if (!bot_enhanced_tactics.integer) { + if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_TACTICS)) { + trap_Cvar_Set("bot_enhanced_tactics", "1"); + migrated = 1; + legacy_used = 1; + } + } else if (BotEnhanced_LegacyCvarActive(BOT_ENHANCED_LEGACY_TACTICS)) { + legacy_used = 1; + } + + trap_Cvar_Update(&bot_enhanced); + if (migrated && !bot_enhanced.integer) { + trap_Cvar_Set("bot_enhanced", "1"); + } + + if (legacy_used) { + G_Printf( + "Bot enhanced: deprecated cvars %s / %s / %s detected; " + "use bot_enhanced and bot_enhanced_aim|weapons|tactics.\n", + BOT_ENHANCED_LEGACY_AIM, + BOT_ENHANCED_LEGACY_WEAPONS, + BOT_ENHANCED_LEGACY_TACTICS); + } + + trap_Cvar_Update(&bot_enhanced); + trap_Cvar_Update(&bot_enhanced_aim); + trap_Cvar_Update(&bot_enhanced_weapons); + trap_Cvar_Update(&bot_enhanced_tactics); +} + +void BotEnhanced_RegisterCvars(void) { + trap_Cvar_Register(&bot_enhanced, "bot_enhanced", "0", CVAR_ARCHIVE); + trap_Cvar_Update(&bot_enhanced); + BotAimHarness_RegisterCvars(); + BotWpnSelect_RegisterCvars(); + BotTactics_RegisterCvars(); + BotItems_RegisterCvars(); + BotEnhanced_MigrateLegacyCvars(); +} + +int BotEnhanced_IsActive(void) { + trap_Cvar_Update(&bot_enhanced); + return bot_enhanced.integer != 0; +} + +int BotEnhanced_AimActive(void) { + if (!BotEnhanced_IsActive()) { + return 0; + } + trap_Cvar_Update(&bot_enhanced_aim); + return bot_enhanced_aim.integer != 0; +} + +int BotEnhanced_WeaponsActive(void) { + if (!BotEnhanced_IsActive()) { + return 0; + } + trap_Cvar_Update(&bot_enhanced_weapons); + return bot_enhanced_weapons.integer != 0; +} + +int BotEnhanced_TacticsActive(void) { + if (!BotEnhanced_IsActive()) { + return 0; + } + trap_Cvar_Update(&bot_enhanced_tactics); + return bot_enhanced_tactics.integer != 0; +} + +int BotEnhanced_ItemsActive(void) { + return BotItems_IsActive(); +} + +int BotEnhanced_ClientIsChatting(int clientnum) { + gentity_t *ent; + aas_entityinfo_t entinfo; + + if (clientnum < 0 || clientnum >= MAX_CLIENTS) { + return 0; + } + ent = &g_entities[clientnum]; + if (!ent->inuse || !ent->client) { + return 0; + } + if (ent->s.eFlags & EF_TALK) { + return 1; + } + BotEntityInfo(clientnum, &entinfo); + if (entinfo.valid && (entinfo.flags & EF_TALK)) { + return 1; + } + return 0; +} + +int BotEnhanced_CanEngageClient(bot_state_t *bs, int clientnum) { + if (!bs) { + return 0; + } + if (clientnum < 0 || clientnum >= MAX_CLIENTS) { + return 0; + } + if (clientnum == bs->client) { + return 0; + } + if (BotSameTeam(bs, clientnum)) { + return 0; + } + if (EntityClientIsDead(clientnum)) { + return 0; + } + if (g_entities[clientnum].flags & FL_NOTARGET) { + return 0; + } + if (BotEnhanced_ClientIsChatting(clientnum)) { + return 0; + } + return 1; +} + +int BotEnhanced_AllowsCamping(void) { + return !BotEnhanced_IsActive(); +} + +static void BotEnhanced_DropChattingEnemy(bot_state_t *bs) { + if (!bs || bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return; + } + if (!BotEnhanced_ClientIsChatting(bs->enemy)) { + return; + } + bs->enemy = -1; + bs->enemydeath_time = 0; +} + +static void BotEnhanced_CancelCampLongTermGoal(bot_state_t *bs) { + if (!bs) { + return; + } + if (bs->ltgtype != LTG_CAMP && bs->ltgtype != LTG_CAMPORDER) { + return; + } + bs->ltgtype = 0; + bs->teamgoal_time = 0; +} + +void BotEnhanced_OnThinkStart(bot_state_t *bs) { + BotEvents_Drain(bs); + if (BotEnhanced_IsActive()) { + BotEnhanced_DropChattingEnemy(bs); + BotEnhanced_CancelCampLongTermGoal(bs); + BotCombat_UpdateIntent(bs); + BotItems_Tick(bs); + } +} + +int BotEnhanced_ShouldSuppressFightRetreat(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return 0; + } + if (BotCombat_IsRushOpponent(bs)) { + return 1; + } + if (BotEnhanced_TacticsActive() && BotTactics_BattleFightSuppressRetreat(bs)) { + return 1; + } + return 0; +} + +int BotEnhanced_AllowsVoluntaryCloseGauntlet(bot_state_t *bs) { + if (!bs || !BotEnhanced_IsActive()) { + return 0; + } + return bs->settings.skill >= 4.0f; +} + +void BotEnhanced_ResetBot(bot_state_t *bs) { + BotMoveHarness_Reset(bs); + BotEvents_Reset(bs); + BotCombat_Reset(bs); + BotAimHarness_Reset(bs); + BotWpnSelect_Reset(bs); + BotTactics_Reset(bs); + BotItems_Reset(bs); +} diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.h b/ratoa_gamecode/code/game/ai_bot_enhanced.h new file mode 100644 index 0000000..a398066 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.h @@ -0,0 +1,48 @@ +/* +=========================================================================== +BOT ENHANCED — master gate and facade for aim / weapons / tactics modules. + +North-facing include for legacy hooks: register, reset, and feature-active checks +go through this header. Implementation modules keep their own logic in +ai_aim_harness.c, ai_weapon_select.c, ai_bot_tactics.c. + +Master: bot_enhanced (default 0). Sub-cvars only apply when master is 1. + +Legacy names (read once at init if new cvars are still default): bot_humanizeaim, +bot_smartWeaponChoice, bot_tacticalAI — see BotEnhanced_RegisterCvars migration. +=========================================================================== +*/ + +#ifndef AI_BOT_ENHANCED_H +#define AI_BOT_ENHANCED_H + +struct bot_state_s; + +void BotEnhanced_RegisterCvars(void); +void BotEnhanced_ResetBot(struct bot_state_s *bs); + +int BotEnhanced_IsActive(void); +int BotEnhanced_AimActive(void); +int BotEnhanced_WeaponsActive(void); +int BotEnhanced_TacticsActive(void); + +void BotEnhanced_OnThinkStart(struct bot_state_s *bs); + +/* Battle fight: suppress BotWantsToRetreat when rushing or gauntlet-only tactics. */ +int BotEnhanced_ShouldSuppressFightRetreat(struct bot_state_s *bs); + +/* bs->settings.skill (1–5): voluntary close gauntlet only at skill 4 or 5. */ +int BotEnhanced_AllowsVoluntaryCloseGauntlet(struct bot_state_s *bs); + +int BotEnhanced_ItemsActive(void); + +/* EF_TALK on client — typing in chat (chat balloon). */ +int BotEnhanced_ClientIsChatting(int clientnum); + +/* False for chatting players; use before acquiring or engaging an enemy. */ +int BotEnhanced_CanEngageClient(struct bot_state_s *bs, int clientnum); + +/* Enhanced bots do not use info_camp / BotWantsToCamp roaming. */ +int BotEnhanced_AllowsCamping(void); + +#endif /* AI_BOT_ENHANCED_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_events.c b/ratoa_gamecode/code/game/ai_bot_events.c new file mode 100644 index 0000000..cf26fd1 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_events.c @@ -0,0 +1,73 @@ +/* +=========================================================================== +BOT EVENTS — ingress queue; drain delegates scan/process to ai_bot_tactics.c. +=========================================================================== +*/ + +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_events.h" +#include "ai_bot_tactics.h" + +void BotEvents_Reset(bot_state_t *bs) { + if (!bs) { + return; + } + bs->evt_pending = 0; + bs->evt_attacker = -1; + bs->evt_damage = 0; + bs->evt_mod = MOD_UNKNOWN; +} + +void BotEvents_Push(bot_state_t *bs, int evt_bits, int ent, int parm) { + if (!bs || !evt_bits) { + return; + } + bs->evt_pending |= evt_bits; + if (evt_bits & BOT_EVT_HURT_BY_OTHER) { + bs->evt_attacker = ent; + bs->evt_damage = parm; + /* mod: use parm only for damage today; mod stays MOD_UNKNOWN until extended */ + } +} + +static void BotEvents_FlushToTactics(bot_state_t *bs) { + if (!bs->evt_pending) { + return; + } + bs->tact_pending |= bs->evt_pending; + if (bs->evt_pending & BOT_EVT_HURT_BY_OTHER) { + bs->tact_evt_attacker = bs->evt_attacker; + bs->tact_evt_damage = bs->evt_damage; + bs->tact_evt_mod = bs->evt_mod; + } + BotEvents_Reset(bs); +} + +void BotEvents_Drain(bot_state_t *bs) { + if (!bs) { + return; + } + if (!BotEnhanced_IsActive()) { + bs->tact_pending = 0; + BotEvents_Reset(bs); + return; + } + if (!BotEnhanced_TacticsActive()) { + bs->tact_pending = 0; + BotEvents_Reset(bs); + return; + } + + BotTactics_ScanEvents(bs); + BotEvents_FlushToTactics(bs); + BotTactics_ProcessPending(bs); +} diff --git a/ratoa_gamecode/code/game/ai_bot_events.h b/ratoa_gamecode/code/game/ai_bot_events.h new file mode 100644 index 0000000..30533d4 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_events.h @@ -0,0 +1,26 @@ +/* +=========================================================================== +BOT EVENTS — per-bot world-event ingress (bounded queue, no malloc). + +Contract: + - Producers call BotEvents_Push once when something happens (or ScanEvents + inside tactics during drain until damage scan moves here). + - BotEvents_Drain runs once per think from BotEnhanced_OnThinkStart only. + - Pending bits and payload fields live on bot_state_t (evt_*); cleared on drain. + +BOT_EVT_* bits match BOT_TACT_EVT_* while tactics still owns handlers. +=========================================================================== +*/ + +#ifndef AI_BOT_EVENTS_H +#define AI_BOT_EVENTS_H + +#define BOT_EVT_HURT_BY_OTHER 1 + +struct bot_state_s; + +void BotEvents_Reset(struct bot_state_s *bs); +void BotEvents_Push(struct bot_state_s *bs, int evt_bits, int ent, int parm); +void BotEvents_Drain(struct bot_state_s *bs); + +#endif /* AI_BOT_EVENTS_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_items.c b/ratoa_gamecode/code/game/ai_bot_items.c new file mode 100644 index 0000000..4c764b2 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_items.c @@ -0,0 +1,1472 @@ +/* + +=========================================================================== + +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_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_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_ITEMS_DBG_GOT 1 + +#define BOT_ITEMS_DBG_TIMEOUT 2 + +#define BOT_ITEMS_DBG_GONE 3 + +#define BOT_ITEMS_DBG_RESET 4 + + + +static const int botItemsScanKinds[] = { + + BOT_ITEM_QUAD, + + BOT_ITEM_ENEMY_FLAG, + + BOT_ITEM_MEGA_HEALTH, + + BOT_ITEM_RED_ARMOR, + + BOT_ITEM_YELLOW_ARMOR, + + 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 + +}; + + + +#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 } + +}; + + + +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) { + + 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); + + 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; + + default: + + wdef = BotItems_WeaponDef(kind); + + if (wdef) { + + Q_strncpyz(buf, wdef->label, bufsize); + + } else { + + Q_strncpyz(buf, "item", bufsize); + + } + + break; + + } + +} + + + +static void BotItems_DebugLine(bot_state_t *bs, int kind, const char *event) { + + char botName[64]; + + char itemName[32]; + + + + if (!BotItems_DebugEnabled() || !bs || !event) { + + return; + + } + + ClientName(bs->client, botName, sizeof(botName)); + + BotItems_KindLabel(kind, itemName, sizeof(itemName)); + + 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); + + } + + 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; + + default: + + wdef = BotItems_WeaponDef(kind); + + if (wdef) { + + Q_strncpyz(buf, wdef->goalname, bufsize); + + } + + break; + + } + +} + + + +static float BotItems_PriorityScale(int kind) { + + const botItemWeaponDef_t *wdef; + + + + switch (kind) { + + case BOT_ITEM_QUAD: + + return 0.45f; + + case BOT_ITEM_ENEMY_FLAG: + + return 0.55f; + + case BOT_ITEM_MEGA_HEALTH: + + return 0.70f; + + case BOT_ITEM_RED_ARMOR: + + return 0.80f; + + case BOT_ITEM_YELLOW_ARMOR: + + return 0.90f; + + default: + + wdef = BotItems_WeaponDef(kind); + + if (wdef) { + + return wdef->priorityScale; + + } + + return 1.0f; + + } + +} + + + +void BotItems_RegisterCvars(void) { + + trap_Cvar_Register(&bot_enhanced_items, "bot_enhanced_items", "1", CVAR_ARCHIVE); + + trap_Cvar_Update(&bot_enhanced_items); + + trap_Cvar_Register(&bot_enhanced_items_debug, "bot_enhanced_items_debug", "0", 0); + + trap_Cvar_Update(&bot_enhanced_items_debug); + +} + + + +int BotItems_IsActive(void) { + + if (!BotEnhanced_IsActive()) { + + return 0; + + } + + trap_Cvar_Update(&bot_enhanced_items); + + return bot_enhanced_items.integer != 0; + +} + + + +void BotItems_Reset(bot_state_t *bs) { + + if (!bs) { + + return; + + } + + bs->item_commit_active = qfalse; + + bs->item_commit_kind = BOT_ITEM_NONE; + + bs->item_commit_until = 0.0f; + + bs->item_next_scan_time = 0.0f; + + memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); + + bs->item_commit_snap_health = 0; + + bs->item_commit_snap_armor = 0; + + bs->item_commit_snap_quad = 0; + + bs->item_commit_snap_redflag = 0; + + bs->item_commit_snap_blueflag = 0; + + bs->item_commit_snap_weapon = 0; + +} + + + +static void BotItems_ClearCommit(bot_state_t *bs, int endEvent) { + + bot_goal_t top; + + int kind; + + qboolean wasActive; + + int goalNumber; + + + + if (!bs) { + + return; + + } + + wasActive = bs->item_commit_active; + + kind = bs->item_commit_kind; + + goalNumber = bs->item_commit_goal.number; + + if (bs->item_commit_active && trap_BotGetTopGoal(bs->gs, &top)) { + + if (top.number == bs->item_commit_goal.number) { + + trap_BotPopGoal(bs->gs); + + } + + } + + bs->item_commit_active = qfalse; + + bs->item_commit_kind = BOT_ITEM_NONE; + + bs->item_commit_until = 0.0f; + + memset(&bs->item_commit_goal, 0, sizeof(bot_goal_t)); + + bs->item_commit_snap_health = 0; + + bs->item_commit_snap_armor = 0; + + bs->item_commit_snap_quad = 0; + + bs->item_commit_snap_redflag = 0; + + bs->item_commit_snap_blueflag = 0; + + bs->item_commit_snap_weapon = 0; + + + + if (wasActive && endEvent == BOT_ITEMS_DBG_GONE && goalNumber) { + + trap_BotSetAvoidGoalTime(bs->gs, goalNumber, BOT_ITEMS_GONE_AVOID_TIME); + + } + + + + if (wasActive && endEvent) { + + switch (endEvent) { + + case BOT_ITEMS_DBG_GOT: + + BotItems_DebugLine(bs, kind, "picked up"); + + break; + + case BOT_ITEMS_DBG_TIMEOUT: + + (bs, kind, "abandoned (timeout)"); + + break; + + case BOT_ITEMS_DBG_GONE: + + BotItems_DebugLine(bs, kind, "abandoned (gone)"); + + break; + + case BOT_ITEMS_DBG_RESET: + + BotItems_DebugLine(bs, kind, "abandoned (reset)"); + + break; + + default: + + break; + + } + + } + +} + + + +static qboolean BotItems_CanConsider(bot_state_t *bs) { + + if (!bs || !bs->inuse) { + + return qfalse; + + } + + if (BotIsDead(bs) || BotIsObserver(bs)) { + + return qfalse; + + } + + if (gametype == GT_CTF || gametype == GT_CTF_ELIMINATION) { + + if (BotCTFCarryingFlag(bs)) { + + return qfalse; + + } + + } + + return qtrue; + +} + + + +static qboolean BotItems_DenialPickupGametype(void) { + + return gametype == GT_FFA || gametype == GT_TOURNAMENT; + +} + + + +static qboolean BotItems_FlagCaptureGametype(void) { + + return gametype == GT_CTF || gametype == GT_CTF_ELIMINATION; + +} + + + +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; + + } + + + + wdef = BotItems_WeaponDef(kind); + + if (wdef) { + + return bs->inventory[wdef->inventoryIndex] <= 0; + + } + + + + if (BotItems_DenialPickupGametype()) { + + switch (kind) { + + case BOT_ITEM_MEGA_HEALTH: + + case BOT_ITEM_RED_ARMOR: + + case BOT_ITEM_YELLOW_ARMOR: + + return qtrue; + + case BOT_ITEM_QUAD: + + case BOT_ITEM_ENEMY_FLAG: + + return qtrue; + + default: + + return qfalse; + + } + + } + + + + switch (kind) { + + case BOT_ITEM_QUAD: + + return qtrue; + + case BOT_ITEM_ENEMY_FLAG: + + return BotItems_EnemyFlagAtBase(bs); + + case BOT_ITEM_MEGA_HEALTH: + + return bs->inventory[INVENTORY_HEALTH] < 150; + + case BOT_ITEM_RED_ARMOR: + + return bs->inventory[INVENTORY_ARMOR] < 140; + + case BOT_ITEM_YELLOW_ARMOR: + + return bs->inventory[INVENTORY_ARMOR] < 80; + + default: + + return qfalse; + + } + +} + + + +static void BotItems_SnapshotCommitInventory(bot_state_t *bs, int kind) { + + const botItemWeaponDef_t *wdef; + + + + if (!bs) { + + return; + + } + + bs->item_commit_snap_health = bs->inventory[INVENTORY_HEALTH]; + + bs->item_commit_snap_armor = bs->inventory[INVENTORY_ARMOR]; + + bs->item_commit_snap_quad = bs->inventory[INVENTORY_QUAD]; + + bs->item_commit_snap_redflag = bs->inventory[INVENTORY_REDFLAG]; + + bs->item_commit_snap_blueflag = bs->inventory[INVENTORY_BLUEFLAG]; + + wdef = BotItems_WeaponDef(kind); + + bs->item_commit_snap_weapon = wdef ? bs->inventory[wdef->inventoryIndex] : 0; + +} + + + +/* Goal achieved: no longer need item, or inventory improved since commit (denial modes). */ + +static qboolean BotItems_CommitAchieved(bot_state_t *bs) { + + const botItemWeaponDef_t *wdef; + + int kind; + + + + if (!bs || !bs->item_commit_active) { + + return qfalse; + + } + + kind = bs->item_commit_kind; + + if (kind == BOT_ITEM_NONE) { + + return qfalse; + + } + + if (!BotItems_NeedsKind(bs, kind)) { + + return qtrue; + + } + + wdef = BotItems_WeaponDef(kind); + + if (wdef) { + + return bs->inventory[wdef->inventoryIndex] > bs->item_commit_snap_weapon; + + } + + switch (kind) { + + case BOT_ITEM_QUAD: + + 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: + + return bs->inventory[INVENTORY_HEALTH] > bs->item_commit_snap_health; + + case BOT_ITEM_RED_ARMOR: + + case BOT_ITEM_YELLOW_ARMOR: + + return bs->inventory[INVENTORY_ARMOR] > bs->item_commit_snap_armor; + + default: + + return qfalse; + + } + +} + + + +/* Pickup still on the map (not taken / hidden for respawn). */ + +static qboolean BotItems_PickupEntityActive(int entnum) { + + gentity_t *ent; + + + + if (entnum <= MAX_CLIENTS || entnum >= level.num_entities) { + + return qfalse; + + } + + ent = &g_entities[entnum]; + + if (!ent->inuse || !ent->item) { + + return qfalse; + + } + + if (ent->s.eType != ET_ITEM && ent->s.eType != ET_GENERAL) { + + return qfalse; + + } + + if (ent->s.eFlags & EF_NODRAW) { + + return qfalse; + + } + + if (ent->r.svFlags & SVF_NOCLIENT) { + + return qfalse; + + } + + return qtrue; + +} + + + +static qboolean BotItems_RefreshGoalByNumber(bot_state_t *bs, bot_goal_t *goal, int kind) { + + char goalname[64]; + + int index; + + bot_goal_t tmp; + + + + if (!goal || kind == BOT_ITEM_NONE) { + + return qfalse; + + } + + BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + + if (!goalname[0]) { + + return qfalse; + + } + + index = -1; + + while ((index = trap_BotGetLevelItemGoal(index, goalname, &tmp)) >= 0) { + + if (tmp.number == goal->number) { + + memcpy(goal, &tmp, sizeof(bot_goal_t)); + + return qtrue; + + } + + } + + return qfalse; + +} + + + +/* Same LOS test as BotNearestVisibleItem — trace only, no view FOV. */ + +static qboolean BotItems_GoalVisibleToBot(bot_state_t *bs, bot_goal_t *goal) { + + vec3_t dir; + + float dist; + + bsp_trace_t trace; + + + + if (!bs || !goal) { + + return qfalse; + + } + + + + 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); + + return trace.fraction >= 1.0f; + +} + + + +static qboolean BotItems_GoalIsPresent(bot_state_t *bs, bot_goal_t *goal) { + + if (!bs || !goal || !(goal->flags & GFL_ITEM)) { + + return qfalse; + + } + + if (!BotItems_PickupEntityActive(goal->entitynum)) { + + return qfalse; + + } + + if (trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) { + + return qfalse; + + } + + return qtrue; + +} + + + +/* Acquire-time AAS reachability (func_button pattern in ai_dmq3.c). */ + +static qboolean BotItems_GoalReachable(bot_state_t *bs, bot_goal_t *goal) { + + int t; + + + + if (!bs || !goal || !goal->areanum) { + + return qfalse; + + } + + if (!trap_AAS_AreaReachability(bs->areanum)) { + + return qfalse; + + } + + t = trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, goal->areanum, bs->tfl); + + return t > 0; + +} + + + +static void BotItems_BeginCommit(bot_state_t *bs, bot_goal_t *goal, int kind) { + + if (!bs || !goal) { + + return; + + } + + + + memcpy(&bs->item_commit_goal, goal, sizeof(bot_goal_t)); + + bs->item_commit_active = qtrue; + + bs->item_commit_kind = kind; + + bs->item_commit_until = FloatTime() + BOT_ITEMS_COMMIT_DURATION; + + BotItems_SnapshotCommitInventory(bs, kind); + + + + trap_BotRemoveFromAvoidGoals(bs->gs, goal->number); + + trap_BotPushGoal(bs->gs, goal); + + bs->nbg_time = bs->item_commit_until; + + BotItems_DebugLine(bs, kind, "going for"); + +} + + + +static void BotItems_EnsureGoalOnStack(bot_state_t *bs) { + + bot_goal_t top; + + + + if (!bs || !bs->item_commit_active) { + + return; + + } + + if (!trap_BotGetTopGoal(bs->gs, &top) || top.number != bs->item_commit_goal.number) { + + trap_BotPushGoal(bs->gs, &bs->item_commit_goal); + + } + + if (bs->nbg_time < FloatTime() + 1.0f) { + + bs->nbg_time = bs->item_commit_until; + + } + +} + + + +static qboolean BotItems_TryAcquireVisible(bot_state_t *bs) { + + int 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; + + } + + if (bs->item_commit_active) { + + return qfalse; + + } + + if (FloatTime() < bs->item_next_scan_time) { + + return qfalse; + + } + + bs->item_next_scan_time = FloatTime() + BOT_ITEMS_SCAN_INTERVAL; + + + + bestKind = BOT_ITEM_NONE; + + bestDist = BOT_ITEMS_VISIBLE_RANGE; + + + + for (k = 0; k < BOT_ITEMS_SCAN_KIND_COUNT; k++) { + + kind = botItemsScanKinds[k]; + + if (!BotItems_NeedsKind(bs, kind)) { + + continue; + + } + + BotItems_GoalName(bs, kind, goalname, sizeof(goalname)); + + if (!goalname[0]) { + + continue; + + } + + + + index = -1; + + while ((index = trap_BotGetLevelItemGoal(index, goalname, &goal)) >= 0) { + + 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; + + } + + + + BotItems_BeginCommit(bs, &bestGoal, bestKind); + + return qtrue; + +} + + + +static void BotItems_DebugConfigOnce(void) { + + static qboolean done; + + + + if (done || !BotItems_DebugEnabled()) { + + return; + + } + + done = qtrue; + + if (!BotEnhanced_IsActive()) { + + G_Printf("BotItems: debug on but bot_enhanced is 0 — no scans\n"); + + return; + + } + + trap_Cvar_Update(&bot_enhanced_items); + + if (!bot_enhanced_items.integer) { + + G_Printf("BotItems: debug on but bot_enhanced_items is 0 — no scans\n"); + + return; + + } + + G_Printf("BotItems: debug active (quad, flag, armor, mega, weapons)\n"); + +} + + + +void BotItems_Tick(bot_state_t *bs) { + + BotItems_DebugConfigOnce(); + + if (!BotItems_IsActive()) { + + return; + + } + + if (!BotItems_CanConsider(bs)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_RESET); + + return; + + } + + + + if (bs->item_commit_active) { + + if (FloatTime() >= bs->item_commit_until) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_TIMEOUT); + + return; + + } + + if (BotItems_CommitAchieved(bs)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); + + return; + + } + + if (!BotItems_RefreshGoalByNumber(bs, &bs->item_commit_goal, bs->item_commit_kind)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + + return; + + } + + if (!BotItems_GoalIsPresent(bs, &bs->item_commit_goal)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + + return; + + } + + BotItems_EnsureGoalOnStack(bs); + + return; + + } + + + + BotItems_TryAcquireVisible(bs); + +} + + + +int BotItems_HasActiveCommit(const bot_state_t *bs) { + + if (!bs || !BotItems_IsActive()) { + + return 0; + + } + + return bs->item_commit_active && FloatTime() < bs->item_commit_until; + +} + + + +int BotItems_ShouldRunPickupNode(bot_state_t *bs) { + + return BotItems_HasActiveCommit(bs); + +} + + + +float BotItems_CommitNbgTime(bot_state_t *bs) { + + if (!bs || !BotItems_HasActiveCommit(bs)) { + + return FloatTime(); + + } + + return bs->item_commit_until; + +} + + + +int BotItems_IsCommittedGoal(const bot_state_t *bs, const bot_goal_t *goal) { + + if (!bs || !goal || !bs->item_commit_active) { + + return 0; + + } + + return goal->number == bs->item_commit_goal.number; + +} + + + +int BotItems_HandleReachedGoal(bot_state_t *bs, bot_goal_t *goal) { + + if (!BotItems_IsActive() || !goal || !(goal->flags & GFL_ITEM)) { + + return -1; + + } + + if (!BotItems_IsCommittedGoal(bs, goal)) { + + return -1; + + } + + + + if (trap_BotTouchingGoal(bs->origin, goal)) { + + trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); + + return 1; + + } + + + + if (BotItems_CommitAchieved(bs)) { + + trap_BotSetAvoidGoalTime(bs->gs, goal->number, -1); + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GOT); + + return 1; + + } + + + + if (!BotItems_PickupEntityActive(goal->entitynum) || + + trap_BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) { + + BotItems_ClearCommit(bs, BOT_ITEMS_DBG_GONE); + + return 1; + + } + + + + return 0; + +} + + + +int BotItems_ShouldPreserveGoalStack(bot_state_t *bs) { + + if (!BotItems_HasActiveCommit(bs)) { + + return 0; + + } + + return 1; + +} + + diff --git a/ratoa_gamecode/code/game/ai_bot_items.h b/ratoa_gamecode/code/game/ai_bot_items.h new file mode 100644 index 0000000..647cf84 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_items.h @@ -0,0 +1,31 @@ +/* +=========================================================================== +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. +=========================================================================== +*/ + +#ifndef AI_BOT_ITEMS_H +#define AI_BOT_ITEMS_H + +struct bot_state_s; +struct bot_goal_s; + +void BotItems_RegisterCvars(void); +int BotItems_IsActive(void); +void BotItems_Reset(struct bot_state_s *bs); + +void BotItems_Tick(struct bot_state_s *bs); + +int BotItems_HasActiveCommit(const struct bot_state_s *bs); +int BotItems_ShouldRunPickupNode(struct bot_state_s *bs); +float BotItems_CommitNbgTime(struct bot_state_s *bs); + +/* -1 = use vanilla; 0 = not reached; 1 = reached (touch or commit done) */ +int BotItems_HandleReachedGoal(struct bot_state_s *bs, struct bot_goal_s *goal); + +int BotItems_ShouldPreserveGoalStack(struct bot_state_s *bs); + +#endif /* AI_BOT_ITEMS_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_move_harness.c b/ratoa_gamecode/code/game/ai_bot_move_harness.c new file mode 100644 index 0000000..9cea37a --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_move_harness.c @@ -0,0 +1,24 @@ +/* +=========================================================================== +BOT MOVE HARNESS — no-op stubs (Phase 6 scaffold). +=========================================================================== +*/ + +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ea.h" +#include "../botlib/be_ai_char.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" +#include "../botlib/be_ai_weap.h" +#include "ai_main.h" +#include "ai_bot_move_harness.h" + +void BotMoveHarness_Reset(bot_state_t *bs) { + (void)bs; +} + +int BotMoveHarness_IsActive(void) { + return 0; +} diff --git a/ratoa_gamecode/code/game/ai_bot_move_harness.h b/ratoa_gamecode/code/game/ai_bot_move_harness.h new file mode 100644 index 0000000..4265b6e --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_move_harness.h @@ -0,0 +1,18 @@ +/* +=========================================================================== +BOT MOVE HARNESS — stub for future movement actuation (not wired yet). + +Reset from BotEnhanced_ResetBot; BotMoveHarness_IsActive always false until +combat.move_policy is read from BotAttackMove / BotUpdateInput. +=========================================================================== +*/ + +#ifndef AI_BOT_MOVE_HARNESS_H +#define AI_BOT_MOVE_HARNESS_H + +struct bot_state_s; + +void BotMoveHarness_Reset(struct bot_state_s *bs); +int BotMoveHarness_IsActive(void); + +#endif /* AI_BOT_MOVE_HARNESS_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.c b/ratoa_gamecode/code/game/ai_bot_tactics.c index 232e047..b5b795e 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.c +++ b/ratoa_gamecode/code/game/ai_bot_tactics.c @@ -16,6 +16,7 @@ BOT TACTICAL AI — see ai_bot_tactics.h #include "inv.h" #include "ai_dmq3.h" #include "ai_bot_tactics.h" +#include "ai_bot_enhanced.h" void AIEnter_Battle_Retreat(bot_state_t *bs, char *s); void AIEnter_Battle_Fight(bot_state_t *bs, char *s); @@ -23,9 +24,8 @@ int AINode_Battle_Fight(bot_state_t *bs); qboolean EntityCarriesFlag(aas_entityinfo_t *entinfo); -vmCvar_t bot_tacticalAI; +vmCvar_t bot_enhanced_tactics; -#define BOT_TACTICS_GAUNTLET_RUSH_DIST 192 #define BOT_TACTICS_FAR_ENGAGE_DIST 512 #define BOT_TACTICS_CLOSER_MARGIN 128 #define BOT_TACTICS_FINISH_HEALTH 40 @@ -42,8 +42,7 @@ typedef enum { } tact_action_t; static int BotTactics_IsActive(void) { - trap_Cvar_Update(&bot_tacticalAI); - return bot_tacticalAI.integer != 0; + return BotEnhanced_TacticsActive(); } static int BotTactics_HasUsableNonGauntletWeapon(bot_state_t *bs) { @@ -63,7 +62,7 @@ static int BotTactics_HasUsableNonGauntletWeapon(bot_state_t *bs) { return 0; } -static int BotTactics_IsGauntletOnly(bot_state_t *bs) { +int BotTactics_IsGauntletOnly(bot_state_t *bs) { if (!(bs->cur_ps.stats[STAT_WEAPONS] & (1 << WP_GAUNTLET))) { return 0; } @@ -100,6 +99,9 @@ static int BotTactics_IsValidEnemyClient(bot_state_t *bs, int clientnum) { if (g_entities[clientnum].flags & FL_NOTARGET) { return 0; } + if (BotEnhanced_IsActive() && BotEnhanced_ClientIsChatting(clientnum)) { + return 0; + } return 1; } @@ -170,7 +172,7 @@ static void BotTactics_QueueEvent(bot_state_t *bs, int evt, int attacker, int da bs->tact_evt_mod = mod; } -static void BotTactics_ScanEvents(bot_state_t *bs) { +void BotTactics_ScanEvents(bot_state_t *bs) { int damage, attacker, mod; gclient_t *cl; @@ -290,7 +292,7 @@ static void BotTactics_ApplyHurtByOther(bot_state_t *bs) { } } -static void BotTactics_ProcessPending(bot_state_t *bs) { +void BotTactics_ProcessPending(bot_state_t *bs) { int pending; if (!BotTactics_IsActive()) { @@ -307,8 +309,9 @@ static void BotTactics_ProcessPending(bot_state_t *bs) { } void BotTactics_RegisterCvars(void) { - trap_Cvar_Register(&bot_tacticalAI, "bot_tacticalAI", "0", CVAR_ARCHIVE); - trap_Cvar_Update(&bot_tacticalAI); + trap_Cvar_Register(&bot_enhanced_tactics, "bot_enhanced_tactics", "0", + CVAR_ARCHIVE); + trap_Cvar_Update(&bot_enhanced_tactics); } void BotTactics_Reset(bot_state_t *bs) { @@ -323,15 +326,6 @@ void BotTactics_Reset(bot_state_t *bs) { bs->tact_last_hurt_time = -999999.0f; } -void BotTactics_OnThink(bot_state_t *bs) { - if (!BotTactics_IsActive()) { - bs->tact_pending = 0; - return; - } - BotTactics_ScanEvents(bs); - BotTactics_ProcessPending(bs); -} - int BotTactics_BattleFightTryFlee(bot_state_t *bs) { if (!BotTactics_IsActive()) { bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; @@ -344,7 +338,7 @@ int BotTactics_BattleFightTryFlee(bot_state_t *bs) { if (bs->enemy < 0) { return qfalse; } - if (bs->inventory[ENEMY_HORIZONTAL_DIST] <= BOT_TACTICS_GAUNTLET_RUSH_DIST) { + if (bs->inventory[ENEMY_HORIZONTAL_DIST] <= BOT_TACTICS_GAUNTLET_FLEE_DIST) { bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; return qfalse; } @@ -363,7 +357,7 @@ int BotTactics_BattleFightSuppressRetreat(bot_state_t *bs) { if (bs->enemy < 0) { return qfalse; } - if (bs->inventory[ENEMY_HORIZONTAL_DIST] <= BOT_TACTICS_GAUNTLET_RUSH_DIST) { + if (bs->inventory[ENEMY_HORIZONTAL_DIST] <= BOT_TACTICS_GAUNTLET_FLEE_DIST) { return qtrue; } return qfalse; @@ -382,7 +376,7 @@ void BotTactics_RetreatAfterInventory(bot_state_t *bs) { bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; return; } - if (bs->inventory[ENEMY_HORIZONTAL_DIST] > BOT_TACTICS_GAUNTLET_RUSH_DIST) { + if (bs->inventory[ENEMY_HORIZONTAL_DIST] > BOT_TACTICS_GAUNTLET_FLEE_DIST) { bs->flags |= BFL_TACTICS_SURVIVAL_FLEE; } } diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.h b/ratoa_gamecode/code/game/ai_bot_tactics.h index b7d463b..2bcc007 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.h +++ b/ratoa_gamecode/code/game/ai_bot_tactics.h @@ -1,8 +1,9 @@ /* =========================================================================== -BOT TACTICAL AI — decision hooks + minimal events, gated by bot_tacticalAI. +BOT TACTICAL AI — decision hooks + minimal events, gated by bot_enhanced_tactics. -Ringfenced: logic in ai_bot_tactics.c. Toggle with bot_tacticalAI (0/1). +Ringfenced: logic in ai_bot_tactics.c. Toggle with bot_enhanced_tactics (requires bot_enhanced). +Was bot_tacticalAI (migrated at init if bot_enhanced_tactics is unset). Remove: delete ai_bot_tactics.c/h, Makefile/q3asm/bat entries, revert hooks in ai_main.h, ai_main.c, ai_dmq3.c, ai_dmnet.c. @@ -14,14 +15,21 @@ ai_main.h, ai_main.c, ai_dmq3.c, ai_dmnet.c. struct bot_state_s; -/* Pending event bits (set by scan/notify, cleared in Process). */ -#define BOT_TACT_EVT_HURT_BY_OTHER 1 +#include "ai_bot_events.h" +#include "ai_bot_combat.h" +#define BOT_TACT_EVT_HURT_BY_OTHER BOT_EVT_HURT_BY_OTHER + +/* Gauntlet-only: engage within last-resort rush range; flee beyond (see ai_bot_combat.h). */ +#define BOT_TACTICS_GAUNTLET_FLEE_DIST BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST void BotTactics_RegisterCvars(void); void BotTactics_Reset(struct bot_state_s *bs); -/* Once per think after inventory: edge-detect damage, dispatch pending events. */ -void BotTactics_OnThink(struct bot_state_s *bs); +/* Called from BotEvents_Drain only (world scan + pending dispatch). */ +void BotTactics_ScanEvents(struct bot_state_s *bs); +void BotTactics_ProcessPending(struct bot_state_s *bs); + +int BotTactics_IsGauntletOnly(struct bot_state_s *bs); /* Gauntlet-only survival */ int BotTactics_BattleFightTryFlee(struct bot_state_s *bs); diff --git a/ratoa_gamecode/code/game/ai_dmnet.c b/ratoa_gamecode/code/game/ai_dmnet.c index f8d5e57..1a30139 100644 --- a/ratoa_gamecode/code/game/ai_dmnet.c +++ b/ratoa_gamecode/code/game/ai_dmnet.c @@ -42,6 +42,9 @@ 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_bot_combat.h" +#include "ai_bot_items.h" #include "ai_bot_tactics.h" #include "ai_dmq3.h" #include "ai_chat.h" @@ -231,6 +234,14 @@ BotReachedGoal */ int BotReachedGoal(bot_state_t *bs, bot_goal_t *goal) { if (goal->flags & GFL_ITEM) { + { + int itemReached; + + itemReached = BotItems_HandleReachedGoal(bs, goal); + if (itemReached >= 0) { + return itemReached; + } + } //if touching the goal if (trap_BotTouchingGoal(bs->origin, goal)) { if (!(goal->flags & GFL_DROPPED)) { @@ -1700,7 +1711,7 @@ int AINode_Seek_ActivateEntity(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } } - else if (!(bs->flags & BFL_IDEALVIEWSET)) { + else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { if (trap_BotMovementViewTarget(bs->ms, goal, bs->tfl, 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); @@ -1711,8 +1722,10 @@ int AINode_Seek_ActivateEntity(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } // if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) + if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { bs->weaponnum = moveresult.weapon; + } + BotAI_HandleWeaponJumpMove(bs, goal, &moveresult); // if there is an enemy if (BotFindEnemy(bs, -1)) { if (BotWantsToRetreat(bs)) { @@ -1835,7 +1848,7 @@ int AINode_Seek_NBG(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } } - else if (!(bs->flags & BFL_IDEALVIEWSET)) { + else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { if (!trap_BotGetSecondGoal(bs->gs, &goal)) trap_BotGetTopGoal(bs->gs, &goal); if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) { VectorSubtract(target, bs->origin, dir); @@ -1846,14 +1859,17 @@ int AINode_Seek_NBG(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon; + if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { + bs->weaponnum = moveresult.weapon; + } + BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); //if there is an enemy if (BotFindEnemy(bs, -1)) { if (BotWantsToRetreat(bs)) { //keep the current long term goal and retreat AIEnter_Battle_NBG(bs, "seek nbg: found enemy"); } - else { + else if (!BotItems_ShouldPreserveGoalStack(bs)) { trap_BotResetLastAvoidReach(bs->ms); //empty the goal stack trap_BotEmptyGoalStack(bs->gs); @@ -1962,7 +1978,9 @@ int AINode_Seek_LTG(bot_state_t *bs) if (bs->check_time < FloatTime()) { bs->check_time = FloatTime() + 0.5; //check if the bot wants to camp - BotWantsToCamp(bs); + if (BotEnhanced_AllowsCamping()) { + BotWantsToCamp(bs); + } // if (bs->ltgtype == LTG_DEFENDKEYAREA) range = 400; else range = 150; @@ -1985,6 +2003,11 @@ int AINode_Seek_LTG(bot_state_t *bs) } */ // + if (BotItems_ShouldRunPickupNode(bs)) { + bs->nbg_time = BotItems_CommitNbgTime(bs); + AIEnter_Seek_NBG(bs, "items: committed pickup"); + return qfalse; + } if (BotNearbyGoal(bs, bs->tfl, &goal, range)) { trap_BotResetLastAvoidReach(bs->ms); //get the goal at the top of the stack @@ -2028,7 +2051,7 @@ int AINode_Seek_LTG(bot_state_t *bs) bs->ideal_viewangles[2] *= 0.5; } } - else if (!(bs->flags & BFL_IDEALVIEWSET)) { + else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { if (trap_BotMovementViewTarget(bs->ms, &goal, bs->tfl, 300, target)) { VectorSubtract(target, bs->origin, dir); vectoangles(dir, bs->ideal_viewangles); @@ -2046,7 +2069,10 @@ int AINode_Seek_LTG(bot_state_t *bs) bs->ideal_viewangles[2] *= 0.5; } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon; + if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { + bs->weaponnum = moveresult.weapon; + } + BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); // return qtrue; } @@ -2162,6 +2188,11 @@ int AINode_Battle_Fight(bot_state_t *bs) { } //update the attack inventory values BotUpdateBattleInventory(bs, bs->enemy); + if (BotItems_ShouldRunPickupNode(bs)) { + bs->nbg_time = BotItems_CommitNbgTime(bs); + AIEnter_Battle_NBG(bs, "items: committed pickup"); + return qfalse; + } BotTactics_PreferCloserEnemy(bs); if (BotTactics_BattleFightTryFlee(bs)) { return qfalse; @@ -2212,6 +2243,9 @@ int AINode_Battle_Fight(bot_state_t *bs) { } //choose the best weapon to fight with BotChooseWeapon(bs); + if (BotEnhanced_IsActive()) { + BotCombat_UpdateIntent(bs); + } //do attack movements moveresult = BotAttackMove(bs, bs->tfl); //if the movement failed @@ -2229,7 +2263,7 @@ int AINode_Battle_Fight(bot_state_t *bs) { BotCheckAttack(bs); //if the bot wants to retreat if (!(bs->flags & BFL_FIGHTSUICIDAL)) { - if (!BotTactics_BattleFightSuppressRetreat(bs) && BotWantsToRetreat(bs)) { + if (!BotEnhanced_ShouldSuppressFightRetreat(bs) && BotWantsToRetreat(bs)) { AIEnter_Battle_Retreat(bs, "battle fight: wants to retreat"); return qtrue; } @@ -2328,6 +2362,11 @@ int AINode_Battle_Chase(bot_state_t *bs) bs->check_time = FloatTime() + 1; range = 150; // + if (BotItems_ShouldRunPickupNode(bs)) { + bs->nbg_time = BotItems_CommitNbgTime(bs); + AIEnter_Battle_NBG(bs, "items: committed pickup"); + return qfalse; + } if (BotNearbyGoal(bs, bs->tfl, &goal, range)) { //the bot gets 5 seconds to pick up the nearby goal item bs->nbg_time = FloatTime() + 0.1 * range + 1; @@ -2356,7 +2395,7 @@ 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)) { + else if (!(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs)) { if (bs->chase_time > FloatTime() - 2) { BotAimAtEnemy(bs); } @@ -2372,7 +2411,10 @@ int AINode_Battle_Chase(bot_state_t *bs) bs->ideal_viewangles[2] *= 0.5; } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon; + if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { + bs->weaponnum = moveresult.weapon; + } + BotAI_HandleWeaponJumpMove(bs, &goal, &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) @@ -2447,6 +2489,11 @@ int AINode_Battle_Retreat(bot_state_t *bs) { //update the attack inventory values BotUpdateBattleInventory(bs, bs->enemy); BotTactics_RetreatAfterInventory(bs); + if (BotCombat_ShouldEngageFromRetreat(bs)) { + bs->flags &= ~BFL_TACTICS_SURVIVAL_FLEE; + AIEnter_Battle_Fight(bs, "enhanced: close gauntlet charge"); + return qfalse; + } //if the bot doesn't want to retreat anymore... probably picked up some nice items if (BotWantsToChase(bs)) { //empty the goal stack, when chasing, only the enemy is the goal @@ -2518,6 +2565,11 @@ int AINode_Battle_Retreat(bot_state_t *bs) { } */ // + if (BotItems_ShouldRunPickupNode(bs)) { + bs->nbg_time = BotItems_CommitNbgTime(bs); + AIEnter_Battle_NBG(bs, "items: committed pickup"); + return qfalse; + } if (BotNearbyGoal(bs, bs->tfl, &goal, range)) { trap_BotResetLastAvoidReach(bs->ms); //time the bot gets to pick up the nearby goal item @@ -2542,11 +2594,11 @@ int AINode_Battle_Retreat(bot_state_t *bs) { //choose the best weapon to fight with BotChooseWeapon(bs); //if the view is fixed for the movement - if (moveresult.flags & (MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) { + if (moveresult.flags & (MOVERESULT_MOVEMENTVIEWSET|MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) { VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles); } else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET) - && !(bs->flags & BFL_IDEALVIEWSET) ) { + && !(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(bs) ) { attack_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1); //if the bot is skilled anough if (attack_skill > 0.3) { @@ -2564,7 +2616,10 @@ int AINode_Battle_Retreat(bot_state_t *bs) { } } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon; + if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { + bs->weaponnum = moveresult.weapon; + } + BotAI_HandleWeaponJumpMove(bs, &goal, &moveresult); //attack the enemy if possible BotCheckAttack(bs); // @@ -2686,11 +2741,11 @@ int AINode_Battle_NBG(bot_state_t *bs) { //choose the best weapon to fight with BotChooseWeapon(bs); //if the view is fixed for the movement - if (moveresult.flags & (MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) { + if (moveresult.flags & (MOVERESULT_MOVEMENTVIEWSET|MOVERESULT_MOVEMENTVIEW|MOVERESULT_SWIMVIEW)) { VectorCopy(moveresult.ideal_viewangles, bs->ideal_viewangles); } else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET) - && !(bs->flags & BFL_IDEALVIEWSET)) { + && !(bs->flags & BFL_IDEALVIEWSET) && !BotAI_WeaponJumpActive(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) { @@ -2709,7 +2764,10 @@ int AINode_Battle_NBG(bot_state_t *bs) { } } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon; + if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) { + bs->weaponnum = moveresult.weapon; + } + BotAI_HandleWeaponJumpMove(bs, &goal, &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 8cac3c1..3630513 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -33,6 +33,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "g_local.h" #include "../botlib/botlib.h" +#include "../botlib/aasfile.h" #include "../botlib/be_aas.h" #include "../botlib/be_ea.h" #include "../botlib/be_ai_char.h" @@ -47,6 +48,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #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_chat.h" #include "ai_cmd.h" #include "ai_dmnet.h" @@ -1730,12 +1733,22 @@ void BotChooseWeapon(bot_state_t *bs) { trap_EA_SelectWeapon(bs->client, bs->weaponnum); } else { + if (BotEnhanced_WeaponsActive() && !BotWpnSelect_ShouldRunChooser(bs)) { + trap_EA_SelectWeapon(bs->client, bs->weaponnum); + return; + } if(g_instantgib.integer) newweaponnum = WP_RAILGUN; else if(g_rockets.integer) newweaponnum = WP_ROCKET_LAUNCHER; else { - newweaponnum = BotWpnSelect_Choose(bs); + if (bs->enemy >= 0) { + newweaponnum = BotWpnSelect_Choose(bs); + } else if (BotEnhanced_WeaponsActive()) { + newweaponnum = BotWpnSelect_ChooseRoaming(bs); + } else { + newweaponnum = -1; + } if (newweaponnum < 0 || newweaponnum >= WP_NUM_WEAPONS) { newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory); } @@ -2664,6 +2677,9 @@ int BotWantsToCamp(bot_state_t *bs) { int cs, traveltime, besttraveltime; bot_goal_t goal, bestgoal; + if (!BotEnhanced_AllowsCamping()) { + return qfalse; + } camper = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_CAMPER, 0, 1); if (camper < 0.1) return qfalse; //if the bot has a team goal @@ -2877,6 +2893,18 @@ 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 */ + if (BotCombat_IsRushOpponent(bs) && + bs->combat.move_policy == BOT_MOVE_CLOSE_MELEE) { + movetype = MOVE_WALK; + if (trap_BotMoveInDirection(bs->ms, forward, 400, movetype)) { + return moveresult; + } + if (trap_BotMoveInDirection(bs->ms, forward, 400, MOVE_RUN)) { + return moveresult; + } + return moveresult; + } //if the bot is stupid if (attack_skill <= 0.4) { //just walk to or away from the enemy @@ -2961,6 +2989,330 @@ 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 @@ -3120,6 +3472,11 @@ int BotFindEnemy(bot_state_t *bs, int curenemy) { bs->enemy = -1; curenemy = -1; } + if (BotEnhanced_IsActive() && curenemy >= 0 && curenemy < MAX_CLIENTS && + !BotEnhanced_CanEngageClient(bs, curenemy)) { + bs->enemy = -1; + curenemy = -1; + } if (curenemy >= 0) { BotEntityInfo(curenemy, &curenemyinfo); if (EntityCarriesFlag(&curenemyinfo)) return qfalse; @@ -3177,8 +3534,14 @@ int BotFindEnemy(bot_state_t *bs, int curenemy) { // this has nothing to do with lag compensation, but it's great for testing if ( g_entities[i].flags & FL_NOTARGET ) continue; //unlagged - misc - //if not an easy fragger don't shoot at chatting players - if (easyfragger < 0.5 && EntityIsChatting(&entinfo)) continue; + /* Enhanced: never acquire or switch to a chatting player. */ + if (BotEnhanced_IsActive()) { + if (!BotEnhanced_CanEngageClient(bs, i)) { + continue; + } + } else if (easyfragger < 0.5 && EntityIsChatting(&entinfo)) { + continue; + } // if (lastteleport_time > FloatTime() - 3) { VectorSubtract(entinfo.origin, lastteleport_origin, dir); @@ -3462,6 +3825,9 @@ void BotAimAtEnemy(bot_state_t *bs) { if (BotTargetPlayerIsDead(bs)) { return; } + if (BotEnhanced_IsActive() && !BotEnhanced_CanEngageClient(bs, bs->enemy)) { + return; + } //get the enemy entity information BotEntityInfo(bs->enemy, &entinfo); //if this is not a player (should be an obelisk) @@ -3660,10 +4026,13 @@ void BotAimAtEnemy(bot_state_t *bs) { } } } - if (!BotAimHarness_IsActive()) { + /* ENHANCED: aim — combat origin jitter when harness off */ + if (!BotEnhanced_AimActive()) { 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 { @@ -3715,24 +4084,24 @@ void BotAimAtEnemy(bot_state_t *bs) { f = 0.6 + dist / 150 * 0.4; aim_accuracy *= f; } - if (!BotAimHarness_IsActive()) { + if (!BotEnhanced_AimActive()) { if (aim_accuracy < 0.8) { VectorNormalize(dir); for (i = 0; i < 3; i++) dir[i] += 0.3 * crandom() * (1 - aim_accuracy); } } vectoangles(dir, bs->ideal_viewangles); - if (!BotAimHarness_IsActive()) { + if (!BotEnhanced_AimActive()) { bs->ideal_viewangles[PITCH] += 6 * wi.vspread * crandom() * (1 - aim_accuracy); bs->ideal_viewangles[PITCH] = AngleMod(bs->ideal_viewangles[PITCH]); bs->ideal_viewangles[YAW] += 6 * wi.hspread * crandom() * (1 - aim_accuracy); bs->ideal_viewangles[YAW] = AngleMod(bs->ideal_viewangles[YAW]); } else { - BotAimHarness_SetCombatGoal(bs, bs->ideal_viewangles, aim_accuracy, + BotAimHarness_SetCombatGoal(bs, bs->ideal_viewangles, aim_skill, aim_accuracy, wi.vspread, wi.hspread); } - //if the bots should be really challenging - if (bot_challenge.integer) { + /* Legacy challenge snap-aim; skipped when enhanced aim harness is active */ + if (bot_challenge.integer && !BotEnhanced_AimActive()) { //if the bot is really accurate and has the enemy in view for some time if (aim_accuracy > 0.9 && bs->enemysight_time < FloatTime() - 1) { //set the view angles directly @@ -3804,10 +4173,23 @@ void BotCheckAttack(bot_state_t *bs) { VectorSubtract(bs->aimtarget, bs->eye, dir); // if (bs->weaponnum == WP_GAUNTLET) { + if (BotCombat_IsRushOpponent(bs)) { + if (VectorLengthSquared(dir) > Square(BOT_COMBAT_GAUNTLET_ATTACK_DIST)) { + return; + } + trap_EA_Attack(bs->client); + bs->flags ^= BFL_ATTACKED; + return; + } if (VectorLengthSquared(dir) > Square(60)) { return; } } + /* ENHANCED: aim — suppressive fire (loose aim + per-frame hold for hitscan) */ + if (BotEnhanced_AimActive() && bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + BotAimHarness_CheckAttack(bs); + return; + } if (VectorLengthSquared(dir) < Square(100)) fov = 120; else @@ -4639,6 +5021,11 @@ void BotAIBlocked(bot_state_t *bs, bot_moveresult_t *moveresult, int activate) { bs->notblocked_time = FloatTime(); return; } + // weapon jump uses fixed view/weapon; sidestep avoidance breaks alignment + if (moveresult->flags & (MOVERESULT_MOVEMENTVIEWSET | MOVERESULT_MOVEMENTVIEW | + MOVERESULT_MOVEMENTWEAPON | MOVERESULT_SWIMVIEW)) { + return; + } // if stuck in a solid area if ( moveresult->type == RESULTTYPE_INSOLIDAREA ) { // move in a random direction in the hope to get out @@ -5449,7 +5836,10 @@ void BotDeathmatchAI(bot_state_t *bs, float thinktime) { BotSetTeleportTime(bs); //update some inventory values BotUpdateInventory(bs); - BotTactics_OnThink(bs); + BotEnhanced_OnThinkStart(bs); + if (BotEnhanced_WeaponsActive() && bs->enemy < 0 && !BotIsObserver(bs)) { + BotWpnSelect_TickRoaming(bs); + } //check out the snapshot BotCheckSnapshot(bs); //check for air diff --git a/ratoa_gamecode/code/game/ai_dmq3.h b/ratoa_gamecode/code/game/ai_dmq3.h index 46aec82..8763a40 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.h +++ b/ratoa_gamecode/code/game/ai_dmq3.h @@ -128,6 +128,11 @@ 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 89d89b0..47f1fde 100644 --- a/ratoa_gamecode/code/game/ai_main.c +++ b/ratoa_gamecode/code/game/ai_main.c @@ -49,6 +49,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "ai_cmd.h" #include "ai_dmnet.h" #include "ai_vcmd.h" +#include "ai_bot_enhanced.h" #include "ai_aim_harness.h" #include "ai_weapon_select.h" #include "ai_bot_tactics.h" @@ -535,7 +536,7 @@ void BotSetInfoConfigString(bot_state_t *bs) { } case LTG_POINTA: { - Com_sprintf(action, sizeof(action), "going for point A"); + Com_sprintf(action, sizeof(action), " point A"); break; } case LTG_POINTB: @@ -943,18 +944,50 @@ BotUpdateInput void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { bot_input_t bi; int j; + float thinktime; + + thinktime = (float)elapsed_time / 1000.0f; + + /* ENHANCED: aim — input-frame motor path */ + if (BotEnhanced_AimActive()) { + if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { + return; + } + for (j = 0; j < 3; j++) { + bs->viewangles[j] = AngleMod(bs->viewangles[j] + + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); + } + 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) { + if (bs->lastucmd.buttons & BUTTON_ATTACK) { + 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] - + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); + } + return; + } //add the delta angles to the bot's current view angles for (j = 0; j < 3; j++) { - if (j == PITCH && BotAimHarness_IsActive()) { - bs->viewangles[PITCH] = BotAimHarness_ClampPitchAngle(bs->viewangles[PITCH] + - SHORT2ANGLE(bs->cur_ps.delta_angles[PITCH])); - } else { - bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); - } + bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); } //change the bot view angles - BotChangeViewAngles(bs, (float) elapsed_time / 1000); + BotChangeViewAngles(bs, thinktime); //retrieve the bot input trap_EA_GetInput(bs->client, (float) time / 1000, &bi); //respawn hack @@ -965,12 +998,7 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { BotInputToUserCommand(&bi, &bs->lastucmd, bs->cur_ps.delta_angles, time); //subtract the delta angles for (j = 0; j < 3; j++) { - if (j == PITCH && BotAimHarness_IsActive()) { - bs->viewangles[PITCH] = BotAimHarness_ClampPitchAngle(bs->viewangles[PITCH] - - SHORT2ANGLE(bs->cur_ps.delta_angles[PITCH])); - } else { - bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); - } + bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); } } @@ -1076,13 +1104,11 @@ int BotAI(int client, float thinktime) { else if (!Q_stricmp(buf, "clientLevelShot")) { /*ignore*/ } } - //add the delta angles to the bot's current view angles - for (j = 0; j < 3; j++) { - if (j == PITCH && BotAimHarness_IsActive()) { - bs->viewangles[PITCH] = BotAimHarness_ClampPitchAngle(bs->viewangles[PITCH] + + /* ENHANCED: aim — legacy delta-angle rebase when harness off */ + if (!BotEnhanced_AimActive()) { + for (j = 0; j < 3; j++) { + bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); - } else { - bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j])); } } //increase the local time of the bot @@ -1100,13 +1126,10 @@ int BotAI(int client, float thinktime) { BotDeathmatchAI(bs, thinktime); //set the weapon selection every AI frame trap_EA_SelectWeapon(bs->client, bs->weaponnum); - //subtract the delta angles - for (j = 0; j < 3; j++) { - if (j == PITCH && BotAimHarness_IsActive()) { - bs->viewangles[PITCH] = BotAimHarness_ClampPitchAngle(bs->viewangles[PITCH] - + if (!BotEnhanced_AimActive()) { + for (j = 0; j < 3; j++) { + bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); - } else { - bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); } } //everything was ok @@ -1308,9 +1331,8 @@ int BotAISetupClient(int client, struct bot_settings_s *settings, qboolean resta if (restart) { BotReadSessionData(bs); } - BotAimHarness_Reset(bs); - BotWpnSelect_Reset(bs); - BotTactics_Reset(bs); + BotEnhanced_ResetBot(bs); + BotAimHarness_SyncClientDebug(client); //bot has been setup succesfully return qtrue; } @@ -1413,9 +1435,7 @@ void BotResetState(bot_state_t *bs) { if (bs->ws) trap_BotResetWeaponState(bs->ws); if (bs->gs) trap_BotResetAvoidGoals(bs->gs); if (bs->ms) trap_BotResetAvoidReach(bs->ms); - BotAimHarness_Reset(bs); - BotWpnSelect_Reset(bs); - BotTactics_Reset(bs); + BotEnhanced_ResetBot(bs); } /* @@ -1496,6 +1516,7 @@ int BotAIStartFrame(int time) { botstates[i]->lastucmd.buttons = 0; botstates[i]->lastucmd.serverTime = time; trap_BotUserCommand(botstates[i]->client, &botstates[i]->lastucmd); + BotAimHarness_PostInputSync(botstates[i]); } return qtrue; } @@ -1639,6 +1660,7 @@ int BotAIStartFrame(int time) { BotUpdateInput(botstates[i], time, elapsed_time); trap_BotUserCommand(botstates[i]->client, &botstates[i]->lastucmd); + BotAimHarness_PostInputSync(botstates[i]); } return qtrue; @@ -1738,9 +1760,7 @@ int BotAISetup( int restart ) { trap_Cvar_Register(&bot_interbreedbots, "bot_interbreedbots", "10", 0); trap_Cvar_Register(&bot_interbreedcycle, "bot_interbreedcycle", "20", 0); trap_Cvar_Register(&bot_interbreedwrite, "bot_interbreedwrite", "", 0); - BotAimHarness_RegisterCvars(); - BotWpnSelect_RegisterCvars(); - BotTactics_RegisterCvars(); + BotEnhanced_RegisterCvars(); //if the game is restarted for a tournament if (restart) { diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 1bdc0f1..fc966aa 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -128,6 +128,8 @@ typedef struct bot_activategoal_s struct bot_activategoal_s *next; //next activate goal on stack } bot_activategoal_t; +#include "ai_bot_combat.h" + //bot state typedef struct bot_state_s { @@ -288,6 +290,28 @@ typedef struct bot_state_s bot_waypoint_t *curpatrolpoint; //current patrol point the bot is going for int patrolflags; //patrol flags + /* ---- BOT ENHANCED (foundation): ai_bot_combat.c, ai_bot_events.c ---- */ + bot_combat_intent_t combat; + int evt_pending; + int evt_attacker; + int evt_damage; + int evt_mod; + /* ---- end BOT ENHANCED ---- */ + + /* ---- BOT ITEMS: ai_bot_items.c — remove this block to revert ---- */ + qboolean item_commit_active; + int item_commit_kind; /* BOT_ITEM_* while committed */ + float item_commit_until; + float item_next_scan_time; + bot_goal_t item_commit_goal; + int item_commit_snap_health; + int item_commit_snap_armor; + int item_commit_snap_quad; + int item_commit_snap_redflag; + int item_commit_snap_blueflag; + int item_commit_snap_weapon; + /* ---- end BOT ITEMS ---- */ + /* ---- BOT AIM HARNESS (v1): ai_aim_harness.c — remove this block to revert ---- */ vec3_t aimh_goal; float aimh_vel[2]; @@ -300,10 +324,30 @@ typedef struct bot_state_s float aimh_acquire_until; int aimh_last_sanity_enemy; int aimh_sanity_miss_streak; + float aimh_aim_skill; + float aimh_aim_accuracy; + float aimh_last_goal_pitch; + float aimh_last_goal_yaw; + float aimh_last_goal_time; + float aimh_smooth_goal_pitch; + float aimh_smooth_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; /* ---- end BOT AIM HARNESS ---- */ /* ---- BOT SMART WEAPON SELECT (v1): ai_weapon_select.c — remove to revert ---- */ float wps_next_eval_time; + float wps_next_roam_eval_time; + float wps_enhanced_latch_until; /* no fight weapon re-eval until this time */ float wps_last_switch_time; int wps_last_chosen_weapon; int wps_desired_weapon; diff --git a/ratoa_gamecode/code/game/ai_weapon_select.c b/ratoa_gamecode/code/game/ai_weapon_select.c index e028ee5..72bd4c3 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.c +++ b/ratoa_gamecode/code/game/ai_weapon_select.c @@ -16,8 +16,11 @@ BOT SMART WEAPON SELECT (v1) — see ai_weapon_select.h #include "chars.h" #include "inv.h" #include "ai_weapon_select.h" +#include "ai_bot_enhanced.h" +#include "ai_bot_combat.h" +#include "ai_bot_tactics.h" -vmCvar_t bot_smartWeaponChoice; +vmCvar_t bot_enhanced_weapons; /* Forward — defined in ai_dmq3.c */ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int ent); @@ -35,6 +38,30 @@ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int #define WPNSEL_SPLASH_PENALTY 48.0f /* Extra weight on range fit vs legacy bias (higher = hitscan wins at long range). */ #define WPNSEL_RANGE_WEIGHT 1.45f +/* Machinegun: fallback / plink / chip — not a primary DPS weapon. */ +#define WPNSEL_MG_OVERSHADOW_PENALTY 55.0f +#define WPNSEL_MG_FALLBACK_BONUS 40.0f +#define WPNSEL_MG_PLINK_DIST 1200.0f +#define WPNSEL_MG_PLINK_BONUS 28.0f +#define WPNSEL_MG_CHIP_HEALTH 40 +#define WPNSEL_MG_CHIP_MAX 25.0f +#define WPNSEL_MG_DOWNGRADE_HYSTERESIS 18.0f +#define WPNSEL_MG_OBVIOUS_GAP_BASE 8.0f +#define WPNSEL_MG_OBVIOUS_GAP_SKILL 10.0f +#define WPNSEL_MG_LEGACY_BIAS_SCALE 0.12f +/* Roaming: throttled ready-weapon selection, prefer silent over audible. */ +#define WPNSEL_ROAM_EVAL_MIN 0.85f +#define WPNSEL_ROAM_EVAL_MAX 1.45f +#define WPNSEL_ROAM_HYSTERESIS 14.0f +#define WPNSEL_ROAM_AUDIBLE_LG 50.0f +#define WPNSEL_ROAM_AUDIBLE_RAIL 40.0f +#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. */ +#define WPNSEL_ENHANCED_MIN_SWITCH_INTERVAL 1.0f +#define WPNSEL_ENHANCED_GAUNTLET_LATCH 3.5f +#define WPNSEL_ENHANCED_MIN_EVAL_INTERVAL 1.0f static int BotWpnSel_HasWeaponAndAmmo(bot_state_t *bs, int wp) { if (wp <= WP_NONE || wp >= WP_NUM_WEAPONS) { @@ -81,12 +108,12 @@ static float BotWpnSel_RangeScore(int wp, float dist) { if (d < 400.0f) return 40.0f; return 15.0f; case WP_MACHINEGUN: - if (d < 200.0f) return 88.0f; - if (d < 550.0f) return 78.0f; - if (d < 1000.0f) return 72.0f; - if (d < 1800.0f) return 80.0f; - if (d < 3200.0f) return 76.0f; - return 62.0f; + if (d < 200.0f) return 58.0f; + if (d < 550.0f) return 42.0f; + if (d < 1000.0f) return 40.0f; + if (d < 1800.0f) return 52.0f; + if (d < 3200.0f) return 48.0f; + return 42.0f; case WP_LIGHTNING: if (d < 280.0f) return 94.0f; if (d < 450.0f) return 58.0f; @@ -238,6 +265,100 @@ static float BotWpnSel_SwitchInCost(const weaponinfo_t *wi) { return t; } +static int BotWpnSel_EnemyHealth(bot_state_t *bs) { + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return 999; + } + if (!g_entities[bs->enemy].client) { + return 999; + } + return g_entities[bs->enemy].health; +} + +static int BotWpnSel_HasLongRangeHitscan(bot_state_t *bs, float dist) { + (void)dist; + if (BotWpnSel_HasWeaponAndAmmo(bs, WP_RAILGUN)) { + return 1; + } +#ifdef MISSIONPACK + if (dist > 800.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { + return 1; + } +#endif + return 0; +} + +/* + * Weapons clearly better than MG at this distance (for fallback / overshadow logic). + */ +static int BotWpnSel_CountCombatAlternatives(bot_state_t *bs, float dist) { + int n; + + n = 0; + if (dist < 200.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { + n++; + } + if (dist < 650.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_LIGHTNING)) { + n++; + } + if (dist > 350.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_RAILGUN)) { + n++; + } + 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)) { + n++; + } + if (dist > 96.0f && dist < 900.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_GRENADE_LAUNCHER)) { + n++; + } +#ifdef MISSIONPACK + if (dist > 600.0f && BotWpnSel_HasWeaponAndAmmo(bs, WP_CHAINGUN)) { + n++; + } +#endif + return n; +} + +static float BotWpnSel_MachinegunModifier(bot_state_t *bs, float dist, + float skillCombat) { + int alternatives, enemyHealth; + float mod, penScale, chipBonus, plinkBonus; + + alternatives = BotWpnSel_CountCombatAlternatives(bs, dist); + if (alternatives <= 0) { + return WPNSEL_MG_FALLBACK_BONUS; + } + + penScale = 0.35f + 0.65f * skillCombat; + mod = 0.0f; + chipBonus = 0.0f; + plinkBonus = 0.0f; + + enemyHealth = BotWpnSel_EnemyHealth(bs); + if (enemyHealth > 0 && enemyHealth <= WPNSEL_MG_CHIP_HEALTH) { + chipBonus = (float)(WPNSEL_MG_CHIP_HEALTH - enemyHealth) * 0.6f; + chipBonus *= (0.45f + 0.55f * skillCombat); + if (chipBonus > WPNSEL_MG_CHIP_MAX) { + chipBonus = WPNSEL_MG_CHIP_MAX; + } + } + + if (dist >= WPNSEL_MG_PLINK_DIST && !BotWpnSel_HasLongRangeHitscan(bs, dist)) { + plinkBonus = WPNSEL_MG_PLINK_BONUS * (0.55f + 0.45f * skillCombat); + } + + if (chipBonus > 0.0f || plinkBonus > 0.0f) { + mod = chipBonus + plinkBonus; + mod -= WPNSEL_MG_OVERSHADOW_PENALTY * penScale * 0.22f; + } else { + mod -= WPNSEL_MG_OVERSHADOW_PENALTY * penScale; + } + + return mod; +} + static float BotWpnSel_SwitchFatigue(bot_state_t *bs, float skillCombat) { float dt, u, cool; @@ -254,21 +375,168 @@ static float BotWpnSel_SwitchFatigue(bot_state_t *bs, float skillCombat) { } void BotWpnSelect_RegisterCvars(void) { - trap_Cvar_Register(&bot_smartWeaponChoice, "bot_smartWeaponChoice", "0", + trap_Cvar_Register(&bot_enhanced_weapons, "bot_enhanced_weapons", "0", CVAR_ARCHIVE); - trap_Cvar_Update(&bot_smartWeaponChoice); + trap_Cvar_Update(&bot_enhanced_weapons); } int BotWpnSelect_IsActive(void) { - trap_Cvar_Update(&bot_smartWeaponChoice); - if (!bot_smartWeaponChoice.integer) { + return BotEnhanced_WeaponsActive(); +} + +static float BotWpnSel_RoamStealth(bot_state_t *bs) { + float stealth; + + stealth = 0.78f; + if (bs->enemysight_time > 0.0f && FloatTime() - bs->enemysight_time < 4.0f) { + stealth = 0.42f; + } + if (bs->inventory[INVENTORY_REDFLAG] || bs->inventory[INVENTORY_BLUEFLAG] || + bs->inventory[INVENTORY_NEUTRALFLAG]) { + stealth = 0.32f; + } + return stealth; +} + +static int BotWpnSel_HasBetterSilentRoamer(bot_state_t *bs) { + if (BotWpnSel_HasWeaponAndAmmo(bs, WP_ROCKET_LAUNCHER)) { + return 1; + } + if (BotWpnSel_HasWeaponAndAmmo(bs, WP_SHOTGUN)) { + return 1; + } + if (BotWpnSel_HasWeaponAndAmmo(bs, WP_PLASMAGUN)) { + return 1; + } + if (BotWpnSel_HasWeaponAndAmmo(bs, WP_GRENADE_LAUNCHER)) { + return 1; + } +#ifdef MISSIONPACK + if (BotWpnSel_HasWeaponAndAmmo(bs, WP_NAILGUN)) { + return 1; + } + if (BotWpnSel_HasWeaponAndAmmo(bs, WP_PROX_LAUNCHER)) { + return 1; + } +#endif + return 0; +} + +static float BotWpnSel_RoamArsenalTier(int wp) { + switch (wp) { + case WP_RAILGUN: + return 92.0f; + case WP_LIGHTNING: + return 90.0f; + case WP_BFG: + return 78.0f; + case WP_PLASMAGUN: + return 82.0f; + case WP_ROCKET_LAUNCHER: + return 80.0f; + case WP_GRENADE_LAUNCHER: + return 72.0f; + case WP_SHOTGUN: + return 68.0f; +#ifdef MISSIONPACK + case WP_CHAINGUN: + return 74.0f; + case WP_NAILGUN: + return 66.0f; + case WP_PROX_LAUNCHER: + return 62.0f; +#endif + case WP_MACHINEGUN: + return 34.0f; + case WP_GAUNTLET: + return 8.0f; + default: + return 40.0f; + } +} + +static float BotWpnSel_RoamAudiblePenalty(int wp, float stealth, float skillCombat) { + float pen; + + pen = 0.0f; + if (wp == WP_LIGHTNING) { + pen = WPNSEL_ROAM_AUDIBLE_LG; + } else if (wp == WP_RAILGUN) { + pen = WPNSEL_ROAM_AUDIBLE_RAIL; + } else { + return 0.0f; + } + return pen * stealth * (0.4f + 0.6f * skillCombat); +} + +static float BotWpnSel_MachinegunRoamModifier(bot_state_t *bs) { + if (!BotWpnSel_HasWeaponAndAmmo(bs, WP_MACHINEGUN)) { + return 0.0f; + } + if (BotWpnSel_HasBetterSilentRoamer(bs)) { + return -WPNSEL_ROAM_MG_LASTRESORT_PEN; + } + return WPNSEL_ROAM_MG_ONLY_BONUS; +} + +static qboolean BotWpnSel_IsCloseGauntletCommit(bot_state_t *bs, int wp) { + float dist; + + if (!bs || wp != WP_GAUNTLET) { + return qfalse; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + dist = BotWpnSel_EnemyDistance(bs); + if (BotTactics_IsGauntletOnly(bs)) { + return dist <= (float)BOT_COMBAT_GAUNTLET_LASTRESORT_RUSH_DIST; + } + if (!BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + return qfalse; + } + return dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST; +} + +static void BotWpnSel_EnhancedApplyLatch(bot_state_t *bs, int prev_wp, int new_wp) { + float now, latch; + + if (!BotEnhanced_WeaponsActive() || prev_wp == new_wp) { + return; + } + now = FloatTime(); + latch = now + WPNSEL_ENHANCED_MIN_SWITCH_INTERVAL; + if (BotWpnSel_IsCloseGauntletCommit(bs, new_wp)) { + latch = now + WPNSEL_ENHANCED_GAUNTLET_LATCH; + } + if (latch > bs->wps_enhanced_latch_until) { + bs->wps_enhanced_latch_until = latch; + } + bs->wps_next_eval_time = bs->wps_enhanced_latch_until; +} + +int BotWpnSelect_ShouldRunChooser(bot_state_t *bs) { + if (!bs || !BotEnhanced_WeaponsActive()) { + return 1; + } + if (FloatTime() < bs->wps_enhanced_latch_until) { return 0; } return 1; } +void BotWpnSelect_OnVoluntaryGauntletAborted(bot_state_t *bs) { + if (!bs) { + return; + } + bs->wps_enhanced_latch_until = 0.0f; + bs->wps_next_eval_time = 0.0f; +} + void BotWpnSelect_Reset(bot_state_t *bs) { bs->wps_next_eval_time = 0.0f; + bs->wps_next_roam_eval_time = 0.0f; + bs->wps_enhanced_latch_until = 0.0f; bs->wps_last_switch_time = -999999.0f; bs->wps_last_chosen_weapon = 0; bs->wps_desired_weapon = BOTWPN_DESIRE_NONE; @@ -278,8 +546,10 @@ void BotWpnSelect_Reset(bot_state_t *bs) { void BotWpnSelect_NotifyWeaponCommitted(bot_state_t *bs, int prev_wp, int new_wp) { if (prev_wp != new_wp) { bs->wps_last_switch_time = FloatTime(); + BotWpnSel_EnhancedApplyLatch(bs, prev_wp, new_wp); } bs->wps_last_chosen_weapon = new_wp; + BotCombat_OnWeaponCommitted(bs, prev_wp, new_wp); } void BotWpnSelect_GetDesire(bot_state_t *bs, bot_weapon_desire_t *out) { @@ -292,9 +562,10 @@ 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; float dist, score, best_score, cur_score, skillCombat, react, eval_dt; - float hysteresis, noiseAmp, vis; - float miss_score, best_miss_score; + float hysteresis, noiseAmp, vis, mgMod, legacyBias; + float miss_score, best_miss_score, best_non_mg_score, obviousGap; int best_miss_wp; weaponinfo_t wi; @@ -314,6 +585,14 @@ int BotWpnSelect_Choose(bot_state_t *bs) { eval_dt = WPNSEL_EVAL_MIN + (WPNSEL_EVAL_MAX - WPNSEL_EVAL_MIN) * (0.35f + 0.4f * react + 0.25f * (1.0f - skillCombat)); + if (BotEnhanced_WeaponsActive()) { + if (eval_dt < WPNSEL_ENHANCED_MIN_EVAL_INTERVAL) { + eval_dt = WPNSEL_ENHANCED_MIN_EVAL_INTERVAL; + } + if (FloatTime() < bs->wps_enhanced_latch_until) { + return bs->weaponnum; + } + } if (bs->wps_next_eval_time > FloatTime()) { return bs->weaponnum; @@ -331,6 +610,9 @@ int BotWpnSelect_Choose(bot_state_t *bs) { dist *= 1.15f; } + alternatives = BotWpnSel_CountCombatAlternatives(bs, dist); + mgMod = BotWpnSel_MachinegunModifier(bs, dist, skillCombat); + n_weaps = 0; weap_list[n_weaps++] = WP_GAUNTLET; weap_list[n_weaps++] = WP_MACHINEGUN; @@ -350,6 +632,8 @@ int BotWpnSelect_Choose(bot_state_t *bs) { best_wp = legacy_best; best_score = -1e12f; cur_score = -1e12f; + best_non_mg_score = -1e12f; + best_non_mg_wp = -1; for (i = 0; i < n_weaps; i++) { wp = weap_list[i]; @@ -365,8 +649,28 @@ int BotWpnSelect_Choose(bot_state_t *bs) { score -= BotWpnSel_AmmoPressure(bs, wp); score -= BotWpnSel_SplashPenalty(bs, wp, dist, &wi); + if (wp == WP_MACHINEGUN) { + score += mgMod; + } + + if (wp == WP_GAUNTLET && dist <= (float)BOT_COMBAT_GAUNTLET_RUSH_DIST) { + if (BotTactics_IsGauntletOnly(bs)) { + score += 35.0f; + } else if (FloatTime() < bs->combat.gauntlet_voluntary_abandon_until) { + score -= 120.0f; + } else if (BotEnhanced_AllowsVoluntaryCloseGauntlet(bs)) { + score += 78.0f; + } else { + score -= 120.0f; + } + } + if (wp == legacy_best) { - score += WPNSEL_LEGACY_BIAS; + legacyBias = WPNSEL_LEGACY_BIAS; + if (wp == WP_MACHINEGUN && alternatives > 0) { + legacyBias *= WPNSEL_MG_LEGACY_BIAS_SCALE; + } + score += legacyBias; } else { score += WPNSEL_LEGACY_BIAS * 0.15f; } @@ -384,6 +688,10 @@ int BotWpnSelect_Choose(bot_state_t *bs) { if (wp == bs->weaponnum) { cur_score = score; } + if (wp != WP_MACHINEGUN && score > best_non_mg_score) { + best_non_mg_score = score; + best_non_mg_wp = wp; + } if (score > best_score) { best_score = score; best_wp = wp; @@ -399,6 +707,23 @@ int BotWpnSelect_Choose(bot_state_t *bs) { } } + if (best_wp == WP_MACHINEGUN && bs->weaponnum != WP_MACHINEGUN && + (bs->weaponnum == WP_LIGHTNING || bs->weaponnum == WP_RAILGUN)) { + float downgradeHyst; + + downgradeHyst = WPNSEL_MG_DOWNGRADE_HYSTERESIS * (0.45f + 0.55f * skillCombat); + if (best_score < cur_score + hysteresis + downgradeHyst) { + best_wp = bs->weaponnum; + } + } + + if (best_wp == WP_MACHINEGUN && best_non_mg_wp >= 0 && skillCombat > 0.45f) { + obviousGap = WPNSEL_MG_OBVIOUS_GAP_BASE + WPNSEL_MG_OBVIOUS_GAP_SKILL * skillCombat; + if (best_non_mg_score > best_score - obviousGap) { + best_wp = best_non_mg_wp; + } + } + bs->wps_desired_weapon = BOTWPN_DESIRE_NONE; bs->wps_desire_strength = 0.0f; best_miss_score = -1e12f; @@ -429,5 +754,163 @@ int BotWpnSelect_Choose(bot_state_t *bs) { (best_miss_score - best_score) / 85.0f); } + if (BotEnhanced_WeaponsActive() && best_wp != bs->weaponnum) { + if (FloatTime() - bs->wps_last_switch_time < WPNSEL_ENHANCED_MIN_SWITCH_INTERVAL) { + best_wp = bs->weaponnum; + } + } + + if (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; + } + } + + return best_wp; +} + +int BotWpnSelect_ChooseRoaming(bot_state_t *bs) { + int wp, best_wp, weap_list[16], n_weaps, i; + float score, best_score, cur_score, skillCombat, react, eval_dt; + float stealth, hysteresis, noiseAmp; + weaponinfo_t wi; + + if (!BotWpnSelect_IsActive()) { + return -1; + } + if (bs->enemy >= 0) { + return -1; + } + + skillCombat = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL, + 0.0f, 1.0f); + react = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, + 0.0f, 1.0f); + + eval_dt = WPNSEL_ROAM_EVAL_MIN + (WPNSEL_ROAM_EVAL_MAX - WPNSEL_ROAM_EVAL_MIN) * + (0.3f + 0.35f * react + 0.35f * (1.0f - skillCombat)); + + if (bs->wps_next_roam_eval_time > FloatTime()) { + return bs->weaponnum; + } + bs->wps_next_roam_eval_time = FloatTime() + eval_dt; + + stealth = BotWpnSel_RoamStealth(bs); + + n_weaps = 0; + weap_list[n_weaps++] = WP_GAUNTLET; + weap_list[n_weaps++] = WP_MACHINEGUN; + weap_list[n_weaps++] = WP_SHOTGUN; + weap_list[n_weaps++] = WP_GRENADE_LAUNCHER; + weap_list[n_weaps++] = WP_ROCKET_LAUNCHER; + weap_list[n_weaps++] = WP_LIGHTNING; + weap_list[n_weaps++] = WP_RAILGUN; + weap_list[n_weaps++] = WP_PLASMAGUN; + weap_list[n_weaps++] = WP_BFG; +#ifdef MISSIONPACK + weap_list[n_weaps++] = WP_NAILGUN; + weap_list[n_weaps++] = WP_PROX_LAUNCHER; + weap_list[n_weaps++] = WP_CHAINGUN; +#endif + + best_wp = bs->weaponnum; + best_score = -1e12f; + cur_score = -1e12f; + + for (i = 0; i < n_weaps; i++) { + wp = weap_list[i]; + if (wp == WP_GAUNTLET) { + continue; + } + if (!BotWpnSel_HasWeaponAndAmmo(bs, wp)) { + continue; + } + trap_BotGetWeaponInfo(bs->ws, wp, &wi); + if (!wi.valid) { + continue; + } + + score = BotWpnSel_RoamArsenalTier(wp); + score -= BotWpnSel_AmmoPressure(bs, wp) * 0.35f; + score -= BotWpnSel_RoamAudiblePenalty(wp, stealth, skillCombat); + if (wp == WP_MACHINEGUN) { + score += BotWpnSel_MachinegunRoamModifier(bs); + } + + if (wp != bs->weaponnum) { + score -= BotWpnSel_SwitchFatigue(bs, skillCombat) * 0.5f; + score -= (BotWpnSel_SwitchOutCost(bs, bs->weaponnum) + + BotWpnSel_SwitchInCost(&wi)) * WPNSEL_SWITCH_COST_SCALE * 0.65f; + } + + noiseAmp = WPNSEL_ROAM_NOISE_MAX * (0.3f + 0.7f * (1.0f - skillCombat)); + score += crandom() * noiseAmp; + + if (wp == bs->weaponnum) { + cur_score = score; + } + if (score > best_score) { + best_score = score; + best_wp = wp; + } + } + + hysteresis = WPNSEL_ROAM_HYSTERESIS + + WPNSEL_HYSTERESIS_SKILL * 0.35f * (1.0f - skillCombat); + + if (best_wp != bs->weaponnum) { + if (best_score < cur_score + hysteresis) { + best_wp = bs->weaponnum; + } + } + + if (best_wp == WP_MACHINEGUN && BotWpnSel_HasBetterSilentRoamer(bs) && + skillCombat > 0.4f) { + for (i = 0; i < n_weaps; i++) { + wp = weap_list[i]; + if (wp == WP_MACHINEGUN || wp == WP_GAUNTLET) { + continue; + } + if (!BotWpnSel_HasWeaponAndAmmo(bs, wp)) { + continue; + } + if (BotWpnSel_RoamArsenalTier(wp) + BotWpnSel_MachinegunRoamModifier(bs) > + best_score - 6.0f) { + best_wp = wp; + break; + } + } + } + return best_wp; } + +void BotWpnSelect_TickRoaming(bot_state_t *bs) { + int new_wp, prev_wp; + + if (!BotWpnSelect_IsActive()) { + return; + } + if (bs->enemy >= 0) { + return; + } + if (bs->cur_ps.weaponstate == WEAPON_RAISING || + bs->cur_ps.weaponstate == WEAPON_DROPPING) { + return; + } + + prev_wp = bs->weaponnum; + new_wp = BotWpnSelect_ChooseRoaming(bs); + if (new_wp < WP_MACHINEGUN || new_wp >= WP_NUM_WEAPONS) { + return; + } + if (new_wp == prev_wp) { + return; + } + + bs->weaponchange_time = FloatTime(); + bs->weaponnum = new_wp; + BotWpnSelect_NotifyWeaponCommitted(bs, prev_wp, new_wp); +} diff --git a/ratoa_gamecode/code/game/ai_weapon_select.h b/ratoa_gamecode/code/game/ai_weapon_select.h index 6d52cdc..fb1c69e 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.h +++ b/ratoa_gamecode/code/game/ai_weapon_select.h @@ -2,7 +2,8 @@ =========================================================================== BOT SMART WEAPON SELECT (v1) — situational weapon choice. -Ringfenced: logic in ai_weapon_select.c. Toggle with bot_smartWeaponChoice. +Ringfenced: logic in ai_weapon_select.c. Toggle with bot_enhanced_weapons (requires bot_enhanced). +Was bot_smartWeaponChoice (migrated at init if bot_enhanced_weapons is unset). Include after g_local.h and ai_main.h in .c files (ai_main.h has no guards). Remove: delete ai_weapon_select.c/h, Makefile/q3asm entries, revert hooks in @@ -30,9 +31,14 @@ void BotWpnSelect_Reset(struct bot_state_s *bs); * trap_BotChooseBestFightWeapon (legacy). */ int BotWpnSelect_Choose(struct bot_state_s *bs); +int BotWpnSelect_ChooseRoaming(struct bot_state_s *bs); +void BotWpnSelect_TickRoaming(struct bot_state_s *bs); /* Call after weaponnum is committed so fatigue/switch timers stay accurate. */ void BotWpnSelect_NotifyWeaponCommitted(struct bot_state_s *bs, int prev_wp, int new_wp); /* For future item pickup / goal weighting (v1 fills when active + enemy). */ void BotWpnSelect_GetDesire(struct bot_state_s *bs, bot_weapon_desire_t *out); +/* When false, BotChooseWeapon keeps current weaponnum (enhanced latch / min interval). */ +int BotWpnSelect_ShouldRunChooser(struct bot_state_s *bs); +void BotWpnSelect_OnVoluntaryGauntletAborted(struct bot_state_s *bs); #endif /* AI_WEAPON_SELECT_H */ diff --git a/ratoa_gamecode/code/game/bg_public.h b/ratoa_gamecode/code/game/bg_public.h index f584498..e3164f9 100644 --- a/ratoa_gamecode/code/game/bg_public.h +++ b/ratoa_gamecode/code/game/bg_public.h @@ -333,8 +333,9 @@ typedef enum { } persEnum_t; // stats[STAT_EXTFLAGS] -#define EXTFL_ZOOMING 1 -#define EXTFL_SLIDING 2 +#define EXTFL_ZOOMING 1 +#define EXTFL_SLIDING 2 +#define EXTFL_BOT_AIM_DEBUG 4 /* bot_debugAim: ps.grapplePoint has harness aim */ // stats[STAT_MOVEMENT_KEYS] @@ -369,6 +370,7 @@ typedef enum { #define EF_AWARD_ASSIST 0x00020000 // draw a assist sprite #define EF_AWARD_DENIED 0x00040000 // denied #define EF_TEAMVOTED 0x00080000 // already cast a team vote +#define EF_BOT_AIM_DEBUG 0x00100000 // bot_debugAim: ps.grapplePoint + origin2 = aim point // Additional awards (not visible to other players) /* diff --git a/ratoa_gamecode/code/game/g_active.c b/ratoa_gamecode/code/game/g_active.c index f92dea4..15df7c7 100644 --- a/ratoa_gamecode/code/game/g_active.c +++ b/ratoa_gamecode/code/game/g_active.c @@ -22,6 +22,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #include "g_local.h" +#include "ai_bot_enhanced.h" +#include "ai_aim_harness.h" /* @@ -1912,6 +1914,9 @@ void ClientEndFrame( gentity_t *ent ) { else { */ BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, (qboolean)!g_floatPlayerPosition.integer ); // } + if ( ent->r.svFlags & SVF_BOT ) { + BotAimHarness_SyncEntityFromPlayerState( ent ); + } SendPendingPredictableEvents( &ent->client->ps ); //unlagged - smooth clients #1 diff --git a/ratoa_gamecode/code/game/g_client.c b/ratoa_gamecode/code/game/g_client.c index 17af3ac..03b7386 100644 --- a/ratoa_gamecode/code/game/g_client.c +++ b/ratoa_gamecode/code/game/g_client.c @@ -21,6 +21,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ // #include "g_local.h" +#include "ai_bot_enhanced.h" +#include "ai_aim_harness.h" // g_client.c -- client functions that don't happen every frame @@ -3122,6 +3124,10 @@ void ClientBegin( int clientNum ) { // locate ent at a spawn point ClientSpawn( ent ); + if (ent->r.svFlags & SVF_BOT) { + BotAimHarness_SyncClientDebug(clientNum); + } + if( ( client->sess.sessionTeam != TEAM_SPECTATOR ) && ( ( !( client->isEliminated ) /*&& ( ( !client->ps.pm_type ) == PM_SPECTATOR ) */ ) || //Sago: Yes, it made no sense diff --git a/ratoa_gamecode/windows_scripts/game.q3asm b/ratoa_gamecode/windows_scripts/game.q3asm index dbbe793..ec11ae6 100644 --- a/ratoa_gamecode/windows_scripts/game.q3asm +++ b/ratoa_gamecode/windows_scripts/game.q3asm @@ -9,9 +9,14 @@ q_math q_shared ai_dmnet ai_dmq3 +ai_bot_enhanced +ai_bot_combat +ai_bot_events +ai_bot_move_harness ai_aim_harness ai_weapon_select ai_bot_tactics +ai_bot_items ai_team ai_main ai_chat diff --git a/ratoa_gamecode/windows_scripts/game_mp.q3asm b/ratoa_gamecode/windows_scripts/game_mp.q3asm index 95b78b3..e9ea0ab 100644 --- a/ratoa_gamecode/windows_scripts/game_mp.q3asm +++ b/ratoa_gamecode/windows_scripts/game_mp.q3asm @@ -9,9 +9,14 @@ q_math q_shared ai_dmnet ai_dmq3 +ai_bot_enhanced +ai_bot_combat +ai_bot_events +ai_bot_move_harness ai_aim_harness ai_weapon_select ai_bot_tactics +ai_bot_items ai_team ai_main ai_chat diff --git a/ratoa_gamecode/windows_scripts/windows_compile_game.bat b/ratoa_gamecode/windows_scripts/windows_compile_game.bat index c97c4c7..b03e397 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game.bat @@ -21,9 +21,14 @@ cd windows\build\game %cc% ../../../code/game/ai_cmd.c %cc% ../../../code/game/ai_dmnet.c %cc% ../../../code/game/ai_dmq3.c +%cc% ../../../code/game/ai_bot_enhanced.c +%cc% ../../../code/game/ai_bot_combat.c +%cc% ../../../code/game/ai_bot_events.c +%cc% ../../../code/game/ai_bot_move_harness.c %cc% ../../../code/game/ai_aim_harness.c %cc% ../../../code/game/ai_weapon_select.c %cc% ../../../code/game/ai_bot_tactics.c +%cc% ../../../code/game/ai_bot_items.c %cc% ../../../code/game/ai_main.c %cc% ../../../code/game/ai_team.c %cc% ../../../code/game/ai_vcmd.c diff --git a/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat b/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat index 07a78e4..076be9a 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat @@ -21,9 +21,14 @@ cd windows\build\game %cc% ../../../code/game/ai_cmd.c %cc% ../../../code/game/ai_dmnet.c %cc% ../../../code/game/ai_dmq3.c +%cc% ../../../code/game/ai_bot_enhanced.c +%cc% ../../../code/game/ai_bot_combat.c +%cc% ../../../code/game/ai_bot_events.c +%cc% ../../../code/game/ai_bot_move_harness.c %cc% ../../../code/game/ai_aim_harness.c %cc% ../../../code/game/ai_weapon_select.c %cc% ../../../code/game/ai_bot_tactics.c +%cc% ../../../code/game/ai_bot_items.c %cc% ../../../code/game/ai_main.c %cc% ../../../code/game/ai_team.c %cc% ../../../code/game/ai_vcmd.c