diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt
index 6c808aa11..5aaed0550 100644
--- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt
+++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.kt
@@ -120,7 +120,8 @@ class NpcCombatMode(obj: AIObject) : NpcMode(obj) {
// If we're close, angle towards target
val myLocation = obj.location
val targetLocation = target.location
- MoveObjectIntent.broadcast(obj, obj.parent, Location.builder(myLocation).setHeading(myLocation.getHeadingTo(targetLocation)).build(), npcRunSpeed)
+ val headingTo = myLocation.getHeadingTo(targetLocation.position)
+ MoveObjectIntent.broadcast(obj, obj.parent, Location.builder(myLocation).setHeading(headingTo).build(), npcRunSpeed)
if (target.posture == Posture.INCAPACITATED) {
QueueCommandIntent.broadcast(obj, target, "", DataLoader.commands().getCommand("deathblow"), 0)
diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/spawn/DynamicSpawnService.java b/src/main/java/com/projectswg/holocore/services/support/npc/spawn/DynamicSpawnService.java
index 3da915d3c..d32bdc892 100644
--- a/src/main/java/com/projectswg/holocore/services/support/npc/spawn/DynamicSpawnService.java
+++ b/src/main/java/com/projectswg/holocore/services/support/npc/spawn/DynamicSpawnService.java
@@ -7,22 +7,22 @@
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
- * This file is part of PSWGCommon. *
+ * This file is part of Holocore. *
* *
* --------------------------------------------------------------------------------*
* *
- * PSWGCommon is free software: you can redistribute it and/or modify *
+ * Holocore is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
- * PSWGCommon is distributed in the hope that it will be useful, *
+ * Holocore is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
- * along with PSWGCommon. If not, see . *
+ * along with Holocore. If not, see . *
***********************************************************************************/
package com.projectswg.holocore.services.support.npc.spawn;
@@ -30,6 +30,7 @@ import com.projectswg.common.data.location.Location;
import com.projectswg.common.data.location.Terrain;
import com.projectswg.holocore.intents.gameplay.world.spawn.CreateSpawnIntent;
import com.projectswg.holocore.intents.support.global.zone.PlayerTransformedIntent;
+import com.projectswg.holocore.intents.support.objects.swg.DestroyObjectIntent;
import com.projectswg.holocore.resources.support.data.location.ClosestLocationReducer;
import com.projectswg.holocore.resources.support.data.server_info.StandardLog;
import com.projectswg.holocore.resources.support.data.server_info.loader.DynamicSpawnLoader;
@@ -39,10 +40,13 @@ import com.projectswg.holocore.resources.support.data.server_info.loader.Terrain
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader;
import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase;
import com.projectswg.holocore.resources.support.npc.spawn.SimpleSpawnInfo;
+import com.projectswg.holocore.resources.support.npc.spawn.Spawner;
import com.projectswg.holocore.resources.support.npc.spawn.SpawnerType;
+import com.projectswg.holocore.resources.support.objects.swg.SWGObject;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
import com.projectswg.holocore.resources.support.objects.swg.custom.AIBehavior;
+import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import org.jetbrains.annotations.Nullable;
@@ -54,19 +58,21 @@ import java.util.concurrent.ThreadLocalRandom;
public class DynamicSpawnService extends Service {
- private static final int SPAWN_DISTANCE_TO_PLAYER = 70; // Spawner is created 70m away from the player and NPCs are spawned around the spawner
- private static final SpawnerType SPAWNER_TYPE = SpawnerType.RANDOM;
+ private static final int MAX_SPAWN_DISTANCE_TO_PLAYER = 110; // Spawner is created up to this amount of meters away from the player
+ private static final SpawnerType SPAWNER_TYPE = SpawnerType.RANDOM; // Important that this type is only used by dynamic spawns
private final DynamicSpawnLoader dynamicSpawnLoader;
private final NoSpawnZoneLoader noSpawnZoneLoader;
private final TerrainLevelLoader terrainLevelLoader;
- private final long spawnsPerArea;
+ private final long npcSpawnChance; // Chance in % that a NPC is dynamically spawned when a player moves
+ private final long maxObservedNpcs; // A player should never see more than this amount of alive NPCs
public DynamicSpawnService() {
dynamicSpawnLoader = ServerData.INSTANCE.getDynamicSpawns();
noSpawnZoneLoader = ServerData.INSTANCE.getNoSpawnZones();
terrainLevelLoader = ServerData.INSTANCE.getTerrainLevels();
- spawnsPerArea = PswgDatabase.INSTANCE.getConfig().getLong(this, "eggsPerArea", 4) * 3; // Amount of spawns in an area
+ npcSpawnChance = PswgDatabase.INSTANCE.getConfig().getLong(this, "npcSpawnChance", 1);
+ maxObservedNpcs = PswgDatabase.INSTANCE.getConfig().getLong(this, "maxObservedNpcs", 5);
}
@IntentHandler
@@ -76,6 +82,22 @@ public class DynamicSpawnService extends Service {
spawnNewNpcs(intent.getPlayer(), location);
}
+ @IntentHandler
+ private void handleDestroyObjectIntent(DestroyObjectIntent intent) {
+ SWGObject object = intent.getObject();
+
+ if (object instanceof AIObject) {
+ AIObject npc = (AIObject) object;
+ Spawner spawner = npc.getSpawner();
+ SWGObject egg = spawner.getEgg();
+
+ if (SPAWNER_TYPE.getObjectTemplate().equals(egg.getTemplate())) {
+ // If the dynamic NPC dies, don't let it respawn to prevent overcrowding an area
+ DestroyObjectIntent.broadcast(egg);
+ }
+ }
+ }
+
private void spawnNewNpcs(CreatureObject player, Location location) {
Terrain terrain = location.getTerrain();
Collection spawnInfos = dynamicSpawnLoader.getSpawnInfos(terrain);
@@ -85,15 +107,39 @@ public class DynamicSpawnService extends Service {
return;
}
+ TerrainLevelLoader.TerrainLevelInfo terrainLevelInfo = terrainLevelLoader.getTerrainLevelInfo(terrain);
+
+ if (terrainLevelInfo == null) {
+ // Terrain has no level range defined, we can't spawn anything without
+ return;
+ }
+
+ // Random chance to create a spawn
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ int randomChance = random.nextInt(0, 100);
+
+ if (randomChance > npcSpawnChance) {
+ return;
+ }
+
if (noSpawnZoneLoader.isInNoSpawnZone(location)) {
// The player is in a no spawn zone. Don't spawn anything.
return;
}
- TerrainLevelLoader.TerrainLevelInfo terrainLevelInfo = terrainLevelLoader.getTerrainLevelInfo(terrain);
+ String dynamicSpawnEggTemplate = SPAWNER_TYPE.getObjectTemplate();
- if (terrainLevelInfo == null) {
- // Terrain has no level range defined
+ long dynamicSpawnsWithAliveNpcs = player.getAware().stream()
+ .filter(swgObject -> swgObject instanceof AIObject)
+ .map(swgObject -> (AIObject) swgObject)
+ .map(AIObject::getSpawner)
+ .map(Spawner::getEgg)
+ .map(SWGObject::getTemplate)
+ .filter(dynamicSpawnEggTemplate::equals)
+ .count();
+
+ if (dynamicSpawnsWithAliveNpcs >= maxObservedNpcs) {
+ // Plenty spawns near this player already - do nothing
return;
}
@@ -102,12 +148,16 @@ public class DynamicSpawnService extends Service {
if (!noSpawnZoneInfos.isEmpty()) {
Optional closestZoneOpt = noSpawnZoneInfos.stream()
- .map(noSpawnZoneInfo -> Location.builder().setX(noSpawnZoneInfo.getX()).setZ(noSpawnZoneInfo.getZ())
- .setTerrain(location.getTerrain()).build()).reduce(new ClosestLocationReducer(location));
+ .map(noSpawnZoneInfo -> Location.builder()
+ .setX(noSpawnZoneInfo.getX())
+ .setZ(noSpawnZoneInfo.getZ())
+ .setTerrain(location.getTerrain())
+ .build())
+ .reduce(new ClosestLocationReducer(location));
Location closestZoneLocation = closestZoneOpt.get();
- boolean tooCloseToNoSpawnZone = location.isWithinFlatDistance(closestZoneLocation, SPAWN_DISTANCE_TO_PLAYER);
+ boolean tooCloseToNoSpawnZone = location.isWithinFlatDistance(closestZoneLocation, MAX_SPAWN_DISTANCE_TO_PLAYER);
if (tooCloseToNoSpawnZone) {
// Player is too close to a no spawn zone. Don't spawn anything.
@@ -115,22 +165,14 @@ public class DynamicSpawnService extends Service {
}
}
- String eggTemplate = SPAWNER_TYPE.getObjectTemplate();
-
- long nearbyEggs = player.getAware().stream().filter(swgObject -> eggTemplate.equals(swgObject.getTemplate())).count();
-
- if (nearbyEggs >= spawnsPerArea) {
- // Plenty spawns near this player already - do nothing
- return;
- }
-
// Spawn the egg
- ThreadLocalRandom random = ThreadLocalRandom.current();
- boolean usePositiveDirectionX = random.nextBoolean();
- boolean usePositiveDirectionZ = random.nextBoolean();
- double eggX = (usePositiveDirectionX ? SPAWN_DISTANCE_TO_PLAYER : -SPAWN_DISTANCE_TO_PLAYER) + location.getX();
- double eggZ = (usePositiveDirectionZ ? SPAWN_DISTANCE_TO_PLAYER : -SPAWN_DISTANCE_TO_PLAYER) + location.getZ();
- Location eggLocation = Location.builder(location).setX(eggX).setZ(eggZ)
+ double randomOffsetX = random.nextDouble(-MAX_SPAWN_DISTANCE_TO_PLAYER, MAX_SPAWN_DISTANCE_TO_PLAYER);
+ double randomOffsetZ = random.nextDouble(-MAX_SPAWN_DISTANCE_TO_PLAYER, MAX_SPAWN_DISTANCE_TO_PLAYER);
+ double eggX = location.getX() + randomOffsetX;
+ double eggZ = location.getZ() + randomOffsetZ;
+ Location eggLocation = Location.builder(location)
+ .setX(eggX)
+ .setZ(eggZ)
.build(); // TODO y parameter should be set and calculated based on X and Z in relevant terrain. Currently they'll spawn in the air or below ground.
int randomSpawnInfoIndex = random.nextInt(0, spawnInfos.size());
DynamicSpawnLoader.DynamicSpawnInfo spawnInfo = new ArrayList<>(spawnInfos).get(randomSpawnInfoIndex);