Added automatic spawn location selection and /debugspawn.

master
James T. Martin 2020-11-19 14:41:38 -08:00
parent 643e8e0f62
commit 82b2ac79bd
Signed by: james
GPG Key ID: 4B7F3DA9351E577C
6 changed files with 156 additions and 4 deletions

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import java.sql.SQLException;
import java.util.logging.Level;
import me.jamestmartin.wasteland.commands.CommandDebugSpawn;
import me.jamestmartin.wasteland.commands.CommandDebugSpawnWeights;
import me.jamestmartin.wasteland.commands.CommandOfficial;
import me.jamestmartin.wasteland.commands.CommandRank;
@ -92,6 +93,7 @@ public class Wasteland extends JavaPlugin {
this.getCommand("official").setExecutor(new CommandOfficial());
// debug commands
this.getCommand("debugspawn").setExecutor(new CommandDebugSpawn());
this.getCommand("debugspawnweights").setExecutor(new CommandDebugSpawnWeights());
}

View File

@ -0,0 +1,60 @@
package me.jamestmartin.wasteland.commands;
import java.util.Optional;
import java.util.Random;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.Wasteland;
public class CommandDebugSpawn implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player)) {
sender.sendMessage("You must be a player to use this command.");
return false;
}
Player player = (Player) sender;
if (args.length > 1) {
sender.sendMessage("Too many arguments.");
return false;
}
final int attempts;
if (args.length == 0) {
attempts = 1;
} else {
attempts = Integer.parseUnsignedInt(args[0]);
}
Random rand = new Random();
long time = System.currentTimeMillis();
int successfulSpawns = 0;
for (int attempt = 0; attempt < attempts; attempt++) {
Optional<LivingEntity> tryMonster = Wasteland.getInstance().getSpawner().trySpawn(rand, player.getLocation());
if (tryMonster.isEmpty()) {
continue;
}
LivingEntity monster = tryMonster.get();
Location loc = monster.getLocation();
sender.sendMessage("Spawned a " + monster.getType() + " at location " + loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ() + ".");
successfulSpawns++;
}
sender.sendMessage("Spawned " + successfulSpawns + " monsters.");
sender.sendMessage("Took " + (System.currentTimeMillis() - time) + " milliseconds.");
return true;
}
}

View File

@ -1,12 +1,24 @@
package me.jamestmartin.wasteland.spawns;
import org.bukkit.entity.EntityType;
public enum MonsterType {
/** A creeper or charged creeper, as appropriate. */
CREEPER,
CREEPER(EntityType.CREEPER),
/** A skeleton, wither skeleton, stray, etc., as appropriate. */
SKELETON,
SKELETON(EntityType.SKELETON),
/** A spider, cave spider, or spider jockey, as appropriate. */
SPIDER,
SPIDER(EntityType.SPIDER),
/** A zombie, husk, pig zombie, etc., as appropriate. */
ZOMBIE,
ZOMBIE(EntityType.ZOMBIE);
private final EntityType defaultVariant;
private MonsterType(final EntityType defaultVariant) {
this.defaultVariant = defaultVariant;
}
public EntityType getDefaultVariant() {
return defaultVariant;
}
}

View File

@ -4,6 +4,9 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import org.bukkit.Location;
@ -15,6 +18,7 @@ import org.bukkit.entity.Monster;
import org.bukkit.entity.Player;
import me.jamestmartin.wasteland.config.MonsterSpawnConfig;
import me.jamestmartin.wasteland.util.Pair;
import me.jamestmartin.wasteland.world.MoonPhase;
public class WastelandSpawner {
@ -177,4 +181,58 @@ public class WastelandSpawner {
public HashMap<MonsterType, Float> calculateSpawnProbabilities(Block block) {
return calculateSpawnProbabilities(block.getLocation());
}
public static Location spawnBiasedRandomLocation(Random rand, Location center, int minRadius, int maxRadius) {
// An angle on the xy plane, from 0 to 1.
double xzAngle = 2 * Math.PI * rand.nextDouble();
// An angle on the plane specified by the xz angle and the y axis, from -0.5 to 0.5.
// We have a bias towards y-levels closer to the player;
// the angle 0 is the xy line, so the bell curve is centered on the player's y level.
double xzyAngle = Math.PI * rand.nextGaussian() / 3;
// Prefer to spawn closer to the player, using half a bell curve.
double magnitude = (maxRadius - minRadius) * Math.abs(rand.nextGaussian() / 3) + minRadius;
double offX = magnitude * Math.cos(xzAngle) * Math.cos(xzyAngle);
double offZ = magnitude * Math.sin(xzAngle) * Math.cos(xzyAngle);
double offY = magnitude * Math.sin(xzyAngle);
return new Location(center.getWorld(), center.getX() + offX, center.getY() + offY, center.getZ() + offZ);
}
public static Optional<Pair<MonsterType, Double>> chooseWeightedRandomMonster(Random rand, Map<MonsterType, Float> weights) {
double overallSpawnProbability = weights.values().stream().reduce(0.0f, (x, y) -> x + y);
if (rand.nextDouble() >= overallSpawnProbability) {
return Optional.empty();
}
double whichMonster = rand.nextDouble() * overallSpawnProbability;
for (Entry<MonsterType, Float> monster : weights.entrySet()) {
double successMargin = monster.getValue() - whichMonster;
if (successMargin > 0) {
return Optional.of(new Pair<>(monster.getKey(), successMargin));
}
whichMonster -= monster.getValue();
}
// This should only be able to happen due to rounding error.
return Optional.empty();
}
public Optional<Pair<MonsterType, Double>> pickRandomMonster(Random rand, Block block) {
Map<MonsterType, Float> weights = calculateSpawnProbabilities(block);
return chooseWeightedRandomMonster(rand, weights);
}
public Optional<LivingEntity> trySpawn(Random rand, Location center) {
Location location = spawnBiasedRandomLocation(rand, center, 20, 54);
return pickRandomMonster(rand, location.getBlock()).map(mm -> {
MonsterType type = mm.x;
return (LivingEntity) location.getWorld().spawnEntity(location, type.getDefaultVariant());
});
}
public Optional<LivingEntity> trySpawn(Location center) {
return trySpawn(new Random(), center);
}
}

View File

@ -0,0 +1,11 @@
package me.jamestmartin.wasteland.util;
public class Pair<A, B> {
public final A x;
public final B y;
public Pair(A x, B y) {
this.x = x;
this.y = y;
}
}

View File

@ -32,6 +32,11 @@ commands:
permission-message: You are not a staff member with an officer rank.
# debug commands
debugspawn:
description: Spawn a random monster.
usage: "Usage: /<command> [<attempts>]"
permission: wasteland.debug.spawns.spawn
permission-message: You do not have permission to spawn debug monsters.
debugspawnweights:
description: View the monster spawn weights at the block you are looking at.
usage: "Usage: /<command>"
@ -82,7 +87,11 @@ permissions:
description: Commands for debugging the spawning system.
default: false
children:
wasteland.debug.spawns.spawn: true
wasteland.debug.spawns.weights: true
wasteland.debug.spawns.spawn:
description: Allows you to randomly spawn monsters using a command.
default: false
wasteland.debug.spawns.weights:
description: Allows you to see the spawn weights by mob type at a block.
default: false