fix: Bot combat - draw weapon and validate targets

Root cause: DrawnSlot was never set, so getWeapon() always returned None
and bots couldn't shoot.

Changes:
- Set DrawnSlot = 2 (suppressor) on spawn and respawn
- Broadcast ObjectHeld so clients see weapon drawn
- Add logging: "has drawn", "acquired target", "is attacking"
- Validate target.Actor before sending Vitality.Damage
- Fix startFiring() to accept target name for logging

The two-force accuracy system (adjustment vs recoil from GAME_FEEL.md)
was already implemented but never triggered due to this bug.
This commit is contained in:
Claude 2025-11-23 06:25:52 +00:00
parent 88d547a88a
commit 905f156e50
No known key found for this signature in database

View file

@ -8,7 +8,7 @@ import net.psforever.actors.session.AvatarActor
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.Avatar
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.{Player, Tool} import net.psforever.objects.{Default, Player, Tool}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution import net.psforever.objects.vital.base.DamageResolution
@ -136,6 +136,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
player.Spawn() player.Spawn()
val typedSystem = context.system.toTyped val typedSystem = context.system.toTyped
@ -179,6 +180,13 @@ class BotManager(zone: Zone) extends Actor {
) )
) )
// Broadcast that bot has drawn a weapon (slot 2 = suppressor)
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.ObjectHeld(player.GUID, player.DrawnSlot, player.LastDrawnSlot)
)
log.info(s"$name has drawn a suppressor from its holster")
// Initialize movement state // Initialize movement state
val moveAngle = random.nextFloat() * 360f val moveAngle = random.nextFloat() * 360f
val moveState = MovementState( val moveState = MovementState(
@ -284,6 +292,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
player.Spawn() player.Spawn()
val typedSystem = context.system.toTyped val typedSystem = context.system.toTyped
@ -456,6 +465,8 @@ class BotManager(zone: Zone) extends Actor {
// Look for new target // Look for new target
findTarget(botState) match { findTarget(botState) match {
case Some(newTarget) => case Some(newTarget) =>
val distance = Vector3.Distance(player.Position, newTarget.Position)
log.info(s"${botState.name} acquired target ${newTarget.Name} at distance ${distance.toInt}m")
combat = combat.copy( combat = combat.copy(
target = Some(newTarget.GUID), target = Some(newTarget.GUID),
targetAcquiredTick = tickCount, targetAcquiredTick = tickCount,
@ -478,7 +489,7 @@ class BotManager(zone: Zone) extends Actor {
if (ticksSinceAcquired >= recognitionTime) { if (ticksSinceAcquired >= recognitionTime) {
// Start firing if not already // Start firing if not already
if (!combat.isFiring) { if (!combat.isFiring) {
startFiring(botState) startFiring(botState, targetPlayer.Name)
combat = combat.copy(isFiring = true, burstStartTick = tickCount, shotsFired = 0) combat = combat.copy(isFiring = true, burstStartTick = tickCount, shotsFired = 0)
} }
@ -519,8 +530,9 @@ class BotManager(zone: Zone) extends Actor {
} }
/** Broadcast that bot started firing */ /** Broadcast that bot started firing */
private def startFiring(botState: BotState): Unit = { private def startFiring(botState: BotState, targetName: String): Unit = {
getWeapon(botState.player).foreach { weapon => getWeapon(botState.player).foreach { weapon =>
log.info(s"${botState.name} is attacking $targetName")
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
zone.id, zone.id,
AvatarAction.ChangeFireState_Start(botState.player.GUID, weapon.GUID) AvatarAction.ChangeFireState_Start(botState.player.GUID, weapon.GUID)
@ -569,14 +581,19 @@ class BotManager(zone: Zone) extends Actor {
DamageResolution.Hit DamageResolution.Hit
) )
// Send damage to target's actor (calculate() returns the function needed by Vitality.Damage) // Validate target can receive damage (has a proper Actor)
target.Actor ! Vitality.Damage(damageInteraction.calculate()) if (target.CanDamage && target.Actor != Default.Actor) {
// Send damage to target's actor (calculate() returns the function needed by Vitality.Damage)
target.Actor ! Vitality.Damage(damageInteraction.calculate())
// Send hit hint to target (so they know they're being shot) // Send hit hint to target (so they know they're being shot)
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
zone.id, zone.id,
AvatarAction.HitHint(botState.player.GUID, target.GUID) AvatarAction.HitHint(botState.player.GUID, target.GUID)
) )
} else {
log.warn(s"${botState.name} cannot damage ${target.Name} - target has no valid Actor")
}
} }
} }