Skip to content

Commit cae344e

Browse files
committed
Added NPC spawning to dynamic movement objects
1 parent 826b24a commit cae344e

5 files changed

Lines changed: 197 additions & 16 deletions

File tree

src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementObject.kt

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,96 @@
2626
package com.projectswg.holocore.resources.support.npc.ai.dynamic
2727

2828
import com.projectswg.common.data.location.Location
29+
import com.projectswg.holocore.intents.support.objects.DestroyObjectIntent
30+
import com.projectswg.holocore.intents.support.objects.ObjectCreatedIntent
2931
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData
32+
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader
33+
import com.projectswg.holocore.resources.support.npc.spawn.NPCCreator
34+
import com.projectswg.holocore.resources.support.npc.spawn.SimpleSpawnInfo
35+
import com.projectswg.holocore.resources.support.npc.spawn.Spawner
36+
import com.projectswg.holocore.resources.support.npc.spawn.SpawnerType
37+
import com.projectswg.holocore.resources.support.objects.ObjectCreator
38+
import com.projectswg.holocore.resources.support.objects.permissions.AdminPermissions
39+
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty
40+
import com.projectswg.holocore.resources.support.objects.swg.custom.AIBehavior
41+
import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
3042
import java.util.concurrent.ThreadLocalRandom
3143
import kotlin.math.cos
3244
import kotlin.math.sin
3345

34-
class DynamicMovementObject(var location: Location) {
46+
class DynamicMovementObject(var location: Location, val name: String, val baseSpeed: Double = 0.0) {
3547

3648
var heading = ThreadLocalRandom.current().nextDouble() * 2 * Math.PI
49+
private val groupMarker = ObjectCreator.createObjectFromTemplate("object/path_waypoint/shared_path_waypoint_droid.iff")
50+
private val npcs = ArrayList<AIObject>()
51+
private var lastUpdate = System.nanoTime()
3752

38-
fun move() {
53+
init {
54+
groupMarker.location = location
55+
groupMarker.objectName = name
56+
groupMarker.containerPermissions = AdminPermissions.getPermissions()
57+
}
58+
59+
fun launch() {
60+
ObjectCreatedIntent(groupMarker).broadcast()
61+
val simpleSpawnInfo = SimpleSpawnInfo.builder()
62+
.withNpcId("humanoid_tusken_soldier")
63+
.withDifficulty(CreatureDifficulty.NORMAL)
64+
.withSpawnerType(SpawnerType.WAYPOINT_AUTO_SPAWN)
65+
.withMinLevel(10)
66+
.withMaxLevel(30)
67+
.withSpawnerFlag(NpcStaticSpawnLoader.SpawnerFlag.AGGRESSIVE)
68+
.withBehavior(AIBehavior.IDLE)
69+
.withLocation(location)
70+
val bossSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.BOSS).build(), groupMarker)
71+
val eliteSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.ELITE).build(), groupMarker)
72+
val normalSpawner = Spawner(simpleSpawnInfo.withDifficulty(CreatureDifficulty.NORMAL).build(), groupMarker)
73+
val random = ThreadLocalRandom.current()
74+
if (random.nextDouble() < 0.25)
75+
npcs.add(NPCCreator.createSingleNpc(bossSpawner))
76+
npcs.add(NPCCreator.createSingleNpc(eliteSpawner))
77+
repeat(15) {
78+
npcs.add(NPCCreator.createSingleNpc(normalSpawner))
79+
}
80+
}
81+
82+
fun destroy() {
83+
DestroyObjectIntent(groupMarker).broadcast()
84+
}
85+
86+
fun act() {
3987
assert(!isOutOfBounds(location))
4088
assert(heading in 0.0..Math.TAU)
41-
moveNextPosition()
89+
val currentTime = System.nanoTime()
90+
val npcSpeed = if (baseSpeed != 0.0) baseSpeed else npcs[0].walkSpeed.toDouble()
91+
val elapsedTime = (currentTime - lastUpdate) / 1E9
92+
val distance = elapsedTime * npcSpeed // TODO: scale based on terrain, somewhat
93+
lastUpdate = currentTime
94+
moveNextPosition(distance)
95+
groupMarker.moveToLocation(location)
96+
npcs.forEachIndexed { i, it ->
97+
// Spiral shape for now
98+
val radius = 1 + i * 0.5
99+
val angle = i * (Math.PI * 0.61)
100+
val newLocationBuilder = Location.builder(location)
101+
.setX(location.x + radius * cos(angle))
102+
.setZ(location.z + radius * sin(angle))
103+
newLocationBuilder.setY(ServerData.terrains.getHeight(newLocationBuilder))
104+
val newLocation = newLocationBuilder.build()
105+
val speed = it.worldLocation.distanceTo(newLocation) / elapsedTime
106+
it.moveTo(null, newLocationBuilder.build(), speed)
107+
}
42108
}
43109

