added MobArena game mode with custom mobs and arena mechanics

This commit is contained in:
MineTec committed 2026-11-06 21:49:22 +01:00
1 parent dc32b053a3
commit 9e757378c1
17 files changed
+1227 -2

No files matched your search

@@ -17,6 +17,7 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge.Fastbr
import eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround.HighGroundFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.jumpDive.JumpDiveFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArenaFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.pillars.PillarsFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.PlayerRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake.SpaceSnakeFactory;
@@ -56,7 +57,8 @@ public enum GameList {
BLOCKBATTLE(new BlockBattleFactory(), GameType.PVP),
COLORJUMP(new ColorJumpFactory(), GameType.JUMPNRUN),
PLAYERRACE(new PlayerRaceFactory(), GameType.JUMPNRUN),
BUILDBATTLE(new BuildBattleFactory(), GameType.PROTOTYPE);
BUILDBATTLE(new BuildBattleFactory(), GameType.PROTOTYPE),
MOBARENA(new MobArenaFactory(), GameType.PVE);
private final GameFactory factory;
private final GameType type;
@@ -0,0 +1,18 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity;
/**
* Knockback in viewing direction of the attacker
*/
public record Knockback(double x, double z) {
public static Knockback of(Entity attacker) {
double yaw = Math.toRadians(attacker.getPosition().yaw());
return new Knockback(Math.sin(yaw), -Math.cos(yaw));
}
public void apply(LivingEntity victim, float strength) {
victim.takeKnockback(strength, this.x, this.z);
}
}
@@ -0,0 +1,342 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.MobType;
import eu.mhsl.minenet.minigames.message.type.ActionBarMessage;
import eu.mhsl.minenet.minigames.message.type.TitleMessage;
import eu.mhsl.minenet.minigames.score.PointsWinScore;
import eu.mhsl.minenet.minigames.util.BatchUtil;
import eu.mhsl.minenet.minigames.util.NumberUtil;
import eu.mhsl.minenet.minigames.world.BlockPallet;
import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.attribute.AttributeModifier;
import net.minestom.server.entity.attribute.AttributeOperation;
import net.minestom.server.entity.damage.Damage;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.item.PickupExperienceEvent;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
public class MobArena extends StatelessGame {
private static final int MAX_MOBS = 150;
private static final int SPAWN_INTERVAL_SECONDS = 2;
private static final float SWORD_DAMAGE = 6;
private static final double SWEEP_RANGE = 3;
private static final long ATTACK_COOLDOWN_MILLIS = 500;
private static final int WALL_HEIGHT = 3;
private static final Key SPEED_MODIFIER = Key.key("minenet", "mob_arena_speed");
private final int seconds;
private final int radius;
private final int difficulty;
private final int deathPenalty;
private final boolean powerUps;
private int elapsedSeconds = 0;
private final Map<Player, Integer> points = new WeakHashMap<>();
private final Map<Player, Long> lastAttack = new WeakHashMap<>();
private final Map<Player, Long> strengthUntil = new WeakHashMap<>();
private final Map<Player, Long> speedUntil = new WeakHashMap<>();
public MobArena(int seconds, int radius, int difficulty, int deathPenalty, boolean powerUps) {
super(Dimension.OVERWORLD.key, "MobArena", new PointsWinScore());
this.seconds = seconds;
this.radius = radius;
this.difficulty = difficulty;
this.deathPenalty = deathPenalty;
this.powerUps = powerUps;
this.setTime(18000);
this.defaultClock().pause();
this.setGenerator(
new CircularPlateTerrainGenerator(radius)
.setPlateHeight(1)
.setPlatePallet(BlockPallet.STONE)
);
this.eventNode()
.addListener(EntityAttackEvent.class, this::onPlayerAttack)
.addListener(EntityDamageEvent.class, this::onPlayerDamage)
.addListener(EntityDeathEvent.class, this::onMobDeath)
.addListener(PickupItemEvent.class, this::onPickupItem)
.addListener(PickupExperienceEvent.class, pickupExperienceEvent -> {
// Orbs sind nur Effekt, das Level zeigt die Punkte an
pickupExperienceEvent.setCancelled(true);
Knockback knockback = Knockback.of(pickupExperienceEvent.getPlayer());
pickupExperienceEvent.getExperienceOrb().setVelocity(new Vec(knockback.x(), 0, knockback.z()).mul(5));
});
}
@Override
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
// Mauer am Plattformrand, damit weder Spieler noch Mobs die Arena verlassen
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
int outer = this.radius + 2;
for(int x = -outer; x <= outer; x++) {
for(int z = -outer; z <= outer; z++) {
double distance = new Pos(x, 0, z).distance(Pos.ZERO);
if(distance <= this.radius || distance > this.radius + 1.5) continue;
for(int y = 0; y <= WALL_HEIGHT; y++) batch.setBlock(x, y, z, Block.STONE_BRICKS);
}
}
BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null));
}
@Override
protected void onStart() {
this.getPlayers().forEach(player -> {
this.points.put(player, 0);
player.setLevel(0);
player.getInventory().clear();
player.getInventory().addItemStack(ItemStack.of(Material.IRON_SWORD));
player.setHealth(this.maxHealth(player));
});
this.setTimeLimit(this.seconds);
this.scheduler().submitTask(() -> {
if(!this.isRunning) return TaskSchedule.stop();
int amount = (int) Math.ceil(this.getPlayers().size() * this.difficulty / 2d * this.escalation());
for(int i = 0; i < amount; i++) this.spawnMob(MobType.getRandom().create());
this.elapsedSeconds += SPAWN_INTERVAL_SECONDS;
return TaskSchedule.seconds(SPAWN_INTERVAL_SECONDS);
});
this.scheduler().submitTask(() -> {
if(!this.isRunning) return TaskSchedule.stop();
this.getPlayers().forEach(player -> this.heal(player, 1));
return TaskSchedule.seconds(3);
});
}
@Override
protected void onStop() {
this.points.forEach((player, points) -> this.getScore().insertResult(player, points));
}
private void onPlayerAttack(@NotNull EntityAttackEvent event) {
if(!(event.getEntity() instanceof Player attacker)) return;
if(!(event.getTarget() instanceof ArenaMob target)) return;
if(!this.isRunning) return;
long now = System.currentTimeMillis();
if(now - this.lastAttack.getOrDefault(attacker, 0L) < ATTACK_COOLDOWN_MILLIS) return;
this.lastAttack.put(attacker, now);
float damage = now < this.strengthUntil.getOrDefault(attacker, 0L) ? SWORD_DAMAGE * 2 : SWORD_DAMAGE;
Collection<Entity> nearby = this.getNearbyEntities(target.getPosition(), SWEEP_RANGE);
if(nearby.size() > 1)
ArenaMob.particle(this, Particle.SWEEP_ATTACK, target.getPosition().add(0, 1, 0), new Vec(0.5), (int) NumberUtil.map(nearby.size(), 1, 10, 1, 5));
Knockback knockback = Knockback.of(attacker);
for(Entity entity : nearby) {
if(!(entity instanceof ArenaMob mob) || mob.isDead()) continue;
double distance = mob.getDistance(target);
mob.damage(Damage.fromPlayer(attacker, (float) NumberUtil.map(distance, SWEEP_RANGE, 0, 0, damage)));
knockback.apply(mob, (float) NumberUtil.map(distance, SWEEP_RANGE, 0, 0.1, 0.5));
ArenaMob.particle(this, Particle.FALLING_OBSIDIAN_TEAR, mob.getPosition(), new Vec(knockback.x(), 0, knockback.z()), 10);
this.scheduler().scheduleNextTick(() -> {
if(mob.isDead()) knockback.apply(mob, 1f);
});
}
}
private void onPlayerDamage(@NotNull EntityDamageEvent event) {
if(!(event.getEntity() instanceof Player player)) return;
if(!this.isRunning) {
event.setCancelled(true);
return;
}
if(event.getDamage().getAmount() < player.getHealth()) return;
// Spieler sterben nicht wirklich, sondern verlieren Punkte und starten am Spawn neu
event.setCancelled(true);
this.addPoints(player, -this.deathPenalty);
player.teleport(this.getSpawn());
player.setHealth(this.maxHealth(player));
player.setInvulnerable(true);
player.scheduler().scheduleTask(() -> player.setInvulnerable(false), TaskSchedule.seconds(3), TaskSchedule.stop());
new TitleMessage(Duration.ofSeconds(1))
.subtitle(subtitleMessage -> subtitleMessage.appendStatic(Component.text("-" + this.deathPenalty, NamedTextColor.RED)))
.appendTranslated("game_MobArena#death")
.send(player);
}
private void onMobDeath(@NotNull EntityDeathEvent event) {
if(!(event.getEntity() instanceof ArenaMob mob)) return;
if(!this.isRunning) return;
if(mob.getLastDamageSource() == null) return;
if(!(mob.getLastDamageSource().getAttacker() instanceof Player killer)) return;
this.addPoints(killer, mob.getPoints());
this.heal(killer, 1);
if(this.powerUps && this.rnd.nextDouble() < mob.getDropChance()) this.dropPowerUp(mob.getPosition());
}
private void dropPowerUp(Pos pos) {
ItemEntity item = new ItemEntity(ItemStack.of(PowerUp.getRandom().getMaterial()).withGlowing(true));
item.setPickupDelay(Duration.ofMillis(500));
item.setInstance(this, pos.add(0, 0.5, 0));
item.setVelocity(new Vec(0, 4, 0));
item.scheduleRemove(Duration.ofSeconds(15));
}
private void onPickupItem(@NotNull PickupItemEvent event) {
if(!(event.getLivingEntity() instanceof Player player)) {
event.setCancelled(true);
return;
}
PowerUp.byMaterial(event.getItemStack().material()).ifPresentOrElse(powerUp -> {
this.applyPowerUp(player, powerUp);
player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.PLAYER, 0.5f, 1.5f));
new ActionBarMessage().appendTranslated(powerUp.getTranslationKey()).send(player);
}, () -> event.setCancelled(true));
}
private void applyPowerUp(Player player, PowerUp powerUp) {
long until = System.currentTimeMillis() + PowerUp.DURATION_SECONDS * 1000L;
switch(powerUp) {
case HEAL -> this.heal(player, 8);
case STRENGTH -> this.strengthUntil.put(player, until);
case SPEED -> {
this.speedUntil.put(player, until);
player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER);
player.getAttribute(Attribute.MOVEMENT_SPEED).addModifier(new AttributeModifier(SPEED_MODIFIER, 0.4, AttributeOperation.ADD_MULTIPLIED_BASE));
player.scheduler().scheduleTask(() -> {
// ein erneut aufgesammeltes Power-up verlaengert die Wirkung
if(System.currentTimeMillis() < this.speedUntil.getOrDefault(player, 0L)) return;
player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER);
}, TaskSchedule.seconds(PowerUp.DURATION_SECONDS), TaskSchedule.stop());
}
}
}
@Override
protected void onUnload() {
this.getPlayers().forEach(player -> player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER));
}
private void addPoints(Player player, int amount) {
int newPoints = Math.max(0, this.points.getOrDefault(player, 0) + amount);
this.points.put(player, newPoints);
player.setLevel(newPoints);
}
private float maxHealth(Player player) {
return (float) player.getAttributeValue(Attribute.MAX_HEALTH);
}
private void heal(Player player, float amount) {
player.setHealth(Math.min(this.maxHealth(player), player.getHealth() + amount));
}
public void spawnMob(ArenaMob mob) {
this.spawnMob(mob, this.randomSpawnPosition(), 0);
}
/**
* Only living arena mobs count towards the limits, orbs, items, projectiles and corpses are ignored
*/
public void spawnMob(ArenaMob mob, Pos pos, int area) {
if(!this.isRunning) return;
int players = this.getPlayers().size();
int maxMobs = (int) Math.min(MAX_MOBS, (16 + 8 * players) * this.difficulty / 2d * this.escalation());
long livingMobs = this.livingMobs(ArenaMob.class);
if(livingMobs >= maxMobs) return;
if(this.livingMobs(mob.getClass()) >= mob.maxAlive(players)) return;
Pos spawnPos = area > 0 ? pos.add(this.rnd.nextInt(-area, area), 0, this.rnd.nextInt(-area, area)) : pos;
mob.setInstance(this, spawnPos).thenRun(() -> ArenaMob.particle(this, Particle.POOF, spawnPos, new Vec(1), 3));
}
/**
* Grows over the course of the round, more and more mobs spawn the longer the game runs
*/
private double escalation() {
return Math.min(2.5, 1 + this.elapsedSeconds / 90d);
}
public float getDamageMultiplier() {
return switch(this.difficulty) {
case 1 -> 0.5f;
case 3 -> 1f;
default -> 0.75f;
};
}
private long livingMobs(Class<? extends ArenaMob> type) {
return this.getEntities().stream()
.filter(type::isInstance)
.filter(entity -> !((ArenaMob) entity).isDead())
.count();
}
/**
* Random position on the arena floor, preferably not right next to a player
*/
private Pos randomSpawnPosition() {
Pos pos = this.getSpawn();
for(int i = 0; i < 10; i++) {
double angle = this.rnd.nextDouble(2 * Math.PI);
double distance = this.rnd.nextDouble(this.radius - 3);
pos = this.getSpawn().add(Math.cos(angle) * distance, 0, Math.sin(angle) * distance);
Pos candidate = pos;
if(this.getPlayers().stream().noneMatch(player -> player.getPosition().distance(candidate) < 6)) break;
}
return pos;
}
public void explode(Pos pos, @Nullable Entity source, @Nullable Entity attacker, float damage, double range) {
this.sendGroupedPacket(new ParticlePacket(Particle.EXPLOSION, pos.x(), pos.y(), pos.z(), 1, 1, 1, 0, 5));
this.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.NEUTRAL, 0.5f, 1), pos.x(), pos.y(), pos.z());
for(Entity entity : this.getNearbyEntities(pos, range)) {
if(!(entity instanceof LivingEntity livingEntity) || livingEntity == source) continue;
float amount = (float) NumberUtil.map(entity.getDistance(pos), range, 0, 0, damage);
if(livingEntity instanceof Player) amount *= this.getDamageMultiplier();
livingEntity.damage(new Damage(DamageType.EXPLOSION, source, attacker, pos, amount));
}
}
@Override
public Pos getSpawn() {
return new Pos(0.5, 1, 0.5);
}
}
@@ -0,0 +1,51 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
import eu.mhsl.minenet.minigames.instance.game.Game;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.BoolOption;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
import net.minestom.server.item.Material;
import java.util.Map;
public class MobArenaFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_MobArena#name");
}
@Override
public TranslatedComponent description() {
return TranslatedComponent.byId("game_MobArena#description");
}
@Override
public Material symbol() {
return Material.ZOMBIE_HEAD;
}
@Override
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 120, 180, 300, 60))
.addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 30, 40, 50, 20))
.addOption(new NumericOption("difficulty", Material.SKELETON_SKULL, TranslatedComponent.byId("game_MobArena#difficulty"), 2, 3, 1))
.addOption(new NumericOption("deathPenalty", Material.TOTEM_OF_UNDYING, TranslatedComponent.byId("game_MobArena#deathPenalty"), 3, 5, 10, 0))
.addOption(new BoolOption("powerUps", Material.GOLDEN_APPLE, TranslatedComponent.byId("game_MobArena#powerUps")));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) {
return new MobArena(
configuration.get("seconds").getAsInt(),
configuration.get("radius").getAsInt(),
configuration.get("difficulty").getAsInt(),
configuration.get("deathPenalty").getAsInt(),
configuration.get("powerUps").getAsBoolean()
).setParent(parent);
}
}
@@ -0,0 +1,40 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
import net.minestom.server.item.Material;
import java.util.Arrays;
import java.util.Optional;
import java.util.Random;
public enum PowerUp {
HEAL(Material.GOLDEN_APPLE, "game_MobArena#powerUp_heal"),
STRENGTH(Material.BLAZE_POWDER, "game_MobArena#powerUp_strength"),
SPEED(Material.SUGAR, "game_MobArena#powerUp_speed");
public static final int DURATION_SECONDS = 10;
private static final Random RANDOM = new Random();
private final Material material;
private final String translationKey;
PowerUp(Material material, String translationKey) {
this.material = material;
this.translationKey = translationKey;
}
public Material getMaterial() {
return this.material;
}
public String getTranslationKey() {
return this.translationKey;
}
public static Optional<PowerUp> byMaterial(Material material) {
return Arrays.stream(values()).filter(powerUp -> powerUp.material == material).findFirst();
}
public static PowerUp getRandom() {
return values()[RANDOM.nextInt(values().length)];
}
}
@@ -0,0 +1,193 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.Knockback;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.component.DataComponents;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.ExperienceOrb;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.attribute.AttributeInstance;
import net.minestom.server.entity.ai.goal.MeleeAttackGoal;
import net.minestom.server.entity.ai.goal.RandomLookAroundGoal;
import net.minestom.server.entity.ai.goal.RandomStrollGoal;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.entity.damage.Damage;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class ArenaMob extends EntityCreature {
private static final int MAX_ORBS = 8;
private static final int BLOCK_LENGTH = 6;
private static final List<String> CHARACTERS = List.of("", "▏", "▎", "▍", "▌", "▋", "▊", "▉");
private static final String FULL_BLOCK_CHAR = "█";
private float damage = 2;
private int experience = 10;
private int points = 1;
private double dropChance = 0.08;
public ArenaMob(@NotNull EntityType entityType) {
super(entityType);
this.set(DataComponents.CUSTOM_NAME, generateHealthBar(this.getMaxHealth(), this.getHealth()));
this.setCustomNameVisible(true);
this.setRemovalAnimationDelay(3500);
this.aiGroup();
this.eventNode()
.addListener(EntityAttackEvent.class, this::onAttack)
.addListener(EntityDamageEvent.class, this::onDamage)
.addListener(EntityDeathEvent.class, this::onDeath);
}
public static void particle(Instance instance, Particle particle, Point pos, Vec offset, int count) {
instance.sendGroupedPacket(new ParticlePacket(particle, pos.x(), pos.y(), pos.z(), (float) offset.x(), (float) offset.y(), (float) offset.z(), 0, count));
}
protected void particle(Particle particle, Point pos, float offset, int count) {
if(this.getInstance() == null) return;
particle(this.getInstance(), particle, pos, new Vec(offset), count);
}
protected ClosestEntityTarget playerTarget() {
return new ClosestEntityTarget(this, 32, entity -> entity instanceof Player player && player.getGameMode() != GameMode.SPECTATOR);
}
protected void aiGroup() {
this.addAIGroup(
List.of(
new MeleeAttackGoal(this, 1.2, 20, TimeUnit.SERVER_TICK),
new RandomLookAroundGoal(this, 30),
new RandomStrollGoal(this, 5)
),
List.of(this.playerTarget())
);
}
protected void onAttack(@NotNull EntityAttackEvent event) {
if(!(event.getTarget() instanceof Player target)) return;
this.particle(Particle.LANDING_LAVA, target.getPosition(), 0.5f, 10);
Knockback.of(this).apply(target, 0.2f);
target.damage(Damage.fromEntity(this, this.scaledDamage(this.damage)));
}
protected void onDamage(@NotNull EntityDamageEvent event) {
this.particle(Particle.DAMAGE_INDICATOR, this.getPosition(), 1, 5);
event.setSound(SoundEvent.BLOCK_AMETHYST_BLOCK_STEP);
}
protected void onDeath(@NotNull EntityDeathEvent event) {
Instance instance = this.getInstance();
this.particle(Particle.SOUL, this.getPosition(), 0.5f, 3);
this.setCustomNameVisible(false);
for(int i = 0; i < Math.min(this.experience, MAX_ORBS); i++) {
ExperienceOrb orb = new ExperienceOrb((short) 1);
orb.setNoGravity(true);
orb.setInstance(instance, this.getPosition().add(0, 0.1, 0));
orb.setVelocity(new Vec(this.random(-5, 5), this.random(1, 3), this.random(-5, 5)));
instance.scheduler().scheduleTask(() -> {
particle(instance, Particle.LAVA, orb.getPosition(), Vec.ZERO, 1);
orb.remove();
}, TaskSchedule.millis((long) this.random(1000, 5000)), TaskSchedule.stop());
}
this.scheduler().scheduleTask(() -> this.teleport(this.getPosition().sub(0, 0.1, 0)), TaskSchedule.seconds(3), TaskSchedule.stop());
}
private double random(double min, double max) {
return min + Math.random() * (max - min);
}
@Override
public void setHealth(float health) {
super.setHealth(health);
if(!this.isDead()) this.set(DataComponents.CUSTOM_NAME, generateHealthBar(this.getMaxHealth(), this.getHealth()));
}
/**
* Damage against players, scaled by the arena difficulty
*/
protected float scaledDamage(float damage) {
if(this.getInstance() instanceof MobArena arena) return damage * arena.getDamageMultiplier();
return damage;
}
protected void setMaxHealth(float maxHealth) {
this.getAttribute(Attribute.MAX_HEALTH).setBaseValue(maxHealth);
this.setHealth(maxHealth);
}
protected void multiplySpeed(double factor) {
AttributeInstance speed = this.getAttribute(Attribute.MOVEMENT_SPEED);
speed.setBaseValue(speed.getBaseValue() * factor);
}
/**
* Maximum amount of living mobs of this type in the arena
*/
public int maxAlive(int players) {
return 6 + 4 * players;
}
public double getDropChance() {
return this.dropChance;
}
public void setDropChance(double dropChance) {
this.dropChance = dropChance;
}
public float getMaxHealth() {
return (float) this.getAttributeValue(Attribute.MAX_HEALTH);
}
public void setDamage(float damage) {
this.damage = damage;
}
public void setExperience(int experience) {
this.experience = experience;
}
public int getPoints() {
return this.points;
}
public void setPoints(int points) {
this.points = points;
}
private static @NotNull Component generateHealthBar(float maxHealth, float currentHealth) {
// Converts the health percentage into a number from 0-{blockLength} -- only 0 if the mob's health is 0
final double charHealth = (currentHealth / maxHealth) * BLOCK_LENGTH;
return Component.text()
.append(Component.text(FULL_BLOCK_CHAR.repeat((int) Math.floor(charHealth)), NamedTextColor.DARK_RED))
.append(Component.text(
CHARACTERS.get((int) Math.round((charHealth - Math.floor(charHealth)) * (CHARACTERS.size() - 1))),
NamedTextColor.GOLD
))
.build();
}
}
@@ -0,0 +1,52 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob;
import io.github.togar2.pvp.entity.projectile.CustomEntityProjectile;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.damage.Damage;
import net.minestom.server.entity.damage.DamageType;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
/**
* Arrow shot by arena mobs, only hits players and flies through other mobs
*/
public class MobArrow extends CustomEntityProjectile {
private final float damage;
public MobArrow(@NotNull Entity shooter, float damage) {
super(shooter, EntityType.ARROW);
this.damage = damage;
}
/**
* Velocity has to be set before spawning, otherwise the client never receives it
*/
public void shootAt(Point target, double power, double spread) {
Entity shooter = this.getShooter();
if(shooter == null || shooter.getInstance() == null) return;
this.shootTo(target, power, spread);
this.setInstance(shooter.getInstance(), shooter.getPosition().add(0, shooter.getEyeHeight(), 0));
}
@Override
protected boolean canHit(Entity entity) {
return entity instanceof Player player && player.getGameMode() != GameMode.SPECTATOR;
}
@Override
public boolean onHit(Entity entity) {
if(entity instanceof Player player) player.damage(new Damage(DamageType.ARROW, this, this.getShooter(), null, this.damage));
return true;
}
@Override
public boolean onStuck() {
this.scheduleRemove(Duration.ofSeconds(2));
return false;
}
}
@@ -0,0 +1,47 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Creeper;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Evoker;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Pillager;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Skeleton;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Slime;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Spider;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Zombie;
import java.util.Arrays;
import java.util.Random;
import java.util.function.Supplier;
public enum MobType {
ZOMBIE(Zombie::new, 30),
SKELETON(Skeleton::new, 14),
SPIDER(Spider::new, 14),
PILLAGER(Pillager::new, 12),
CREEPER(Creeper::new, 10),
SLIME(Slime::new, 10),
EVOKER(Evoker::new, 6);
private static final Random RANDOM = new Random();
private static final int TOTAL_WEIGHT = Arrays.stream(values()).mapToInt(type -> type.weight).sum();
private final Supplier<ArenaMob> factory;
private final int weight;
MobType(Supplier<ArenaMob> factory, int weight) {
this.factory = factory;
this.weight = weight;
}
public ArenaMob create() {
return this.factory.get();
}
public static MobType getRandom() {
int roll = RANDOM.nextInt(TOTAL_WEIGHT);
for(MobType type : values()) {
roll -= type.weight;
if(roll < 0) return type;
}
return ZOMBIE;
}
}
@@ -0,0 +1,39 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.kyori.adventure.sound.Sound;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.metadata.monster.CreeperMeta;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
public class Creeper extends ArenaMob {
private boolean fusing = false;
public Creeper() {
super(EntityType.CREEPER);
this.setMaxHealth(12);
this.setPoints(2);
}
/**
* Ignites instead of attacking and explodes shortly after, no matter if the target is still close
*/
@Override
protected void onAttack(@NotNull EntityAttackEvent event) {
if(this.fusing) return;
this.fusing = true;
((CreeperMeta) this.getEntityMeta()).setState(CreeperMeta.State.FUSE);
this.getInstance().playSound(Sound.sound(SoundEvent.ENTITY_CREEPER_PRIMED, Sound.Source.HOSTILE, 1, 1), this.getPosition());
this.scheduler().scheduleTask(() -> {
if(this.isDead() || this.isRemoved()) return;
if(this.getInstance() instanceof MobArena arena) arena.explode(this.getPosition(), this, null, 8, 4);
this.remove();
}, TaskSchedule.millis(1500), TaskSchedule.stop());
}
}
@@ -0,0 +1,121 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.kyori.adventure.sound.Sound;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.goal.RangedAttackGoal;
import net.minestom.server.entity.metadata.monster.raider.EvokerMeta;
import net.minestom.server.entity.metadata.monster.raider.SpellcasterIllagerMeta;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.time.Cooldown;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;
public class Evoker extends ArenaMob {
private boolean hasTotem;
public Evoker() {
super(EntityType.EVOKER);
this.hasTotem = Math.random() < 0.5;
this.setMaxHealth(14);
this.setPoints(3);
this.setDropChance(0.3);
}
@Override
protected void aiGroup() {
Random rnd = new Random();
int range = rnd.nextInt(3, 16);
// keeps distance to the target without actually shooting
RangedAttackGoal keepDistance = new RangedAttackGoal(this, Duration.ofDays(1), range, range, true, 0, 0);
keepDistance.setProjectileGenerator((shooter, to, power, spread) -> {});
this.addAIGroup(
List.of(
new ActionGoal(this, Duration.ofSeconds(25), target -> {
this.lookAt(target);
((EvokerMeta) this.getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.SUMMON_VEX);
this.scheduler().scheduleTask(() -> {
if(this.isDead()) return;
if(this.getInstance() instanceof MobArena arena) {
int amount = rnd.nextInt(1, 3);
for(int i = 0; i < amount; i++) arena.spawnMob(new Vex(), this.getPosition().add(0, 2, 0), 3);
}
((EvokerMeta) this.getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.NONE);
}, TaskSchedule.seconds(2), TaskSchedule.stop());
}),
keepDistance
),
List.of(this.playerTarget())
);
}
@Override
protected void onDamage(@NotNull EntityDamageEvent event) {
super.onDamage(event);
if(!this.hasTotem || event.getDamage().getAmount() < this.getHealth()) return;
event.setCancelled(true);
this.hasTotem = false;
this.particle(Particle.TOTEM_OF_UNDYING, this.getPosition(), 2, 60);
this.getInstance().playSound(Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.HOSTILE, 0.5f, 1), this.getPosition());
this.setHealth(6);
}
private static final class ActionGoal extends GoalSelector {
private final Duration cooldown;
private final Consumer<Entity> consumer;
private long lastAction;
public ActionGoal(@NotNull EntityCreature entityCreature, @NotNull Duration cooldown, Consumer<Entity> consumer) {
super(entityCreature);
this.cooldown = cooldown;
this.consumer = consumer;
}
@Override
public boolean shouldStart() {
Entity target = this.entityCreature.getTarget();
if(target == null) target = this.findTarget();
if(target == null) return false;
if(Cooldown.hasCooldown(System.currentTimeMillis(), this.lastAction, this.cooldown)) return false;
this.entityCreature.setTarget(target);
return true;
}
@Override
public void start() {
}
@Override
public void tick(long time) {
if(!Cooldown.hasCooldown(time, this.lastAction, this.cooldown) && this.entityCreature.getTarget() != null) {
this.lastAction = time;
this.consumer.accept(this.entityCreature.getTarget());
}
}
@Override
public boolean shouldEnd() {
final Entity target = this.entityCreature.getTarget();
return target == null || target.isRemoved() || Cooldown.hasCooldown(System.currentTimeMillis(), this.lastAction, this.cooldown);
}
@Override
public void end() {
}
}
}
@@ -0,0 +1,15 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public class Pillager extends ArenaMob {
public Pillager() {
super(EntityType.PILLAGER);
this.setItemInMainHand(ItemStack.of(Material.STONE_AXE));
this.setMaxHealth(12);
this.setDamage(3);
}
}
@@ -0,0 +1,141 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.MobArrow;
import net.kyori.adventure.sound.Sound;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.goal.RandomStrollGoal;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.sound.SoundEvent;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Skeleton extends ArenaMob {
private static final float ARROW_DAMAGE = 2;
public Skeleton() {
super(EntityType.SKELETON);
this.setItemInMainHand(ItemStack.of(Material.BOW));
this.setMaxHealth(10);
this.multiplySpeed(0.6);
this.setPoints(2);
}
@Override
protected void aiGroup() {
this.addAIGroup(
List.of(new ArcherGoal(this), new RandomStrollGoal(this, 5)),
List.of(this.playerTarget())
);
}
@Override
public int maxAlive(int players) {
return 3 + 2 * players;
}
private void shoot(Entity target) {
if(this.getInstance() == null) return;
Pos to = target.getPosition().add(0, target.getEyeHeight() * 0.7, 0);
new MobArrow(this, this.scaledDamage(ARROW_DAMAGE)).shootAt(to, 1.6, 4);
this.getInstance().playSound(Sound.sound(SoundEvent.ENTITY_SKELETON_SHOOT, Sound.Source.HOSTILE, 1, 1), this.getPosition());
}
/**
* Keeps a medium distance to the target, backs off when it gets too close and shoots in fixed intervals
*/
private static final class ArcherGoal extends GoalSelector {
private static final double MIN_DISTANCE = 5;
private static final double MAX_DISTANCE = 12;
private static final double SHOOT_RANGE = 18;
private static final long DRAW_MILLIS = 1000;
private static final long COOLDOWN_MILLIS = 2500;
private final Skeleton skeleton;
private long drawStart = 0;
private long lastShot = 0;
private long lastPathUpdate = 0;
private boolean retreating = false;
ArcherGoal(@NotNull Skeleton skeleton) {
super(skeleton);
this.skeleton = skeleton;
}
@Override
public boolean shouldStart() {
return this.findTarget() != null;
}
@Override
public void start() {
}
@Override
public void tick(long time) {
Entity target = this.findTarget();
if(target == null) return;
this.entityCreature.setTarget(target);
double distance = this.entityCreature.getDistance(target);
this.updateMovement(target, distance, time);
this.entityCreature.lookAt(target);
// beim Zurückweichen wird nicht geschossen
if(this.retreating || distance > SHOOT_RANGE) {
this.stopDrawing();
return;
}
if(this.drawStart == 0) {
if(time - this.lastShot < COOLDOWN_MILLIS) return;
this.drawStart = time;
this.skeleton.refreshActiveHand(true, false, false);
} else if(time - this.drawStart >= DRAW_MILLIS) {
this.skeleton.shoot(target);
this.lastShot = time;
this.stopDrawing();
}
}
private void updateMovement(Entity target, double distance, long time) {
if(time - this.lastPathUpdate < 500) return;
this.lastPathUpdate = time;
Pos position = this.entityCreature.getPosition();
this.retreating = distance < MIN_DISTANCE;
if(distance > MAX_DISTANCE) {
this.entityCreature.getNavigator().setPathTo(target.getPosition());
} else if(this.retreating) {
Vec away = position.sub(target.getPosition()).asVec().withY(0);
if(away.isZero()) away = new Vec(1, 0, 0);
this.entityCreature.getNavigator().setPathTo(position.add(away.normalize().mul(4)));
} else {
this.entityCreature.getNavigator().setPathTo(null);
}
}
private void stopDrawing() {
if(this.drawStart == 0) return;
this.drawStart = 0;
this.skeleton.refreshActiveHand(false, false, false);
}
@Override
public boolean shouldEnd() {
return this.findTarget() == null;
}
@Override
public void end() {
this.stopDrawing();
this.entityCreature.getNavigator().setPathTo(null);
}
}
}
@@ -0,0 +1,34 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.metadata.cube.SlimeMeta;
import net.minestom.server.event.entity.EntityDeathEvent;
import org.jetbrains.annotations.NotNull;
public class Slime extends ArenaMob {
private final int size;
public Slime() {
this(Math.random() < 0.25 ? 3 : 2);
}
public Slime(int size) {
super(EntityType.SLIME);
this.size = size;
((SlimeMeta) this.getEntityMeta()).setSize(size);
this.setMaxHealth(size * size);
this.setDamage(size);
this.setExperience(size * 2);
}
@Override
protected void onDeath(@NotNull EntityDeathEvent event) {
super.onDeath(event);
if(this.size <= 1 || !(this.getInstance() instanceof MobArena arena)) return;
for(int i = 0; i < 2; i++) arena.spawnMob(new Slime(this.size - 1), this.getPosition(), 1);
}
}
@@ -0,0 +1,12 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
public class Spider extends ArenaMob {
public Spider() {
super(EntityType.SPIDER);
this.setMaxHealth(4);
this.setDamage(1);
}
}
@@ -0,0 +1,32 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.particle.Particle;
import net.minestom.server.timer.TaskSchedule;
public class Vex extends ArenaMob {
private static final int LIFETIME_SECONDS = 12;
public Vex() {
super(EntityType.VEX);
this.setItemInMainHand(ItemStack.of(Material.GOLDEN_SWORD));
this.setMaxHealth(4);
this.setDamage(1);
this.setExperience(2);
this.setDropChance(0);
this.scheduler().scheduleTask(() -> {
if(this.isDead() || this.isRemoved()) return;
this.particle(Particle.POOF, this.getPosition(), 0.3f, 5);
this.remove();
}, TaskSchedule.seconds(LIFETIME_SECONDS), TaskSchedule.stop());
}
@Override
public int maxAlive(int players) {
return 2 + players / 2;
}
}
@@ -0,0 +1,75 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.entity.metadata.monster.zombie.ZombieMeta;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
public class Zombie extends ArenaMob {
private enum Variant {
NORMAL, BABY, ARMORED, EXPLODER, EXPERIENCE;
static Variant random() {
double roll = Math.random();
if(roll < 0.10) return EXPLODER;
if(roll < 0.18) return EXPERIENCE;
if(roll < 0.33) return BABY;
if(roll < 0.45) return ARMORED;
return NORMAL;
}
}
private final Variant variant = Variant.random();
public Zombie() {
super(EntityType.ZOMBIE);
this.setMaxHealth(14);
switch(this.variant) {
case BABY -> {
((ZombieMeta) this.getEntityMeta()).setBaby(true);
this.setMaxHealth(6);
this.multiplySpeed(1.4);
this.setDamage(1);
this.setPoints(2);
}
case ARMORED -> {
this.setEquipment(EquipmentSlot.HELMET, ItemStack.of(Material.IRON_HELMET));
this.setEquipment(EquipmentSlot.CHESTPLATE, ItemStack.of(Material.IRON_CHESTPLATE));
this.setItemInMainHand(ItemStack.of(Material.IRON_SWORD));
this.setMaxHealth(24);
this.setDamage(3);
this.multiplySpeed(0.8);
this.setPoints(3);
this.setDropChance(0.25);
}
case EXPLODER -> {
this.setItemInMainHand(ItemStack.of(Material.TNT));
this.setItemInOffHand(ItemStack.of(Material.FLINT_AND_STEEL));
}
case EXPERIENCE -> {
this.setItemInMainHand(ItemStack.of(Material.EXPERIENCE_BOTTLE));
this.setExperience(50);
this.setPoints(3);
this.setDropChance(0.5);
}
case NORMAL -> {
}
}
}
@Override
protected void onDeath(@NotNull EntityDeathEvent event) {
super.onDeath(event);
if(this.variant == Variant.EXPLODER && this.getInstance() instanceof MobArena arena) {
Entity killer = this.getLastDamageSource() != null ? this.getLastDamageSource().getAttacker() : null;
arena.explode(this.getPosition(), this, killer, 7, 3);
}
}
}
+12 -1
View File
@@ -195,4 +195,15 @@ description;Get to the other side as quickly as possible!;Komme so schnell wie m
ns:game_BuildBattle#;;
name;Build Battle;Bauwettbewerb
description;Build the best creation for the given theme before time runs out and win through the other players’ ratings.;Baue in der Zeit das beste Bauwerk zum Thema und gewinne durch die Bewertung der anderen Spieler.
seconds;Build Time;Bauzeit
seconds;Build Time;Bauzeit
;;
ns:game_MobArena#;;
name;Mob Arena;Mob-Arena
description;Fight hordes of monsters with a sweeping sword and collect points for every kill. Dying costs points!;Kämpfe mit deinem Schwert gegen Horden von Monstern und sammle Punkte für jeden Kill. Sterben kostet Punkte!
death;You died!;Du bist gestorben!
difficulty;Difficulty;Schwierigkeit
deathPenalty;Points lost on death;Punktabzug bei Tod
powerUps;Power-ups;Power-ups
powerUp_heal;Healed!;Geheilt!
powerUp_strength;Double damage for 10 seconds!;Doppelter Schaden für 10 Sekunden!
powerUp_speed;Speed for 10 seconds!;Schneller für 10 Sekunden!
1 map en_us de_de
195 ns:game_BuildBattle#
196 name Build Battle Bauwettbewerb
197 description Build the best creation for the given theme before time runs out and win through the other players’ ratings. Baue in der Zeit das beste Bauwerk zum Thema und gewinne durch die Bewertung der anderen Spieler.
198 seconds Build Time Bauzeit
199
200 ns:game_MobArena#
201 name Mob Arena Mob-Arena
202 description Fight hordes of monsters with a sweeping sword and collect points for every kill. Dying costs points! Kämpfe mit deinem Schwert gegen Horden von Monstern und sammle Punkte für jeden Kill. Sterben kostet Punkte!
203 death You died! Du bist gestorben!
204 difficulty Difficulty Schwierigkeit
205 deathPenalty Points lost on death Punktabzug bei Tod
206 powerUps Power-ups Power-ups
207 powerUp_heal Healed! Geheilt!
208 powerUp_strength Double damage for 10 seconds! Doppelter Schaden für 10 Sekunden!
209 powerUp_speed Speed for 10 seconds! Schneller für 10 Sekunden!