diff --git a/src/main/java/io/icker/factions/FactionsMod.java b/src/main/java/io/icker/factions/FactionsMod.java index fe654eab..3636284b 100644 --- a/src/main/java/io/icker/factions/FactionsMod.java +++ b/src/main/java/io/icker/factions/FactionsMod.java @@ -39,6 +39,7 @@ public void onInitialize() { ServerManager.register(); SoundManager.register(); WorldManager.register(); + WarManager.register(); CommandRegistrationCallback.EVENT.register(FactionsMod::registerCommands); } @@ -74,6 +75,7 @@ private static void registerCommands(CommandDispatcher disp new ModifyCommand(), new RankCommand(), new SafeCommand(), + new WarCommand(), }; for (Command command : commands) { diff --git a/src/main/java/io/icker/factions/api/events/RelationshipEvents.java b/src/main/java/io/icker/factions/api/events/RelationshipEvents.java index 68928bd9..fd1999eb 100644 --- a/src/main/java/io/icker/factions/api/events/RelationshipEvents.java +++ b/src/main/java/io/icker/factions/api/events/RelationshipEvents.java @@ -3,6 +3,7 @@ import io.icker.factions.api.persistents.Relationship; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; /** * All events related to relationships (UNIMPLEMENTED) @@ -26,6 +27,12 @@ public final class RelationshipEvents { } }); + public static final Event ON_TRESPASSING = EventFactory.createArrayBacked(OnTrespassing.class, callbacks -> (player) -> { + for (OnTrespassing callback : callbacks) { + callback.onTrespassing(player); + } + }); + @FunctionalInterface public interface NewDecleration { void onNewDecleration(Relationship relationship); @@ -40,4 +47,9 @@ public interface NewMutual { public interface EndMutual { void onEndMutual(Relationship relationship, Relationship.Status oldStatus); } + + @FunctionalInterface + public interface OnTrespassing { + void onTrespassing(ServerPlayerEntity player); + } } diff --git a/src/main/java/io/icker/factions/api/persistents/Faction.java b/src/main/java/io/icker/factions/api/persistents/Faction.java index dac69b56..4032e6a9 100644 --- a/src/main/java/io/icker/factions/api/persistents/Faction.java +++ b/src/main/java/io/icker/factions/api/persistents/Faction.java @@ -6,9 +6,13 @@ import io.icker.factions.database.Field; import io.icker.factions.database.Name; import net.minecraft.inventory.SimpleInventory; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Predicate; @Name("Faction") public class Faction { @@ -68,6 +72,7 @@ public String getKey() { return id.toString(); } + @Nullable public static Faction get(UUID id) { return STORE.get(id); } @@ -169,6 +174,19 @@ public List getUsers() { return User.getByFaction(id); } + public void sendCommandTree(PlayerManager playerManager) { + sendCommandTree(playerManager, user -> true); + } + + public void sendCommandTree(PlayerManager playerManager, Predicate filter) { + getUsers().stream().filter(filter).forEach(user -> { + ServerPlayerEntity player = playerManager.getPlayer(user.getID()); + if (player != null) { + playerManager.sendCommandTree(player); + } + }); + } + public List getClaims() { return Claim.getByFaction(id); } @@ -210,6 +228,10 @@ public boolean isMutualAllies(UUID target) { return rel.status == Relationship.Status.ALLY && getReverse(rel).status == Relationship.Status.ALLY; } + public boolean atWarWith(Faction target) { + return War.getByFactions(this, target) != null; + } + public List getMutualAllies() { return relationships.stream().filter(rel -> isMutualAllies(rel.target)).toList(); } @@ -222,6 +244,10 @@ public List getEnemiesOf() { return relationships.stream().filter(rel -> getReverse(rel).status == Relationship.Status.ENEMY).toList(); } + public List getWars() { + return War.getByFaction(this); + } + public void removeRelationship(UUID target) { relationships = new ArrayList<>(relationships.stream().filter(rel -> !rel.target.equals(target)).toList()); } diff --git a/src/main/java/io/icker/factions/api/persistents/Relationship.java b/src/main/java/io/icker/factions/api/persistents/Relationship.java index 25f808a7..54574c38 100644 --- a/src/main/java/io/icker/factions/api/persistents/Relationship.java +++ b/src/main/java/io/icker/factions/api/persistents/Relationship.java @@ -8,12 +8,15 @@ public class Relationship { public enum Status { ALLY, NEUTRAL, - ENEMY, + ENEMY } @Field("Target") public UUID target; + @Field("Aggression") + public int aggression; + @Field("Status") public Status status; diff --git a/src/main/java/io/icker/factions/api/persistents/User.java b/src/main/java/io/icker/factions/api/persistents/User.java index 5e01185f..0a705e9f 100644 --- a/src/main/java/io/icker/factions/api/persistents/User.java +++ b/src/main/java/io/icker/factions/api/persistents/User.java @@ -1,5 +1,6 @@ package io.icker.factions.api.persistents; +import io.icker.factions.FactionsMod; import io.icker.factions.api.events.FactionEvents; import io.icker.factions.database.Database; import io.icker.factions.database.Field; @@ -53,10 +54,16 @@ public enum SoundMode { @Field("Sounds") public SoundMode sounds = SoundMode.ALL; + @Field("Lives") + public int lives = FactionsMod.CONFIG.WAR != null ? FactionsMod.CONFIG.WAR.NUM_LIVES : 3; + public boolean autoclaim = false; public boolean bypass = false; public String language = "en_us"; + public boolean isTrespassing = false; + public int startedTrespassing = 0; + private User spoof; public User(UUID id) { diff --git a/src/main/java/io/icker/factions/api/persistents/War.java b/src/main/java/io/icker/factions/api/persistents/War.java new file mode 100644 index 00000000..73f93438 --- /dev/null +++ b/src/main/java/io/icker/factions/api/persistents/War.java @@ -0,0 +1,159 @@ +package io.icker.factions.api.persistents; + +import io.icker.factions.FactionsMod; +import io.icker.factions.database.Database; +import io.icker.factions.database.Field; +import io.icker.factions.database.Name; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +@Name("War") +public class War { + private static final HashMap STORE = Database.load(War.class, War::getID); + + @Field("ID") + private UUID id; + + @Field("Source") + private UUID source; + + @Field("Target") + private UUID target; + + @Field("SourceTeam") + private ArrayList sourceTeam = new ArrayList<>(); + + @Field("TargetTeam") + private ArrayList targetTeam = new ArrayList<>(); + + @Field("Name") + private String name; + + public boolean sourceReadyToEnd = false; + public boolean targetReadyToEnd = false; + + public War(Faction source, Faction target) { + this.source = source.getID(); + this.target = target.getID(); + this.targetTeam.add(target.getID()); + this.sourceTeam.add(source.getID()); + this.id = UUID.randomUUID(); + name = String.format("The %s-%s war", source.getName(), target.getName()); + + for (Faction faction : getFactions()) { + for (User user : faction.getUsers()) { + user.lives = FactionsMod.CONFIG.WAR.NUM_LIVES; + } + } + } + + public War() {} + + public String getKey() { + return id.toString(); + } + + public static War get(UUID id) { + return STORE.get(id); + } + + public static War getByFactions(Faction source, Faction target) { + UUID id = STORE + .keySet() + .stream() + .filter(warKey -> { + War war = get(warKey); + return (war.getSourceTeam().contains(source) && war.getTargetTeam().contains(target)) || (war.getSourceTeam().contains(target) && war.getTargetTeam().contains(source)); + }) + .findFirst() + .orElse(null); + + if (id == null) return null; + return War.get(id); + } + + public static List getByFaction(Faction source) { + return STORE + .keySet() + .stream() + .filter(warKey -> { + War war = get(warKey); + return war.getSourceTeam().contains(source) || war.getTargetTeam().contains(source); + }) + .map(War::get) + .toList(); + + } + + public static War getByName(String name) { + FactionsMod.LOGGER.info(all().size()); + return all().stream().filter(war -> war.getName().equals(name)).findFirst().orElse(null); + } + + public static List all() { + return STORE.keySet().stream().map(War::get).toList(); + } + + public static void add(War war) { + STORE.put(war.getID(), war); + } + + public UUID getID() { + return id; + } + + public void addSource(Faction source) { + for (User user : source.getUsers()) { + user.lives = FactionsMod.CONFIG.WAR.NUM_LIVES; + } + this.sourceTeam.add(source.getID()); + } + + public List getSourceTeam() { + return sourceTeam.stream().map(Faction::get).toList(); + } + + public Faction getSource() { + return Faction.get(source); + } + + public void addTarget(Faction target) { + for (User user : target.getUsers()) { + user.lives = FactionsMod.CONFIG.WAR.NUM_LIVES; + } + this.targetTeam.add(target.getID()); + } + + public List getTargetTeam() { + return targetTeam.stream().map(Faction::get).toList(); + } + + public Faction getTarget() { + return Faction.get(target); + } + + public String getName() { + return name; + } + + public List getFactions() { + return Stream.concat(getTargetTeam().stream(), getSourceTeam().stream()).toList(); + } + + public void end() { + STORE.remove(this.getID()); + for (Faction faction : getFactions()) { + for (User user : faction.getUsers()) { + user.lives = FactionsMod.CONFIG.WAR.NUM_LIVES; + } + } + } + + public static void save() { + Database.save(War.class, STORE.values().stream().toList()); + } +} diff --git a/src/main/java/io/icker/factions/command/AdminCommand.java b/src/main/java/io/icker/factions/command/AdminCommand.java index 0b5033a1..cb258cc4 100644 --- a/src/main/java/io/icker/factions/command/AdminCommand.java +++ b/src/main/java/io/icker/factions/command/AdminCommand.java @@ -77,6 +77,23 @@ private int power(CommandContext context) throws CommandSyn return 1; } + private int lives(CommandContext context) throws CommandSyntaxException { + ServerCommandSource source = context.getSource(); + ServerPlayerEntity player = source.getPlayer(); + + ServerPlayerEntity targetEntity = EntityArgumentType.getPlayer(context, "player"); + User target = User.get(targetEntity.getUuid()); + + int numLives = IntegerArgumentType.getInteger(context, "numLives"); + + target.lives = numLives; + + new Message("Set %s's lives to %d", targetEntity.getName().getString(), numLives).send(player, false); + new Message("Your lives have been set to %d by %s", numLives, player.getName().getString()).send(targetEntity, false); + + return 1; + } + private int spoof(CommandContext context) throws CommandSyntaxException { ServerCommandSource source = context.getSource(); ServerPlayerEntity player = source.getPlayer(); @@ -140,6 +157,17 @@ public LiteralCommandNode getNode() { ) .executes(this::clearSpoof) ) + .then( + CommandManager.literal("lives") + .requires(Requires.hasPerms("factions.admin.lives", FactionsMod.CONFIG.REQUIRED_BYPASS_LEVEL)) + .then( + CommandManager.argument("numLives", IntegerArgumentType.integer()) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes(this::lives) + ) + ) + ) .build(); } } diff --git a/src/main/java/io/icker/factions/command/DeclareCommand.java b/src/main/java/io/icker/factions/command/DeclareCommand.java index 7f61942b..69ada561 100644 --- a/src/main/java/io/icker/factions/command/DeclareCommand.java +++ b/src/main/java/io/icker/factions/command/DeclareCommand.java @@ -1,12 +1,9 @@ package io.icker.factions.command; -import java.util.Locale; - import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.tree.LiteralCommandNode; - import io.icker.factions.api.persistents.Faction; import io.icker.factions.api.persistents.Relationship; import io.icker.factions.util.Command; @@ -16,6 +13,8 @@ import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.Formatting; +import java.util.Locale; + public class DeclareCommand implements Command { private int ally(CommandContext context) throws CommandSyntaxException { return updateRelationship(context, Relationship.Status.ALLY); @@ -57,8 +56,10 @@ private int updateRelationship(CommandContext context, Rela Relationship rev = targetFaction.getRelationship(sourceFaction.getID()); sourceFaction.setRelationship(rel); + rel.aggression = 0; + Message msgStatus = rel.status == Relationship.Status.ALLY ? new Message("allies").format(Formatting.GREEN) - : rel.status == Relationship.Status.ENEMY ? new Message("enemies").format(Formatting.RED) + : rel.status == Relationship.Status.ENEMY ? new Message("enemies").format(Formatting.RED) : new Message("neutral"); if (rel.status == rev.status) { diff --git a/src/main/java/io/icker/factions/command/InfoCommand.java b/src/main/java/io/icker/factions/command/InfoCommand.java index bf64b127..644ff5ce 100644 --- a/src/main/java/io/icker/factions/command/InfoCommand.java +++ b/src/main/java/io/icker/factions/command/InfoCommand.java @@ -8,6 +8,7 @@ import io.icker.factions.FactionsMod; import io.icker.factions.api.persistents.Faction; import io.icker.factions.api.persistents.User; +import io.icker.factions.api.persistents.War; import io.icker.factions.util.Command; import io.icker.factions.util.Message; import net.minecraft.server.command.CommandManager; @@ -73,6 +74,10 @@ public static int info(ServerPlayerEntity player, Faction faction) { .map(fac -> fac.getColor() + fac.getName()) .collect(Collectors.joining(Formatting.GRAY + ", ")); + String warsJoined = Formatting.GRAY + faction.getWars().stream() + .map(War::getName) + .collect(Collectors.joining(Formatting.GRAY + ", ")); + int requiredPower = faction.getClaims().size() * FactionsMod.CONFIG.POWER.CLAIM_WEIGHT; int maxPower = users.size() * FactionsMod.CONFIG.POWER.MEMBER + FactionsMod.CONFIG.POWER.BASE; @@ -101,6 +106,9 @@ public static int info(ServerPlayerEntity player, Faction faction) { new Message(Formatting.RED + "Enemies (" + Formatting.WHITE + faction.getEnemiesWith().size() + Formatting.RED + "): ") .add(enemiesWith) .send(player, false); + new Message(Formatting.DARK_RED + "Wars in (" + Formatting.WHITE + faction.getWars().size() + Formatting.DARK_RED + "): ") + .add(warsJoined) + .send(player, false); return 1; } diff --git a/src/main/java/io/icker/factions/command/MemberCommand.java b/src/main/java/io/icker/factions/command/MemberCommand.java index 78c0b508..c2b98377 100644 --- a/src/main/java/io/icker/factions/command/MemberCommand.java +++ b/src/main/java/io/icker/factions/command/MemberCommand.java @@ -16,10 +16,30 @@ import net.minecraft.util.UserCache; import net.minecraft.util.Util; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.stream.Collector; public class MemberCommand implements Command { + private static final Collector, Message> COLLECTOR = Collector.of( + ArrayList::new, + (result, user) -> result.add(getName(user)), + (result1, result2) -> { + result1.addAll(result2); + return result1; + }, + (result) -> { + Message message = new Message(""); + if (result.size() == 0) return message; + + result.subList(0, result.size()-1).forEach(message1 -> message.add(message1).add(", ")); + message.add(result.get(result.size()-1)); + return message; + } + ); + + private static UserCache cache; + private int self(CommandContext context) throws CommandSyntaxException { ServerCommandSource source = context.getSource(); ServerPlayerEntity player = source.getPlayer(); @@ -48,36 +68,32 @@ private int any(CommandContext context) throws CommandSynta return members(player, faction); } + private static Message getName(User user) { + return new Message(cache.getByUuid(user.getID()).orElse(new GameProfile(Util.NIL_UUID, "{Uncached Player}")).getName()).hover(String.format("Lives: %d", user.lives)); + } + public static int members(ServerPlayerEntity player, Faction faction) { List users = faction.getUsers(); - UserCache cache = player.getServer().getUserCache(); + cache = player.getServer().getUserCache(); long memberCount = users.stream().filter(u -> u.rank == User.Rank.MEMBER).count(); - String members = Formatting.WHITE + - users.stream() + Message members = users.stream() .filter(u -> u.rank == User.Rank.MEMBER) - .map(user -> cache.getByUuid(user.getID()).orElse(new GameProfile(Util.NIL_UUID, "{Uncached Player}")).getName()) - .collect(Collectors.joining(", ")); + .collect(COLLECTOR).format(Formatting.WHITE); long commanderCount = users.stream().filter(u -> u.rank == User.Rank.COMMANDER).count(); - String commanders = Formatting.WHITE + - users.stream() + Message commanders = users.stream() .filter(u -> u.rank == User.Rank.COMMANDER) - .map(user -> cache.getByUuid(user.getID()).orElse(new GameProfile(Util.NIL_UUID, "{Uncached Player}")).getName()) - .collect(Collectors.joining(", ")); + .collect(COLLECTOR).format(Formatting.WHITE); long leaderCount = users.stream().filter(u -> u.rank == User.Rank.LEADER).count(); - String leaders = Formatting.WHITE + - users.stream() + Message leaders = users.stream() .filter(u -> u.rank == User.Rank.LEADER) - .map(user -> cache.getByUuid(user.getID()).orElse(new GameProfile(Util.NIL_UUID, "{Uncached Player}")).getName()) - .collect(Collectors.joining(", ")); + .collect(COLLECTOR).format(Formatting.WHITE); - String owner = Formatting.WHITE + - users.stream() + Message owner = users.stream() .filter(u -> u.rank == User.Rank.OWNER) - .map(user -> cache.getByUuid(user.getID()).orElse(new GameProfile(Util.NIL_UUID, "{Uncached Player}")).getName()) - .collect(Collectors.joining(", ")); + .collect(COLLECTOR).format(Formatting.WHITE); // generate the --- int numDashes = 32 - faction.getName().length(); diff --git a/src/main/java/io/icker/factions/command/WarCommand.java b/src/main/java/io/icker/factions/command/WarCommand.java new file mode 100644 index 00000000..415a85f4 --- /dev/null +++ b/src/main/java/io/icker/factions/command/WarCommand.java @@ -0,0 +1,195 @@ +package io.icker.factions.command; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import io.icker.factions.FactionsMod; +import io.icker.factions.api.persistents.Faction; +import io.icker.factions.api.persistents.User; +import io.icker.factions.api.persistents.War; +import io.icker.factions.core.WarManager; +import io.icker.factions.util.Command; +import io.icker.factions.util.Message; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +import static net.minecraft.util.Formatting.RED; +import static net.minecraft.util.Formatting.RESET; + +public class WarCommand implements Command { + private int declare (CommandContext context) { + if (FactionsMod.CONFIG.WAR == null) return 0; + + String name = StringArgumentType.getString(context, "faction"); + ServerCommandSource source = context.getSource(); + ServerPlayerEntity player = source.getPlayer(); + + Faction targetFaction = Faction.getByName(name); + + if (targetFaction == null) { + new Message("Cannot go to war with a nonexistent faction").fail().send(player, false); + return 0; + } + + Faction sourceFaction = Command.getUser(player).getFaction(); + + if (sourceFaction.equals(targetFaction)) { + new Message("Cannot declare war on yourself").fail().send(player, false); + return 0; + } + + if (War.getByFactions(sourceFaction, targetFaction) != null) { + new Message("You are already at war with that faction").fail().send(player, false); + return 0; + } + + targetFaction.getRelationship(sourceFaction.getID()).aggression = 0; + sourceFaction.getRelationship(targetFaction.getID()).aggression = 0; + + War.add(new War(sourceFaction, targetFaction)); + + new Message("You have declared " + RED + "war" + RESET + " on " + targetFaction.getName()).send(sourceFaction); + + PlayerManager playerManager = player.getServer().getPlayerManager(); + sourceFaction.sendCommandTree(playerManager, user -> (user.rank == User.Rank.LEADER || user.rank == User.Rank.OWNER) && playerManager.getPlayer(user.getID()) != null); + + sourceFaction.getUsers().forEach(user -> user.lives = FactionsMod.CONFIG.WAR.NUM_LIVES); + + new Message(sourceFaction.getName() + " have declared " + RED + "war" + RESET + " on you") + .send(targetFaction); + + return 1; + } + + private int join (CommandContext context) { + if (FactionsMod.CONFIG.WAR == null) return 0; + String name = StringArgumentType.getString(context, "war"); + ServerCommandSource source = context.getSource(); + ServerPlayerEntity player = source.getPlayer(); + + War war = War.getByName(name); + FactionsMod.LOGGER.info(war); + Faction faction = User.get(player.getUuid()).getFaction(); + + if (war == null) { + new Message("Couldn't find that war").fail().send(player, false); + return 0; + } + + boolean alliesOnTarget = war.getTargetTeam().stream().anyMatch(faction1 -> faction.isMutualAllies(faction1.getID())); + boolean alliesOnSource = war.getSourceTeam().stream().anyMatch(faction1 -> faction.isMutualAllies(faction1.getID())); + + if (alliesOnSource && alliesOnTarget) { + new Message("Since you have allies on both sides you cannot take part in this war").fail().send(player, false); + return 0; + } + + String teamName; + + if (alliesOnTarget) { + war.addTarget(faction); + teamName = war.getTarget().getName(); + } else if (alliesOnSource) { + war.addSource(faction); + teamName = war.getSource().getName(); + } else { + new Message("You must be allied with someone in that war to join").fail().send(player, false); + return 0; + } + + new Message("You have joined %s in the %s", teamName, war.getName()).send(faction); + + PlayerManager playerManager = player.getServer().getPlayerManager(); + faction.sendCommandTree(playerManager, user -> (user.rank == User.Rank.LEADER || user.rank == User.Rank.OWNER) && playerManager.getPlayer(user.getID()) != null); + + faction.getUsers().forEach(user -> user.lives = FactionsMod.CONFIG.WAR.NUM_LIVES); + + new Message("%s have joined the war", faction.getName()) + .send(war); + + return 1; + } + + private int end (CommandContext context) { + String name = StringArgumentType.getString(context, "war"); + ServerCommandSource source = context.getSource(); + ServerPlayerEntity player = source.getPlayer(); + + War war = War.getByName(name); + Faction faction = User.get(player.getUuid()).getFaction(); + + if (war == null) { + new Message("Could not find the war").fail().send(player, false); + return 0; + } + + boolean isSource = war.getSource().equals(faction); + + if (!isSource && !war.getTarget().equals(faction)) { + new Message("You cannot end the war").fail().send(player, false); + return 0; + } + + Faction other = isSource ? war.getTarget() : war.getSource(); + + if ((isSource && !war.targetReadyToEnd) || (!isSource && !war.sourceReadyToEnd)) { + if (isSource) war.sourceReadyToEnd = true; + else war.targetReadyToEnd = true; + + PlayerManager playerManager = player.getServer().getPlayerManager(); + other.sendCommandTree(playerManager, user -> (user.rank == User.Rank.LEADER || user.rank == User.Rank.OWNER) && playerManager.getPlayer(user.getID()) != null); + + + new Message("%s must also agree to end the war (they have been asked if they want to end it)", other.getName()).send(player, false); + new Message("%s would like to end the war", faction.getName()).hover("Click to end the war").click(String.format("/f war end %s", war.getName())).send(other); + return 0; + } + + war.end(); + + new Message("You are no longer at war with " + other.getName()).send(faction); + new Message(faction.getName() + " has ended the war").send(other); + + PlayerManager playerManager = player.getServer().getPlayerManager(); + faction.sendCommandTree(playerManager, user -> (user.rank == User.Rank.LEADER || user.rank == User.Rank.OWNER) && playerManager.getPlayer(user.getID()) != null); + other.sendCommandTree(playerManager, user -> (user.rank == User.Rank.LEADER || user.rank == User.Rank.OWNER) && playerManager.getPlayer(user.getID()) != null); + + return 1; + } + + @Override + public LiteralCommandNode getNode() { + return CommandManager + .literal("war") + .requires(Requires.multiple(Requires.isLeader(), (source) -> FactionsMod.CONFIG.WAR != null, Requires.hasPerms("factions.war", 0))) + .then( + CommandManager.literal("declare") + .requires(Requires.multiple(WarManager::eligibleToStartWar, Requires.hasPerms("factions.war.declare", 0))) + .then( + CommandManager.argument("faction", StringArgumentType.greedyString()) + .suggests(Suggests.eligibleForWar()) + .executes(this::declare) + ) + ) + .then( + CommandManager.literal("join") + .requires(Requires.multiple(WarManager::eligibleToJoinWar, Requires.hasPerms("factions.war.join", 0))) + .then( + CommandManager.argument("war", StringArgumentType.greedyString()) + .suggests(Suggests.joinableWars()) + .executes(this::join) + ) + ) + .then( + CommandManager.literal("end") + .requires(Requires.multiple(WarManager::eligibleToEndWar, Requires.hasPerms("factions.war.end", 0))) + .then( + CommandManager.argument("war", StringArgumentType.greedyString()) + .suggests(Suggests.atWar()) + .executes(this::end) + ) + ).build(); + } +} diff --git a/src/main/java/io/icker/factions/config/Config.java b/src/main/java/io/icker/factions/config/Config.java index 715b8f24..21dfc156 100644 --- a/src/main/java/io/icker/factions/config/Config.java +++ b/src/main/java/io/icker/factions/config/Config.java @@ -24,6 +24,7 @@ public static Config load() { .registerTypeAdapter(HomeConfig.class, new Deserializer<>(HomeConfig.class)) .registerTypeAdapter(PowerConfig.class, new Deserializer<>(PowerConfig.class)) .registerTypeAdapter(SafeConfig.class, new Deserializer<>(SafeConfig.class)) + .registerTypeAdapter(WarConfig.class, new Deserializer<>(WarConfig.class)) .create(); try { @@ -66,6 +67,10 @@ public static Config load() { @Nullable public HomeConfig HOME = new HomeConfig(); + @SerializedName("war") + @Nullable + public WarConfig WAR = new WarConfig(); + @SerializedName("display") public DisplayConfig DISPLAY = new DisplayConfig(); diff --git a/src/main/java/io/icker/factions/config/WarConfig.java b/src/main/java/io/icker/factions/config/WarConfig.java new file mode 100644 index 00000000..0a8e080f --- /dev/null +++ b/src/main/java/io/icker/factions/config/WarConfig.java @@ -0,0 +1,20 @@ +package io.icker.factions.config; + +import com.google.gson.annotations.SerializedName; + +public class WarConfig { + @SerializedName("attackAggression") + public int ATTACK_AGGRESSION = 50; + + @SerializedName("aggressionLevelForWar") + public int AGGRESSION_LEVEL = 100; + + @SerializedName("trespassingTime") + public int TRESPASSING_TIME = 70; + + @SerializedName("trespassingAggression") + public int TRESPASSING_AGGRESSION = 7; + + @SerializedName("numLives") + public int NUM_LIVES = 2; +} diff --git a/src/main/java/io/icker/factions/core/InteractionManager.java b/src/main/java/io/icker/factions/core/InteractionManager.java index eb83a36b..e8a16273 100644 --- a/src/main/java/io/icker/factions/core/InteractionManager.java +++ b/src/main/java/io/icker/factions/core/InteractionManager.java @@ -150,6 +150,8 @@ private static ActionResult checkPermissions(PlayerEntity player, BlockPos posit return ActionResult.PASS; } + if (user.lives <= 0) return ActionResult.FAIL; + String dimension = world.getRegistryKey().getValue().toString(); ChunkPos chunkPosition = world.getChunk(position).getPos(); @@ -168,7 +170,11 @@ private static ActionResult checkPermissions(PlayerEntity player, BlockPos posit Faction userFaction = user.getFaction(); - if (claimFaction == userFaction && getRankLevel(claim.accessLevel) <= getRankLevel(user.rank)) { + if (claimFaction.equals(userFaction) && getRankLevel(claim.accessLevel) <= getRankLevel(user.rank)) { + return ActionResult.PASS; + } + + if (userFaction.atWarWith(claimFaction)) { return ActionResult.PASS; } diff --git a/src/main/java/io/icker/factions/core/ServerManager.java b/src/main/java/io/icker/factions/core/ServerManager.java index 6b5fa080..095d078d 100644 --- a/src/main/java/io/icker/factions/core/ServerManager.java +++ b/src/main/java/io/icker/factions/core/ServerManager.java @@ -4,6 +4,7 @@ import io.icker.factions.api.persistents.Claim; import io.icker.factions.api.persistents.Faction; import io.icker.factions.api.persistents.User; +import io.icker.factions.api.persistents.War; import io.icker.factions.util.Message; import net.fabricmc.fabric.api.networking.v1.PacketSender; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; @@ -21,6 +22,7 @@ private static void save(MinecraftServer server) { Claim.save(); Faction.save(); User.save(); + War.save(); } private static void playerJoin(ServerPlayNetworkHandler handler, PacketSender sender, MinecraftServer server) { diff --git a/src/main/java/io/icker/factions/core/WarManager.java b/src/main/java/io/icker/factions/core/WarManager.java new file mode 100644 index 00000000..4226288a --- /dev/null +++ b/src/main/java/io/icker/factions/core/WarManager.java @@ -0,0 +1,123 @@ +package io.icker.factions.core; + +import io.icker.factions.FactionsMod; +import io.icker.factions.api.events.PlayerEvents; +import io.icker.factions.api.events.RelationshipEvents; +import io.icker.factions.api.persistents.*; +import io.icker.factions.api.persistents.User.Rank; +import io.icker.factions.util.Message; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.math.ChunkPos; + +import static net.minecraft.util.Formatting.RED; +import static net.minecraft.util.Formatting.RESET; + +public class WarManager { + public static void register() { + PlayerEvents.ON_KILLED_BY_PLAYER.register(WarManager::onKilled); + PlayerEvents.ON_MOVE.register(WarManager::onMove); + RelationshipEvents.ON_TRESPASSING.register(WarManager::onTrespassing); + } + + private static void onKilled(ServerPlayerEntity player, DamageSource source) { + Faction attackingFaction = User.get(source.getSource().getUuid()).getFaction(); + User killed = User.get(player.getUuid()); + Faction targetFaction = killed.getFaction(); + + if (attackingFaction == null || targetFaction == null) return; + + Relationship rel = attackingFaction.getRelationship(targetFaction.getID()); + Relationship rev = attackingFaction.getReverse(rel); + + if (rel.status == Relationship.Status.ENEMY && rev.status == Relationship.Status.ENEMY && FactionsMod.CONFIG.WAR != null && !targetFaction.atWarWith(attackingFaction)) { + rev.aggression += FactionsMod.CONFIG.WAR.ATTACK_AGGRESSION; + if (rev.aggression >= FactionsMod.CONFIG.WAR.AGGRESSION_LEVEL) { + PlayerManager playerManager = player.getServer().getPlayerManager(); + + targetFaction.sendCommandTree(playerManager, user -> (user.rank == Rank.LEADER || user.rank == Rank.OWNER) && playerManager.getPlayer(user.getID()) != null); + + new Message("Your faction is now eligible to go to " + RED + "war" + RESET + " with %s", attackingFaction.getName()).hover("Click to go to war").click(String.format("/f war declare %s", attackingFaction.getName())).send(targetFaction); + } + } else if (War.getByFactions(attackingFaction, targetFaction) != null) { + killed.lives--; + } + } + + private static void onTrespassing(ServerPlayerEntity player) { + if (FactionsMod.CONFIG.WAR == null) return; + + User user = User.get(player.getUuid()); + + String dimension = player.world.getRegistryKey().getValue().toString(); + ChunkPos chunkPosition = player.getChunkPos(); + + Claim claim = Claim.get(chunkPosition.x, chunkPosition.z, dimension); + + claim.getFaction().getRelationship(user.getFaction().getID()).aggression += FactionsMod.CONFIG.WAR.TRESPASSING_AGGRESSION; + } + + private static void onMove(ServerPlayerEntity player) { + if (FactionsMod.CONFIG.WAR == null) return; + + User user = User.get(player.getUuid()); + + if (!user.isInFaction()) return; + + String dimension = player.world.getRegistryKey().getValue().toString(); + ChunkPos chunkPosition = player.getChunkPos(); + + Claim claim = Claim.get(chunkPosition.x, chunkPosition.z, dimension); + + if (claim == null) return; + + if (claim.getFaction().getRelationship(user.getFaction().getID()).status == Relationship.Status.ENEMY && !user.isTrespassing) { + user.startedTrespassing = player.age; + } + + user.isTrespassing = claim.getFaction().getRelationship(user.getFaction().getID()).status == Relationship.Status.ENEMY; + } + + public static boolean eligibleToStartWar(ServerCommandSource source) { + if (FactionsMod.CONFIG.WAR == null) return false; + + ServerPlayerEntity player = source.getPlayer(); + if (source.getPlayer() == null) return false; + + User user = User.get(player.getUuid()); + + if (!user.isInFaction()) return false; + + return user.getFaction().getEnemiesWith().stream().anyMatch(rel -> (rel.aggression >= FactionsMod.CONFIG.WAR.AGGRESSION_LEVEL)); + } + + public static boolean eligibleToJoinWar(ServerCommandSource source) { + if (FactionsMod.CONFIG.WAR == null) return false; + + ServerPlayerEntity player = source.getPlayer(); + if (source.getPlayer() == null) return false; + + User user = User.get(player.getUuid()); + + if (!user.isInFaction()) return false; + + return War.all().stream().anyMatch(war -> user.getFaction().getMutualAllies().stream().map(rel -> Faction.get(rel.target)).anyMatch(faction -> war.getSourceTeam().contains(faction) || war.getTargetTeam().contains(faction))); + } + + public static boolean eligibleToEndWar(ServerCommandSource source) { + if (FactionsMod.CONFIG.WAR == null) return false; + + ServerPlayerEntity player = source.getPlayer(); + if (source.getPlayer() == null) return false; + + User user = User.get(player.getUuid()); + + if (!user.isInFaction()) return false; + + Faction faction = user.getFaction(); + + return faction.getWars().stream().anyMatch(war -> war.getSource().equals(faction) || war.getTarget().equals(faction)); + } +} diff --git a/src/main/java/io/icker/factions/mixin/ServerPlayerEntityMixin.java b/src/main/java/io/icker/factions/mixin/ServerPlayerEntityMixin.java index 6c924c8d..b34da831 100644 --- a/src/main/java/io/icker/factions/mixin/ServerPlayerEntityMixin.java +++ b/src/main/java/io/icker/factions/mixin/ServerPlayerEntityMixin.java @@ -2,6 +2,7 @@ import io.icker.factions.FactionsMod; import io.icker.factions.api.events.PlayerEvents; +import io.icker.factions.api.events.RelationshipEvents; import io.icker.factions.api.persistents.Faction; import io.icker.factions.api.persistents.User; import io.icker.factions.util.Message; @@ -23,7 +24,6 @@ @Mixin(ServerPlayerEntity.class) public abstract class ServerPlayerEntityMixin extends LivingEntity { - protected ServerPlayerEntityMixin(EntityType entityType, World world) { super(entityType, world); } @@ -43,6 +43,10 @@ public void onDeath(DamageSource source, CallbackInfo info) { @Inject(at = @At("HEAD"), method = "tick") public void tick(CallbackInfo info) { + User user = User.get(((ServerPlayerEntity) (Object) this).getUuid()); + if (FactionsMod.CONFIG.WAR != null && user.isTrespassing && (age - user.startedTrespassing) % FactionsMod.CONFIG.WAR.TRESPASSING_TIME == 0) + RelationshipEvents.ON_TRESPASSING.invoker().onTrespassing((ServerPlayerEntity) (Object) this); + if (age % FactionsMod.CONFIG.POWER.POWER_TICKS.TICKS != 0 || age == 0) return; PlayerEvents.ON_POWER_TICK.invoker().onPowerTick((ServerPlayerEntity) (Object) this); } diff --git a/src/main/java/io/icker/factions/util/Command.java b/src/main/java/io/icker/factions/util/Command.java index 53b2bb88..8042f909 100644 --- a/src/main/java/io/icker/factions/util/Command.java +++ b/src/main/java/io/icker/factions/util/Command.java @@ -1,18 +1,18 @@ package io.icker.factions.util; -import java.util.function.Predicate; - import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.tree.LiteralCommandNode; - import io.icker.factions.FactionsMod; import io.icker.factions.api.persistents.Faction; import io.icker.factions.api.persistents.User; +import io.icker.factions.api.persistents.War; import me.lucko.fabric.api.permissions.v0.Permissions; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; +import java.util.function.Predicate; + public interface Command { public LiteralCommandNode getNode(); @@ -86,6 +86,41 @@ public static SuggestionProvider allFactions(boolean includ ); } + public static SuggestionProvider atWar() { + return suggest(user -> + user.getFaction().getWars() + .stream() + .map(War::getName) + .toArray(String[]::new) + ); + } + + public static SuggestionProvider joinableWars() { + return suggest(user -> War.all() + .stream() + .filter(war -> + user.getFaction().getMutualAllies() + .stream() + .map(rel -> Faction.get(rel.target)) + .anyMatch(faction -> war.getSourceTeam().contains(faction) || war.getTargetTeam().contains(faction)) + ).map(War::getName) + .toArray(String[]::new) + + ); + } + + public static SuggestionProvider eligibleForWar() { + return suggest(user -> { + Faction source = user.getFaction(); + return source.getEnemiesWith() + .stream() + .filter(rel -> rel.aggression >= FactionsMod.CONFIG.WAR.AGGRESSION_LEVEL) + .map(rel -> Faction.get(rel.target).getName()) + .toArray(String[]::new); + } + ); + } + public static SuggestionProvider openFactions() { return suggest(user -> Faction.all() diff --git a/src/main/java/io/icker/factions/util/Message.java b/src/main/java/io/icker/factions/util/Message.java index cffc35cd..259b4c7a 100644 --- a/src/main/java/io/icker/factions/util/Message.java +++ b/src/main/java/io/icker/factions/util/Message.java @@ -2,6 +2,7 @@ import io.icker.factions.api.persistents.Faction; import io.icker.factions.api.persistents.User; +import io.icker.factions.api.persistents.War; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.PlayerManager; import net.minecraft.server.network.ServerPlayerEntity; @@ -72,6 +73,14 @@ public Message send(Faction faction) { return this; } + public Message send(War war) { + Message message = this.prependMessage(new Message(war.getName()).format(Formatting.RED)); + for (Faction faction : war.getFactions()) { + message.sendToFactionChat(faction); + } + return this; + } + public void sendToGlobalChat() { for (ServerPlayerEntity player : manager.getPlayerList()) { User.ChatMode option = User.get(player.getUuid()).chat; @@ -87,8 +96,12 @@ public void sendToFactionChat(Faction faction) { } public Message prependFaction(Faction faction) { + return prependMessage(new Message(faction.getColor().toString() + Formatting.BOLD + faction.getName()).hover(faction.getDescription())); + } + + public Message prependMessage(Message message) { text = new Message("") - .add(new Message(faction.getColor().toString() + Formatting.BOLD + faction.getName()).hover(faction.getDescription())) + .add(message) .filler("ยป") .raw() .append(text);