44-
private fun moveNextPosition() {
45-
val firstProposed = calculateNextPosition(heading)
110+
private fun moveNextPosition(distance: Double) {
111+
val firstProposed = calculateNextPosition(heading, distance)
46112
if (isValidNextPosition(firstProposed)) {
47113
location = firstProposed
48114
return
49115
}
50116

51117
val newHeading = heading + Math.PI * (1 + ThreadLocalRandom.current().nextDouble() - 0.5)
52-
val secondProposed = calculateNextPosition(newHeading)
118+
val secondProposed = calculateNextPosition(newHeading, distance)
53119
if (isValidNextPosition(secondProposed)) {
54120
location = secondProposed
55121
heading = if (newHeading >= Math.TAU) newHeading - Math.TAU else newHeading
@@ -60,7 +126,7 @@ class DynamicMovementObject(var location: Location) {
60126
val randomRotationFromNorth = ThreadLocalRandom.current().nextDouble() * Math.TAU
61127
for (clockwiseRotation in 0..35) {
62128
val bruteForceHeading = (clockwiseRotation * 10) * Math.PI / 180.0 + randomRotationFromNorth
63-
val proposed = calculateNextPosition(bruteForceHeading)
129+
val proposed = calculateNextPosition(bruteForceHeading, distance)
64130
if (isValidNextPosition(proposed)) {
65131
location = proposed
66132
heading = if (bruteForceHeading >= Math.TAU) bruteForceHeading - Math.TAU else bruteForceHeading
@@ -80,10 +146,10 @@ class DynamicMovementObject(var location: Location) {
80146
return location.x < -7000 || location.x > 7000 || location.z < -7000 || location.z > 7000
81147
}
82148

83-
private fun calculateNextPosition(heading: Double): Location {
149+
private fun calculateNextPosition(heading: Double, distance: Double): Location {
84150
val nextLocationBuilder = Location.builder(location)
85-
.setX(location.x + 50 * cos(heading))
86-
.setZ(location.z + 50 * sin(heading))
151+
.setX(location.x + distance * cos(heading))
152+
.setZ(location.z + distance * sin(heading))
87153
.setHeading(heading)
88154
nextLocationBuilder.setY(ServerData.terrains.getHeight(nextLocationBuilder))
89155
return nextLocationBuilder.build()

src/main/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/DynamicMovementProcessor.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,41 @@ package com.projectswg.holocore.resources.support.npc.ai.dynamic
2727

2828
import com.projectswg.common.data.location.Location
2929
import com.projectswg.common.data.location.Point2f
30+
import com.projectswg.common.data.location.Terrain
3031
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData
32+
import java.util.concurrent.ThreadLocalRandom
3133
import kotlin.math.sqrt
3234

3335
object DynamicMovementProcessor {
3436

37+
fun createSpawnLocation(terrain: Terrain): Location? {
38+
val randomizer = ThreadLocalRandom.current()
39+
repeat(100) {
40+
val locationBuilder = Location.builder()
41+
.setTerrain(terrain)
42+
.setX(randomizer.nextDouble() * 7000 * 2 - 7000)
43+
.setZ(randomizer.nextDouble() * 7000 * 2 - 7000)
44+
locationBuilder.setY(ServerData.terrains.getHeight(locationBuilder))
45+
val location = locationBuilder.build()
46+
if (isGoodSpawnLocation(location))
47+
return location
48+
}
49+
return null
50+
}
51+
52+
fun isGoodSpawnLocation(point: Location): Boolean {
53+
val zones = ServerData.noSpawnZones.getNoSpawnZoneInfos(point.terrain)
54+
val point2f = Point2f(point.x.toFloat(), point.z.toFloat())
55+
for (zone in zones) {
56+
val zoneCenter = Point2f(zone.x.toFloat(), zone.z.toFloat())
57+
val distance = sqrt(point2f.squaredDistanceTo(zoneCenter))
58+
if (distance < zone.radius.toFloat()) {
59+
return false
60+
}
61+
}
62+
return true
63+
}
64+
3565
fun isIntersectingProtectedZone(start: Location, end: Location): Boolean {
3666
assert(start.terrain == end.terrain)
3767
val zones = ServerData.noSpawnZones.getNoSpawnZoneInfos(start.terrain)

src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
package com.projectswg.holocore.resources.support.objects.swg.custom
2727

2828
import com.projectswg.common.data.encodables.oob.StringId
29+
import com.projectswg.common.data.location.Location
2930
import com.projectswg.common.network.packets.swg.zone.baselines.Baseline.BaselineType
3031
import com.projectswg.common.network.packets.swg.zone.object_controller.ShowFlyText
3132
import com.projectswg.holocore.resources.support.color.SWGColor.Reds.red
33+
import com.projectswg.holocore.resources.support.npc.ai.NavigationPoint
3234
import com.projectswg.holocore.resources.support.npc.ai.NpcCombatMode
3335
import com.projectswg.holocore.resources.support.npc.ai.NpcIdleMode
3436
import com.projectswg.holocore.resources.support.npc.spawn.Spawner
@@ -39,12 +41,11 @@ import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureSt
3941
import com.projectswg.holocore.resources.support.objects.swg.tangible.OptionFlag
4042
import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject
4143
import com.projectswg.holocore.utilities.cancelAndWait
42-
import kotlinx.coroutines.CoroutineScope
43-
import kotlinx.coroutines.Job
44-
import kotlinx.coroutines.SupervisorJob
44+
import kotlinx.coroutines.*
4545
import java.time.Instant
4646
import java.util.*
4747
import java.util.concurrent.CopyOnWriteArraySet
48+
import java.util.concurrent.atomic.AtomicReference
4849

4950
class AIObject(objectId: Long) : CreatureObject(objectId) {
5051
private val playersNearby: MutableSet<CreatureObject> = CopyOnWriteArraySet()
@@ -55,6 +56,7 @@ class AIObject(objectId: Long) : CreatureObject(objectId) {
5556
private var coroutineScope: CoroutineScope? = null
5657
private var previousScheduled: Job? = null
5758
private var questionMarkBlockedUntil: Instant = Instant.now()
59+
private var movementJob = AtomicReference<Job?>(null)
5860

5961
val defaultWeapons: List<WeaponObject>
6062
get() {
@@ -205,6 +207,17 @@ class AIObject(objectId: Long) : CreatureObject(objectId) {
205207
}
206208
}
207209

210+
fun moveTo(newParent: SWGObject?, location: Location, speed: Double) {
211+
val newMovementTask = coroutineScope?.launch {
212+
val route = NavigationPoint.from(this@AIObject.parent, this@AIObject.location, newParent, location, speed)
213+
for (point in route) {
214+
point.move(this@AIObject)
215+
delay(1000L)
216+
}
217+
} ?: return
218+
movementJob.getAndSet(newMovementTask)?.cancel()
219+
}
220+
208221
fun start(coroutineScope: CoroutineScope) {
209222
this.coroutineScope = CoroutineScope(coroutineScope.coroutineContext + SupervisorJob())
210223
startMode(activeMode ?: defaultMode)

src/main/java/com/projectswg/holocore/services/support/npc/ai/AIDynamicMovementService.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,82 @@
2525
***********************************************************************************/
2626
package com.projectswg.holocore.services.support.npc.ai
2727

28+
import com.projectswg.common.data.location.Location
29+
import com.projectswg.common.data.location.Terrain
30+
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData
31+
import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase.config
32+
import com.projectswg.holocore.resources.support.npc.ai.dynamic.DynamicMovementObject
33+
import com.projectswg.holocore.resources.support.npc.ai.dynamic.DynamicMovementProcessor
34+
import com.projectswg.holocore.utilities.HolocoreCoroutine
35+
import com.projectswg.holocore.utilities.cancelAndWait
36+
import kotlinx.coroutines.CoroutineScope
37+
import kotlinx.coroutines.delay
38+
import kotlinx.coroutines.isActive
39+
import kotlinx.coroutines.launch
2840
import me.joshlarson.jlcommon.control.Service
41+
import me.joshlarson.jlcommon.log.Log
42+
import java.util.*
43+
import java.util.concurrent.CopyOnWriteArrayList
2944

3045
class AIDynamicMovementService : Service() {
46+
47+
private val coroutineScope = HolocoreCoroutine.childScope()
48+
private val dynamicObjects = EnumMap<Terrain, CopyOnWriteArrayList<DynamicMovementObject>>(Terrain::class.java)
3149

50+
init {
51+
// Guarantee all terrains are set so that we don't have to worry about concurrent access later
52+
for (terrain in Terrain.entries) {
53+
dynamicObjects[terrain] = CopyOnWriteArrayList()
54+
}
55+
}
3256

57+
override fun start(): Boolean {
58+
val spawnsPerPlanet = config.getInt(this, "spawnsPerPlanet", 10)
59+
for (terrain in listOf(Terrain.TATOOINE)) {
60+
launchDynamicMovementObjectHandler(terrain, "DynamicObject-${terrain.name}-dev")
61+
repeat(spawnsPerPlanet) {
62+
launchDynamicMovementObjectHandler(terrain, "DynamicObject-${terrain.name}-$it")
63+
}
64+
}
65+
return super.start()
66+
}
67+
68+
override fun stop(): Boolean {
69+
coroutineScope.cancelAndWait()
70+
return super.stop()
71+
}
3372

73+
private fun launchDynamicMovementObjectHandler(terrain: Terrain, objectName: String) {
74+
coroutineScope.launch {
75+
try {
76+
while (isActive) {
77+
delay(5_000L)
78+
val spawnLocation = if (objectName.endsWith("-dev"))
79+
Location.builder().setTerrain(terrain).setX(1024.0).setZ(1024.0).setY(ServerData.terrains.getHeight(terrain, 1024.0, 1024.0)).build()
80+
else
81+
DynamicMovementProcessor.createSpawnLocation(terrain) ?: continue
82+
Log.d("Created dynamic movement object: %s", spawnLocation)
83+
val dynamicObject = DynamicMovementObject(spawnLocation, objectName)
84+
dynamicObject.launch()
85+
dynamicObjects[terrain]!!.add(dynamicObject)
86+
try {
87+
handleObjectLoop(dynamicObject)
88+
} finally {
89+
dynamicObject.destroy()
90+
dynamicObjects[terrain]!!.remove(dynamicObject)
91+
}
92+
}
93+
} finally {
94+
95+
}
96+
}
97+
}
98+
99+
private suspend fun CoroutineScope.handleObjectLoop(dynamicObject: DynamicMovementObject) {
100+
while (isActive) {
101+
dynamicObject.act()
102+
delay(10_000L)
103+
}
104+
}
105+
34106
}

src/test/java/com/projectswg/holocore/resources/support/npc/ai/dynamic/TestDynamicMovement.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ class TestDynamicMovement : TestRunnerNoIntents() {
4141
.setX(1024.toDouble())
4242
.setY(ServerData.terrains.getHeight(Terrain.TATOOINE, 1024.toDouble(), 1024.toDouble()))
4343
.setZ(1024.toDouble())
44-
.build())
44+
.build(), "devtest", 100.0)
4545

4646
var previousLocation = obj.location
47-
for (i in 0 until 1000) {
48-
obj.move()
47+
repeat(1000) {
48+
obj.act()
4949
val newLocation = obj.location
5050
Assertions.assertFalse(DynamicMovementProcessor.isIntersectingProtectedZone(previousLocation, newLocation))
5151
previousLocation = newLocation

0 commit comments

Comments
 (0)