Repository navigation
feat: share profession levels and carry transferred class EXP across characters #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
src/main/java/net/tfminecraft/rpcharacters/mmocore/CarriedExperience.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| package net.tfminecraft.rpcharacters.mmocore; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.UUID; | ||
| import java.util.function.BooleanSupplier; | ||
|
|
||
| import org.bukkit.Bukkit; | ||
| import org.bukkit.command.CommandSender; | ||
| import org.bukkit.entity.Player; | ||
|
|
||
| import net.Indyuce.mmocore.MMOCore; | ||
| import net.Indyuce.mmocore.api.player.PlayerData; | ||
| import net.Indyuce.mmocore.experience.EXPSource; | ||
| import net.tfminecraft.rpcharacters.RPCharacters; | ||
| import net.tfminecraft.rpcharacters.progress.CharacterProgress; | ||
| import net.tfminecraft.rpcharacters.progress.CharacterProgressService; | ||
| import net.tfminecraft.rpcharacters.progress.ProgressRouting; | ||
| import net.tfminecraft.rpcharacters.utils.RPTexts; | ||
|
|
||
| /** | ||
| * Class EXP a character receives from its account's other characters, such as the EXP of shared Codex | ||
| * discoveries. MMOCore throws away EXP given at a class's level cap, so the class takes only what it can | ||
| * hold; the rest waits on the character and is added once it can level further, after a subclass pick | ||
| * or a raised cap. Amounts are raw, before MMOCore's EXP stat and boosters, like a discovery's own reward. | ||
| */ | ||
| public final class CarriedExperience { | ||
| public static final String SUBCOMMAND = "carryexp"; | ||
| private static final double EPSILON = 1e-6; | ||
| private static final int MAX_STEPS = 1000; | ||
| /** Carried EXP at the start of each player's batch still to be reported. Main thread only. */ | ||
| private static final Map<UUID, Double> NOTICES = new HashMap<>(); | ||
|
|
||
| private CarriedExperience() {} | ||
|
|
||
| /** | ||
| * Gives EXP to the player's live character, carrying what its class cannot hold yet. While progress | ||
| * is shared by the account, it is plain MMOCore EXP. @return false when there is no character to give it to | ||
| */ | ||
| public static boolean carry(Player player, double amount) { | ||
| if (player == null || !(amount > 0) || !Double.isFinite(amount)) { | ||
| return false; | ||
| } | ||
| if (!ProgressRouting.enabled()) { | ||
| PlayerData.get(player).giveExperience(amount, EXPSource.COMMAND); | ||
| return true; | ||
| } | ||
| CharacterProgress progress = ProgressRouting.activeProgress(player); | ||
| if (progress == null || ClassService.isApplying(player.getUniqueId())) { | ||
| return false; | ||
| } | ||
| double before = progress.getCarriedExperience(); | ||
| progress.setCarriedExperience(before + amount); | ||
| release(player, progress); | ||
| noticeAfterBatch(player, before); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Tells the player once a batch of carried rewards is done (they arrive many in one tick) how much | ||
| * EXP is waiting, if the batch added to it. | ||
| */ | ||
| private static void noticeAfterBatch(Player player, double before) { | ||
| if (RPCharacters.plugin == null || NOTICES.putIfAbsent(player.getUniqueId(), before) != null) { | ||
| return; | ||
| } | ||
| BooleanSupplier current = ProgressRouting.sessionGuard(player); | ||
| Bukkit.getScheduler().runTask(RPCharacters.plugin, () -> { | ||
| double start = NOTICES.remove(player.getUniqueId()); | ||
| CharacterProgress progress = ProgressRouting.activeProgress(player); | ||
| if (current.getAsBoolean() && progress != null && progress.getCarriedExperience() > start + EPSILON) { | ||
| RPTexts.send(player, RPTexts.MUTED + "Your class can't level further yet, so " + RPTexts.WARN | ||
| + format(progress.getCarriedExperience()) + " EXP" + RPTexts.MUTED | ||
| + " from your other characters is waiting. It is added once your class can level, such as after you pick a subclass with " | ||
| + RPTexts.COMMAND + "/class" + RPTexts.MUTED + "."); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** Adds the live character's carried EXP that its class can hold now. */ | ||
| public static void release(Player player) { | ||
| CharacterProgress progress = player == null ? null : ProgressRouting.activeProgress(player); | ||
| if (progress == null || progress.getCarriedExperience() <= EPSILON || ClassService.isApplying(player.getUniqueId())) { | ||
| return; | ||
| } | ||
| double before = progress.getCarriedExperience(); | ||
| release(player, progress); | ||
| double added = before - progress.getCarriedExperience(); | ||
| if (added > EPSILON) { | ||
| RPTexts.send(player, RPTexts.SUCCESS + format(added) + " carried EXP" + RPTexts.MUTED | ||
| + " from your other characters was added to your class."); | ||
| } | ||
| } | ||
|
|
||
| /** Releases carried EXP next tick, only while the character that changed class is still live. */ | ||
| public static void scheduleRelease(Player player) { | ||
| if (player == null || RPCharacters.plugin == null || ProgressRouting.activeProgress(player) == null) { | ||
| return; | ||
| } | ||
| BooleanSupplier current = ProgressRouting.sessionGuard(player); | ||
| Bukkit.getScheduler().runTask(RPCharacters.plugin, () -> { | ||
| if (current.getAsBoolean()) { | ||
| release(player); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Gives the carried EXP level by level until it runs out or the class reaches its cap, so MMOCore | ||
| * never drops any of it and each level-up claims its rewards. The character is saved by a coalesced | ||
| * checkpoint, since a batch of shared rewards carries EXP many times in one tick. | ||
| */ | ||
| private static void release(Player player, CharacterProgress progress) { | ||
| PlayerData mmo = PlayerData.get(player); | ||
| double multiplier = (1 + mmo.getStats().getStat("ADDITIONAL_EXPERIENCE") / 100) | ||
| * MMOCore.plugin.boosterManager.getMultiplier(null); | ||
| double left = progress.getCarriedExperience(); | ||
| for (int step = 0; step < MAX_STEPS && left > EPSILON && multiplier > 0 && !mmo.hasReachedMaxLevel(); step++) { | ||
| double needed = Math.max(0, mmo.getLevelUpExperience() - mmo.getExperience()) / multiplier; | ||
| double given = Math.min(left, needed); | ||
| if (given <= 0) { | ||
| break; | ||
| } | ||
| mmo.giveExperience(given, EXPSource.COMMAND); | ||
| left -= given; | ||
| } | ||
| if (left != progress.getCarriedExperience()) { | ||
| progress.setCarriedExperience(left <= EPSILON ? 0 : left); | ||
| } | ||
| CharacterProgressService.scheduleCheckpoint(player); | ||
| } | ||
|
|
||
| /** {@code /rpcharacter admin carryexp <player> <amount>}, for Codex and ConditionalEvents shared rewards. */ | ||
| public static boolean handle(CommandSender sender, String[] args, int index) { | ||
| if (args.length != index + 2) { | ||
| RPTexts.send(sender, RPTexts.ERROR + "Usage: /rpcharacter admin " + SUBCOMMAND + " <player> <amount>"); | ||
| return true; | ||
| } | ||
| Player player = Bukkit.getPlayerExact(args[index]); | ||
| if (player == null) { | ||
| RPTexts.send(sender, RPTexts.ERROR + args[index] + " is not online."); | ||
| return true; | ||
| } | ||
| double amount; | ||
| try { | ||
| amount = Double.parseDouble(args[index + 1]); | ||
| } catch (NumberFormatException invalid) { | ||
| amount = Double.NaN; | ||
| } | ||
| if (!(amount > 0) || !Double.isFinite(amount)) { | ||
| RPTexts.send(sender, RPTexts.ERROR + "The amount must be a positive number."); | ||
| return true; | ||
| } | ||
| if (!carry(player, amount)) { | ||
| RPTexts.send(sender, RPTexts.ERROR + player.getName() + " has no character in play, or its class is being applied; nothing was carried."); | ||
| return true; | ||
| } | ||
| RPTexts.send(sender, RPTexts.MUTED + "Carried " + format(amount) + " EXP to " + player.getName() + "."); | ||
| return true; | ||
| } | ||
|
|
||
| private static String format(double amount) { | ||
| return amount == Math.rint(amount) ? String.valueOf((long) amount) : String.format(Locale.ROOT, "%.1f", amount); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/main/java/net/tfminecraft/rpcharacters/progress/SharedProfessions.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package net.tfminecraft.rpcharacters.progress; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Map; | ||
| import java.util.TreeMap; | ||
|
|
||
| import net.tfminecraft.rpcharacters.mmocore.MmoProgressCodec; | ||
| import net.tfminecraft.rpcharacters.mmocore.MmoProgressCodec.ProfessionLevel; | ||
|
|
||
| /** | ||
| * Professions belong to the account. A character plays each profession at the best level any of its | ||
| * account's characters, alive or dead, has reached, and holds at least as many lifetime points for it as | ||
| * any of them. Upgrades stay with the character that bought them, so the points arrive unspent. Levels | ||
| * never go down, so taking the best also settles accounts whose characters levelled apart before this. | ||
| */ | ||
| final class SharedProfessions { | ||
| private SharedProfessions() {} | ||
|
|
||
| /** | ||
| * Raises the target's professions and profession points to the best of the account's characters. | ||
| * The target's MMOCore copy is replaced, never edited, since other copies may share it. | ||
| * @return whether anything was raised | ||
| */ | ||
| static boolean raise(CharacterProgress target, Collection<CharacterProgress> account) { | ||
| Map<String, ProfessionLevel> levels = new TreeMap<>(MmoProgressCodec.professions(target.mmo())); | ||
| boolean levelled = false; | ||
| boolean pointed = false; | ||
| for (CharacterProgress other : account) { | ||
| if (other == null || other == target) { | ||
| continue; | ||
| } | ||
| for (Map.Entry<String, ProfessionLevel> profession : MmoProgressCodec.professions(other.mmo()).entrySet()) { | ||
| if (profession.getValue().isAbove(levels.get(profession.getKey()))) { | ||
| levels.put(profession.getKey(), profession.getValue()); | ||
| levelled = true; | ||
| } | ||
| } | ||
| for (Map.Entry<String, Integer> points : other.getProfessionPointsMap().entrySet()) { | ||
| if (points.getValue() > target.getProfessionPoints(points.getKey())) { | ||
| target.setProfessionPoints(points.getKey(), points.getValue()); | ||
| pointed = true; | ||
| } | ||
| } | ||
| } | ||
| if (levelled) { | ||
| target.setMmo(MmoProgressCodec.withProfessions(target.mmo(), levels)); | ||
| } | ||
| return levelled || pointed; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.