fix: Correct weapon draw timing and add LOS check

Weapon draw fix:
- Spawn bots with hands down (don't set DrawnSlot on spawn)
- After 2 second delay, SET DrawnSlot=2 then broadcast ObjectHeld
- This fixes the visual bug where weapon appeared out, then went away

Line-of-sight check:
- Added Sidedness.equals() check to findTarget and target validation
- Uses same logic as turrets - bot and target must be on same side
  of walls (both inside or both outside a building)
- Prevents bots from shooting through walls
This commit is contained in:
Claude 2025-11-23 06:55:29 +00:00
parent ba0e43cfed
commit 10f682b72e
No known key found for this signature in database

View file

@ -20,6 +20,7 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.DefinitionUtil import net.psforever.util.DefinitionUtil
import net.psforever.zones.Zones import net.psforever.zones.Zones
import net.psforever.objects.serverobject.interior.Sidedness
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@ -152,7 +153,7 @@ class BotManager(zone: Zone) extends Actor {
player.Position = position player.Position = position
player.Orientation = Vector3(0, 0, 0) player.Orientation = Vector3(0, 0, 0)
DefinitionUtil.applyDefaultLoadout(player) DefinitionUtil.applyDefaultLoadout(player)
player.DrawnSlot = 2 // Draw suppressor (slot 2) so bot can shoot // DON'T set DrawnSlot here - spawn with hands down, draw weapon after delay
player.Spawn() player.Spawn()
val typedSystem = context.system.toTyped val typedSystem = context.system.toTyped
@ -304,7 +305,7 @@ class BotManager(zone: Zone) extends Actor {
player.Position = info.spawnPosition player.Position = info.spawnPosition
player.Orientation = Vector3(0, 0, 0) player.Orientation = Vector3(0, 0, 0)
DefinitionUtil.applyDefaultLoadout(player) DefinitionUtil.applyDefaultLoadout(player)
player.DrawnSlot = 2 // Draw suppressor (slot 2) so bot can shoot // DON'T set DrawnSlot here - spawn with hands down, draw weapon after delay
player.Spawn() player.Spawn()
val typedSystem = context.system.toTyped val typedSystem = context.system.toTyped
@ -335,10 +336,13 @@ class BotManager(zone: Zone) extends Actor {
// Check if bot is ready (spawn delay passed) and weapon not yet drawn // Check if bot is ready (spawn delay passed) and weapon not yet drawn
val isReady = tickCount >= botState.readyTick val isReady = tickCount >= botState.readyTick
if (isReady && !botState.weaponDrawn) { if (isReady && !botState.weaponDrawn) {
// Set DrawnSlot to weapon slot 2 (suppressor), then broadcast
val previousSlot = player.DrawnSlot
player.DrawnSlot = 2
// Now broadcast the weapon draw - client has had time to load // Now broadcast the weapon draw - client has had time to load
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
zone.id, zone.id,
AvatarAction.ObjectHeld(player.GUID, player.DrawnSlot, player.LastDrawnSlot) AvatarAction.ObjectHeld(player.GUID, player.DrawnSlot, previousSlot)
) )
log.info(s"${botState.name} has drawn a suppressor from its holster") log.info(s"${botState.name} has drawn a suppressor from its holster")
currentBotState = currentBotState.copy(weaponDrawn = true) currentBotState = currentBotState.copy(weaponDrawn = true)
@ -433,13 +437,15 @@ class BotManager(zone: Zone) extends Actor {
private def findTarget(botState: BotState): Option[Player] = { private def findTarget(botState: BotState): Option[Player] = {
val player = botState.player val player = botState.player
val botFaction = botState.avatar.faction val botFaction = botState.avatar.faction
val botSide = player.WhichSide
zone.LivePlayers zone.LivePlayers
.filter { p => .filter { p =>
p.isAlive && p.isAlive &&
p.Faction != botFaction && p.Faction != botFaction &&
!p.Name.startsWith("xxBOTxx") && // Don't target other bots for now !p.Name.startsWith("xxBOTxx") && // Don't target other bots for now
Vector3.Distance(player.Position, p.Position) <= MaxEngagementRange Vector3.Distance(player.Position, p.Position) <= MaxEngagementRange &&
Sidedness.equals(botSide, p.WhichSide) // Must be on same side of walls (LOS check)
} }
.sortBy(p => Vector3.Distance(player.Position, p.Position)) .sortBy(p => Vector3.Distance(player.Position, p.Position))
.headOption .headOption
@ -486,7 +492,9 @@ class BotManager(zone: Zone) extends Actor {
// Find or validate target // Find or validate target
val currentTarget = combat.target.flatMap(guid => zone.LivePlayers.find(_.GUID == guid)) val currentTarget = combat.target.flatMap(guid => zone.LivePlayers.find(_.GUID == guid))
val targetStillValid = currentTarget.exists { t => val targetStillValid = currentTarget.exists { t =>
t.isAlive && Vector3.Distance(player.Position, t.Position) <= MaxEngagementRange t.isAlive &&
Vector3.Distance(player.Position, t.Position) <= MaxEngagementRange &&
Sidedness.equals(player.WhichSide, t.WhichSide) // LOS check - same side of walls
} }
if (!targetStillValid) { if (!targetStillValid) {