diff --git a/src/main/java/com/projectswg/holocore/intents/CivilWarPointIntent.java b/src/main/java/com/projectswg/holocore/intents/CivilWarPointIntent.java new file mode 100644 index 000000000..e64314f1c --- /dev/null +++ b/src/main/java/com/projectswg/holocore/intents/CivilWarPointIntent.java @@ -0,0 +1,59 @@ +/*********************************************************************************** + * Copyright (c) 2018 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * 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 Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * 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. * + * * + * 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 Holocore. If not, see . * + ***********************************************************************************/ +package com.projectswg.holocore.intents; + +import com.projectswg.common.control.Intent; +import com.projectswg.holocore.resources.objects.player.PlayerObject; + +import javax.annotation.Nonnull; + +public class CivilWarPointIntent extends Intent { + + private final PlayerObject receiver; + private final int points; + + @Nonnull + private CivilWarPointIntent(PlayerObject receiver, int points) { + this.receiver = receiver; + this.points = points; + } + + @Nonnull + public PlayerObject getReceiver() { + return receiver; + } + + public int getPoints() { + return points; + } + + @Nonnull + public static void broadcast(PlayerObject receiver, int points) { + new CivilWarPointIntent(receiver, points).broadcast(); + } + +} diff --git a/src/main/java/com/projectswg/holocore/resources/commands/callbacks/QaToolCmdCallback.java b/src/main/java/com/projectswg/holocore/resources/commands/callbacks/QaToolCmdCallback.java index 76a38ba2d..509b8d6ae 100644 --- a/src/main/java/com/projectswg/holocore/resources/commands/callbacks/QaToolCmdCallback.java +++ b/src/main/java/com/projectswg/holocore/resources/commands/callbacks/QaToolCmdCallback.java @@ -32,6 +32,8 @@ import com.projectswg.common.data.location.Terrain; import com.projectswg.common.data.sui.SuiEvent; import com.projectswg.common.debug.Log; +import com.projectswg.holocore.intents.CivilWarPointIntent; +import com.projectswg.holocore.resources.objects.player.PlayerObject; import com.projectswg.holocore.scripts.commands.admin.qatool.QaToolDetails; import com.projectswg.holocore.intents.chat.SystemMessageIntent; import com.projectswg.holocore.intents.experience.ExperienceIntent; @@ -102,6 +104,9 @@ public class QaToolCmdCallback implements ICmdCallback { else SystemMessageIntent.broadcastPersonal(player, "QATool XP: Expected format: /qatool xp "); break; + case "gcw": + grantGcw(player, command[1]); + break; default: displayMainWindow(player); break; @@ -226,6 +231,19 @@ public class QaToolCmdCallback implements ICmdCallback { } } + private void grantGcw(Player player, String pointsArg) { + PlayerObject receiver = player.getCreatureObject().getPlayerObject(); + + try { + int points = Integer.parseInt(pointsArg); + CivilWarPointIntent.broadcast(receiver, points); + Log.i("GCW command: %s gave themselves %d GCW points", player.getUsername(), points); + } catch (NumberFormatException e) { + SystemMessageIntent.broadcastPersonal(player, String.format("GCW command: %s is not a number", pointsArg)); + } + + } + /* Utility Methods */ private void createMessageBox(Player player, String title, String prompt) { diff --git a/src/main/java/com/projectswg/holocore/services/faction/CivilWarService.java b/src/main/java/com/projectswg/holocore/services/faction/CivilWarService.java new file mode 100644 index 000000000..b6492a863 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/faction/CivilWarService.java @@ -0,0 +1,406 @@ +/************************************************************************************ + * Copyright (c) 2015 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * 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 Holocore. * + * * + * -------------------------------------------------------------------------------- * + * * + * 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. * + * * + * 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 Holocore. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.holocore.services.faction; + +import com.projectswg.common.concurrency.PswgScheduledThreadPool; +import com.projectswg.common.control.Service; +import com.projectswg.common.data.encodables.oob.ProsePackage; +import com.projectswg.common.data.encodables.oob.StringId; +import com.projectswg.common.data.encodables.tangible.PvpFaction; +import com.projectswg.common.data.encodables.tangible.PvpStatus; +import com.projectswg.common.debug.Log; +import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectMessage; +import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage; +import com.projectswg.holocore.intents.CivilWarPointIntent; +import com.projectswg.holocore.intents.chat.SystemMessageIntent; +import com.projectswg.holocore.intents.combat.CreatureKilledIntent; +import com.projectswg.holocore.intents.object.DestroyObjectIntent; +import com.projectswg.holocore.intents.object.ObjectCreatedIntent; +import com.projectswg.holocore.resources.objects.SWGObject; +import com.projectswg.holocore.resources.objects.creature.CreatureDifficulty; +import com.projectswg.holocore.resources.objects.creature.CreatureObject; +import com.projectswg.holocore.resources.objects.player.PlayerObject; +import com.projectswg.holocore.resources.server_info.SdbLoader; +import com.projectswg.holocore.resources.server_info.StandardLog; + +import java.io.File; +import java.io.IOException; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +final class CivilWarService extends Service { + + private static final int IMPERIAL_INDEX = 0; + private static final int REBEL_INDEX = 1; + + private final Map rankAbilities; + private final Set playerObjects; + private final PswgScheduledThreadPool threadPool; + private final DayOfWeek updateWeekDay; + private final LocalTime updateTime; + private final ZoneOffset updateOffset; + private int rankEpoch; + + CivilWarService() { + rankAbilities = new HashMap<>(); + playerObjects = ConcurrentHashMap.newKeySet(); + threadPool = new PswgScheduledThreadPool(1, "civil-war-service"); + // Rank update time is the night between Thursday and Friday at 00:00 UTC + updateWeekDay = DayOfWeek.FRIDAY; + updateTime = LocalTime.MIDNIGHT; + updateOffset = ZoneOffset.UTC; + + loadRankAbilities(); + + registerForIntent(CivilWarPointIntent.class, this::handleCivilWarPointIntent); + registerForIntent(CreatureKilledIntent.class, this::handleCreatureKilledIntent); + registerForIntent(DestroyObjectIntent.class, this::handleDestroyObjectIntent); + registerForIntent(ObjectCreatedIntent.class, this::handleObjectCreatedIntent); + } + + @Override + public boolean initialize() { + threadPool.start(); + scheduleRankUpdate(); + + return super.initialize(); + } + + @Override + public boolean terminate() { + threadPool.stop(); // To stop more threads from running + threadPool.awaitTermination(1000); // Waits for them to complete executing + + return super.terminate(); + } + + int leftoverPoints(float progress, int points) { + return (int) ((progress - 100) / 100 * (float) points); + } + + boolean isRankDown(float oldProgress, float newProgress) { + return oldProgress + newProgress < 0; + } + + int nextUpdateTime(LocalDate now) { + LocalDate nextUpdateDate = now.with(updateWeekDay); + + return (int) nextUpdateDate.toEpochSecond(updateTime, updateOffset); + } + + boolean isDecayRank(int rank) { + return rank >= 7; // Lieutenant and above has decay + } + + float rankProgress(float currentProgress, float decay, int currentRank, int points) { + float progress = currentProgress - decay; + + progress += (float) points / (currentRank * 100.0f); + + return progress; + } + + boolean isFactionEligible(PvpFaction killerFaction, PvpFaction corpseFaction, boolean specialForces) { + return killerFaction != PvpFaction.NEUTRAL && corpseFaction != PvpFaction.NEUTRAL && killerFaction != corpseFaction && specialForces; + } + + byte makeMultiplier(boolean specialForces, boolean player) { + byte multiplier = 1; + + if (specialForces) { + multiplier += 1; + } + + if (player) { + multiplier += 18; + } + + return multiplier; + } + + int baseForDifficulty(CreatureDifficulty difficulty) { + switch (difficulty) { + case NORMAL: + return 5; + case ELITE: + return 10; + case BOSS: + return 15; + default: + throw new IllegalArgumentException("Unhandled CreatureDifficulty: " + difficulty); + } + } + + int pointsGranted(int base, byte multiplier) { + return base * multiplier; + } + + private void loadRankAbilities() { + String what = "rank abilities"; + long startTime = StandardLog.onStartLoad(what); + + try (SdbLoader.SdbResultSet set = SdbLoader.load(new File("serverdata/gcw/abilities.sdb"))) { + while (set.next()) { + int rank = (int) set.getInt("rank"); + String imperial = set.getText("imperial"); + String rebel = set.getText("rebel"); + String[] abilities = new String[2]; + + abilities[IMPERIAL_INDEX] = imperial; + abilities[REBEL_INDEX] = rebel; + + rankAbilities.put(rank, abilities); + } + } catch (IOException e) { + Log.e(e); + } + + StandardLog.onEndLoad(rankAbilities.size(), what, startTime); + } + + private void scheduleRankUpdate() { + LocalDate now = LocalDate.now(); + int nowEpoch = (int) now.toEpochSecond(updateTime, updateOffset); + rankEpoch = nextUpdateTime(now); // Is in the future + int delay = (rankEpoch - nowEpoch) * 1000; // Must be milliseconds + + threadPool.execute(delay, this::updateRanks); + playerObjects.forEach(this::updateTimer); + } + + private void updateTimer(PlayerObject playerObject) { + playerObject.setGcwNextUpdate(rankEpoch); + } + + private void changeRank(PlayerObject playerObject, int newRank) { + assert (newRank < 1 || newRank > 12); + + int oldRank = playerObject.getCurrentRank(); + + assert (oldRank > 1); + + int highestRank = Math.max(oldRank, newRank); + int lowestRank = Math.min(oldRank, newRank); + CreatureObject creature = (CreatureObject) playerObject.getParent(); + PvpFaction faction = creature.getPvpFaction(); + int abilityIndex = faction == PvpFaction.IMPERIAL ? IMPERIAL_INDEX : REBEL_INDEX; + + playerObject.setCurrentRank(newRank); + + if (oldRank > newRank) { + // They've been demoted + for (int rank = highestRank; rank >= lowestRank; rank--) { + if (!isDecayRank(rank)) { + break; + } + + String ability = rankAbilities.get(rank)[abilityIndex]; + + creature.removeAbility(ability); + } + } else if (oldRank < newRank) { + // They've been promoted + for (int rank = lowestRank; rank <= highestRank; rank++) { + if (!isDecayRank(rank)) { + continue; + } + + String ability = rankAbilities.get(rank)[abilityIndex]; + + creature.addAbility(ability); + } + } + } + + private void updateRank(PlayerObject playerObject) { + int currentRank = playerObject.getCurrentRank(); + float oldProgress = playerObject.getRankProgress(); + int points = playerObject.getGcwPoints(); + float decay = 0; + + if (isDecayRank(currentRank)) { + decay = currentRank; + } + + float newProgress = rankProgress(oldProgress, decay, currentRank, points); + + if (newProgress >= 100) { + int promotion = playerObject.getCurrentRank() + 1; + + changeRank(playerObject, promotion); + + // Leftover points carry over + int leftoverPoints = leftoverPoints(newProgress, points); + + // Calculate progress within the new rank using leftover points + float nextRankProgress = rankProgress(newProgress, 0, promotion, leftoverPoints); + + playerObject.setRankProgress(nextRankProgress); + } else if (newProgress > 0) { + // Set their new progress + playerObject.setRankProgress(newProgress); + } else if (newProgress < 0 && isRankDown(oldProgress, newProgress)) { + int demotion = playerObject.getCurrentRank() - 1; + + changeRank(playerObject, demotion); + + // Push their progress backwards in the new rank + int leftoverPoints = leftoverPoints(newProgress, points); + + // Calculate progress within the new rank using leftover points + float nextRankProgress = 100 - rankProgress(newProgress, 0, demotion, leftoverPoints); + + playerObject.setRankProgress(nextRankProgress); + } + + int kills = playerObject.getPvpKills(); + + // Reset current kills and points + playerObject.setGcwPoints(0); + playerObject.setPvpKills(0); + + // Add current stats to lifetime stats + playerObject.setLifetimeGcwPoints(playerObject.getLifetimeGcwPoints() + points); + playerObject.setLifetimePvpKills(playerObject.getLifetimePvpKills() + kills); + + // TODO award GCW abilities + } + + private void updateRanks() { + playerObjects.forEach(playerObject -> { + if (playerObject.getCurrentRank() > 0) + updateRank(playerObject); + }); + + scheduleRankUpdate(); // Schedule next rank update + } + + private void grantPoints(ProsePackage prose, PlayerObject receiver, int points) { + receiver.setGcwPoints(receiver.getGcwPoints() + points); + SystemMessageIntent.broadcastPersonal(receiver.getOwner(), prose); + } + + private void handleCivilWarPointIntent(CivilWarPointIntent cwpi) { + int points = cwpi.getPoints(); + PlayerObject receiver = cwpi.getReceiver(); + ProsePackage prose = new ProsePackage(new StringId("gcw", "gcw_rank_generic_point_grant"), "DI", points); + + grantPoints(prose, receiver, points); + } + + private void handleCreatureKilledIntent(CreatureKilledIntent cki) { + CreatureObject corpseCreature = cki.getCorpse(); + CreatureObject killerCreature = cki.getKiller(); + PvpFaction killerFaction = killerCreature.getPvpFaction(); + + if (!killerCreature.isPlayer()) { + return; + } + + boolean specialForces = corpseCreature.getPvpStatus() == PvpStatus.SPECIALFORCES; + + if (!isFactionEligible(killerFaction, corpseCreature.getPvpFaction(), specialForces)) { + return; + } + + PlayerObject killerPlayer = killerCreature.getPlayerObject(); + + byte multiplier = makeMultiplier(specialForces, corpseCreature.isPlayer()); + int base = baseForDifficulty(corpseCreature.getDifficulty()); + int granted = pointsGranted(base, multiplier); + ProsePackage prose; + + if (specialForces) { + // Increment kill counter + killerPlayer.setPvpKills(killerPlayer.getPvpKills() + 1); + + // Determine which effect and sound to play + String effectFile; + String soundFile; + + if (killerFaction == PvpFaction.REBEL) { + effectFile = "clienteffect/holoemote_rebel.cef"; + soundFile = "sound/music_themequest_victory_rebel.snd"; + } else { + effectFile = "clienteffect/holoemote_imperial.cef"; + soundFile = "sound/music_themequest_victory_imperial.snd"; + } + + // PvP GCW point system message + prose = new ProsePackage("StringId", new StringId("gcw", "gcw_rank_pvp_kill_point_grant"), "DI", granted, "TT", corpseCreature + .getObjectName()); + + // Send visual effect to killer and everyone around + killerCreature.sendObserversAndSelf(new PlayClientEffectObjectMessage(effectFile, "head", killerCreature.getObjectId(), "")); + + // Send sound to just to the killer + killerCreature.sendSelf(new PlayMusicMessage(0, soundFile, 0, false)); + + // TODO planetary control points? + } else { + // NPC GCW point system message + prose = new ProsePackage(new StringId("gcw", "gcw_rank_generic_point_grant"), "DI", granted); + } + + // Increment GCW point counter + grantPoints(prose, killerPlayer, granted); + } + + private void handleDestroyObjectIntent(DestroyObjectIntent doi) { + SWGObject object = doi.getObject(); + + if (!(object instanceof PlayerObject)) { + return; + } + + playerObjects.remove(object); + } + + private void handleObjectCreatedIntent(ObjectCreatedIntent oci) { + SWGObject object = oci.getObject(); + + if (!(object instanceof PlayerObject)) { + return; + } + + PlayerObject playerObject = (PlayerObject) object; + + // Let's make sure the timer's displayed for them + updateTimer(playerObject); + + playerObjects.add(playerObject); + } + +} diff --git a/src/main/java/com/projectswg/holocore/services/faction/FactionService.java b/src/main/java/com/projectswg/holocore/services/faction/FactionManager.java similarity index 96% rename from src/main/java/com/projectswg/holocore/services/faction/FactionService.java rename to src/main/java/com/projectswg/holocore/services/faction/FactionManager.java index 8e6337722..e7529c180 100644 --- a/src/main/java/com/projectswg/holocore/services/faction/FactionService.java +++ b/src/main/java/com/projectswg/holocore/services/faction/FactionManager.java @@ -32,7 +32,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import com.projectswg.common.concurrency.PswgScheduledThreadPool; -import com.projectswg.common.control.Service; +import com.projectswg.common.control.Manager; import com.projectswg.common.data.encodables.tangible.PvpFaction; import com.projectswg.common.data.encodables.tangible.PvpFlag; import com.projectswg.common.data.encodables.tangible.PvpStatus; @@ -45,15 +45,17 @@ import com.projectswg.holocore.resources.objects.creature.CreatureObject; import com.projectswg.holocore.resources.objects.tangible.TangibleObject; import com.projectswg.holocore.resources.player.Player; -public final class FactionService extends Service { +public final class FactionManager extends Manager { private final Map> statusChangers; private final PswgScheduledThreadPool executor; - public FactionService() { + public FactionManager() { statusChangers = new ConcurrentHashMap<>(); executor = new PswgScheduledThreadPool(1, "faction-service"); + addChildService(new CivilWarService()); + registerForIntent(FactionIntent.class, this::handleFactionIntent); } @@ -97,7 +99,13 @@ public final class FactionService extends Service { if(target.getBaselineType() == BaselineType.CREO && target.getPvpFaction() != PvpFaction.NEUTRAL) { // We're given rank 1 upon joining a non-neutral faction - ((CreatureObject) target).setFactionRank((byte) 1); + CreatureObject creatureTarget = (CreatureObject) target; + + if (!creatureTarget.isPlayer()) { + return; + } + + creatureTarget.getPlayerObject().setCurrentRank(1); } } diff --git a/src/main/java/com/projectswg/holocore/services/galaxy/GameManager.java b/src/main/java/com/projectswg/holocore/services/galaxy/GameManager.java index 72222d059..24df2f667 100644 --- a/src/main/java/com/projectswg/holocore/services/galaxy/GameManager.java +++ b/src/main/java/com/projectswg/holocore/services/galaxy/GameManager.java @@ -36,7 +36,7 @@ import com.projectswg.holocore.services.commands.CommandService; import com.projectswg.holocore.services.commands.EntertainmentService; import com.projectswg.holocore.services.crafting.CraftingManager; import com.projectswg.holocore.services.experience.ExperienceManager; -import com.projectswg.holocore.services.faction.FactionService; +import com.projectswg.holocore.services.faction.FactionManager; import com.projectswg.holocore.services.group.GroupService; import com.projectswg.holocore.services.sui.SuiService; @@ -49,7 +49,7 @@ public class GameManager extends Manager { addChildService(new CollectionService()); addChildService(new CollectionBadgeManager()); addChildService(new EnvironmentService()); - addChildService(new FactionService()); + addChildService(new FactionManager()); addChildService(new GroupService()); addChildService(new SkillModService()); addChildService(new EntertainmentService()); diff --git a/src/test/java/com/projectswg/holocore/services/TestServices.java b/src/test/java/com/projectswg/holocore/services/TestServices.java index 99c6412bb..346fd3be4 100644 --- a/src/test/java/com/projectswg/holocore/services/TestServices.java +++ b/src/test/java/com/projectswg/holocore/services/TestServices.java @@ -27,6 +27,7 @@ ***********************************************************************************/ package com.projectswg.holocore.services; +import com.projectswg.holocore.services.faction.TestCivilWarService; import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @@ -38,6 +39,7 @@ import com.projectswg.holocore.services.player.TestPlayer; @RunWith(Suite.class) @SuiteClasses({ + TestCivilWarService.class, TestRareLootService.class, TestCrafting.class, TestPlayer.class, diff --git a/src/test/java/com/projectswg/holocore/services/faction/TestCivilWarService.java b/src/test/java/com/projectswg/holocore/services/faction/TestCivilWarService.java new file mode 100644 index 000000000..d91f5c3fe --- /dev/null +++ b/src/test/java/com/projectswg/holocore/services/faction/TestCivilWarService.java @@ -0,0 +1,106 @@ +/************************************************************************************ + * Copyright (c) 2015 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * 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 Holocore. * + * * + * -------------------------------------------------------------------------------- * + * * + * 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. * + * * + * 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 Holocore. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.holocore.services.faction; + +import com.projectswg.common.data.encodables.tangible.PvpFaction; +import com.projectswg.holocore.resources.objects.creature.CreatureDifficulty; +import org.junit.Assert; +import org.junit.Test; + +import java.time.LocalDate; + +public class TestCivilWarService { + + private final CivilWarService service; + + public TestCivilWarService() { + service = new CivilWarService(); + } + + @Test + public void testIsFactionEligible() { + Assert.assertTrue(service.isFactionEligible(PvpFaction.REBEL, PvpFaction.IMPERIAL, true)); + Assert.assertFalse(service.isFactionEligible(PvpFaction.NEUTRAL, PvpFaction.NEUTRAL, false)); + Assert.assertFalse(service.isFactionEligible(PvpFaction.NEUTRAL, PvpFaction.REBEL, false)); + Assert.assertFalse(service.isFactionEligible(PvpFaction.NEUTRAL, PvpFaction.IMPERIAL, false)); + Assert.assertFalse(service.isFactionEligible(PvpFaction.REBEL, PvpFaction.REBEL, true)); + } + + @Test + public void testMakeMultiplier() { + Assert.assertEquals(1, service.makeMultiplier(false, false)); + Assert.assertEquals(2, service.makeMultiplier(true, false)); + Assert.assertEquals(19, service.makeMultiplier(false, true)); + Assert.assertEquals(20, service.makeMultiplier(true, true)); + } + + @Test + public void testBaseForDifficulty() { + Assert.assertEquals(5, service.baseForDifficulty(CreatureDifficulty.NORMAL)); + Assert.assertEquals(10, service.baseForDifficulty(CreatureDifficulty.ELITE)); + Assert.assertEquals(15, service.baseForDifficulty(CreatureDifficulty.BOSS)); + } + + @Test + public void testPointsGranted() { + Assert.assertEquals(200, service.pointsGranted(10, (byte) 20)); + } + + @Test + public void testNextUpdateTime() { + LocalDate now = LocalDate.of(2018, 2, 19); // It's a Monday + LocalDate rankDay = LocalDate.of(2018, 2, 23); // It's a Friday + int expectedRankTime = 1519344000; // The Friday that week, at 00:00 UTC (night between thursday and friday) + + Assert.assertEquals(expectedRankTime, service.nextUpdateTime(now)); + Assert.assertEquals(expectedRankTime, service.nextUpdateTime(rankDay)); + } + + @Test + public void testIsDecayRank() { + Assert.assertFalse(service.isDecayRank(6)); + Assert.assertTrue(service.isDecayRank(7)); + } + + @Test + public void testRankProgress() { + Assert.assertEquals(2.86f, service.rankProgress(10.0f, 20.0f, 7, 9000), 1); + } + + @Test + public void testIsRankDown() { + Assert.assertFalse(service.isRankDown(40, 50)); + Assert.assertTrue(service.isRankDown(3, -10)); + } + + @Test + public void testLeftoverPoints() { + Assert.assertEquals(6000, service.leftoverPoints(130, 20000)); + } +}