experience tables

This commit is contained in:
Indyuce 2021-12-24 15:19:58 +01:00
parent 0650138a77
commit c1f3829e63
8 changed files with 363 additions and 103 deletions

View File

@ -11,7 +11,10 @@ import net.Indyuce.mmocore.api.event.PlayerLevelUpEvent;
import net.Indyuce.mmocore.api.player.PlayerData;
import net.Indyuce.mmocore.api.util.MMOCoreUtils;
import net.Indyuce.mmocore.api.util.math.particle.SmallParticleEffect;
import net.Indyuce.mmocore.experience.droptable.ExperienceItem;
import net.Indyuce.mmocore.experience.droptable.ExperienceTable;
import net.Indyuce.mmocore.manager.SoundManager;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
@ -26,6 +29,7 @@ import java.util.Map.Entry;
public class PlayerProfessions {
private final Map<String, Integer> exp = new HashMap<>();
private final Map<String, Integer> level = new HashMap<>();
private final Map<String, Integer> timesClaimed = new HashMap<>();
private final PlayerData playerData;
public PlayerProfessions(PlayerData playerData) {
@ -39,6 +43,11 @@ public class PlayerProfessions {
level.put(key, config.getInt(key + ".level"));
}
if (config.contains("times-claimed"))
// Watch out for the deep section lookup
for (String key : config.getConfigurationSection("times-claimed").getKeys(true))
this.timesClaimed.put(key, config.getInt("times-claimed." + key));
return this;
}
@ -47,6 +56,8 @@ public class PlayerProfessions {
config.set(id + ".exp", exp.get(id));
for (String id : level.keySet())
config.set(id + ".level", level.get(id));
timesClaimed.forEach((key, value) -> config.set("times-claimed." + key, value));
}
public String toJsonString() {
@ -58,18 +69,29 @@ public class PlayerProfessions {
json.add(profession.getId(), object);
}
JsonObject timesClaimed = new JsonObject();
this.timesClaimed.forEach((key, value) -> timesClaimed.addProperty(key, value));
json.add("timesClaimed", timesClaimed);
return json.toString();
}
public void load(String json) {
Gson parser = new Gson();
JsonObject jo = parser.fromJson(json, JsonObject.class);
for (Entry<String, JsonElement> entry : jo.entrySet()) {
JsonObject obj = new Gson().fromJson(json, JsonObject.class);
// Load profession exp and levels
for (Entry<String, JsonElement> entry : obj.entrySet())
if (MMOCore.plugin.professionManager.has(entry.getKey())) {
exp.put(entry.getKey(), entry.getValue().getAsJsonObject().get("exp").getAsInt());
level.put(entry.getKey(), entry.getValue().getAsJsonObject().get("level").getAsInt());
JsonObject value = entry.getValue().getAsJsonObject();
exp.put(entry.getKey(), value.get("exp").getAsInt());
level.put(entry.getKey(), value.get("level").getAsInt());
}
}
// Load times claimed
if (obj.has("timesClaimed"))
for (Entry<String, JsonElement> entry : obj.getAsJsonObject("timesClaimed").entrySet())
timesClaimed.put(entry.getKey(), entry.getValue().getAsInt());
}
public PlayerData getPlayerData() {
@ -129,7 +151,9 @@ public class PlayerProfessions {
}
public void giveExperience(Profession profession, double value, EXPSource source, @Nullable Location hologramLocation) {
if (hasReachedMaxLevel(profession)) {
Validate.isTrue(playerData.isOnline(), "Cannot give experience to offline player");
if (hasReachedMaxLevel(profession)) {
setExperience(profession, 0);
return;
}
@ -137,7 +161,7 @@ public class PlayerProfessions {
value = MMOCore.plugin.boosterManager.calculateExp(profession, value);
// display hologram
if (hologramLocation != null && playerData.isOnline())
if (hologramLocation != null )
MMOCoreUtils.displayIndicator(hologramLocation.add(.5, 1.5, .5), MMOCore.plugin.configManager.getSimpleMessage("exp-hologram", "exp", "" + value).message());
PlayerExperienceGainEvent event = new PlayerExperienceGainEvent(playerData, profession, (int) value, source);
@ -166,13 +190,17 @@ public class PlayerProfessions {
playerData.giveExperience((int) profession.getExperience().calculate(level), null);
}
if (check && playerData.isOnline()) {
if (check) {
Bukkit.getPluginManager().callEvent(new PlayerLevelUpEvent(playerData, profession, oldLevel, level));
new SmallParticleEffect(playerData.getPlayer(), Particle.SPELL_INSTANT);
new ConfigMessage("profession-level-up").addPlaceholders("level", "" + level, "profession", profession.getName())
.send(playerData.getPlayer());
MMOCore.plugin.soundManager.play(playerData.getPlayer(), SoundManager.SoundEvent.LEVEL_UP);
playerData.getStats().updateStats();
// Apply profession experience table
if (profession.hasExperienceTable())
profession.getExperienceTable().claim(playerData, level, this);
}
StringBuilder bar = new StringBuilder("" + ChatColor.BOLD);
@ -183,4 +211,20 @@ public class PlayerProfessions {
MMOCore.plugin.configManager.getSimpleMessage("exp-notification", "profession", profession.getName(), "progress", bar.toString(), "ratio",
MythicLib.plugin.getMMOConfig().decimal.format((double) exp / needed * 100)).send(playerData.getPlayer());
}
/**
* @return The amount of times that specific experience item was claimed by the player
*/
public int getTimesClaimed(ExperienceTable table, ExperienceItem item) {
String path = table.getId() + "." + item.getId();
return timesClaimed.getOrDefault(path, 0);
}
/**
* See {@link #getTimesClaimed(ExperienceTable, ExperienceItem)}
*/
public void setTimesClaimed(ExperienceTable table, ExperienceItem item, int timesClaimed) {
String path = table.getId() + "." + item.getId();
this.timesClaimed.put(path, timesClaimed);
}
}

View File

@ -1,48 +1,55 @@
package net.Indyuce.mmocore.experience;
import io.lumine.mythic.lib.MythicLib;
import io.lumine.mythic.lib.api.MMOLineConfig;
import io.lumine.mythic.lib.api.util.PostLoadObject;
import net.Indyuce.mmocore.MMOCore;
import net.Indyuce.mmocore.api.util.math.formula.LinearValue;
import net.Indyuce.mmocore.experience.droptable.ExperienceTable;
import net.Indyuce.mmocore.experience.provider.ExperienceDispenser;
import net.Indyuce.mmocore.experience.provider.ProfessionExperienceDispenser;
import org.apache.commons.lang.Validate;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.potion.PotionType;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
public class Profession extends PostLoadObject {
public class Profession {
private final String id, name;
private final ExpCurve expCurve;
private final int maxLevel;
private final Map<ProfessionOption, Boolean> options = new HashMap<>();
private final ExperienceTable expTable;
/*
* experience given to the main player level whenever he levels up this
* profession
/**
* Experience given to the main player level whenever he levels up this profession
*
* @deprecated Being replaced by {@link ExperienceTable}
*/
@Deprecated
private final LinearValue experience;
public Profession(String id, FileConfiguration config) {
super(config);
this.id = id.toLowerCase().replace("_", "-").replace(" ", "-");
this.name = config.getString("name");
Validate.notNull(name, "Could not load name");
expCurve = config.contains("exp-curve")
? MMOCore.plugin.experience.getOrThrow(config.get("exp-curve").toString().toLowerCase().replace("_", "-").replace(" ", "-"))
? MMOCore.plugin.experience.getCurveOrThrow(config.get("exp-curve").toString().toLowerCase().replace("_", "-").replace(" ", "-"))
: ExpCurve.DEFAULT;
experience = new LinearValue(config.getConfigurationSection("experience"));
ExperienceTable expTable = null;
if (config.contains("exp-table"))
try {
expTable = loadExperienceTable(config.get("exp-table"));
} catch (RuntimeException exception) {
MMOCore.plugin.getLogger().log(Level.WARNING, "Could not load exp table from profession '" + id + "': " + exception.getMessage());
}
this.expTable = expTable;
if (config.contains("options"))
for (String key : config.getConfigurationSection("options").getKeys(false))
try {
@ -59,71 +66,25 @@ public class Profession extends PostLoadObject {
ExperienceDispenser dispenser = new ProfessionExperienceDispenser(this);
for (String key : config.getStringList("exp-sources"))
try {
MMOCore.plugin.professionManager.registerExpSource(MMOCore.plugin.loadManager.loadExperienceSource(new MMOLineConfig(key), dispenser));
MMOCore.plugin.experience.registerSource(MMOCore.plugin.loadManager.loadExperienceSource(new MMOLineConfig(key), dispenser));
} catch (IllegalArgumentException exception) {
MMOCore.plugin.getLogger().log(Level.WARNING,
"Could not register exp source '" + key + "' from profession '" + id + "': " + exception.getMessage());
}
}
MMOCore.plugin.professionManager.loadProfessionConfigurations(config);
}
/*
* drop tables must be loaded after professions are initialized
*/
@Override
protected void whenPostLoaded(ConfigurationSection config) {
private ExperienceTable loadExperienceTable(Object obj) {
if (config.contains("on-fish"))
MMOCore.plugin.fishingManager.loadDropTables(config.getConfigurationSection("on-fish"));
if (obj instanceof ConfigurationSection)
return new ExperienceTable((ConfigurationSection) obj);
if (config.contains("on-mine"))
MMOCore.plugin.mineManager.loadDropTables(config.getConfigurationSection("on-mine"));
if (obj instanceof String)
return MMOCore.plugin.experience.getTableOrThrow(obj.toString());
if (config.contains("alchemy-experience")) {
MMOCore.plugin.alchemyManager.splash = 1 + config.getDouble("alchemy-experience.special.splash") / 100;
MMOCore.plugin.alchemyManager.lingering = 1 + config.getDouble("alchemy-experience.special.lingering") / 100;
MMOCore.plugin.alchemyManager.extend = 1 + config.getDouble("alchemy-experience.special.extend") / 100;
MMOCore.plugin.alchemyManager.upgrade = 1 + config.getDouble("alchemy-experience.special.upgrade") / 100;
for (String key : config.getConfigurationSection("alchemy-experience.effects").getKeys(false))
try {
PotionType type = PotionType.valueOf(key.toUpperCase().replace("-", "_").replace(" ", "_"));
MMOCore.plugin.alchemyManager.registerBaseExperience(type, config.getDouble("alchemy-experience.effects." + key));
} catch (IllegalArgumentException exception) {
MMOCore.log(Level.WARNING, "[PlayerProfessions:" + id + "] Could not read potion type from " + key);
}
}
if (config.contains("base-enchant-exp"))
for (String key : config.getConfigurationSection("base-enchant-exp").getKeys(false))
try {
Enchantment enchant = MythicLib.plugin.getVersion().getWrapper().getEnchantmentFromString(key.toLowerCase().replace("-", "_"));
MMOCore.plugin.enchantManager.registerBaseExperience(enchant, config.getDouble("base-enchant-exp." + key));
} catch (IllegalArgumentException exception) {
MMOCore.log(Level.WARNING, "[PlayerProfessions:" + id + "] Could not read enchant from " + key);
}
if (config.contains("repair-exp"))
for (String key : config.getConfigurationSection("repair-exp").getKeys(false))
try {
Material material = Material.valueOf(key.toUpperCase().replace("-", "_").replace(" ", "_"));
MMOCore.plugin.smithingManager.registerBaseExperience(material, config.getDouble("repair-exp." + key));
} catch (IllegalArgumentException exception) {
MMOCore.log(Level.WARNING, "[PlayerProfessions:" + id + "] Could not read material from " + key);
}
// if (config.contains("effect-weight"))
// for (String key :
// config.getConfigurationSection("effect-weight").getKeys(false))
// try {
// MMOCore.plugin.alchemyManager.registerEffectWeight(PotionEffectType.getByName(key.toUpperCase().replace("-",
// "_").replace(" ", "_")), config.getDouble("effect-weight." + key));
// } catch (IllegalArgumentException exception) {
// MMOCore.log(Level.WARNING, "[PlayerProfessions:" + id + "] Could not
// read
// potion effect type from " + key);
// }
throw new IllegalArgumentException("Please provide either a string (exp table name) or a config section (locally define an exp table)");
}
public boolean getOption(ProfessionOption option) {
@ -158,6 +119,14 @@ public class Profession extends PostLoadObject {
return experience;
}
public boolean hasExperienceTable() {
return expTable != null;
}
public ExperienceTable getExperienceTable() {
return Objects.requireNonNull(expTable, "Profession has no exp table");
}
public static enum ProfessionOption {
/**

View File

@ -0,0 +1,79 @@
package net.Indyuce.mmocore.experience.droptable;
import io.lumine.mythic.lib.api.MMOLineConfig;
import net.Indyuce.mmocore.MMOCore;
import net.Indyuce.mmocore.api.player.PlayerData;
import net.Indyuce.mmocore.api.quest.trigger.Trigger;
import org.apache.commons.lang.Validate;
import org.bukkit.configuration.ConfigurationSection;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class ExperienceItem {
private final String id;
private final int period;
private final double claimChance, failReduction;
private final List<Trigger> triggers;
private static final Random random = new Random();
/**
* One item for an experience table
*
* @param period The experience item is claimed every X level ups
* @param claimChance Chance for that item to be claimed every X level ups
* @param failReduction Between 0 and 1, by how much the fail chance is reduced
* every time the item is not claimed when leveling up.
* <p>
* Failing chance follows a geometric sequence therefore
* <code>successChance = 1 - (1 - initialSuccessChance) * failReduction^n</code>
* where n is the amount of successive claiming fails
* @param triggers Actions cast when the exp item is claimed
*/
public ExperienceItem(String id, int period, double claimChance, double failReduction, List<Trigger> triggers) {
this.id = id;
this.period = period;
this.claimChance = claimChance;
this.failReduction = failReduction;
this.triggers = triggers;
}
public ExperienceItem(ConfigurationSection config) {
Validate.notNull(config, "Config cannot be null");
Validate.isTrue(config.contains("triggers"));
id = config.getName();
period = config.getInt("period", 1);
claimChance = config.getDouble("chance", 100) / 100;
failReduction = config.getDouble("fail-reduction", 80) / 100;
triggers = new ArrayList<>();
for (String triggerFormat : config.getStringList("triggers"))
triggers.add(MMOCore.plugin.loadManager.loadTrigger(new MMOLineConfig(triggerFormat)));
}
public String getId() {
return id;
}
/**
* @param professionLevel The profession level the player just reached
* @param timesCollected Amount of times the exp item has already been claimed by the player
* @return If the item should be claimed right now taking into
* account the randomness factor from the 'chance' parameter
*/
public boolean roll(int professionLevel, int timesCollected) {
int claimsRequired = professionLevel - (timesCollected + 1) * period;
if (claimsRequired < 1)
return false;
double chance = 1 - (1 - claimChance) * Math.pow(failReduction, claimsRequired);
return random.nextDouble() < chance;
}
public void applyTriggers(PlayerData levelingUp) {
for (Trigger trigger : triggers)
trigger.apply(levelingUp);
}
}

View File

@ -0,0 +1,55 @@
package net.Indyuce.mmocore.experience.droptable;
import net.Indyuce.mmocore.MMOCore;
import net.Indyuce.mmocore.api.player.PlayerData;
import net.Indyuce.mmocore.experience.PlayerProfessions;
import org.apache.commons.lang.Validate;
import org.bukkit.configuration.ConfigurationSection;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
public class ExperienceTable {
private final String id;
private final List<ExperienceItem> items = new ArrayList<>();
public ExperienceTable(ConfigurationSection config) {
Validate.notNull(config, "Config cannot be null");
id = config.getName();
for (String str : config.getKeys(false))
try {
Validate.isTrue(config.isConfigurationSection(str), "Key '" + str + "' is not a configuration section");
items.add(new ExperienceItem(config.getConfigurationSection(str)));
} catch (RuntimeException exception) {
MMOCore.plugin.getLogger().log(Level.WARNING, "Could not load item '" + str + "' from experience table '" + id + "': " + exception.getMessage());
}
}
public String getId() {
return id;
}
public List<ExperienceItem> getItems() {
return items;
}
/**
* Called when a player levels up one of his professions
*
* @param levelingUp Player leveling up
* @param professionLevel New profession level
* @param professionData Player profession data
*/
public void claim(PlayerData levelingUp, int professionLevel, PlayerProfessions professionData) {
for (ExperienceItem item : items) {
int timesClaimed = professionData.getTimesClaimed(this, item);
if (!item.roll(professionLevel, timesClaimed))
continue;
professionData.setTimesClaimed(this, item, timesClaimed + 1);
item.applyTriggers(levelingUp);
}
}
}

View File

@ -86,6 +86,7 @@ public class ConfigManager {
loadDefaultFile("sounds.yml");
loadDefaultFile("loot-chests.yml");
loadDefaultFile("commands.yml");
loadDefaultFile("exp-tables.yml");
loadDefaultFile("guilds.yml");
commandVerbose.reload(MMOCore.plugin.getConfig().getConfigurationSection("command-verbose"));

View File

@ -1,45 +1,98 @@
package net.Indyuce.mmocore.manager;
import net.Indyuce.mmocore.MMOCore;
import net.Indyuce.mmocore.api.ConfigFile;
import net.Indyuce.mmocore.experience.ExpCurve;
import net.Indyuce.mmocore.experience.droptable.ExperienceTable;
import net.Indyuce.mmocore.experience.source.type.ExperienceSource;
import net.Indyuce.mmocore.manager.profession.ExperienceSourceManager;
import org.apache.commons.lang.Validate;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.HandlerList;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import org.apache.commons.lang.Validate;
public class ExperienceManager implements MMOCoreManager {
private final Map<String, ExpCurve> expCurves = new HashMap<>();
private final Map<String, ExperienceTable> expTables = new HashMap<>();
import net.Indyuce.mmocore.MMOCore;
import net.Indyuce.mmocore.experience.ExpCurve;
/**
* Saves different experience sources based on experience source type.
*/
private final Map<Class<?>, ExperienceSourceManager<?>> managers = new HashMap<>();
public class ExperienceManager {
private final Map<String, ExpCurve> expCurves = new HashMap<>();
@SuppressWarnings("unchecked")
public <T extends ExperienceSource> ExperienceSourceManager<T> getManager(Class<T> t) {
return (ExperienceSourceManager<T>) managers.get(t);
}
public boolean hasCurve(String id) {
return expCurves.containsKey(id);
}
@SuppressWarnings("unchecked")
public <T extends ExperienceSource> void registerSource(T source) {
Class<T> path = (Class<T>) source.getClass();
public ExpCurve getOrThrow(String id) {
Validate.isTrue(hasCurve(id), "Could not find exp curve with ID '" + id + "'");
return expCurves.get(id);
}
if (!managers.containsKey(path))
managers.put(path, source.newManager());
getManager(path).registerSource(source);
}
public ExpCurve getCurve(String id) {
return expCurves.get(id);
}
public boolean hasCurve(String id) {
return expCurves.containsKey(id);
}
public Collection<ExpCurve> getCurves() {
return expCurves.values();
}
public ExpCurve getCurveOrThrow(String id) {
Validate.isTrue(hasCurve(id), "Could not find exp curve with ID '" + id + "'");
return expCurves.get(id);
}
public void reload() {
expCurves.clear();
for (File file : new File(MMOCore.plugin.getDataFolder() + "/expcurves").listFiles())
try {
ExpCurve curve = new ExpCurve(file);
expCurves.put(curve.getId(), curve);
} catch (IllegalArgumentException | IOException exception) {
MMOCore.plugin.getLogger().log(Level.WARNING, "Could not load exp curve '" + file.getName() + "': " + exception.getMessage());
}
}
public boolean hasTable(String id) {
return expTables.containsKey(id);
}
public ExperienceTable getTableOrThrow(String id) {
return Objects.requireNonNull(expTables.get(id), "Could not find exp table with ID '" + id + "'");
}
public Collection<ExpCurve> getCurves() {
return expCurves.values();
}
public Collection<ExperienceTable> getTables() {
return expTables.values();
}
@Override
public void initialize(boolean clearBefore) {
if (clearBefore) {
expCurves.clear();
expTables.clear();
managers.values().forEach(HandlerList::unregisterAll);
managers.clear();
}
expCurves.clear();
for (File file : new File(MMOCore.plugin.getDataFolder() + "/expcurves").listFiles())
try {
ExpCurve curve = new ExpCurve(file);
expCurves.put(curve.getId(), curve);
} catch (IllegalArgumentException | IOException exception) {
MMOCore.plugin.getLogger().log(Level.WARNING, "Could not load exp curve '" + file.getName() + "': " + exception.getMessage());
}
expTables.clear();
FileConfiguration expTablesConfig = new ConfigFile("exp-tables").getConfig();
for (String key : expTablesConfig.getKeys(false))
try {
ExperienceTable table = new ExperienceTable(expTablesConfig.getConfigurationSection(key));
expTables.put(table.getId(), table);
} catch (RuntimeException exception) {
MMOCore.plugin.getLogger().log(Level.WARNING, "Could not load exp table '" + key + "': " + exception.getMessage());
}
}
}

View File

@ -0,0 +1,13 @@
package net.Indyuce.mmocore.manager;
public interface MMOCoreManager {
/**
* Called either when the server starts when initializing the manager for
* the first time, or when issuing a plugin reload; in that case, stuff
* like listeners must all be cleared before
*
* @param clearBefore True when issuing a plugin reload
*/
void initialize(boolean clearBefore);
}

View File

@ -0,0 +1,46 @@
example_exp_table:
first_table_item:
# This item will drop every 3 level ups
period: 3
# This item has a 80% chance to drop every 3 level ups
chance: 80
# Every successive fail in claiming the item will reduce
# the risk of failing future claims by X%. With a 80%
# fail reduction rate, chances become:
# - 80%
# - 96%
# - 99.2%
# - 99.84%
# so on forever..
#
# This is better than just increasing the claim chance by a
# certain amount each time because otherwise the claim chance
# just becomes/surpasses 100% at some point.
fail-reduction: 80
# What happens when that item is claimed
triggers:
- 'exp{amount=20}'
second_table_item:
period: 2
triggers:
- 'exp{amount=80}'
- 'command{format="broadcast Boy, %player_name% level up twice in one of his(her) professions!"}'
second_exp_table:
# Base exp every level up, sweet.
some_item:
period: 1
triggers:
- 'exp{amount=100}'
# Extra exp every 3 levels
some_other_item:
period: 3
triggers:
- 'exp{amount=100}'