Merge pull request #1284 from ScrawnyRonnie/density-alert

Red Alert!
This commit is contained in:
ScrawnyRonnie 2025-07-22 12:58:05 -04:00 committed by GitHub
commit c2f87fcad4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 128 additions and 5 deletions

View file

@ -508,6 +508,18 @@ game {
capture-experience-points-modifier = 1f capture-experience-points-modifier = 1f
# Don't forget to pay back that debt. # Don't forget to pay back that debt.
} }
alert {
# When a certain number of enemy players are within the SOI of a facility, an alert (DensityLevelUpdateMessage)
# will be dispatched to all players. Players of the owning faction will receive a chat warning (if in
# the same zone) and the map will flash the alert level over the facility until it changes
# Wiki says 25-30
yellow = 20
# Wiki says 30-60
orange = 30
# Wiki says 60+
red = 60
}
} }
anti-cheat { anti-cheat {

View file

@ -18,7 +18,7 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound} import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound, WeatherMessage, CloudInfo, StormInfo}
import net.psforever.services.chat.DefaultChannel import net.psforever.services.chat.DefaultChannel
import scala.collection.mutable import scala.collection.mutable
@ -972,7 +972,7 @@ class ZoningOperations(
*/ */
def initFacility(continentNumber: Int, buildingNumber: Int, building: Building): Unit = { def initFacility(continentNumber: Int, buildingNumber: Int, building: Building): Unit = {
sendResponse(building.infoUpdateMessage()) sendResponse(building.infoUpdateMessage())
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0))) sendResponse(building.densityLevelUpdateMessage(building))
} }
/** /**
@ -2504,6 +2504,19 @@ class ZoningOperations(
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
} }
} }
//make weather happen
sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215),
StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215),
StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215),
StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215),
StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215),
StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215),
StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215),
StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215),
StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215),
StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215))))
//begin looking for conditions to set the avatar //begin looking for conditions to set the avatar
context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200))
} }
@ -2614,6 +2627,19 @@ class ZoningOperations(
log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data")
} }
setupAvatarFunc = AvatarCreate setupAvatarFunc = AvatarCreate
//make weather happen
sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215),
StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215),
StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215),
StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215),
StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215),
StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215),
StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215),
StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215),
StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215),
StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215))))
//begin looking for conditions to set the avatar //begin looking for conditions to set the avatar
context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200))
} }

View file

@ -74,6 +74,8 @@ object BuildingActor {
final case class PowerOff() extends Command final case class PowerOff() extends Command
final case class DensityLevelUpdate(building: Building) extends Command
/** /**
* Set a facility affiliated to one faction to be affiliated to a different faction. * Set a facility affiliated to one faction to be affiliated to a different faction.
* @param details building and event system references * @param details building and event system references
@ -226,6 +228,7 @@ class BuildingActor(
case MapUpdate() => case MapUpdate() =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building)))
Behaviors.same Behaviors.same
case AmenityStateChange(amenity, data) => case AmenityStateChange(amenity, data) =>
@ -245,6 +248,10 @@ class BuildingActor(
case Ntu(msg) => case Ntu(msg) =>
logic.ntu(details, msg) logic.ntu(details, msg)
case DensityLevelUpdate(building) =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building)))
Behaviors.same
} }
} }
} }

View file

@ -11,13 +11,14 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.packet.game.{BuildingInfoUpdateMessage, DensityLevelUpdateMessage}
import net.psforever.types._ import net.psforever.types._
import scalax.collection.{Graph, GraphEdge} import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation} import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.util.Config
class Building( class Building(
private val name: String, private val name: String,
@ -236,6 +237,46 @@ class Building(
) )
} }
def densityLevelUpdateMessage(building: Building): DensityLevelUpdateMessage = {
if (building.PlayersInSOI.nonEmpty) {
val factionCounts: Map[PlanetSideEmpire.Value, Int] =
building.PlayersInSOI.groupBy(_.Faction).view.mapValues(_.size).toMap
val otherEmpireCounts: Map[PlanetSideEmpire.Value, Int] = PlanetSideEmpire.values.map {
faction =>
val otherCount = factionCounts.filterNot(_._1 == faction).values.sum
faction -> otherCount
}.toMap
val trAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.TR, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
val ncAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NC, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
val vsAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.VS, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
val boAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NEUTRAL, 0) match {
case count if count >= Config.app.game.alert.red => 3
case count if count >= Config.app.game.alert.orange => 2
case count if count >= Config.app.game.alert.yellow => 1
case _ => 0
}
DensityLevelUpdateMessage(Zone.Number, MapId, List(0, trAlert, 0, ncAlert, 0, vsAlert, 0, boAlert))
}
else { //nobody is in this SOI
DensityLevelUpdateMessage(Zone.Number, MapId, List(0, 0, 0, 0, 0, 0, 0, 0))
}
}
def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = { def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = {
val baseDownState = (NtuSource match { val baseDownState = (NtuSource match {
case Some(ntu) => ntu.NtuCapacitor < 1f case Some(ntu) => ntu.NtuCapacitor < 1f

View file

@ -9,6 +9,7 @@ import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3}
import net.psforever.util.Config import net.psforever.util.Config
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.avatar.scoring.Kill import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.hackable.Hackable
@ -26,6 +27,9 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]() private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]()
var lastEnemyCount: List[Player] = List.empty
var alertTimeMillis: Long = 0L
def TryUpdate(): Unit = { def TryUpdate(): Unit = {
val list = building.PlayersInSOI val list = building.PlayersInSOI
if (list.nonEmpty) { if (list.nonEmpty) {
@ -37,6 +41,25 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci
updateHotSpotInfoOverTime() updateHotSpotInfoOverTime()
updateTime(now) updateTime(now)
} }
val enemies = list.filter(p => p.Faction != building.Faction) ++
building.Zone.blockMap.sector(building).corpseList
.filter(p => Vector3.DistanceSquared(building.Position.xy, p.Position.xy) < building.Definition.SOIRadius * building.Definition.SOIRadius)
//alert defenders (actually goes to all clients) of population change for base alerts
//straight away if higher alert, delay if pop decreases enough to lower alert
if ((enemies.length >= Config.app.game.alert.yellow && lastEnemyCount.length < Config.app.game.alert.yellow) ||
(enemies.length >= Config.app.game.alert.orange && lastEnemyCount.length < Config.app.game.alert.orange) ||
(enemies.length >= Config.app.game.alert.red && lastEnemyCount.length < Config.app.game.alert.red) ||
(enemies.length < Config.app.game.alert.yellow && lastEnemyCount.length >= Config.app.game.alert.yellow &&
now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) ||
(enemies.length < Config.app.game.alert.orange && lastEnemyCount.length >= Config.app.game.alert.orange &&
now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) ||
(enemies.length < Config.app.game.alert.red && lastEnemyCount.length >= Config.app.game.alert.red &&
now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3))
{
building.Actor ! BuildingActor.DensityLevelUpdate(building)
alertTimeMillis = now
lastEnemyCount = enemies
}
building.CaptureTerminal building.CaptureTerminal
.map(_.HackedBy) .map(_.HackedBy)
.collect { .collect {

View file

@ -55,9 +55,16 @@ class SphereOfInfluenceActor(zone: Zone) extends Actor {
sois.foreach { sois.foreach {
case (facility, radius) => case (facility, radius) =>
val facilityXY = facility.Position.xy val facilityXY = facility.Position.xy
facility.PlayersInSOI = zone.blockMap.sector(facility) val playersOnFoot = zone.blockMap.sector(facility)
.livePlayerList .livePlayerList
.filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius) .filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius)
val vehicleOccupants = zone.blockMap.sector(facility)
.vehicleList
.filter(v => Vector3.DistanceSquared(facilityXY, v.Position.xy) < radius)
.flatMap(_.Seats.values.flatMap(_.occupants))
facility.PlayersInSOI = playersOnFoot ++ vehicleOccupants
} }
populateTick.cancel() populateTick.cancel()
populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate()) populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate())

View file

@ -164,7 +164,8 @@ case class GameConfig(
experience: Experience, experience: Experience,
maxBattleRank: Int, maxBattleRank: Int,
promotion: PromotionSystem, promotion: PromotionSystem,
facilityHackTime: FiniteDuration facilityHackTime: FiniteDuration,
alert: DensityAlert
) )
case class InstantActionConfig( case class InstantActionConfig(
@ -326,3 +327,9 @@ case class PromotionSystem(
supportExperiencePointsModifier: Float, supportExperiencePointsModifier: Float,
captureExperiencePointsModifier: Float captureExperiencePointsModifier: Float
) )
case class DensityAlert(
yellow: Int,
orange: Int,
red: Int
)