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);