From 6a386af364f3fba56871f67e915badb3dd12e195 Mon Sep 17 00:00:00 2001 From: Foo Date: Tue, 19 May 2026 12:05:04 +1200 Subject: [PATCH 01/10] Fix tracking aim lagging behind target & Intermittent firing bug When tracking target with humanized aim, would consistently lag behind the target. This meant hitscan weapons rarely connected, as shots were always just a little too late. This fix adds: - A live updating aim target during combat, replacing the more static earlier version - Angular feed-forward in the harness plus some catch-up impetus, pushing the aim towards the target Fix intermittent firing bug when bots were using continuous-fire weapons --- ratoa_gamecode/code/game/ai_aim_harness.c | 412 +++++++++++++++++----- ratoa_gamecode/code/game/ai_aim_harness.h | 9 +- ratoa_gamecode/code/game/ai_dmq3.c | 8 +- ratoa_gamecode/code/game/ai_main.h | 6 + 4 files changed, 349 insertions(+), 86 deletions(-) diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index 539c2e8..d79d21c 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -13,14 +13,17 @@ 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" vmCvar_t bot_humanizeaim; -/* 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; @@ -37,9 +40,17 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #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 @@ -134,37 +145,151 @@ 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; - - BotEntityInfo(bs->enemy, &entinfo); - if (!entinfo.valid) { - VectorCopy(bs->aimh_goal, angles); - return; + target[2] = bs->aimtarget[2]; + } + + VectorSubtract(entinfo.origin, entinfo.lastvisorigin, vel); + if (entinfo.update_time > 0.001f) { + VectorScale(vel, 1.0f / entinfo.update_time, vel); + } 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; +} + +static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { + vec3_t dir, target; + + if (BotAimHarness_GetCombatTarget(bs, target)) { + VectorSubtract(target, bs->eye, dir); } else { VectorCopy(bs->aimh_goal, angles); return; @@ -174,6 +299,104 @@ static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { angles[YAW] = AngleMod(angles[YAW]); } +static int BotAimHarness_IsSustainedFireWeapon(int wp) { + switch (wp) { + case WP_GAUNTLET: + case WP_MACHINEGUN: + case WP_LIGHTNING: + case WP_PLASMAGUN: +#ifdef MISSIONPACK + case WP_CHAINGUN: +#endif + return 1; + default: + return 0; + } +} + +/* + * Hold attack for continuous-fire weapons while harness is tracking a visible enemy + * with unobstructed aimtarget LOS. Uses attack/intent angles for the hit trace so + * lagging viewangles do not drop fire between bot thinks. + */ +int BotAimHarness_TryAttack(bot_state_t *bs) { + int attackentity; + float points; + vec3_t dir, forward, right, start, end, attackAngles; + vec3_t mins = {-8, -8, -8}, maxs = {8, 8, 8}; + bsp_trace_t bsptrace, trace; + weaponinfo_t wi; + + if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim) { + return qfalse; + } + if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + return qfalse; + } + if (!BotAimHarness_IsSustainedFireWeapon(bs->weaponnum)) { + return qfalse; + } + if (!BotAimHarness_AimTargetValid(bs)) { + return qfalse; + } + + attackentity = bs->enemy; + VectorSubtract(bs->aimtarget, bs->eye, dir); + if (bs->weaponnum == WP_GAUNTLET) { + if (VectorLengthSquared(dir) > Square(60)) { + return qfalse; + } + } + + BotAI_Trace(&bsptrace, bs->eye, NULL, NULL, bs->aimtarget, bs->client, + CONTENTS_SOLID | CONTENTS_PLAYERCLIP); + if (bsptrace.fraction < 1 && bsptrace.ent != attackentity) { + return qfalse; + } + + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + BotAimHarness_GetAttackAimAngles(bs, attackAngles); + + VectorCopy(bs->origin, start); + start[2] += bs->cur_ps.viewheight; + AngleVectors(attackAngles, forward, right, NULL); + start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1]; + start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1]; + start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2]; + VectorMA(start, 1000, forward, end); + VectorMA(start, -12, forward, start); + BotAI_Trace(&trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT); + + if (trace.ent >= 0 && trace.ent < MAX_CLIENTS && trace.ent != attackentity) { + if (BotSameTeam(bs, trace.ent)) { + return qfalse; + } + } + + if (trace.ent != attackentity) { + if (wi.proj.damagetype & DAMAGETYPE_RADIAL) { + if (trace.fraction * 1000 < wi.proj.radius) { + points = (wi.proj.damage - 0.5f * trace.fraction * 1000) * 0.5f; + if (points > 0) { + return qfalse; + } + } + } else { + return qfalse; + } + } + + if (wi.flags & WFL_FIRERELEASED) { + if (bs->flags & BFL_ATTACKED) { + trap_EA_Attack(bs->client); + } + } else { + trap_EA_Attack(bs->client); + } + bs->flags ^= BFL_ATTACKED; + return qtrue; +} + /* * 0 = ok, 1 = soft (motor boost only), 2 = hard (also damp motor). */ @@ -280,6 +503,7 @@ 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; } void BotAimHarness_RegisterCvars(void) { @@ -348,10 +572,16 @@ 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; + VectorClear(bs->aimh_combat_target); } 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,13 +591,22 @@ 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; @@ -375,72 +614,39 @@ void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, } 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; + vec3_t dir, target; - if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { + if (!BotAimHarness_GetCombatTarget(bs, target)) { VectorCopy(bs->aimh_goal, goal); return; } - BotEntityInfo(bs->enemy, &entinfo); - if (!entinfo.valid) { - VectorCopy(bs->aimh_goal, goal); - return; - } + VectorSubtract(target, bs->eye, dir); + vectoangles(dir, goal); + goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); + goal[YAW] = AngleMod(goal[YAW]); +} - 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]; +static void BotAimHarness_ApplyGoalFeedforward(bot_state_t *bs, const vec3_t goal, + float dt) { + float pitchRate, yawRate, ff; - VectorSubtract(entinfo.origin, entinfo.lastvisorigin, vel); - if (entinfo.update_time > 0.001f) { - VectorScale(vel, 1.0f / entinfo.update_time, vel); - } else { - VectorClear(vel); + if (bs->aimh_last_goal_time <= 0.0f || dt <= 0.001f || dt > 0.5f) { + return; } - BotAimHarness_ClampVerticalLeadVel(vel); - trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); + pitchRate = BotAimHarness_PitchDiff(goal[PITCH], bs->aimh_last_goal_pitch) / dt; + yawRate = BotAimHarness_YawDiff(goal[YAW], bs->aimh_last_goal_yaw) / dt; - 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); - } + ff = AIMH_FEEDFORWARD_BASE + AIMH_FEEDFORWARD_SKILL * bs->aimh_aim_skill; + bs->aimh_vel[PITCH] += pitchRate * ff; + bs->aimh_vel[YAW] += yawRate * ff; +} - VectorSubtract(target, bs->eye, dir); - vectoangles(dir, goal); - goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); - goal[YAW] = AngleMod(goal[YAW]); +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, @@ -481,10 +687,10 @@ static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { vec3_t goal; - float skill, maxChange, maxTrack, maxFlick, maxVelPitch; - float stiffness, damping, maxVel, motorNoise; + float skill, aimSkill, maxChange, maxTrack, maxFlick, maxVelPitch; + float stiffness, damping, maxVel, motorNoise, motorScale; float magErr, pitchErr, yawErr, catchup, catchupRate; - float flickAngle; + float flickAngle, goalDt; if (!BotAimHarness_IsActive()) { return 0; @@ -502,6 +708,7 @@ 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; trap_EA_View(bs->client, bs->viewangles); return 1; } @@ -513,6 +720,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->aimh_last_sanity_enemy = -1; BotAimHarness_ClearRecoveryState(bs); bs->aimh_acquire_until = 0.0f; + bs->aimh_last_goal_time = 0.0f; } else { BotAimHarness_OnEnemyChange(bs); } @@ -520,6 +728,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { if (bs->aimh_combat_aim) { BotAimHarness_GetCombatGoal(bs, goal); 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 +736,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); motorNoise = 0.0f; + aimSkill = 0.05f; skill = 0.05f; maxTrack = 360.0f; maxFlick = 360.0f; @@ -567,20 +780,33 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { maxVel = maxFlick; } + if (bs->aimh_combat_aim) { + stiffness *= (0.78f + 0.52f * aimSkill); + damping *= (0.88f + 0.32f * aimSkill); + } + 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; } + if (bs->aimh_combat_aim) { + goalDt = thinktime; + if (bs->aimh_last_goal_time > 0.0f) { + goalDt = FloatTime() - bs->aimh_last_goal_time; + } + BotAimHarness_ApplyGoalFeedforward(bs, goal, goalDt); + } + BotAimHarness_UpdateAxis(bs, PITCH, goal[PITCH], thinktime, stiffness, damping, maxVelPitch, motorNoise); BotAimHarness_UpdateAxis(bs, YAW, goal[YAW], thinktime, @@ -600,6 +826,24 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH] + catchup); } + if (bs->aimh_combat_aim && bs->aimh_acquire_until <= FloatTime() && + fabs(yawErr) > AIMH_YAW_CATCHUP_ERR) { + catchupRate = AIMH_YAW_CATCHUP_RATE; + if (bs->aimh_recover_until > FloatTime()) { + catchupRate *= AIMH_RECOVER_CATCHUP_MULT; + } + catchup = yawErr * thinktime * catchupRate; + if (catchup > yawErr) { + catchup = yawErr; + } else if (catchup < -yawErr) { + catchup = -yawErr; + } + bs->viewangles[YAW] = AngleMod(bs->viewangles[YAW] + catchup); + } + + if (bs->aimh_combat_aim) { + BotAimHarness_SaveGoalHistory(bs, goal); + } if (bs->aimh_combat_aim && bs->enemy >= 0) { BotAimHarness_RunSanityCheck(bs, goal); diff --git a/ratoa_gamecode/code/game/ai_aim_harness.h b/ratoa_gamecode/code/game/ai_aim_harness.h index b850dfb..2f0addc 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -25,7 +25,14 @@ float BotAimHarness_ClampPitchAngle(float pitch); 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); +/* + * Sustained-fire path for BotCheckAttack: fire when combat-aiming with clear LOS to + * aimtarget, using intent angles (not lagging viewangles). Returns qtrue if handled. + */ +int BotAimHarness_TryAttack(struct bot_state_s *bs); #endif /* AI_AIM_HARNESS_H */ diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index 8cac3c1..e9adcce 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -3664,6 +3664,8 @@ void BotAimAtEnemy(bot_state_t *bs) { bestorigin[0] += 20 * crandom() * (1 - aim_accuracy); bestorigin[1] += 20 * crandom() * (1 - aim_accuracy); bestorigin[2] += 10 * crandom() * (1 - aim_accuracy); + } else if (!wi.speed) { + BotAimHarness_ApplyThinkHitscanOrigin(bs, bestorigin, &entinfo, aim_skill); } } else { @@ -3728,7 +3730,7 @@ void BotAimAtEnemy(bot_state_t *bs) { 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 @@ -3808,6 +3810,10 @@ void BotCheckAttack(bot_state_t *bs) { return; } } + /* BOT AIM HARNESS: sustained-fire uses intent angles + aimtarget LOS */ + if (BotAimHarness_TryAttack(bs)) { + return; + } if (VectorLengthSquared(dir) < Square(100)) fov = 120; else diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 1bdc0f1..2210436 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -300,6 +300,12 @@ 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; + vec3_t aimh_combat_target; /* ---- end BOT AIM HARNESS ---- */ /* ---- BOT SMART WEAPON SELECT (v1): ai_weapon_select.c — remove to revert ---- */ From 267ec2b7516430f3a808993bba380a17b72821e8 Mon Sep 17 00:00:00 2001 From: Foo Date: Tue, 19 May 2026 12:43:32 +1200 Subject: [PATCH 02/10] Changed bot aiming characteristics based on stress/distance to target. Improve weapon selection Up close aiming = jerkier, more overshoot, larger movements Distance aiming = smoother, less overshoot, smaller movements Improved roaming weapon selection for bots when not in a fight Roaming bots should now pick a better weapon to hold when roaming: - Not always the machinegun - Generally not loud weapons --- ratoa_gamecode/code/game/ai_aim_harness.c | 127 +++++- ratoa_gamecode/code/game/ai_dmq3.c | 11 +- ratoa_gamecode/code/game/ai_main.h | 1 + ratoa_gamecode/code/game/ai_weapon_select.c | 407 +++++++++++++++++++- ratoa_gamecode/code/game/ai_weapon_select.h | 2 + 5 files changed, 528 insertions(+), 20 deletions(-) diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index d79d21c..a4c8c12 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -67,9 +67,97 @@ 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 static int bot_humanizeaim_last = -1; +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); + + 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; @@ -628,7 +716,7 @@ static void BotAimHarness_GetCombatGoal(bot_state_t *bs, vec3_t goal) { } static void BotAimHarness_ApplyGoalFeedforward(bot_state_t *bs, const vec3_t goal, - float dt) { + float dt, float ffGain) { float pitchRate, yawRate, ff; if (bs->aimh_last_goal_time <= 0.0f || dt <= 0.001f || dt > 0.5f) { @@ -638,7 +726,7 @@ static void BotAimHarness_ApplyGoalFeedforward(bot_state_t *bs, const vec3_t goa pitchRate = BotAimHarness_PitchDiff(goal[PITCH], bs->aimh_last_goal_pitch) / dt; yawRate = BotAimHarness_YawDiff(goal[YAW], bs->aimh_last_goal_yaw) / dt; - ff = AIMH_FEEDFORWARD_BASE + AIMH_FEEDFORWARD_SKILL * bs->aimh_aim_skill; + ff = (AIMH_FEEDFORWARD_BASE + AIMH_FEEDFORWARD_SKILL * bs->aimh_aim_skill) * ffGain; bs->aimh_vel[PITCH] += pitchRate * ff; bs->aimh_vel[YAW] += yawRate * ff; } @@ -690,7 +778,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { float skill, aimSkill, maxChange, maxTrack, maxFlick, maxVelPitch; float stiffness, damping, maxVel, motorNoise, motorScale; float magErr, pitchErr, yawErr, catchup, catchupRate; - float flickAngle, goalDt; + float flickAngle, goalDt, engageFar, ffGain, catchupMult; if (!BotAimHarness_IsActive()) { return 0; @@ -751,6 +839,20 @@ 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; + ffGain = 1.0f; + catchupMult = 1.0f; + goalDt = thinktime; + if (bs->aimh_combat_aim) { + if (bs->aimh_last_goal_time > 0.0f) { + goalDt = FloatTime() - bs->aimh_last_goal_time; + } + engageFar = BotAimHarness_GetEngageFar(bs, goal, goalDt); + ffGain = BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_FF, AIMH_ENGAGE_FAR_FF, engageFar); + 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); @@ -761,6 +863,10 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } 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; @@ -785,6 +891,11 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { damping *= (0.88f + 0.32f * aimSkill); } + if (bs->aimh_combat_aim) { + BotAimHarness_ApplyEngageProfile(engageFar, &stiffness, &damping, &maxVel, + &motorNoise, &ffGain, &catchupMult); + } + if (bs->aimh_combat_aim && maxVel < 140.0f) { maxVel = 140.0f + 80.0f * aimSkill; } else if (maxVel < 90.0f) { @@ -800,11 +911,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } if (bs->aimh_combat_aim) { - goalDt = thinktime; - if (bs->aimh_last_goal_time > 0.0f) { - goalDt = FloatTime() - bs->aimh_last_goal_time; - } - BotAimHarness_ApplyGoalFeedforward(bs, goal, goalDt); + BotAimHarness_ApplyGoalFeedforward(bs, goal, goalDt, ffGain); } BotAimHarness_UpdateAxis(bs, PITCH, goal[PITCH], thinktime, @@ -812,7 +919,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { BotAimHarness_UpdateAxis(bs, YAW, goal[YAW], thinktime, stiffness, damping, maxVel, motorNoise); - catchupRate = AIMH_PITCH_CATCHUP_RATE; + catchupRate = AIMH_PITCH_CATCHUP_RATE * catchupMult; if (bs->aimh_recover_until > FloatTime() && bs->aimh_acquire_until <= FloatTime()) { catchupRate *= AIMH_RECOVER_CATCHUP_MULT; } @@ -828,7 +935,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } if (bs->aimh_combat_aim && bs->aimh_acquire_until <= FloatTime() && fabs(yawErr) > AIMH_YAW_CATCHUP_ERR) { - catchupRate = AIMH_YAW_CATCHUP_RATE; + catchupRate = AIMH_YAW_CATCHUP_RATE * catchupMult; if (bs->aimh_recover_until > FloatTime()) { catchupRate *= AIMH_RECOVER_CATCHUP_MULT; } diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index e9adcce..e88614c 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -1735,7 +1735,13 @@ void BotChooseWeapon(bot_state_t *bs) { else if(g_rockets.integer) newweaponnum = WP_ROCKET_LAUNCHER; else { - newweaponnum = BotWpnSelect_Choose(bs); + if (bs->enemy >= 0) { + newweaponnum = BotWpnSelect_Choose(bs); + } else if (BotWpnSelect_IsActive()) { + newweaponnum = BotWpnSelect_ChooseRoaming(bs); + } else { + newweaponnum = -1; + } if (newweaponnum < 0 || newweaponnum >= WP_NUM_WEAPONS) { newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory); } @@ -5455,6 +5461,9 @@ void BotDeathmatchAI(bot_state_t *bs, float thinktime) { BotSetTeleportTime(bs); //update some inventory values BotUpdateInventory(bs); + if (BotWpnSelect_IsActive() && bs->enemy < 0 && !BotIsObserver(bs)) { + BotWpnSelect_TickRoaming(bs); + } BotTactics_OnThink(bs); //check out the snapshot BotCheckSnapshot(bs); diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 2210436..2d8bc6b 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -310,6 +310,7 @@ typedef struct bot_state_s /* ---- 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_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..f02ad1d 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.c +++ b/ratoa_gamecode/code/game/ai_weapon_select.c @@ -35,6 +35,26 @@ 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 static int BotWpnSel_HasWeaponAndAmmo(bot_state_t *bs, int wp) { if (wp <= WP_NONE || wp >= WP_NUM_WEAPONS) { @@ -81,12 +101,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 +258,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; @@ -267,8 +381,104 @@ int BotWpnSelect_IsActive(void) { return 1; } +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; +} + void BotWpnSelect_Reset(bot_state_t *bs) { bs->wps_next_eval_time = 0.0f; + bs->wps_next_roam_eval_time = 0.0f; bs->wps_last_switch_time = -999999.0f; bs->wps_last_chosen_weapon = 0; bs->wps_desired_weapon = BOTWPN_DESIRE_NONE; @@ -292,9 +502,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; @@ -331,6 +542,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 +564,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 +581,16 @@ 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 == 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 +608,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 +627,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; @@ -431,3 +676,147 @@ int BotWpnSelect_Choose(bot_state_t *bs) { 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..d64086f 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.h +++ b/ratoa_gamecode/code/game/ai_weapon_select.h @@ -30,6 +30,8 @@ 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). */ From 4220427d5d3ce0655ab59c2d7a61c7c5efa17cb6 Mon Sep 17 00:00:00 2001 From: Foo Date: Tue, 19 May 2026 15:36:26 +1200 Subject: [PATCH 03/10] Fix dedicated bot behavior and implement debug route Bot behavior with the new aim harness was behaving differently on dedicated, unexpectedly. Turns out, listen server follows a slightly different path with bots that gives them some data feeds at client framerate (i.e. 125hz). So on dedicated, this broke aiming with the new code. Have fixed up the code so it behaves acceptably at 20hz. Also introduced scaffolding for bot aim debugging. Not fully working yet. --- docs/BOT-CVARS.md | 1 + docs/CLIENT-CVARS.md | 1 + ratoa_gamecode/code/cgame/cg_cvar.h | 1 + ratoa_gamecode/code/cgame/cg_local.h | 1 + ratoa_gamecode/code/cgame/cg_players.c | 1 + ratoa_gamecode/code/cgame/cg_unlagged.c | 148 +++++++++ ratoa_gamecode/code/game/ai_aim_harness.c | 357 ++++++++++++++++------ ratoa_gamecode/code/game/ai_aim_harness.h | 13 +- ratoa_gamecode/code/game/ai_dmq3.c | 7 +- ratoa_gamecode/code/game/ai_main.c | 61 ++-- ratoa_gamecode/code/game/ai_main.h | 4 + ratoa_gamecode/code/game/bg_public.h | 1 + 12 files changed, 467 insertions(+), 129 deletions(-) diff --git a/docs/BOT-CVARS.md b/docs/BOT-CVARS.md index ebb074b..390b1c4 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -16,6 +16,7 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | `bot_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_debugAim` | Devotion | `0` | 0 or 1 | Publishes harness combat aim point on bot entities for `cg_debugBotAim` (sets `origin2` + `EF_BOT_AIM_DEBUG`). Requires `sv_cheats 1`. | | `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. | diff --git a/docs/CLIENT-CVARS.md b/docs/CLIENT-CVARS.md index 460eaf6..a3b843b 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 | Draw bot harness aim debug: `1` = followed bot intent line, `2` = all bots, `4` = add viewangles ray. 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/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..34bf331 100644 --- a/ratoa_gamecode/code/cgame/cg_local.h +++ b/ratoa_gamecode/code/cgame/cg_local.h @@ -1713,6 +1713,7 @@ 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 ); 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..218319d 100644 --- a/ratoa_gamecode/code/cgame/cg_unlagged.c +++ b/ratoa_gamecode/code/cgame/cg_unlagged.c @@ -1572,6 +1572,154 @@ 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 + +/* +================= +CG_DrawDebugBeam + +Silent tracer poly for debug overlays (no sound). +================= +*/ +static void CG_DrawDebugBeam( const vec3_t start, const vec3_t end, int r, int g, int b, int a ) { + vec3_t forward, right; + polyVert_t verts[4]; + vec3_t line; + float len; + vec3_t finish; + static qhandle_t beamShader; + + if (!beamShader) { + beamShader = trap_R_RegisterShader("tracer"); + if (!beamShader) { + beamShader = trap_R_RegisterShader("gfx/misc/teleportEffect2"); + } + } + + VectorSubtract(end, start, forward); + len = VectorLength(forward); + if (len < 8.0f) { + return; + } + VectorNormalize(forward); + + VectorCopy(end, finish); + + line[0] = DotProduct(forward, cg.refdef.viewaxis[1]); + line[1] = DotProduct(forward, cg.refdef.viewaxis[2]); + VectorScale(cg.refdef.viewaxis[1], line[1], right); + VectorMA(right, -line[0], cg.refdef.viewaxis[2], right); + VectorNormalize(right); + + VectorMA(finish, cg_tracerWidth.value, right, verts[0].xyz); + verts[0].st[0] = 0; + verts[0].st[1] = 1; + verts[0].modulate[0] = r; + verts[0].modulate[1] = g; + verts[0].modulate[2] = b; + verts[0].modulate[3] = a; + + VectorMA(finish, -cg_tracerWidth.value, right, verts[1].xyz); + verts[1].st[0] = 1; + verts[1].st[1] = 0; + verts[1].modulate[0] = r; + verts[1].modulate[1] = g; + verts[1].modulate[2] = b; + verts[1].modulate[3] = a; + + VectorMA(start, -cg_tracerWidth.value, right, verts[2].xyz); + verts[2].st[0] = 1; + verts[2].st[1] = 1; + verts[2].modulate[0] = r; + verts[2].modulate[1] = g; + verts[2].modulate[2] = b; + verts[2].modulate[3] = a; + + VectorMA(start, cg_tracerWidth.value, right, verts[3].xyz); + verts[3].st[0] = 0; + verts[3].st[1] = 0; + verts[3].modulate[0] = r; + verts[3].modulate[1] = g; + verts[3].modulate[2] = b; + verts[3].modulate[3] = a; + + trap_R_AddPolyToScene(beamShader, 4, verts); +} + +/* +================= +CG_AddBotAimDebug + +Draw harness combat aim (eye -> origin2) when server sets EF_BOT_AIM_DEBUG. +cg_debugBotAim: 1 = followed bot, 2 = all bots, 4 = also draw viewangles ray. +================= +*/ +void CG_AddBotAimDebug( centity_t *cent ) { + int mode; + vec3_t eye, viewEnd, forward; + int viewHeight; + + if (!cg_debugBotAim.integer) { + return; + } + if (!cg.snap) { + return; + } + if (cent->currentState.eType != ET_PLAYER) { + return; + } + if (cent->currentState.eFlags & EF_DEAD) { + return; + } + if (!(cent->currentState.eFlags & EF_BOT_AIM_DEBUG)) { + return; + } + + mode = cg_debugBotAim.integer; + if (!(mode & CG_BOTAIMDBG_ALL)) { + if (!(cg.snap->ps.pm_flags & PMF_FOLLOW) && !cg.demoPlayback) { + return; + } + if (cent->currentState.clientNum != cg.predictedPlayerState.clientNum) { + return; + } + } + + if (cent->currentState.number == cg.predictedPlayerState.clientNum && + !cg.renderingThirdPerson && (cg.snap->ps.pm_flags & PMF_FOLLOW)) { + return; + } + + viewHeight = 32; + if (cent->currentState.number == cg.predictedPlayerState.clientNum) { + viewHeight = cg.predictedPlayerState.viewheight; + } else { + int x, zd, zu; + + 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, eye); + eye[2] += viewHeight; + + CG_DrawDebugBeam(eye, cent->currentState.origin2, 80, 255, 120, 220); + + if (mode & CG_BOTAIMDBG_VIEW) { + AngleVectors(cent->lerpAngles, forward, NULL, NULL); + VectorMA(eye, 4096.0f, forward, viewEnd); + CG_DrawDebugBeam(eye, viewEnd, 255, 220, 80, 160); + } +} + /* ================ CG_Cvar_ClampInt diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index a4c8c12..4f48b4b 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -18,6 +18,7 @@ BOT AIM HARNESS (v1) — see ai_aim_harness.h #include "ai_aim_harness.h" vmCvar_t bot_humanizeaim; +vmCvar_t bot_debugAim; /* Forward — defined in ai_dmq3.c / ai_main.c */ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int ent); @@ -34,8 +35,12 @@ 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 @@ -85,8 +90,22 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #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 static int bot_humanizeaim_last = -1; +static int bot_debugAim_last = -1; + +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; @@ -208,7 +227,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; } @@ -373,11 +392,17 @@ void BotAimHarness_ApplyThinkHitscanOrigin(bot_state_t *bs, vec3_t bestorigin, (void)dist; } -static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { - vec3_t dir, target; +/* + * 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_GetCombatTarget(bs, target)) { - VectorSubtract(target, bs->eye, 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; @@ -387,27 +412,16 @@ static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { angles[YAW] = AngleMod(angles[YAW]); } -static int BotAimHarness_IsSustainedFireWeapon(int wp) { - switch (wp) { - case WP_GAUNTLET: - case WP_MACHINEGUN: - case WP_LIGHTNING: - case WP_PLASMAGUN: -#ifdef MISSIONPACK - case WP_CHAINGUN: -#endif - return 1; - default: - return 0; - } +static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { + BotAimHarness_GetCombatAimAngles(bs, angles); } /* - * Hold attack for continuous-fire weapons while harness is tracking a visible enemy - * with unobstructed aimtarget LOS. Uses attack/intent angles for the hit trace so - * lagging viewangles do not drop fire between bot thinks. + * BotCheckAttack path when harness is on: LOS to aimtarget, weapon trace along intent + * angles (aimtarget, not lead-only combat_target). Skips view FOV so lagging viewangles + * do not block fire between bot thinks. */ -int BotAimHarness_TryAttack(bot_state_t *bs) { +int BotAimHarness_CheckAttack(bot_state_t *bs) { int attackentity; float points; vec3_t dir, forward, right, start, end, attackAngles; @@ -415,15 +429,12 @@ int BotAimHarness_TryAttack(bot_state_t *bs) { bsp_trace_t bsptrace, trace; weaponinfo_t wi; - if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim) { + if (!BotAimHarness_IsActive()) { return qfalse; } if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return qfalse; } - if (!BotAimHarness_IsSustainedFireWeapon(bs->weaponnum)) { - return qfalse; - } if (!BotAimHarness_AimTargetValid(bs)) { return qfalse; } @@ -485,6 +496,10 @@ int BotAimHarness_TryAttack(bot_state_t *bs) { return qtrue; } +int BotAimHarness_TryAttack(bot_state_t *bs) { + return BotAimHarness_CheckAttack(bs); +} + /* * 0 = ok, 1 = soft (motor boost only), 2 = hard (also damp motor). */ @@ -592,21 +607,72 @@ static void BotAimHarness_OnEnemyChange(bot_state_t *bs) { bs->aimh_vel[PITCH] *= 0.4f; bs->aimh_vel[YAW] *= 0.4f; bs->aimh_last_goal_time = 0.0f; + bs->aimh_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); +} + +static void BotAimHarness_DebugSync(bot_state_t *bs) { + gentity_t *ent; + + trap_Cvar_Update(&bot_debugAim); + ent = &g_entities[bs->entitynum]; + if (!ent->client) { + return; + } + + if (!bot_debugAim.integer) { + BotAimHarness_ClearEntityDebug(ent); + return; + } + + if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim || bs->enemy < 0 || + VectorLengthSquared(bs->aimh_combat_target) < 1.0f) { + BotAimHarness_ClearEntityDebug(ent); + return; + } + + VectorCopy(bs->aimh_combat_target, ent->s.origin2); + SnapVector(ent->s.origin2); + ent->s.eFlags |= EF_BOT_AIM_DEBUG; } void BotAimHarness_RegisterCvars(void) { trap_Cvar_Register(&bot_humanizeaim, "bot_humanizeaim", "0", CVAR_ARCHIVE); + trap_Cvar_Register(&bot_debugAim, "bot_debugAim", "0", CVAR_CHEAT); trap_Cvar_Update(&bot_humanizeaim); + trap_Cvar_Update(&bot_debugAim); } void BotAimHarness_ResetCvarLatch(void) { bot_humanizeaim_last = -1; + bot_debugAim_last = -1; } void BotAimHarness_UpdateCvar(void) { int i; trap_Cvar_Update(&bot_humanizeaim); + 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]); + } + } + } + if (bot_humanizeaim_last == bot_humanizeaim.integer) { return; } @@ -618,6 +684,7 @@ void BotAimHarness_UpdateCvar(void) { if (!bot_humanizeaim.integer) { botstates[i]->viewanglespeed[0] = 0; botstates[i]->viewanglespeed[1] = 0; + BotAimHarness_ClearEntityDebug(&g_entities[i]); } } } @@ -665,6 +732,10 @@ void BotAimHarness_Reset(bot_state_t *bs) { 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); } @@ -699,36 +770,97 @@ void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, 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) { - vec3_t dir, target; +/* + * 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 (dt <= 0.0f) { + dt = 0.001f; + } + + 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]; - if (!BotAimHarness_GetCombatTarget(bs, target)) { - VectorCopy(bs->aimh_goal, goal); + goal[PITCH] = targetAngles[PITCH]; + goal[YAW] = targetAngles[YAW]; + bs->aimh_smooth_goal_pitch = goal[PITCH]; + bs->aimh_smooth_goal_yaw = goal[YAW]; return; } - VectorSubtract(target, bs->eye, dir); - vectoangles(dir, goal); - goal[PITCH] = BotAimHarness_ClampPitch(goal[PITCH]); - goal[YAW] = AngleMod(goal[YAW]); + goal[PITCH] = bs->aimh_smooth_goal_pitch; + goal[YAW] = bs->aimh_smooth_goal_yaw; } -static void BotAimHarness_ApplyGoalFeedforward(bot_state_t *bs, const vec3_t goal, - float dt, float ffGain) { - float pitchRate, yawRate, ff; +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]); - if (bs->aimh_last_goal_time <= 0.0f || dt <= 0.001f || dt > 0.5f) { + 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; } - pitchRate = BotAimHarness_PitchDiff(goal[PITCH], bs->aimh_last_goal_pitch) / dt; - yawRate = BotAimHarness_YawDiff(goal[YAW], bs->aimh_last_goal_yaw) / dt; + 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 { + 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); + } + bs->viewangles[ROLL] = 0.0f; +} + +void BotAimHarness_BeginMotorFrame(bot_state_t *bs) { + if (!BotAimHarness_IsActive()) { + return; + } - ff = (AIMH_FEEDFORWARD_BASE + AIMH_FEEDFORWARD_SKILL * bs->aimh_aim_skill) * ffGain; - bs->aimh_vel[PITCH] += pitchRate * ff; - bs->aimh_vel[YAW] += yawRate * ff; + BotAimHarness_ResyncViewFromPSIfDesynced(bs); } static void BotAimHarness_SaveGoalHistory(bot_state_t *bs, const vec3_t goal) { @@ -738,7 +870,8 @@ static void BotAimHarness_SaveGoalHistory(bot_state_t *bs, const vec3_t goal) { } static void BotAimHarness_UpdateAxis(bot_state_t *bs, int axis, float goalAngle, - float dt, float stiffness, float damping, float maxVel, float motorNoise) { + float dt, float stiffness, float damping, float maxVel, float motorNoise, + float minVel, float catchupGain) { float err, accel, delta; if (axis == PITCH) { @@ -751,6 +884,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) { @@ -760,16 +904,48 @@ 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); } } @@ -777,10 +953,12 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { vec3_t goal; float skill, aimSkill, maxChange, maxTrack, maxFlick, maxVelPitch; float stiffness, damping, maxVel, motorNoise, motorScale; - float magErr, pitchErr, yawErr, catchup, catchupRate; - float flickAngle, goalDt, engageFar, ffGain, catchupMult; + float magErr, pitchErr, yawErr; + float flickAngle, goalDt, engageFar, ffDummy, catchupMult; + float minVel, catchupGain; if (!BotAimHarness_IsActive()) { + BotAimHarness_DebugSync(bs); return 0; } @@ -798,6 +976,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->aimh_acquire_until = 0.0f; bs->aimh_last_goal_time = 0.0f; trap_EA_View(bs->client, bs->viewangles); + BotAimHarness_DebugSync(bs); return 1; } @@ -809,12 +988,14 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { BotAimHarness_ClearRecoveryState(bs); bs->aimh_acquire_until = 0.0f; bs->aimh_last_goal_time = 0.0f; + 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); @@ -828,7 +1009,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { maxTrack *= motorScale; maxFlick *= motorScale; } else { - VectorCopy(bs->ideal_viewangles, goal); + BotAimHarness_GetMotorGoal(bs, goal, thinktime); motorNoise = 0.0f; aimSkill = 0.05f; skill = 0.05f; @@ -840,15 +1021,10 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->viewangles[PITCH] = BotAimHarness_ClampPitch(bs->viewangles[PITCH]); engageFar = 1.0f; - ffGain = 1.0f; catchupMult = 1.0f; goalDt = thinktime; if (bs->aimh_combat_aim) { - if (bs->aimh_last_goal_time > 0.0f) { - goalDt = FloatTime() - bs->aimh_last_goal_time; - } engageFar = BotAimHarness_GetEngageFar(bs, goal, goalDt); - ffGain = BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_FF, AIMH_ENGAGE_FAR_FF, engageFar); catchupMult = BotAimHarness_Lerp(AIMH_ENGAGE_CLOSE_CATCHUP, AIMH_ENGAGE_FAR_CATCHUP, engageFar); } @@ -859,7 +1035,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { 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; @@ -872,14 +1048,16 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { 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; @@ -892,8 +1070,11 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } if (bs->aimh_combat_aim) { + ffDummy = 1.0f; BotAimHarness_ApplyEngageProfile(engageFar, &stiffness, &damping, &maxVel, - &motorNoise, &ffGain, &catchupMult); + &motorNoise, &ffDummy, &catchupMult); + (void)catchupMult; + (void)ffDummy; } if (bs->aimh_combat_aim && maxVel < 140.0f) { @@ -911,44 +1092,19 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { } if (bs->aimh_combat_aim) { - BotAimHarness_ApplyGoalFeedforward(bs, goal, goalDt, ffGain); + minVel = AIMH_COMBAT_MIN_VEL + 45.0f * aimSkill; + catchupGain = AIMH_COMBAT_CATCHUP_GAIN * catchupMult; + } else { + minVel = AIMH_ROAM_MIN_VEL; + catchupGain = 0.0f; } - 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 * catchupMult; - if (bs->aimh_recover_until > FloatTime() && bs->aimh_acquire_until <= FloatTime()) { - catchupRate *= AIMH_RECOVER_CATCHUP_MULT; - } - 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); - } - if (bs->aimh_combat_aim && bs->aimh_acquire_until <= FloatTime() && - fabs(yawErr) > AIMH_YAW_CATCHUP_ERR) { - catchupRate = AIMH_YAW_CATCHUP_RATE * catchupMult; - if (bs->aimh_recover_until > FloatTime()) { - catchupRate *= AIMH_RECOVER_CATCHUP_MULT; - } - catchup = yawErr * thinktime * catchupRate; - if (catchup > yawErr) { - catchup = yawErr; - } else if (catchup < -yawErr) { - catchup = -yawErr; - } - bs->viewangles[YAW] = AngleMod(bs->viewangles[YAW] + 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); } @@ -959,5 +1115,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 2f0addc..7c6d54b 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -7,6 +7,11 @@ 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. Toggle at runtime: bot_humanizeaim 1 (default 0 = legacy aim motor) +Debug: bot_debugAim 1 (server, CVAR_CHEAT) publishes aim point; cg_debugBotAim draws it. + +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). =========================================================================== @@ -22,6 +27,8 @@ void BotAimHarness_ResetCvarLatch(void); void BotAimHarness_UpdateCvar(void); int BotAimHarness_IsActive(void); 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], @@ -29,10 +36,12 @@ void BotAimHarness_SetCombatGoal(struct bot_state_s *bs, const float idealAngles 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); /* - * Sustained-fire path for BotCheckAttack: fire when combat-aiming with clear LOS to - * aimtarget, using intent angles (not lagging viewangles). Returns qtrue if handled. + * BotCheckAttack when harness is on: fire using aimtarget + intent angles (all weapons). + * Returns qtrue if attack was issued. */ +int BotAimHarness_CheckAttack(struct bot_state_s *bs); int BotAimHarness_TryAttack(struct bot_state_s *bs); #endif /* AI_AIM_HARNESS_H */ diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index e88614c..198b952 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -3816,8 +3816,11 @@ void BotCheckAttack(bot_state_t *bs) { return; } } - /* BOT AIM HARNESS: sustained-fire uses intent angles + aimtarget LOS */ - if (BotAimHarness_TryAttack(bs)) { + /* BOT AIM HARNESS: intent angles + aimtarget LOS (all weapons) */ + if (BotAimHarness_CheckAttack(bs)) { + return; + } + if (BotAimHarness_IsActive() && BotAimHarness_AimTargetValid(bs)) { return; } if (VectorLengthSquared(dir) < Square(100)) diff --git a/ratoa_gamecode/code/game/ai_main.c b/ratoa_gamecode/code/game/ai_main.c index 89d89b0..55e7322 100644 --- a/ratoa_gamecode/code/game/ai_main.c +++ b/ratoa_gamecode/code/game/ai_main.c @@ -943,18 +943,40 @@ 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; + + if (BotAimHarness_IsActive()) { + 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); + 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); + } + } + 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 +987,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 +1093,10 @@ 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] + + if (!BotAimHarness_IsActive()) { + 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 +1114,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 (!BotAimHarness_IsActive()) { + 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 diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 2d8bc6b..4e5e27d 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -305,6 +305,10 @@ typedef struct bot_state_s 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; /* ---- end BOT AIM HARNESS ---- */ diff --git a/ratoa_gamecode/code/game/bg_public.h b/ratoa_gamecode/code/game/bg_public.h index f584498..a13461f 100644 --- a/ratoa_gamecode/code/game/bg_public.h +++ b/ratoa_gamecode/code/game/bg_public.h @@ -369,6 +369,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 // origin2 = bot harness combat aim point (bot_debugAim) // Additional awards (not visible to other players) /* From ee1998d03ababd28ddbc96eb93fce2c5188c734b Mon Sep 17 00:00:00 2001 From: Foo Date: Tue, 19 May 2026 18:47:08 +1200 Subject: [PATCH 04/10] Debug - Render traces for bot's aim vs intended aim With new aiming logic the bot has a desired aiming position and an actual aiming position, separately. This debug route (bot_debugAim <0|1> and on the client-side cg_debugBotAim <0-7> (Bitmask: 1 = draw for followed bot. 2 = draw for all bots. 4 = draw second (yellow) ray for bot's actual crosshair direction Fix bot debug path for new joining bots If bots joined while cg_debugBotAim was set, their debug traces weren't being sent. Only existing bots. Now corrected. Fix debug routine Debug aim traces for new bot aiming was only displaying when bot was in combat. When roaming, the trace remained locked to the last fight location. Now fixed - When roaming, shows desired aim correctly. --- docs/BOT-CVARS.md | 2 +- docs/CLIENT-CVARS.md | 2 +- ratoa_gamecode/code/cgame/cg_local.h | 1 + ratoa_gamecode/code/cgame/cg_unlagged.c | 395 +++++++++++++++++----- ratoa_gamecode/code/cgame/cg_view.c | 1 + ratoa_gamecode/code/game/ai_aim_harness.c | 131 ++++++- ratoa_gamecode/code/game/ai_aim_harness.h | 11 +- ratoa_gamecode/code/game/ai_main.c | 3 + ratoa_gamecode/code/game/bg_public.h | 7 +- ratoa_gamecode/code/game/g_active.c | 4 + ratoa_gamecode/code/game/g_client.c | 5 + 11 files changed, 470 insertions(+), 92 deletions(-) diff --git a/docs/BOT-CVARS.md b/docs/BOT-CVARS.md index 390b1c4..1083c93 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -16,7 +16,7 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | `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_debugAim` | Devotion | `0` | 0 or 1 | Publishes harness combat aim point on bot entities for `cg_debugBotAim` (sets `origin2` + `EF_BOT_AIM_DEBUG`). 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 `bot_humanizeaim`. Requires `sv_cheats 1`. | | `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. | diff --git a/docs/CLIENT-CVARS.md b/docs/CLIENT-CVARS.md index a3b843b..ddbaf91 100644 --- a/docs/CLIENT-CVARS.md +++ b/docs/CLIENT-CVARS.md @@ -117,7 +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 | Draw bot harness aim debug: `1` = followed bot intent line, `2` = all bots, `4` = add viewangles ray. Needs `bot_debugAim 1` on server. `CVAR_CHEAT`. | +| `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/ratoa_gamecode/code/cgame/cg_local.h b/ratoa_gamecode/code/cgame/cg_local.h index 34bf331..3323f63 100644 --- a/ratoa_gamecode/code/cgame/cg_local.h +++ b/ratoa_gamecode/code/cgame/cg_local.h @@ -1714,6 +1714,7 @@ 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_unlagged.c b/ratoa_gamecode/code/cgame/cg_unlagged.c index 218319d..fe621db 100644 --- a/ratoa_gamecode/code/cgame/cg_unlagged.c +++ b/ratoa_gamecode/code/cgame/cg_unlagged.c @@ -1576,128 +1576,287 @@ void CG_AddBoundingBox( centity_t *cent ) { #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_DrawDebugBeam +CG_BotAimDebugHasData -Silent tracer poly for debug overlays (no sound). +Server aim debug is signaled via STAT_EXTFLAGS (reliable), with legacy +eFlags / grapplePoint / origin2 fallbacks. ================= */ -static void CG_DrawDebugBeam( const vec3_t start, const vec3_t end, int r, int g, int b, int a ) { - vec3_t forward, right; - polyVert_t verts[4]; - vec3_t line; - float len; - vec3_t finish; - static qhandle_t beamShader; - - if (!beamShader) { - beamShader = trap_R_RegisterShader("tracer"); - if (!beamShader) { - beamShader = trap_R_RegisterShader("gfx/misc/teleportEffect2"); +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 - VectorSubtract(end, start, forward); - len = VectorLength(forward); +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; } - VectorNormalize(forward); - VectorCopy(end, finish); + 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); +} - line[0] = DotProduct(forward, cg.refdef.viewaxis[1]); - line[1] = DotProduct(forward, cg.refdef.viewaxis[2]); - VectorScale(cg.refdef.viewaxis[1], line[1], right); - VectorMA(right, -line[0], cg.refdef.viewaxis[2], right); - VectorNormalize(right); +/* +================= +CG_BotAimDebugSprite - VectorMA(finish, cg_tracerWidth.value, right, verts[0].xyz); - verts[0].st[0] = 0; - verts[0].st[1] = 1; - verts[0].modulate[0] = r; - verts[0].modulate[1] = g; - verts[0].modulate[2] = b; - verts[0].modulate[3] = a; - - VectorMA(finish, -cg_tracerWidth.value, right, verts[1].xyz); - verts[1].st[0] = 1; - verts[1].st[1] = 0; - verts[1].modulate[0] = r; - verts[1].modulate[1] = g; - verts[1].modulate[2] = b; - verts[1].modulate[3] = a; - - VectorMA(start, -cg_tracerWidth.value, right, verts[2].xyz); - verts[2].st[0] = 1; - verts[2].st[1] = 1; - verts[2].modulate[0] = r; - verts[2].modulate[1] = g; - verts[2].modulate[2] = b; - verts[2].modulate[3] = a; +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); +} - VectorMA(start, cg_tracerWidth.value, right, verts[3].xyz); - verts[3].st[0] = 0; - verts[3].st[1] = 0; - verts[3].modulate[0] = r; - verts[3].modulate[1] = g; - verts[3].modulate[2] = b; - verts[3].modulate[3] = a; +/* +================= +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; - trap_R_AddPolyToScene(beamShader, 4, verts); + 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_AddBotAimDebug +CG_BotAimDebugDrawLine -Draw harness combat aim (eye -> origin2) when server sets EF_BOT_AIM_DEBUG. -cg_debugBotAim: 1 = followed bot, 2 = all bots, 4 = also draw viewangles ray. +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). ================= */ -void CG_AddBotAimDebug( centity_t *cent ) { - int mode; - vec3_t eye, viewEnd, forward; - int viewHeight; +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; + } - if (!cg_debugBotAim.integer) { + 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; } - if (!cg.snap) { + + 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 ); + } +} + +/* +================= +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 (cent->currentState.eType != ET_PLAYER) { + if (!(cg.snap->ps.pm_flags & PMF_FOLLOW) && !cg.demoPlayback) { return; } - if (cent->currentState.eFlags & EF_DEAD) { + if (!CG_ClientIsBot(cg.snap->ps.clientNum)) { return; } - if (!(cent->currentState.eFlags & EF_BOT_AIM_DEBUG)) { + if (!CG_BotAimDebugHasData(NULL, qtrue)) { return; } mode = cg_debugBotAim.integer; - if (!(mode & CG_BOTAIMDBG_ALL)) { - if (!(cg.snap->ps.pm_flags & PMF_FOLLOW) && !cg.demoPlayback) { - return; - } - if (cent->currentState.clientNum != cg.predictedPlayerState.clientNum) { - return; - } + drawAim = qfalse; + drawView = qfalse; + if (mode & CG_BOTAIMDBG_VIEW) { + drawView = qtrue; } - - if (cent->currentState.number == cg.predictedPlayerState.clientNum && - !cg.renderingThirdPerson && (cg.snap->ps.pm_flags & PMF_FOLLOW)) { + 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 { - int x, zd, zu; - x = (cent->currentState.solid & 255); zd = ((cent->currentState.solid >> 8) & 255); zu = ((cent->currentState.solid >> 16) & 255) - 32; @@ -1708,15 +1867,87 @@ void CG_AddBotAimDebug( centity_t *cent ) { (void)x; } - VectorCopy(cent->lerpOrigin, eye); - eye[2] += viewHeight; + 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_DrawDebugBeam(eye, cent->currentState.origin2, 80, 255, 120, 220); + 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_DrawDebugBeam(eye, viewEnd, 255, 220, 80, 160); + CG_BotAimDebugDrawLine( eye, viewEnd, cent->currentState.number, + 255, 220, 80, 180 ); } } 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 4f48b4b..dfcbf5f 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -103,6 +103,8 @@ extern bot_state_t *botstates[MAX_CLIENTS]; static int bot_humanizeaim_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); @@ -619,10 +621,69 @@ static void BotAimHarness_ClearEntityDebug(gentity_t *ent) { } 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]; @@ -630,20 +691,79 @@ static void BotAimHarness_DebugSync(bot_state_t *bs) { return; } + BotAI_GetClientState(bs->client, &bs->cur_ps); + if (!bot_debugAim.integer) { BotAimHarness_ClearEntityDebug(ent); return; } - if (!BotAimHarness_IsActive() || !bs->aimh_combat_aim || bs->enemy < 0 || - VectorLengthSquared(bs->aimh_combat_target) < 1.0f) { + BotAimHarness_RefreshEye(bs); + if (!BotAimHarness_GetDebugAimPoint(bs, point)) { BotAimHarness_ClearEntityDebug(ent); return; } - VectorCopy(bs->aimh_combat_target, ent->s.origin2); - SnapVector(ent->s.origin2); + 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) { @@ -670,6 +790,8 @@ void BotAimHarness_UpdateCvar(void) { for (i = 0; i < level.maxclients; i++) { BotAimHarness_ClearEntityDebug(&g_entities[i]); } + } else { + BotAimHarness_SyncAllBotsDebug(); } } @@ -988,6 +1110,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { 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 { diff --git a/ratoa_gamecode/code/game/ai_aim_harness.h b/ratoa_gamecode/code/game/ai_aim_harness.h index 7c6d54b..74f5521 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -7,7 +7,9 @@ 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. Toggle at runtime: bot_humanizeaim 1 (default 0 = legacy aim motor) -Debug: bot_debugAim 1 (server, CVAR_CHEAT) publishes aim point; cg_debugBotAim draws it. +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. 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, @@ -44,4 +46,11 @@ int BotAimHarness_AimTargetValid(struct bot_state_s *bs); int BotAimHarness_CheckAttack(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_main.c b/ratoa_gamecode/code/game/ai_main.c index 55e7322..8608bfa 100644 --- a/ratoa_gamecode/code/game/ai_main.c +++ b/ratoa_gamecode/code/game/ai_main.c @@ -1322,6 +1322,7 @@ int BotAISetupClient(int client, struct bot_settings_s *settings, qboolean resta BotAimHarness_Reset(bs); BotWpnSelect_Reset(bs); BotTactics_Reset(bs); + BotAimHarness_SyncClientDebug(client); //bot has been setup succesfully return qtrue; } @@ -1507,6 +1508,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; } @@ -1650,6 +1652,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; diff --git a/ratoa_gamecode/code/game/bg_public.h b/ratoa_gamecode/code/game/bg_public.h index a13461f..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,7 +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 // origin2 = bot harness combat aim point (bot_debugAim) +#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..110a887 100644 --- a/ratoa_gamecode/code/game/g_active.c +++ b/ratoa_gamecode/code/game/g_active.c @@ -22,6 +22,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #include "g_local.h" +#include "ai_aim_harness.h" /* @@ -1912,6 +1913,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..83461a4 100644 --- a/ratoa_gamecode/code/game/g_client.c +++ b/ratoa_gamecode/code/game/g_client.c @@ -21,6 +21,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ // #include "g_local.h" +#include "ai_aim_harness.h" // g_client.c -- client functions that don't happen every frame @@ -3122,6 +3123,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 From f5539ee5d494e4b5801efcbcdc331a6b13c065ce Mon Sep 17 00:00:00 2001 From: Foo Date: Tue, 19 May 2026 21:07:57 +1200 Subject: [PATCH 05/10] Fix bots not firing LG/MG even when on target Fixed a regression. Bots should now be much more trigger happy. --- ratoa_gamecode/code/cgame/cg_unlagged.c | 5 + ratoa_gamecode/code/game/ai_aim_harness.c | 280 ++++++++++++++++++---- ratoa_gamecode/code/game/ai_aim_harness.h | 10 +- ratoa_gamecode/code/game/ai_dmq3.c | 8 +- ratoa_gamecode/code/game/ai_main.c | 1 + ratoa_gamecode/code/game/ai_main.h | 1 + 6 files changed, 249 insertions(+), 56 deletions(-) diff --git a/ratoa_gamecode/code/cgame/cg_unlagged.c b/ratoa_gamecode/code/cgame/cg_unlagged.c index fe621db..943187a 100644 --- a/ratoa_gamecode/code/cgame/cg_unlagged.c +++ b/ratoa_gamecode/code/cgame/cg_unlagged.c @@ -1774,6 +1774,11 @@ static void CG_BotAimDebugDrawLine( const vec3_t start, const vec3_t endPos, } } +/* + * 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 diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index dfcbf5f..70cb825 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -100,6 +100,13 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_MOTOR_SUBSTEP_DT 0.010f #define AIMH_MOTOR_SUBSTEP_MAX 12 +/* Suppressive fire: loose aim cone (degrees, full width) and view-vs-intent slack */ +#define AIMH_FIRE_FOV_NEAR 72.0f +#define AIMH_FIRE_FOV_FAR 58.0f +#define AIMH_FIRE_FOV_DIST 100.0f +#define AIMH_FIRE_VIEW_SLACK 14.0f +#define AIMH_FIRE_MIN_VISIBILITY 0.12f + static int bot_humanizeaim_last = -1; static int bot_debugAim_last = -1; @@ -419,87 +426,262 @@ static void BotAimHarness_GetAttackAimAngles(bot_state_t *bs, vec3_t angles) { } /* - * BotCheckAttack path when harness is on: LOS to aimtarget, weapon trace along intent - * angles (aimtarget, not lead-only combat_target). Skips view FOV so lagging viewangles - * do not block fire between bot thinks. + * Body aim point for fire permission (no think-time lead; motor still uses lead). */ -int BotAimHarness_CheckAttack(bot_state_t *bs) { - int attackentity; - float points; - vec3_t dir, forward, right, start, end, attackAngles; - vec3_t mins = {-8, -8, -8}, maxs = {8, 8, 8}; - bsp_trace_t bsptrace, trace; - weaponinfo_t wi; +static void BotAimHarness_GetEnemyFirePoint(bot_state_t *bs, vec3_t point) { + aas_entityinfo_t entinfo; - if (!BotAimHarness_IsActive()) { + 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 (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_AimTargetValid(bs)) { + if (!BotAimHarness_WeaponReady(bs)) { return qfalse; } - attackentity = bs->enemy; - VectorSubtract(bs->aimtarget, bs->eye, dir); if (bs->weaponnum == WP_GAUNTLET) { + VectorSubtract(firePoint, bs->eye, dir); if (VectorLengthSquared(dir) > Square(60)) { return qfalse; } } - BotAI_Trace(&bsptrace, bs->eye, NULL, NULL, bs->aimtarget, bs->client, - CONTENTS_SOLID | CONTENTS_PLAYERCLIP); - if (bsptrace.fraction < 1 && bsptrace.ent != attackentity) { + 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 (!BotAimHarness_PassesThinkFireGates(bs)) { + return; + } + trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); - BotAimHarness_GetAttackAimAngles(bs, attackAngles); + BotAimHarness_GetEnemyFirePoint(bs, firePoint); - VectorCopy(bs->origin, start); - start[2] += bs->cur_ps.viewheight; - AngleVectors(attackAngles, forward, right, NULL); - start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1]; - start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1]; - start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2]; - VectorMA(start, 1000, forward, end); - VectorMA(start, -12, forward, start); - BotAI_Trace(&trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT); - - if (trace.ent >= 0 && trace.ent < MAX_CLIENTS && trace.ent != attackentity) { - if (BotSameTeam(bs, trace.ent)) { - return qfalse; - } + if (!BotAimHarness_WantsSuppressiveFire(bs, &wi, firePoint)) { + return; } - if (trace.ent != attackentity) { - if (wi.proj.damagetype & DAMAGETYPE_RADIAL) { - if (trace.fraction * 1000 < wi.proj.radius) { - points = (wi.proj.damage - 0.5f * trace.fraction * 1000) * 0.5f; - if (points > 0) { - return qfalse; - } - } - } else { - return qfalse; - } + 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); } - } else { - trap_EA_Attack(bs->client); + bs->flags ^= BFL_ATTACKED; } - bs->flags ^= BFL_ATTACKED; - return qtrue; +} + +/* + * 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 (!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) { - return BotAimHarness_CheckAttack(bs); + BotAimHarness_CheckAttack(bs); + return bs->aimh_hold_fire; } /* @@ -859,6 +1041,7 @@ void BotAimHarness_Reset(bot_state_t *bs) { 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; } void BotAimHarness_SetCombatGoal(bot_state_t *bs, const vec3_t idealAngles, @@ -1104,6 +1287,7 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { 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; diff --git a/ratoa_gamecode/code/game/ai_aim_harness.h b/ratoa_gamecode/code/game/ai_aim_harness.h index 74f5521..5fbb6b7 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -11,6 +11,9 @@ Debug: bot_debugAim 1 (server, CVAR_CHEAT) publishes motor wish (ideal_viewangle when roaming, aimtarget when fighting) via ps.grapplePoint + EXTFL_BOT_AIM_DEBUG; cg_debugBotAim draws green = wish, yellow (bit 4) = crosshair. +Combat fire: loose FOV + eye LOS to enemy body (not lead-only); MG/LG hold +attack +each input frame while on target. Railgun-style (WFL_FIRERELEASED) uses think cadence. + 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). @@ -40,10 +43,11 @@ void BotAimHarness_ApplyThinkHitscanOrigin(struct bot_state_s *bs, float bestori int BotAimHarness_ChangeViewAngles(struct bot_state_s *bs, float thinktime); int BotAimHarness_AimTargetValid(struct bot_state_s *bs); /* - * BotCheckAttack when harness is on: fire using aimtarget + intent angles (all weapons). - * Returns qtrue if attack was issued. + * Think-time attack (BotCheckAttack): sets aimh_hold_fire for suppressive weapons. + * Per-frame +attack while hold is set: BotAimHarness_ApplyCombatFire (BotUpdateInput). */ -int BotAimHarness_CheckAttack(struct bot_state_s *bs); +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. */ diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index 198b952..f0ecc7f 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -3816,11 +3816,9 @@ void BotCheckAttack(bot_state_t *bs) { return; } } - /* BOT AIM HARNESS: intent angles + aimtarget LOS (all weapons) */ - if (BotAimHarness_CheckAttack(bs)) { - return; - } - if (BotAimHarness_IsActive() && BotAimHarness_AimTargetValid(bs)) { + /* BOT AIM HARNESS: suppressive fire (loose aim + per-frame hold for hitscan) */ + if (BotAimHarness_IsActive() && bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + BotAimHarness_CheckAttack(bs); return; } if (VectorLengthSquared(dir) < Square(100)) diff --git a/ratoa_gamecode/code/game/ai_main.c b/ratoa_gamecode/code/game/ai_main.c index 8608bfa..437a132 100644 --- a/ratoa_gamecode/code/game/ai_main.c +++ b/ratoa_gamecode/code/game/ai_main.c @@ -957,6 +957,7 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { } BotAimHarness_BeginMotorFrame(bs); BotChangeViewAngles(bs, thinktime); + BotAimHarness_ApplyCombatFire(bs); trap_EA_GetInput(bs->client, (float)time / 1000, &bi); if (bi.actionflags & ACTION_RESPAWN) { if (bs->lastucmd.buttons & BUTTON_ATTACK) { diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 4e5e27d..06facc4 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -310,6 +310,7 @@ typedef struct bot_state_s 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 */ /* ---- end BOT AIM HARNESS ---- */ /* ---- BOT SMART WEAPON SELECT (v1): ai_weapon_select.c — remove to revert ---- */ From 06b29880a2fb81b2ee8631e2e346b668d4dd834a Mon Sep 17 00:00:00 2001 From: Foo Date: Wed, 20 May 2026 09:55:10 +1200 Subject: [PATCH 06/10] Refactor bot enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor bot enhancements - Step 2 (and windows build script) Refactor bot enhancement - Step 3 No intended gameplay change. Changes: 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). Refactor bot enhancement - Step 4 No gameplay changes. Code changes: 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. Refactor bot enhancement - Step 6 (Docs) Just documentation stuff. Signoff Small workplan for bot enhancement (WIP) --- build_windows.ps1 | 1 + docs/BOT-CVARS.md | 32 +- docs/BOT-ENHANCED-ARCHITECTURE.md | 222 ++++++++++++ docs/WORKPLAN-BOT-ENHANCEMENTS.md | 316 ++++++++++++++++++ ratoa_gamecode/Makefile | 8 + ratoa_gamecode/code/game/ai_aim_harness.c | 28 +- ratoa_gamecode/code/game/ai_aim_harness.h | 5 +- ratoa_gamecode/code/game/ai_bot_combat.c | 33 ++ ratoa_gamecode/code/game/ai_bot_combat.h | 38 +++ ratoa_gamecode/code/game/ai_bot_enhanced.c | 163 +++++++++ ratoa_gamecode/code/game/ai_bot_enhanced.h | 31 ++ ratoa_gamecode/code/game/ai_bot_events.c | 73 ++++ ratoa_gamecode/code/game/ai_bot_events.h | 26 ++ .../code/game/ai_bot_move_harness.c | 24 ++ .../code/game/ai_bot_move_harness.h | 18 + ratoa_gamecode/code/game/ai_bot_tactics.c | 24 +- ratoa_gamecode/code/game/ai_bot_tactics.h | 14 +- ratoa_gamecode/code/game/ai_dmnet.c | 1 + ratoa_gamecode/code/game/ai_dmq3.c | 18 +- ratoa_gamecode/code/game/ai_main.c | 21 +- ratoa_gamecode/code/game/ai_main.h | 10 + ratoa_gamecode/code/game/ai_weapon_select.c | 15 +- ratoa_gamecode/code/game/ai_weapon_select.h | 3 +- ratoa_gamecode/code/game/g_active.c | 1 + ratoa_gamecode/code/game/g_client.c | 1 + ratoa_gamecode/windows_scripts/game.q3asm | 4 + ratoa_gamecode/windows_scripts/game_mp.q3asm | 4 + .../windows_scripts/windows_compile_game.bat | 4 + .../windows_compile_game_missionpack.bat | 4 + 29 files changed, 1067 insertions(+), 75 deletions(-) create mode 100644 build_windows.ps1 create mode 100644 docs/BOT-ENHANCED-ARCHITECTURE.md create mode 100644 docs/WORKPLAN-BOT-ENHANCEMENTS.md create mode 100644 ratoa_gamecode/code/game/ai_bot_combat.c create mode 100644 ratoa_gamecode/code/game/ai_bot_combat.h create mode 100644 ratoa_gamecode/code/game/ai_bot_enhanced.c create mode 100644 ratoa_gamecode/code/game/ai_bot_enhanced.h create mode 100644 ratoa_gamecode/code/game/ai_bot_events.c create mode 100644 ratoa_gamecode/code/game/ai_bot_events.h create mode 100644 ratoa_gamecode/code/game/ai_bot_move_harness.c create mode 100644 ratoa_gamecode/code/game/ai_bot_move_harness.h diff --git a/build_windows.ps1 b/build_windows.ps1 new file mode 100644 index 0000000..90a23cb --- /dev/null +++ b/build_windows.ps1 @@ -0,0 +1 @@ +msys2_shell.cmd -mingw64 -defterm -no-start -here -c "make" \ No newline at end of file diff --git a/docs/BOT-CVARS.md b/docs/BOT-CVARS.md index 1083c93..ec98da2 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -7,16 +7,19 @@ 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: steadier aim when locked on and more precise view tracking. Disables `bot_enhanced_aim` while enabled. | | `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). No effect while `bot_challenge` is `1`. 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_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_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 `bot_humanizeaim`. Requires `sv_cheats 1`. | | `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. | @@ -30,8 +33,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. | @@ -39,3 +40,24 @@ 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 +``` + +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..317f328 --- /dev/null +++ b/docs/BOT-ENHANCED-ARCHITECTURE.md @@ -0,0 +1,222 @@ +# 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` + `!bot_challenge` | Facade: `BotEnhanced_AimActive()` | +| 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) + +--- + +## First follow-up enhancement + +`BOT_STANCE_MELEE_COMMIT` + `BotAttackMove` reading `combat.move_policy` / `weaponnum` — no new cvars; implement on top of `BotCombat_UpdateIntent` and the move harness when enabled. diff --git a/docs/WORKPLAN-BOT-ENHANCEMENTS.md b/docs/WORKPLAN-BOT-ENHANCEMENTS.md new file mode 100644 index 0000000..1256b0c --- /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, bot_challenge — unchanged; challenge still disables enhanced aim +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_challenge +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 + bot_challenge for aim. +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 +Aim off (challenge wins) +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_gamecode/Makefile b/ratoa_gamecode/Makefile index e6168f2..94eba9c 100644 --- a/ratoa_gamecode/Makefile +++ b/ratoa_gamecode/Makefile @@ -1779,6 +1779,10 @@ 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 \ @@ -1842,6 +1846,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 \ diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index 70cb825..e612eb0 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -16,8 +16,9 @@ BOT AIM HARNESS (v1) — see ai_aim_harness.h #include "ai_dmq3.h" #include "chars.h" #include "ai_aim_harness.h" +#include "ai_bot_enhanced.h" -vmCvar_t bot_humanizeaim; +vmCvar_t bot_enhanced_aim; vmCvar_t bot_debugAim; /* Forward — defined in ai_dmq3.c / ai_main.c */ @@ -107,7 +108,7 @@ extern bot_state_t *botstates[MAX_CLIENTS]; #define AIMH_FIRE_VIEW_SLACK 14.0f #define AIMH_FIRE_MIN_VISIBILITY 0.12f -static int bot_humanizeaim_last = -1; +static int bot_enhanced_aim_last = -1; static int bot_debugAim_last = -1; void BotAimHarness_SyncAllBotsDebug(void); @@ -949,21 +950,21 @@ void BotAimHarness_SyncAllBotsDebug(void) { } void BotAimHarness_RegisterCvars(void) { - trap_Cvar_Register(&bot_humanizeaim, "bot_humanizeaim", "0", CVAR_ARCHIVE); + 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_humanizeaim); + 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); + trap_Cvar_Update(&bot_enhanced_aim); trap_Cvar_Update(&bot_debugAim); if (bot_debugAim_last != bot_debugAim.integer) { @@ -977,15 +978,15 @@ void BotAimHarness_UpdateCvar(void) { } } - if (bot_humanizeaim_last == bot_humanizeaim.integer) { + 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]); @@ -995,14 +996,7 @@ void BotAimHarness_UpdateCvar(void) { } int BotAimHarness_IsActive(void) { - trap_Cvar_Update(&bot_humanizeaim); - if (!bot_humanizeaim.integer) { - return 0; - } - if (bot_challenge.integer) { - return 0; - } - return 1; + return BotEnhanced_AimActive(); } static void BotAimHarness_SyncViewAngles(bot_state_t *bs) { diff --git a/ratoa_gamecode/code/game/ai_aim_harness.h b/ratoa_gamecode/code/game/ai_aim_harness.h index 5fbb6b7..ed4c0a0 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -4,9 +4,10 @@ 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. 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..539ef4f --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_combat.c @@ -0,0 +1,33 @@ +/* +=========================================================================== +BOT COMBAT — intent reset/update and weapon-commit hook (stub). +=========================================================================== +*/ + +#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_combat.h" + +void BotCombat_Reset(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_UpdateIntent(bot_state_t *bs) { + BotCombat_Reset(bs); +} + +void BotCombat_OnWeaponCommitted(bot_state_t *bs, int prev_wp, int new_wp) { + (void)bs; + (void)prev_wp; + (void)new_wp; +} 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..ef0c3e0 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_combat.h @@ -0,0 +1,38 @@ +/* +=========================================================================== +BOT COMBAT — per-think intent scaffold (stance, move/fire policy). + +Defaults match legacy behavior. Stance and policy logic land here in follow-up work. +=========================================================================== +*/ + +#ifndef AI_BOT_COMBAT_H +#define AI_BOT_COMBAT_H + +typedef enum { + BOT_STANCE_NORMAL = 0 + /* BOT_STANCE_MELEE_COMMIT, BOT_STANCE_SURVIVAL_FLEE — reserved */ +} 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; + +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); + +#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..9aa53ba --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -0,0 +1,163 @@ +/* +=========================================================================== +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" + +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(); + 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); + if (!bot_enhanced_aim.integer) { + return 0; + } + trap_Cvar_Update(&bot_challenge); + if (bot_challenge.integer) { + return 0; + } + return 1; +} + +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; +} + +void BotEnhanced_OnThinkStart(bot_state_t *bs) { + BotEvents_Drain(bs); + if (BotEnhanced_IsActive()) { + BotCombat_UpdateIntent(bs); + } +} + +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); +} 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..cde8b79 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.h @@ -0,0 +1,31 @@ +/* +=========================================================================== +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); + +#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_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..3979a52 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,7 +24,7 @@ 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 @@ -42,8 +43,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) { @@ -170,7 +170,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 +290,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 +307,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 +324,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; diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.h b/ratoa_gamecode/code/game/ai_bot_tactics.h index b7d463b..4f6bb6f 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,15 @@ 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" +#define BOT_TACT_EVT_HURT_BY_OTHER BOT_EVT_HURT_BY_OTHER 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); /* 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..f25b711 100644 --- a/ratoa_gamecode/code/game/ai_dmnet.c +++ b/ratoa_gamecode/code/game/ai_dmnet.c @@ -42,6 +42,7 @@ 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_tactics.h" #include "ai_dmq3.h" #include "ai_chat.h" diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index f0ecc7f..dfed365 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -47,6 +47,7 @@ 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_chat.h" #include "ai_cmd.h" #include "ai_dmnet.h" @@ -1737,7 +1738,7 @@ void BotChooseWeapon(bot_state_t *bs) { else { if (bs->enemy >= 0) { newweaponnum = BotWpnSelect_Choose(bs); - } else if (BotWpnSelect_IsActive()) { + } else if (BotEnhanced_WeaponsActive()) { newweaponnum = BotWpnSelect_ChooseRoaming(bs); } else { newweaponnum = -1; @@ -3666,7 +3667,8 @@ 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); @@ -3723,14 +3725,14 @@ 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); @@ -3816,8 +3818,8 @@ void BotCheckAttack(bot_state_t *bs) { return; } } - /* BOT AIM HARNESS: suppressive fire (loose aim + per-frame hold for hitscan) */ - if (BotAimHarness_IsActive() && bs->enemy >= 0 && bs->enemy < MAX_CLIENTS) { + /* 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; } @@ -5462,10 +5464,10 @@ void BotDeathmatchAI(bot_state_t *bs, float thinktime) { BotSetTeleportTime(bs); //update some inventory values BotUpdateInventory(bs); - if (BotWpnSelect_IsActive() && bs->enemy < 0 && !BotIsObserver(bs)) { + BotEnhanced_OnThinkStart(bs); + if (BotEnhanced_WeaponsActive() && bs->enemy < 0 && !BotIsObserver(bs)) { BotWpnSelect_TickRoaming(bs); } - BotTactics_OnThink(bs); //check out the snapshot BotCheckSnapshot(bs); //check for air diff --git a/ratoa_gamecode/code/game/ai_main.c b/ratoa_gamecode/code/game/ai_main.c index 437a132..551bd79 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" @@ -947,7 +948,8 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { thinktime = (float)elapsed_time / 1000.0f; - if (BotAimHarness_IsActive()) { + /* ENHANCED: aim — input-frame motor path */ + if (BotEnhanced_AimActive()) { if (!BotAI_GetClientState(bs->client, &bs->cur_ps)) { return; } @@ -1094,7 +1096,8 @@ int BotAI(int client, float thinktime) { else if (!Q_stricmp(buf, "clientLevelShot")) { /*ignore*/ } } - if (!BotAimHarness_IsActive()) { + /* 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])); @@ -1115,7 +1118,7 @@ int BotAI(int client, float thinktime) { BotDeathmatchAI(bs, thinktime); //set the weapon selection every AI frame trap_EA_SelectWeapon(bs->client, bs->weaponnum); - if (!BotAimHarness_IsActive()) { + if (!BotEnhanced_AimActive()) { for (j = 0; j < 3; j++) { bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j])); @@ -1320,9 +1323,7 @@ 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; @@ -1426,9 +1427,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); } /* @@ -1753,9 +1752,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 06facc4..70decdf 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,14 @@ 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 AIM HARNESS (v1): ai_aim_harness.c — remove this block to revert ---- */ vec3_t aimh_goal; float aimh_vel[2]; diff --git a/ratoa_gamecode/code/game/ai_weapon_select.c b/ratoa_gamecode/code/game/ai_weapon_select.c index f02ad1d..5c42607 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.c +++ b/ratoa_gamecode/code/game/ai_weapon_select.c @@ -16,8 +16,10 @@ 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" -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); @@ -368,17 +370,13 @@ 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 0; - } - return 1; + return BotEnhanced_WeaponsActive(); } static float BotWpnSel_RoamStealth(bot_state_t *bs) { @@ -490,6 +488,7 @@ void BotWpnSelect_NotifyWeaponCommitted(bot_state_t *bs, int prev_wp, int new_wp bs->wps_last_switch_time = FloatTime(); } 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) { diff --git a/ratoa_gamecode/code/game/ai_weapon_select.h b/ratoa_gamecode/code/game/ai_weapon_select.h index d64086f..17d1ed6 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 diff --git a/ratoa_gamecode/code/game/g_active.c b/ratoa_gamecode/code/game/g_active.c index 110a887..15df7c7 100644 --- a/ratoa_gamecode/code/game/g_active.c +++ b/ratoa_gamecode/code/game/g_active.c @@ -22,6 +22,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #include "g_local.h" +#include "ai_bot_enhanced.h" #include "ai_aim_harness.h" diff --git a/ratoa_gamecode/code/game/g_client.c b/ratoa_gamecode/code/game/g_client.c index 83461a4..03b7386 100644 --- a/ratoa_gamecode/code/game/g_client.c +++ b/ratoa_gamecode/code/game/g_client.c @@ -21,6 +21,7 @@ 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 diff --git a/ratoa_gamecode/windows_scripts/game.q3asm b/ratoa_gamecode/windows_scripts/game.q3asm index dbbe793..6dc34ea 100644 --- a/ratoa_gamecode/windows_scripts/game.q3asm +++ b/ratoa_gamecode/windows_scripts/game.q3asm @@ -9,6 +9,10 @@ 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 diff --git a/ratoa_gamecode/windows_scripts/game_mp.q3asm b/ratoa_gamecode/windows_scripts/game_mp.q3asm index 95b78b3..7415b06 100644 --- a/ratoa_gamecode/windows_scripts/game_mp.q3asm +++ b/ratoa_gamecode/windows_scripts/game_mp.q3asm @@ -9,6 +9,10 @@ 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 diff --git a/ratoa_gamecode/windows_scripts/windows_compile_game.bat b/ratoa_gamecode/windows_scripts/windows_compile_game.bat index c97c4c7..520d947 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game.bat @@ -21,6 +21,10 @@ 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 diff --git a/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat b/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat index 07a78e4..2298aae 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat @@ -21,6 +21,10 @@ 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 From 22e2a6bbe77957da56c8841931216c9a0118109a Mon Sep 17 00:00:00 2001 From: Foo Date: Wed, 20 May 2026 13:35:01 +1200 Subject: [PATCH 07/10] Build script for windows Added a basic build script for windows when using MINGW64. This lets agents easily invoke the build without user intervention. Quiet build & fixed bot_challenge override Added a -quiet path for the build_windows.ps1 script. This minimizes console output during builds to just warnings and errors. This is handy because it makes the build a tiny bit faster (no console spam) but mainly because it significantly reduces token consumption when you have a cloud AI running a build on your behalf. Also fixed a mistake in the bot refactor work - bot_challenge 1 was overriding the new bot_enhanced_aim code paths. It doesn't do that any more. --- .gitignore | 1 + BUILD_WINDOWS.md | 25 ++++++- Makefile | 13 ++-- build_windows.ps1 | 79 +++++++++++++++++++++- caca_deterministic_zip.sh | 6 +- docs/BOT-CVARS.md | 4 +- docs/BOT-ENHANCED-ARCHITECTURE.md | 2 +- docs/WORKPLAN-BOT-ENHANCEMENTS.md | 8 +-- ratoa_assets/Makefile | 10 +++ ratoa_gamecode/Makefile | 35 +++++++--- ratoa_gamecode/code/game/ai_aim_harness.c | 1 - ratoa_gamecode/code/game/ai_bot_enhanced.c | 10 +-- ratoa_gamecode/code/game/ai_dmq3.c | 4 +- 13 files changed, 164 insertions(+), 34 deletions(-) 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 index 90a23cb..1a991ca 100644 --- a/build_windows.ps1 +++ b/build_windows.ps1 @@ -1 +1,78 @@ -msys2_shell.cmd -mingw64 -defterm -no-start -here -c "make" \ No newline at end of file +# 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 ec98da2..a999ae9 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -7,12 +7,12 @@ 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_enhanced_aim` 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). No effect while `bot_challenge` is `1`. 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_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). | diff --git a/docs/BOT-ENHANCED-ARCHITECTURE.md b/docs/BOT-ENHANCED-ARCHITECTURE.md index 317f328..e675f41 100644 --- a/docs/BOT-ENHANCED-ARCHITECTURE.md +++ b/docs/BOT-ENHANCED-ARCHITECTURE.md @@ -49,7 +49,7 @@ flowchart TB | Active when | Cvar | Notes | |-------------|------|--------| | Master | `bot_enhanced` | Default `0`. Sub-cvars ignored if off. | -| Aim harness | `bot_enhanced` + `bot_enhanced_aim` + `!bot_challenge` | Facade: `BotEnhanced_AimActive()` | +| 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` | diff --git a/docs/WORKPLAN-BOT-ENHANCEMENTS.md b/docs/WORKPLAN-BOT-ENHANCEMENTS.md index 1256b0c..e8b61a8 100644 --- a/docs/WORKPLAN-BOT-ENHANCEMENTS.md +++ b/docs/WORKPLAN-BOT-ENHANCEMENTS.md @@ -30,7 +30,7 @@ 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, bot_challenge — unchanged; challenge still disables enhanced aim +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 … @@ -57,7 +57,7 @@ bot_enhanced Master; 0 = all enhanced features off regardless of sub-cvars bot_enhanced_aim bot_humanizeaim -bot_enhanced && bot_enhanced_aim && !bot_challenge +bot_enhanced && bot_enhanced_aim bot_enhanced_weapons bot_smartWeaponChoice bot_enhanced && bot_enhanced_weapons @@ -162,7 +162,7 @@ Add: ai_bot_enhanced.c/h. Tasks: Register bot_enhanced (default 0, CVAR_ARCHIVE). -Implement BotEnhanced_IsActive, BotEnhanced_*Active() compositing master + feature cvars + bot_challenge for aim. +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. @@ -254,7 +254,7 @@ All enhanced on Same as old all three on 7 bot_challenge 1 + enhanced aim on -Aim off (challenge wins) +Harness on (challenge ignored on enhanced path) 8 bot_debugAim 1 Debug unchanged 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 94eba9c..2c17cc4 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: @@ -2134,7 +2147,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' >> $@ @@ -2227,13 +2242,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) @@ -2242,13 +2259,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/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index e612eb0..f6ec62b 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -27,7 +27,6 @@ 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]; diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.c b/ratoa_gamecode/code/game/ai_bot_enhanced.c index 9aa53ba..f5038d8 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.c +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -13,7 +13,6 @@ BOT ENHANCED — master cvar, feature gates, centralized register/reset. #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" @@ -120,14 +119,7 @@ int BotEnhanced_AimActive(void) { return 0; } trap_Cvar_Update(&bot_enhanced_aim); - if (!bot_enhanced_aim.integer) { - return 0; - } - trap_Cvar_Update(&bot_challenge); - if (bot_challenge.integer) { - return 0; - } - return 1; + return bot_enhanced_aim.integer != 0; } int BotEnhanced_WeaponsActive(void) { diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index dfed365..098cad1 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -3741,8 +3741,8 @@ void BotAimAtEnemy(bot_state_t *bs) { 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 From 7267a7004a9ef62c3e29685465d41b105cb182ef Mon Sep 17 00:00:00 2001 From: Foo Date: Wed, 20 May 2026 21:43:08 +1200 Subject: [PATCH 08/10] Add proper bot gauntlet attacking Implement so: - Bots close to an opponent may choose to switch to gauntlet - If they do, they will aggressively charge the opponent and attempt to pummel - If it doesn't work within a couple of seconds, they'll break off. Also if a bot is forced to use gauntlet because it has no ammo, it'll make a choice to attack as above, or flee. --- docs/BOT-ENHANCED-ARCHITECTURE.md | 12 +- ratoa_gamecode/code/game/ai_bot_combat.c | 245 +++++++++++++++++++- ratoa_gamecode/code/game/ai_bot_combat.h | 30 ++- ratoa_gamecode/code/game/ai_bot_enhanced.c | 20 ++ ratoa_gamecode/code/game/ai_bot_enhanced.h | 6 + ratoa_gamecode/code/game/ai_bot_tactics.c | 9 +- ratoa_gamecode/code/game/ai_bot_tactics.h | 6 + ratoa_gamecode/code/game/ai_dmnet.c | 11 +- ratoa_gamecode/code/game/ai_dmq3.c | 25 ++ ratoa_gamecode/code/game/ai_main.h | 1 + ratoa_gamecode/code/game/ai_weapon_select.c | 95 ++++++++ ratoa_gamecode/code/game/ai_weapon_select.h | 3 + 12 files changed, 440 insertions(+), 23 deletions(-) diff --git a/docs/BOT-ENHANCED-ARCHITECTURE.md b/docs/BOT-ENHANCED-ARCHITECTURE.md index e675f41..d0a1154 100644 --- a/docs/BOT-ENHANCED-ARCHITECTURE.md +++ b/docs/BOT-ENHANCED-ARCHITECTURE.md @@ -217,6 +217,12 @@ Do not add new stance/move gameplay until: --- -## First follow-up enhancement - -`BOT_STANCE_MELEE_COMMIT` + `BotAttackMove` reading `combat.move_policy` / `weaponnum` — no new cvars; implement on top of `BotCombat_UpdateIntent` and the move harness when enabled. +## 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/ratoa_gamecode/code/game/ai_bot_combat.c b/ratoa_gamecode/code/game/ai_bot_combat.c index 539ef4f..b98a115 100644 --- a/ratoa_gamecode/code/game/ai_bot_combat.c +++ b/ratoa_gamecode/code/game/ai_bot_combat.c @@ -1,9 +1,8 @@ /* =========================================================================== -BOT COMBAT — intent reset/update and weapon-commit hook (stub). +BOT COMBAT — intent reset/update and weapon-commit hook. =========================================================================== */ - #include "g_local.h" #include "../botlib/botlib.h" #include "../botlib/be_aas.h" @@ -13,21 +12,247 @@ BOT COMBAT — intent reset/update and weapon-commit hook (stub). #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" - -void BotCombat_Reset(bot_state_t *bs) { +#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) { - BotCombat_Reset(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 (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) { - (void)bs; - (void)prev_wp; - (void)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 index ef0c3e0..5b7150c 100644 --- a/ratoa_gamecode/code/game/ai_bot_combat.h +++ b/ratoa_gamecode/code/game/ai_bot_combat.h @@ -2,7 +2,7 @@ =========================================================================== BOT COMBAT — per-think intent scaffold (stance, move/fire policy). -Defaults match legacy behavior. Stance and policy logic land here in follow-up work. +Defaults match legacy behavior. Stance/policy logic in BotCombat_UpdateIntent. =========================================================================== */ @@ -10,23 +10,40 @@ Defaults match legacy behavior. Stance and policy logic land here in follow-up w #define AI_BOT_COMBAT_H typedef enum { - BOT_STANCE_NORMAL = 0 - /* BOT_STANCE_MELEE_COMMIT, BOT_STANCE_SURVIVAL_FLEE — reserved */ + 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_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; @@ -35,4 +52,9 @@ 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 index f5038d8..4d8babb 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.c +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -145,6 +145,26 @@ void BotEnhanced_OnThinkStart(bot_state_t *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); diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.h b/ratoa_gamecode/code/game/ai_bot_enhanced.h index cde8b79..dabd568 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.h +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.h @@ -28,4 +28,10 @@ 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); + #endif /* AI_BOT_ENHANCED_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.c b/ratoa_gamecode/code/game/ai_bot_tactics.c index 3979a52..ff7df77 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.c +++ b/ratoa_gamecode/code/game/ai_bot_tactics.c @@ -26,7 +26,6 @@ qboolean EntityCarriesFlag(aas_entityinfo_t *entinfo); 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 @@ -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; } @@ -336,7 +335,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; } @@ -355,7 +354,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; @@ -374,7 +373,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 4f6bb6f..2bcc007 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.h +++ b/ratoa_gamecode/code/game/ai_bot_tactics.h @@ -16,8 +16,12 @@ ai_main.h, ai_main.c, ai_dmq3.c, ai_dmnet.c. struct bot_state_s; #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); @@ -25,6 +29,8 @@ void BotTactics_Reset(struct bot_state_s *bs); 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); int BotTactics_BattleFightSuppressRetreat(struct bot_state_s *bs); diff --git a/ratoa_gamecode/code/game/ai_dmnet.c b/ratoa_gamecode/code/game/ai_dmnet.c index f25b711..0390809 100644 --- a/ratoa_gamecode/code/game/ai_dmnet.c +++ b/ratoa_gamecode/code/game/ai_dmnet.c @@ -43,6 +43,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #include "ai_main.h" #include "ai_bot_enhanced.h" +#include "ai_bot_combat.h" #include "ai_bot_tactics.h" #include "ai_dmq3.h" #include "ai_chat.h" @@ -2213,6 +2214,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 @@ -2230,7 +2234,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; } @@ -2448,6 +2452,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 diff --git a/ratoa_gamecode/code/game/ai_dmq3.c b/ratoa_gamecode/code/game/ai_dmq3.c index 098cad1..1a94b47 100644 --- a/ratoa_gamecode/code/game/ai_dmq3.c +++ b/ratoa_gamecode/code/game/ai_dmq3.c @@ -48,6 +48,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #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" @@ -1731,6 +1732,10 @@ 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) @@ -2884,6 +2889,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 @@ -3814,6 +3831,14 @@ 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; } diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 70decdf..461f8ce 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -326,6 +326,7 @@ typedef struct bot_state_s /* ---- 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 5c42607..72bd4c3 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.c +++ b/ratoa_gamecode/code/game/ai_weapon_select.c @@ -18,6 +18,7 @@ BOT SMART WEAPON SELECT (v1) — see ai_weapon_select.h #include "ai_weapon_select.h" #include "ai_bot_enhanced.h" #include "ai_bot_combat.h" +#include "ai_bot_tactics.h" vmCvar_t bot_enhanced_weapons; @@ -57,6 +58,10 @@ float BotEntityVisible(int viewer, vec3_t eye, vec3_t viewangles, float fov, int #define WPNSEL_ROAM_MG_LASTRESORT_PEN 48.0f #define WPNSEL_ROAM_MG_ONLY_BONUS 22.0f #define WPNSEL_ROAM_NOISE_MAX 18.0f +/* Enhanced fight select: min 1s between swaps; longer latch after close gauntlet commit. */ +#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) { @@ -474,9 +479,64 @@ static float BotWpnSel_MachinegunRoamModifier(bot_state_t *bs) { 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; @@ -486,6 +546,7 @@ 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); @@ -524,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; @@ -584,6 +653,18 @@ int BotWpnSelect_Choose(bot_state_t *bs) { 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) { legacyBias = WPNSEL_LEGACY_BIAS; if (wp == WP_MACHINEGUN && alternatives > 0) { @@ -673,6 +754,20 @@ 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; } diff --git a/ratoa_gamecode/code/game/ai_weapon_select.h b/ratoa_gamecode/code/game/ai_weapon_select.h index 17d1ed6..fb1c69e 100644 --- a/ratoa_gamecode/code/game/ai_weapon_select.h +++ b/ratoa_gamecode/code/game/ai_weapon_select.h @@ -37,5 +37,8 @@ void BotWpnSelect_TickRoaming(struct bot_state_s *bs); 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 */ From d1f3ac7e8282136164526c55c9479f9f6172796c Mon Sep 17 00:00:00 2001 From: Foo Date: Wed, 20 May 2026 23:09:29 +1200 Subject: [PATCH 09/10] Enhanced bots prioritize high-value items High-value items (MH, RA, YA, Quad, Flag) are prioritized as a goal if a bot sees one available for collection. Has some checks to make sure unattainable items (e.g. MH on DM17) are not pursued Bots also prioritize picking up weapons they don't yet have. --- docs/BOT-CVARS.md | 3 + ratoa_gamecode/Makefile | 1 + ratoa_gamecode/code/game/ai_bot_enhanced.c | 8 + ratoa_gamecode/code/game/ai_bot_enhanced.h | 2 + ratoa_gamecode/code/game/ai_bot_items.c | 1326 +++++++++++++++++ ratoa_gamecode/code/game/ai_bot_items.h | 31 + ratoa_gamecode/code/game/ai_dmnet.c | 31 +- ratoa_gamecode/code/game/ai_main.h | 8 + ratoa_gamecode/windows_scripts/game.q3asm | 1 + ratoa_gamecode/windows_scripts/game_mp.q3asm | 1 + .../windows_scripts/windows_compile_game.bat | 1 + .../windows_compile_game_missionpack.bat | 1 + 12 files changed, 1413 insertions(+), 1 deletion(-) create mode 100644 ratoa_gamecode/code/game/ai_bot_items.c create mode 100644 ratoa_gamecode/code/game/ai_bot_items.h diff --git a/docs/BOT-CVARS.md b/docs/BOT-CVARS.md index a999ae9..49c9c11 100644 --- a/docs/BOT-CVARS.md +++ b/docs/BOT-CVARS.md @@ -14,6 +14,8 @@ Console variables that control bot AI, navigation, chat, and fill rules. Set the | `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). | @@ -56,6 +58,7 @@ 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. diff --git a/ratoa_gamecode/Makefile b/ratoa_gamecode/Makefile index 2c17cc4..e90c855 100644 --- a/ratoa_gamecode/Makefile +++ b/ratoa_gamecode/Makefile @@ -1799,6 +1799,7 @@ Q3GOBJ_ = \ $(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 \ diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.c b/ratoa_gamecode/code/game/ai_bot_enhanced.c index 4d8babb..804bffc 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.c +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -20,6 +20,7 @@ BOT ENHANCED — master cvar, feature gates, centralized register/reset. #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; @@ -106,6 +107,7 @@ void BotEnhanced_RegisterCvars(void) { BotAimHarness_RegisterCvars(); BotWpnSelect_RegisterCvars(); BotTactics_RegisterCvars(); + BotItems_RegisterCvars(); BotEnhanced_MigrateLegacyCvars(); } @@ -138,10 +140,15 @@ int BotEnhanced_TacticsActive(void) { return bot_enhanced_tactics.integer != 0; } +int BotEnhanced_ItemsActive(void) { + return BotItems_IsActive(); +} + void BotEnhanced_OnThinkStart(bot_state_t *bs) { BotEvents_Drain(bs); if (BotEnhanced_IsActive()) { BotCombat_UpdateIntent(bs); + BotItems_Tick(bs); } } @@ -172,4 +179,5 @@ void BotEnhanced_ResetBot(bot_state_t *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 index dabd568..d68d789 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.h +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.h @@ -34,4 +34,6 @@ 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); + #endif /* AI_BOT_ENHANCED_H */ diff --git a/ratoa_gamecode/code/game/ai_bot_items.c b/ratoa_gamecode/code/game/ai_bot_items.c new file mode 100644 index 0000000..7d56d28 --- /dev/null +++ b/ratoa_gamecode/code/game/ai_bot_items.c @@ -0,0 +1,1326 @@ +/* + +=========================================================================== + +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 for %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)); + +} + + + +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)); + + + + 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: + + BotItems_DebugLine(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; + + } + +} + + + +/* 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; + + + + 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_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_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_dmnet.c b/ratoa_gamecode/code/game/ai_dmnet.c index 0390809..6b790dc 100644 --- a/ratoa_gamecode/code/game/ai_dmnet.c +++ b/ratoa_gamecode/code/game/ai_dmnet.c @@ -44,6 +44,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "ai_main.h" #include "ai_bot_enhanced.h" #include "ai_bot_combat.h" +#include "ai_bot_items.h" #include "ai_bot_tactics.h" #include "ai_dmq3.h" #include "ai_chat.h" @@ -233,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)) { @@ -1855,7 +1864,7 @@ int AINode_Seek_NBG(bot_state_t *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); @@ -1987,6 +1996,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 @@ -2164,6 +2178,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; @@ -2333,6 +2352,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; @@ -2528,6 +2552,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 diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 461f8ce..85913e4 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -298,6 +298,14 @@ typedef struct bot_state_s 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; + /* ---- end BOT ITEMS ---- */ + /* ---- BOT AIM HARNESS (v1): ai_aim_harness.c — remove this block to revert ---- */ vec3_t aimh_goal; float aimh_vel[2]; diff --git a/ratoa_gamecode/windows_scripts/game.q3asm b/ratoa_gamecode/windows_scripts/game.q3asm index 6dc34ea..ec11ae6 100644 --- a/ratoa_gamecode/windows_scripts/game.q3asm +++ b/ratoa_gamecode/windows_scripts/game.q3asm @@ -16,6 +16,7 @@ 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 7415b06..e9ea0ab 100644 --- a/ratoa_gamecode/windows_scripts/game_mp.q3asm +++ b/ratoa_gamecode/windows_scripts/game_mp.q3asm @@ -16,6 +16,7 @@ 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 520d947..b03e397 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game.bat @@ -28,6 +28,7 @@ cd windows\build\game %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 2298aae..076be9a 100644 --- a/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat +++ b/ratoa_gamecode/windows_scripts/windows_compile_game_missionpack.bat @@ -28,6 +28,7 @@ cd windows\build\game %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 From de7f09f8cbada1d337c61b4723610141a76dbb21 Mon Sep 17 00:00:00 2001 From: Foo Date: Thu, 21 May 2026 01:52:32 +1200 Subject: [PATCH 10/10] Fixup and slightly improve rocket jumping New bot routines were messing with the old rocketjump process. Now fixed, and bots should angle their jumps a bit better so they move towards their target more directly. --- ratoa_gamecode/code/game/ai_aim_harness.c | 48 +++ ratoa_gamecode/code/game/ai_aim_harness.h | 1 + ratoa_gamecode/code/game/ai_bot_combat.c | 5 + ratoa_gamecode/code/game/ai_bot_enhanced.c | 75 +++++ ratoa_gamecode/code/game/ai_bot_enhanced.h | 9 + ratoa_gamecode/code/game/ai_bot_items.c | 150 ++++++++- ratoa_gamecode/code/game/ai_bot_tactics.c | 3 + ratoa_gamecode/code/game/ai_dmnet.c | 49 ++- ratoa_gamecode/code/game/ai_dmq3.c | 351 ++++++++++++++++++++- ratoa_gamecode/code/game/ai_dmq3.h | 5 + ratoa_gamecode/code/game/ai_main.c | 10 +- ratoa_gamecode/code/game/ai_main.h | 13 + 12 files changed, 699 insertions(+), 20 deletions(-) diff --git a/ratoa_gamecode/code/game/ai_aim_harness.c b/ratoa_gamecode/code/game/ai_aim_harness.c index f6ec62b..e595e65 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.c +++ b/ratoa_gamecode/code/game/ai_aim_harness.c @@ -17,6 +17,7 @@ BOT AIM HARNESS (v1) — see ai_aim_harness.h #include "chars.h" #include "ai_aim_harness.h" #include "ai_bot_enhanced.h" +#include "ai_dmq3.h" vmCvar_t bot_enhanced_aim; vmCvar_t bot_debugAim; @@ -538,6 +539,9 @@ static qboolean BotAimHarness_PassesThinkFireGates(bot_state_t *bs) { if (bs->enemy < 0) { return qfalse; } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + return qfalse; + } if (BotTargetPlayerIsDead(bs)) { return qfalse; } @@ -622,6 +626,9 @@ void BotAimHarness_CheckAttack(bot_state_t *bs) { if (bs->enemy < 0 || bs->enemy >= MAX_CLIENTS) { return; } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + return; + } if (!BotAimHarness_PassesThinkFireGates(bs)) { return; } @@ -661,6 +668,10 @@ void BotAimHarness_ApplyCombatFire(bot_state_t *bs) { 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; } @@ -998,6 +1009,20 @@ int BotAimHarness_IsActive(void) { return BotEnhanced_AimActive(); } +void BotAimHarness_SyncMotorToView(bot_state_t *bs) { + if (!bs) { + return; + } + 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) { entityState_t es; @@ -1035,6 +1060,13 @@ void BotAimHarness_Reset(bot_state_t *bs) { 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, @@ -1158,6 +1190,10 @@ void BotAimHarness_BeginMotorFrame(bot_state_t *bs) { return; } + if (BotAI_WeaponJumpActive(bs)) { + return; + } + BotAimHarness_ResyncViewFromPSIfDesynced(bs); } @@ -1273,7 +1309,19 @@ int BotAimHarness_ChangeViewAngles(bot_state_t *bs, float thinktime) { bs->aimh_last_sanity_enemy = -1; bs->aimh_acquire_until = 0.0f; bs->aimh_last_goal_time = 0.0f; + bs->aimh_weapon_jump_until = 0.0f; + bs->aimh_weapon_jump_fired = qfalse; + trap_EA_View(bs->client, bs->viewangles); + BotAimHarness_DebugSync(bs); + return 1; + } + + if (BotAI_WeaponJumpActive(bs)) { + VectorCopy(bs->aimh_weapon_jump_angles, bs->ideal_viewangles); + VectorCopy(bs->aimh_weapon_jump_angles, bs->viewangles); + bs->viewangles[ROLL] = 0.0f; trap_EA_View(bs->client, bs->viewangles); + BotAimHarness_SyncMotorToView(bs); BotAimHarness_DebugSync(bs); return 1; } diff --git a/ratoa_gamecode/code/game/ai_aim_harness.h b/ratoa_gamecode/code/game/ai_aim_harness.h index ed4c0a0..bf376cc 100644 --- a/ratoa_gamecode/code/game/ai_aim_harness.h +++ b/ratoa_gamecode/code/game/ai_aim_harness.h @@ -32,6 +32,7 @@ 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); diff --git a/ratoa_gamecode/code/game/ai_bot_combat.c b/ratoa_gamecode/code/game/ai_bot_combat.c index b98a115..88ff037 100644 --- a/ratoa_gamecode/code/game/ai_bot_combat.c +++ b/ratoa_gamecode/code/game/ai_bot_combat.c @@ -201,6 +201,11 @@ void BotCombat_UpdateIntent(bot_state_t *bs) { BotCombat_ClearVoluntaryPursuit(bs); return; } + if (!BotEnhanced_CanEngageClient(bs, bs->enemy)) { + bs->enemy = -1; + BotCombat_ClearVoluntaryPursuit(bs); + return; + } if (BotCombat_UpdateVoluntaryGauntletAbort(bs)) { return; } diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.c b/ratoa_gamecode/code/game/ai_bot_enhanced.c index 804bffc..a054aff 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.c +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.c @@ -13,6 +13,7 @@ BOT ENHANCED — master cvar, feature gates, centralized register/reset. #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" @@ -144,9 +145,83 @@ 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); } diff --git a/ratoa_gamecode/code/game/ai_bot_enhanced.h b/ratoa_gamecode/code/game/ai_bot_enhanced.h index d68d789..a398066 100644 --- a/ratoa_gamecode/code/game/ai_bot_enhanced.h +++ b/ratoa_gamecode/code/game/ai_bot_enhanced.h @@ -36,4 +36,13 @@ 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_items.c b/ratoa_gamecode/code/game/ai_bot_items.c index 7d56d28..4c764b2 100644 --- a/ratoa_gamecode/code/game/ai_bot_items.c +++ b/ratoa_gamecode/code/game/ai_bot_items.c @@ -315,7 +315,7 @@ static void BotItems_DebugLine(bot_state_t *bs, int kind, const char *event) { BotItems_KindLabel(kind, itemName, sizeof(itemName)); - G_Printf("BotItems: %s %s for %s\n", botName, event, itemName); + G_Printf("BotItems: %s %s the %s\n", botName, event, itemName); } @@ -489,6 +489,18 @@ void BotItems_Reset(bot_state_t *bs) { 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; + } @@ -535,6 +547,18 @@ static void BotItems_ClearCommit(bot_state_t *bs, int endEvent) { 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) { @@ -557,7 +581,7 @@ static void BotItems_ClearCommit(bot_state_t *bs, int endEvent) { case BOT_ITEMS_DBG_TIMEOUT: - BotItems_DebugLine(bs, kind, "abandoned (timeout)"); + (bs, kind, "abandoned (timeout)"); break; @@ -741,6 +765,106 @@ static qboolean BotItems_NeedsKind(bot_state_t *bs, int kind) { +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) { @@ -945,6 +1069,8 @@ static void BotItems_BeginCommit(bot_state_t *bs, bot_goal_t *goal, int kind) { bs->item_commit_until = FloatTime() + BOT_ITEMS_COMMIT_DURATION; + BotItems_SnapshotCommitInventory(bs, kind); + trap_BotRemoveFromAvoidGoals(bs->gs, goal->number); @@ -1185,6 +1311,14 @@ void BotItems_Tick(bot_state_t *bs) { } + 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); @@ -1293,6 +1427,18 @@ int BotItems_HandleReachedGoal(bot_state_t *bs, bot_goal_t *goal) { + 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)) { diff --git a/ratoa_gamecode/code/game/ai_bot_tactics.c b/ratoa_gamecode/code/game/ai_bot_tactics.c index ff7df77..b5b795e 100644 --- a/ratoa_gamecode/code/game/ai_bot_tactics.c +++ b/ratoa_gamecode/code/game/ai_bot_tactics.c @@ -99,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; } diff --git a/ratoa_gamecode/code/game/ai_dmnet.c b/ratoa_gamecode/code/game/ai_dmnet.c index 6b790dc..1a30139 100644 --- a/ratoa_gamecode/code/game/ai_dmnet.c +++ b/ratoa_gamecode/code/game/ai_dmnet.c @@ -1711,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); @@ -1722,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)) { @@ -1846,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); @@ -1857,7 +1859,10 @@ int AINode_Seek_NBG(bot_state_t *bs) { bs->ideal_viewangles[2] *= 0.5; } //if the weapon is used for the bot movement - if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs->weaponnum = moveresult.weapon; + 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)) { @@ -1973,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; @@ -2044,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); @@ -2062,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; } @@ -2385,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); } @@ -2401,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) @@ -2581,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) { @@ -2603,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); // @@ -2725,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) { @@ -2748,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 1a94b47..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" @@ -2676,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 @@ -2985,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 @@ -3144,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; @@ -3201,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); @@ -3486,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) @@ -4679,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 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 551bd79..47f1fde 100644 --- a/ratoa_gamecode/code/game/ai_main.c +++ b/ratoa_gamecode/code/game/ai_main.c @@ -536,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: @@ -959,6 +959,9 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { } 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) { @@ -966,6 +969,11 @@ void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) { bi.actionflags &= ~(ACTION_RESPAWN | ACTION_ATTACK); } } + if (BotAI_WeaponJumpActive(bs) && !bs->aimh_weapon_jump_fired) { + if (!BotAI_WeaponJumpReadyToFire(bs)) { + bi.actionflags &= ~(ACTION_ATTACK | ACTION_JUMP); + } + } BotInputToUserCommand(&bi, &bs->lastucmd, bs->cur_ps.delta_angles, time); for (j = 0; j < 3; j++) { bs->viewangles[j] = AngleMod(bs->viewangles[j] - diff --git a/ratoa_gamecode/code/game/ai_main.h b/ratoa_gamecode/code/game/ai_main.h index 85913e4..fc966aa 100644 --- a/ratoa_gamecode/code/game/ai_main.h +++ b/ratoa_gamecode/code/game/ai_main.h @@ -304,6 +304,12 @@ typedef struct bot_state_s 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 ---- */ @@ -329,6 +335,13 @@ typedef struct bot_state_s 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 ---- */