diff --git a/src/main/scala/net/psforever/objects/BoomerDeployable.scala b/src/main/scala/net/psforever/objects/BoomerDeployable.scala index dd7e600e2..1679bbe36 100644 --- a/src/main/scala/net/psforever/objects/BoomerDeployable.scala +++ b/src/main/scala/net/psforever/objects/BoomerDeployable.scala @@ -5,7 +5,6 @@ import akka.actor.{ActorContext, Props} import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vital.Vitality @@ -39,7 +38,8 @@ class BoomerDeployable(cdef: ExplosiveDeployableDefinition) } } -class BoomerDeployableDefinition(private val objectId: Int) extends ExplosiveDeployableDefinition(objectId) { +class BoomerDeployableDefinition(private val objectId: Int) + extends ExplosiveDeployableDefinition(objectId) { override def Initialize(obj: Deployable, context: ActorContext): Unit = { obj.Actor = context.actorOf(Props(classOf[BoomerDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj)) @@ -119,6 +119,6 @@ class BoomerDeployableControl(mine: BoomerDeployable) * `false`, otherwise */ override def CanDetonate(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { - !mine.Destroyed && (data.cause.isInstanceOf[TriggerUsedReason] || Damageable.CanJammer(obj, data)) + super.CanDetonate(obj, damage, data) || data.cause.isInstanceOf[TriggerUsedReason] } } diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 462df5b39..b32bf2e4d 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -1,7 +1,7 @@ // Copyright (c) 2018 PSForever package net.psforever.objects -import akka.actor.{Actor, ActorContext, ActorRef, Props} +import akka.actor.Actor import net.psforever.objects.ce._ import net.psforever.objects.definition.DeployableDefinition import net.psforever.objects.definition.converter.SmallDeployableConverter @@ -11,8 +11,6 @@ import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} import net.psforever.objects.serverobject.damage.Damageable.Target -import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, SourceEntry} -import net.psforever.objects.vital.etc.TrippedMineReason import net.psforever.objects.vital.resolution.ResolutionCalculations.Output import net.psforever.objects.vital.{SimpleResolutions, Vitality} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} @@ -23,6 +21,7 @@ import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import scala.annotation.unused import scala.concurrent.duration._ class ExplosiveDeployable(cdef: ExplosiveDeployableDefinition) @@ -36,7 +35,7 @@ object ExplosiveDeployable { final case class TriggeredBy(obj: PlanetSideServerObject) } -class ExplosiveDeployableDefinition(private val objectId: Int) +abstract class ExplosiveDeployableDefinition(private val objectId: Int) extends DeployableDefinition(objectId) { Name = "explosive_deployable" DeployCategory = DeployableCategory.Mines @@ -45,6 +44,8 @@ class ExplosiveDeployableDefinition(private val objectId: Int) private var detonateOnJamming: Boolean = true + private var stability: Boolean = false + var triggerRadius: Float = 0f def DetonateOnJamming: Boolean = detonateOnJamming @@ -54,15 +55,11 @@ class ExplosiveDeployableDefinition(private val objectId: Int) DetonateOnJamming } - override def Initialize(obj: Deployable, context: ActorContext): Unit = { - obj.Actor = - context.actorOf(Props(classOf[MineDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj)) - } -} + def Stable: Boolean = stability -object ExplosiveDeployableDefinition { - def apply(dtype: DeployedItem.Value): ExplosiveDeployableDefinition = { - new ExplosiveDeployableDefinition(dtype.id) + def Stable_=(stableState: Boolean): Boolean = { + stability = stableState + Stable } } @@ -98,19 +95,10 @@ abstract class ExplosiveDeployableControl(mine: ExplosiveDeployable) } } - def Interaction(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { - !mine.Destroyed && (if (damage == 0 && data.cause.source.SympatheticExplosion) { - Damageable.CanDamageOrJammer(obj, damage = 1, data) - } else { - Damageable.CanDamageOrJammer(obj, damage, data) - }) - } - final def HandleDamage(target: ExplosiveDeployable, cause: DamageResult, damage: Int): Unit = { target.LogActivity(cause) if (CanDetonate(target, damage, cause.interaction)) { - ExplosiveDeployableControl.explodes(target, cause) - ExplosiveDeployableControl.DestructionAwareness(target, cause) + ExplosiveDeployableControl.doExplosion(target, cause) } else if (target.Health == 0) { ExplosiveDeployableControl.DestructionAwareness(target, cause) } else { @@ -118,6 +106,17 @@ abstract class ExplosiveDeployableControl(mine: ExplosiveDeployable) } } + def Interaction(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { + val actualDamage: Int = if (!mine.Definition.Stable && data.cause.source.SympatheticExplosion) { + math.max(damage, 1) + } else { + damage + } + !mine.Destroyed && + Damageable.adversarialOrHackableChecks(obj, data) && + (CanDetonate(obj, actualDamage, data) || Damageable.CanDamage(obj, actualDamage, data)) + } + /** * A supplement for checking target susceptibility * to account for sympathetic explosives even if there is no damage. @@ -130,21 +129,22 @@ abstract class ExplosiveDeployableControl(mine: ExplosiveDeployable) * @return `true`, if the target can be affected; * `false`, otherwise */ - def CanDetonate(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { - !mine.Destroyed && (if (damage == 0 && data.cause.source.SympatheticExplosion) { - Damageable.CanDamageOrJammer(mine, damage = 1, data) - } else { - Damageable.CanDamageOrJammer(mine, damage, data) - }) - } - - def explode(target: ExplosiveDeployable, cause: DamageResult): Unit = { - ExplosiveDeployableControl.explodes(target, cause) - ExplosiveDeployableControl.DestructionAwareness(target, cause) + def CanDetonate(obj: Vitality with FactionAffinity, @unused damage: Int, data: DamageInteraction): Boolean = { + val sourceDef = data.cause.source + val mineDef = mine.Definition + val explodeFromSympathy: Boolean = sourceDef.SympatheticExplosion && !mineDef.Stable + val explodeFromJammer: Boolean = ExplosiveDeployableControl.CanJammer(mine, data) + !mine.Destroyed && (explodeFromSympathy || explodeFromJammer) } } object ExplosiveDeployableControl { + def CanJammer(mine: ExplosiveDeployable, data: DamageInteraction): Boolean = { + Damageable.adversarialOrHackableChecks(mine, data) && + data.cause.source.AdditionalEffect && + mine.Definition.DetonateOnJamming + } + /** * na * @param target na @@ -152,22 +152,23 @@ object ExplosiveDeployableControl { * @param damage na */ def DamageAwareness(target: ExplosiveDeployable, cause: DamageResult, damage: Int): Unit = { - if (!target.Jammed && Damageable.CanJammer(target, cause.interaction)) { - if ( { - target.Jammed = cause.interaction.cause match { - case o: ProjectileReason => - val radius = o.projectile.profile.DamageRadius - Vector3.DistanceSquared(cause.interaction.hitPos, cause.interaction.target.Position) < radius * radius - case _ => - true + if ( + !target.Jammed && + CanJammer(target, cause.interaction) && + { + target.Jammed = cause.interaction.cause match { + case o: ProjectileReason => + val radius = o.projectile.profile.DamageRadius + Vector3.DistanceSquared(cause.interaction.hitPos, cause.interaction.target.Position) < radius * radius + case _ => + true + } } + ) { + if (target.Definition.DetonateOnJamming) { + explodes(target, cause) } - ) { - if (target.Definition.DetonateOnJamming) { - explodes(target, cause) - } - DestructionAwareness(target, cause) - } + DestructionAwareness(target, cause) } } @@ -190,6 +191,11 @@ object ExplosiveDeployableControl { ) } + def doExplosion(target: ExplosiveDeployable, cause: DamageResult): Unit = { + explodes(target, cause) + DestructionAwareness(target, cause) + } + /** * na * @param target na @@ -272,112 +278,3 @@ object ExplosiveDeployableControl { ) <= maxDistance } } - -class MineDeployableControl(mine: ExplosiveDeployable) - extends ExplosiveDeployableControl(mine) { - - def receive: Receive = - commonMineBehavior - .orElse { - case ExplosiveDeployable.TriggeredBy(obj) => - setTriggered(Some(obj), delay = 200) - - case MineDeployableControl.Triggered() => - explodes(testForTriggeringTarget( - mine, - mine.Definition.innateDamage.map { _.DamageRadius }.getOrElse(mine.Definition.triggerRadius) - )) - - case _ => () - } - - override def finalizeDeployable(callback: ActorRef): Unit = { - super.finalizeDeployable(callback) - //initial triggering upon build - setTriggered(testForTriggeringTarget(mine, mine.Definition.triggerRadius), delay = 1000) - } - - def testForTriggeringTarget(mine: ExplosiveDeployable, range: Float): Option[PlanetSideServerObject] = { - val position = mine.Position - val faction = mine.Faction - val range2 = range * range - val sector = mine.Zone.blockMap.sector(position, range) - (sector.livePlayerList ++ sector.vehicleList) - .find { thing => thing.Faction != faction && Vector3.DistanceSquared(thing.Position, position) < range2 } - } - - def setTriggered(instigator: Option[PlanetSideServerObject], delay: Long): Unit = { - instigator - .collect { - case _ if isConstructed.contains(true) && setup.isCancelled => - //re-use the setup timer here - import scala.concurrent.ExecutionContext.Implicits.global - setup = context.system.scheduler.scheduleOnce(delay milliseconds, self, MineDeployableControl.Triggered()) - } - } - - def explodes(instigator: Option[PlanetSideServerObject]): Unit = { - instigator - .collect { - case _ => - //explosion - mine.Destroyed = true - ExplosiveDeployableControl.DamageAwareness( - mine, - DamageInteraction( - SourceEntry(mine), - MineDeployableControl.trippedMineReason(mine), - mine.Position - ).calculate()(mine), - damage = 0 - ) - } - .orElse { - //reset - setup = Default.Cancellable - None - } - } -} - -object MineDeployableControl { - private case class Triggered() - - def trippedMineReason(mine: ExplosiveDeployable): TrippedMineReason = { - lazy val deployableSource = DeployableSource(mine) - val zone = mine.Zone - val ownerName = mine.OwnerName - val blame = zone - .Players - .find(a => ownerName.contains(a.name)) - .collect { a => - val name = a.name - assignBlameToFrom(name, zone.LivePlayers) - .orElse(assignBlameToFrom(name, zone.Corpses)) - .getOrElse { - val player = PlayerSource(name, mine.Faction, mine.Position) //might report minor inconsistencies, e.g., exo-suit type - player.copy(unique = player.unique.copy(charId = a.id), progress = a.scorecard.CurrentLife) - } - } - .getOrElse(deployableSource) - TrippedMineReason(deployableSource, blame) - } - - /** - * Find a player with a given name from this list of possible players. - * If the player is seated, attach a shallow copy of the mounting information. - * @param name player name - * @param blameList possible players in which to find the player name - * @return discovered player as a reference, or `None` if not found - */ - private def assignBlameToFrom(name: String, blameList: List[Player]): Option[SourceEntry] = { - blameList - .find(_.Name.equals(name)) - .map { player => - PlayerSource - .mountableAndSeat(player) - .map { case (mount, seat) => PlayerSource.inSeat(player, mount, seat) } - .getOrElse { PlayerSource(player) } - } - } -} diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 778f9abeb..d9297e6c7 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -1019,9 +1019,9 @@ object GlobalDefinitions { */ val boomer: BoomerDeployableDefinition = BoomerDeployableDefinition(DeployedItem.boomer) - val he_mine: ExplosiveDeployableDefinition = ExplosiveDeployableDefinition(DeployedItem.he_mine) + val he_mine: MineDeployableDefinition = MineDeployableDefinition(DeployedItem.he_mine) - val jammer_mine: ExplosiveDeployableDefinition = ExplosiveDeployableDefinition(DeployedItem.jammer_mine) + val jammer_mine: MineDeployableDefinition = MineDeployableDefinition(DeployedItem.jammer_mine) val spitfire_turret: TurretDeployableDefinition = TurretDeployableDefinition(DeployedItem.spitfire_turret) diff --git a/src/main/scala/net/psforever/objects/MineDeployableControl.scala b/src/main/scala/net/psforever/objects/MineDeployableControl.scala new file mode 100644 index 000000000..3b393378a --- /dev/null +++ b/src/main/scala/net/psforever/objects/MineDeployableControl.scala @@ -0,0 +1,137 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects + +import akka.actor.{ActorContext, ActorRef, Props} +import net.psforever.objects.ce.{Deployable, DeployedItem} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, SourceEntry} +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.etc.TrippedMineReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.types.Vector3 + +import scala.concurrent.duration._ + +class MineDeployableDefinition(private val objectId: Int) + extends ExplosiveDeployableDefinition(objectId) { + override def Initialize(obj: Deployable, context: ActorContext): Unit = { + obj.Actor = + context.actorOf(Props(classOf[MineDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj)) + } +} + +object MineDeployableDefinition { + def apply(dtype: DeployedItem.Value): MineDeployableDefinition = { + new MineDeployableDefinition(dtype.id) + } +} + +class MineDeployableControl(mine: ExplosiveDeployable) + extends ExplosiveDeployableControl(mine) { + + def receive: Receive = + commonMineBehavior + .orElse { + case ExplosiveDeployable.TriggeredBy(obj) => + setTriggered(Some(obj), delay = 200) + + case MineDeployableControl.Triggered() => + explodes(testForTriggeringTarget( + mine, + mine.Definition.innateDamage.map { _.DamageRadius }.getOrElse(mine.Definition.triggerRadius) + )) + + case _ => () + } + + override def finalizeDeployable(callback: ActorRef): Unit = { + super.finalizeDeployable(callback) + //initial triggering upon build + setTriggered(testForTriggeringTarget(mine, mine.Definition.triggerRadius), delay = 1000) + } + + def testForTriggeringTarget(mine: ExplosiveDeployable, range: Float): Option[PlanetSideServerObject] = { + val position = mine.Position + val faction = mine.Faction + val range2 = range * range + val sector = mine.Zone.blockMap.sector(position, range) + (sector.livePlayerList ++ sector.vehicleList) + .find { thing => thing.Faction != faction && Vector3.DistanceSquared(thing.Position, position) < range2 } + } + + def setTriggered(instigator: Option[PlanetSideServerObject], delay: Long): Unit = { + instigator + .collect { + case _ if isConstructed.contains(true) && setup.isCancelled => + //re-use the setup timer here + import scala.concurrent.ExecutionContext.Implicits.global + setup = context.system.scheduler.scheduleOnce(delay milliseconds, self, MineDeployableControl.Triggered()) + } + } + + override def CanDetonate(obj: Vitality with FactionAffinity, damage: Int, data: DamageInteraction): Boolean = { + super.CanDetonate(obj, damage, data) || data.cause.isInstanceOf[TrippedMineReason] + } + + def explodes(instigator: Option[PlanetSideServerObject]): Unit = { + //reset + setup = Default.Cancellable + instigator + .collect { + case _ => + //explosion + HandleDamage( + mine, + DamageInteraction( + SourceEntry(mine), + MineDeployableControl.trippedMineReason(mine), + mine.Position + ).calculate()(mine), + damage = 0 + ) + } + } +} + +object MineDeployableControl { + private case class Triggered() + + def trippedMineReason(mine: ExplosiveDeployable): TrippedMineReason = { + lazy val deployableSource = DeployableSource(mine) + val zone = mine.Zone + val ownerName = mine.OwnerName + val blame = zone + .Players + .find(a => ownerName.contains(a.name)) + .collect { a => + val name = a.name + assignBlameToFrom(name, zone.LivePlayers) + .orElse(assignBlameToFrom(name, zone.Corpses)) + .getOrElse { + val player = PlayerSource(name, mine.Faction, mine.Position) //might report minor inconsistencies, e.g., exo-suit type + player.copy(unique = player.unique.copy(charId = a.id), progress = a.scorecard.CurrentLife) + } + } + .getOrElse(deployableSource) + TrippedMineReason(deployableSource, blame) + } + + /** + * Find a player with a given name from this list of possible players. + * If the player is seated, attach a shallow copy of the mounting information. + * @param name player name + * @param blameList possible players in which to find the player name + * @return discovered player as a reference, or `None` if not found + */ + private def assignBlameToFrom(name: String, blameList: List[Player]): Option[SourceEntry] = { + blameList + .find(_.Name.equals(name)) + .map { player => + PlayerSource + .mountableAndSeat(player) + .map { case (mount, seat) => PlayerSource.inSeat(player, mount, seat) } + .getOrElse { PlayerSource(player) } + } + } +} diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsDeployable.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsDeployable.scala index 57f234f91..d8df36c58 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsDeployable.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsDeployable.scala @@ -42,6 +42,7 @@ object GlobalDefinitionsDeployable { boomer.DeployTime = Duration.create(1000, "ms") boomer.deployAnimation = DeployAnimation.Standard boomer.interference = InterferenceRange(main = 0.2f) + boomer.Stable = true boomer.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.Splash SympatheticExplosion = true @@ -58,7 +59,7 @@ object GlobalDefinitionsDeployable { he_mine.Name = "he_mine" he_mine.Descriptor = "Mines" - he_mine.MaxHealth = 50 + he_mine.MaxHealth = 25 he_mine.Damageable = true he_mine.DamageableByFriendlyFire = false he_mine.Repairable = false @@ -91,12 +92,12 @@ object GlobalDefinitionsDeployable { jammer_mine.interference = InterferenceRange(main = 7f, sharedGroupId = 1, shared = 7f, deployables = 0.1f) jammer_mine.DetonateOnJamming = false jammer_mine.triggerRadius = 3f + jammer_mine.Stable = true jammer_mine.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.Splash Damage0 = 0 DamageRadius = 10f DamageAtEdge = 1.0f - AdditionalEffect = true JammedEffectDuration += TargetValidation( EffectTarget.Category.Player, EffectTarget.Validation.Player diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala index 28c24a5e9..7f1863ffb 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala @@ -94,12 +94,12 @@ object Damageable { * `false`, otherwise */ def CanJammer(obj: Vitality with FactionAffinity, data: DamageInteraction): Boolean = { - data.cause.source.HasJammedEffectDuration && + (data.cause.source.HasJammedEffectDuration || data.cause.source.AdditionalEffect) && obj.isInstanceOf[JammableUnit] && adversarialOrHackableChecks(obj, data) } - private def adversarialOrHackableChecks(obj: Vitality with FactionAffinity, data: DamageInteraction): Boolean = { + def adversarialOrHackableChecks(obj: Vitality with FactionAffinity, data: DamageInteraction): Boolean = { (data.adversarial match { case Some(adversarial) => adversarial.attacker.Faction != adversarial.defender.Faction case None => true diff --git a/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala b/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala index 97c3c77f9..c7e8c18c3 100644 --- a/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala +++ b/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala @@ -34,7 +34,8 @@ trait DamageProperties * also used to produce staged projectiles */ private var damageProxy: List[Int] = Nil /** na; - * currently used with jammer properties only */ + * currently used with jammer properties only; + * used sepcifically to indicate jammering effect targets explosive deployables */ private var additionalEffect: Boolean = false /** confers aggravated damage burn to its target */ private var aggravatedDamage: Option[AggravatedDamage] = None