/******************************************************************************* * Copyright 2014 Tobias Welther * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package de.tobiyas.racesandclasses.traitcontainer.traits.magic; import static de.tobiyas.racesandclasses.translation.languages.Keys.magic_change_spells; import static de.tobiyas.racesandclasses.translation.languages.Keys.magic_dont_have_enough; import static de.tobiyas.racesandclasses.translation.languages.Keys.magic_no_spells; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.event.Event; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.scheduler.BukkitTask; import de.tobiyas.racesandclasses.RacesAndClasses; import de.tobiyas.racesandclasses.APIs.LanguageAPI; import de.tobiyas.racesandclasses.APIs.SilenceAndKickAPI; import de.tobiyas.racesandclasses.eventprocessing.eventresolvage.EventWrapper; import de.tobiyas.racesandclasses.eventprocessing.eventresolvage.EventWrapperFactory; import de.tobiyas.racesandclasses.eventprocessing.eventresolvage.PlayerAction; import de.tobiyas.racesandclasses.playermanagement.player.RaCPlayer; import de.tobiyas.racesandclasses.playermanagement.player.RaCPlayerManager; import de.tobiyas.racesandclasses.playermanagement.playerdisplay.scoreboard.PlayerRaCScoreboardManager.SBCategory; import de.tobiyas.racesandclasses.traitcontainer.interfaces.TraitResults; import de.tobiyas.racesandclasses.traitcontainer.interfaces.annotations.configuration.TraitConfigurationField; import de.tobiyas.racesandclasses.traitcontainer.interfaces.annotations.configuration.TraitConfigurationNeeded; import de.tobiyas.racesandclasses.traitcontainer.interfaces.annotations.configuration.TraitEventsUsed; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.CostType; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.MagicSpellTrait; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.Trait; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.TraitRestriction; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.TraitWithCost; import de.tobiyas.racesandclasses.traitcontainer.traits.pattern.AbstractActivatableTrait; import de.tobiyas.racesandclasses.translation.languages.Keys; import de.tobiyas.racesandclasses.util.traitutil.TraitConfiguration; import de.tobiyas.racesandclasses.util.traitutil.TraitConfigurationFailedException; import de.tobiyas.racesandclasses.vollotile.Vollotile; import de.tobiyas.util.naming.MCPrettyName; import de.tobiyas.util.vollotile.ParticleEffects; import de.tobiyas.util.vollotile.helper.ParticleHelper; public abstract class AbstractMagicSpellTrait extends AbstractActivatableTrait implements MagicSpellTrait { //static naming for YML elements public static final String COST_TYPE_PATH= "costType"; public static final String COST_PATH= "cost"; public static final String ITEM_TYPE_PATH= "item"; public static final String ITEM_DAMAGE_PATH= "itemDamage"; public static final String ITEM_NAME_PATH= "itemName"; public static final String CHANNELING_PATH = "channeling"; /** * This map is to prevent instant retriggers! */ private static final Map<String, Long> lastCastMap = new HashMap<String, Long>(); /** * The Map of currently channeling players. */ private static final Map<UUID, ChannelingContainer> channelingMap = new HashMap<UUID, ChannelingContainer>(); /** * The Cost of the Spell. * * It has the default Cost of 0. */ protected double cost = 0; /** * The Material for casting with {@link CostType#ITEM} */ protected Material materialForCasting = Material.FEATHER; /** * The Material damage for casting. */ protected byte materialDamageForCasting = 0; /** * The Material Name for casting. */ protected String materialNameForCasting = null; /** * The CostType of the Spell. * * It has the Default CostType: {@link CostType#MANA}. */ protected CostType costType = CostType.MANA; /** * The Channeling time. */ protected double channelingTime = 0; /** * The Plugin to call stuff on */ protected final RacesAndClasses plugin = RacesAndClasses.getPlugin(); /** * The ID of the scheduler. */ private int bukkitSchedulerID = -1; @TraitEventsUsed(registerdClasses = {PlayerInteractEvent.class}) @Override public void generalInit(){ if(channelingTime > 0){ bukkitSchedulerID = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, new Runnable() { @Override public void run() { Iterator<Map.Entry<UUID,ChannelingContainer>> entryIt = channelingMap.entrySet().iterator(); while(entryIt.hasNext()){ Map.Entry<UUID, ChannelingContainer> entry = entryIt.next(); RaCPlayer player = RaCPlayerManager.get().getPlayer(entry.getKey()); if(player == null || !player.isOnline() || player.getLocation().distanceSquared(entry.getValue().loc) > 0.5){ entryIt.remove(); entry.getValue().task.cancel(); //Notifies that the Channeling ends: RacesAndClasses.getPlugin().getSilenceAndKickManager().endChannel(player.getUniqueId(), AbstractMagicSpellTrait.this); LanguageAPI.sendTranslatedMessage(player, Keys.magic_chaneling_failed, "trait_name", AbstractMagicSpellTrait.this.getDisplayName()); } } } }, 10, 10); } } @Override public void deInit(){ super.deInit(); if(bukkitSchedulerID > 0){ Bukkit.getScheduler().cancelTask(bukkitSchedulerID); bukkitSchedulerID = -1; } } @Override public boolean canBeTriggered(EventWrapper wrapper){ if(canOtherEventBeTriggered(wrapper)) return true; if(channelingMap.containsKey(wrapper.getPlayer().getName())) return false; PlayerAction action = wrapper.getPlayerAction(); if(!(action == PlayerAction.CAST_SPELL || action == PlayerAction.CHANGE_SPELL)) return false; RaCPlayer player = wrapper.getPlayer(); boolean playerHasWandInHand = checkWandInHand(player); //early out for not wand in hand. if(!playerHasWandInHand) return false; //check if the Spell is the current selected Spell if(this != player.getSpellManager().getCurrentSpell()) return false; return true; } /** * This is a pre-call to {@link #canBeTriggered(Event)}. * When returning true, true will be passed. * * @param event that wants to be triggered * * @return true if interested, false if not. */ protected boolean canOtherEventBeTriggered(EventWrapper wrapper){ return false; } @Override public boolean triggerButHasUplink(EventWrapper eventWrapper) { PlayerAction action = eventWrapper.getPlayerAction(); if(action != PlayerAction.NONE){ boolean playerHasWandInHand = checkWandInHand(eventWrapper.getPlayer()); if(action == PlayerAction.CHANGE_SPELL && playerHasWandInHand){ changeMagicSpell(eventWrapper.getPlayer()); return true; } if(action == PlayerAction.CAST_SPELL){ return playerHasWandInHand; } } //We ignore all other events except the change spell action. return true; } /** * Checks if the Player has a Wand in his hand. * * @param player to check * @return true if he has, false otherwise. */ public boolean checkWandInHand(RaCPlayer player) { ItemStack holding = player.getPlayer().getInventory().getItem(player.getPlayer().getInventory().getHeldItemSlot()); return player.getSpellManager().isWandItem(holding); } @Override public void triggerButDoesNotHaveEnoghCostType(EventWrapper wrapper){ PlayerAction action = wrapper.getPlayerAction(); if(action == PlayerAction.CHANGE_SPELL){ changeMagicSpell(wrapper.getPlayer()); return; } String costTypeString = getCostType().name(); if(getCostType() == CostType.ITEM){ costTypeString = MCPrettyName.getPrettyName( getCastMaterialType(wrapper.getPlayer()), (short) 0, MCPrettyName.EN); } LanguageAPI.sendTranslatedMessage(wrapper.getPlayer(), magic_dont_have_enough, "cost_type", costTypeString, "trait_name", getDisplayName()); } @Override public TraitResults trigger(EventWrapper eventWrapper) { Event event = eventWrapper.getEvent(); final TraitResults result = new TraitResults(); if(event instanceof PlayerInteractEvent){ PlayerInteractEvent playerInteractEvent = (PlayerInteractEvent) event; final RaCPlayer player = eventWrapper.getPlayer(); boolean playerHasWandInHand = checkWandInHand(player); //early out for not wand in hand. if(!playerHasWandInHand) return result.setTriggered(false); //check if the Spell is the current selected Spell if(this != player.getSpellManager().getCurrentSpell()) return result.setTriggered(false); Action action = playerInteractEvent.getAction(); if(action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK){ if(lastCastMap.containsKey(player.getUniqueId())){ if(System.currentTimeMillis() - lastCastMap.get(player.getUniqueId()) < 100){ //2 casts directly after each other. //lets cancle the second return result.setTriggered(false); }else{ lastCastMap.remove(player.getUniqueId()); } } return trigger(player); } if(action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK){ changeMagicSpell(player); return result.setTriggered(false); } } return otherEventTriggered(eventWrapper, result); } /** * This triggers when NO {@link PlayerInteractEvent} is triggered. * * @param event that triggered * @return true if triggering worked and Mana should be drained. */ protected TraitResults otherEventTriggered(EventWrapper eventWrapper, TraitResults result){ return result; } /** * Changes the current magic spell. * * @param player the player triggering the spell * * @return true if the Spell could be changed, false if not. */ public boolean changeMagicSpell(RaCPlayer player){ if(player.getSpellManager().getCurrentSpell() == null) return false; if(player.getSpellManager().getSpellAmount() == 0){ LanguageAPI.sendTranslatedMessage(player.getPlayer(), magic_no_spells); return true; } boolean toPrev = player.getPlayer().isSneaking(); TraitWithCost nextSpell = null; if(toPrev){ nextSpell = player.getSpellManager().changeToPrevSpell(); }else{ nextSpell = player.getSpellManager().changeToNextSpell(); } if(nextSpell != null){ if(!plugin.getConfigManager().getGeneralConfig().isConfig_enable_permanent_scoreboard()){ player.getScoreboardManager().updateSelectAndShow(SBCategory.Spells); } DecimalFormat formatter = new DecimalFormat("###.#"); String costName = formatter.format(nextSpell.getCost(player)); String costTypeString = nextSpell.getCostType() == CostType.ITEM ? nextSpell.getCastMaterialType(player).name() : nextSpell.getCostType().name(); String newSpellName = ((Trait)nextSpell).getDisplayName(); LanguageAPI.sendTranslatedMessage(player.getPlayer(), magic_change_spells, "trait_name", newSpellName, "cost", costName, "cost_type", costTypeString ); return true; }else{ //switching too fast. return true; } } /** * This method is called, when the caster uses THIS magic spell. * * @param player the Player triggering the spell. * @param result to modify */ protected abstract void magicSpellTriggered(RaCPlayer player, TraitResults result); //This is just for Mana + CostType @TraitConfigurationNeeded( fields = { @TraitConfigurationField(fieldName = COST_PATH, classToExpect = Double.class, optional = true), @TraitConfigurationField(fieldName = COST_TYPE_PATH, classToExpect = String.class, optional = true), @TraitConfigurationField(fieldName = ITEM_TYPE_PATH, classToExpect = Material.class, optional = true), @TraitConfigurationField(fieldName = ITEM_DAMAGE_PATH, classToExpect = Integer.class, optional = true), @TraitConfigurationField(fieldName = ITEM_NAME_PATH, classToExpect = String.class, optional = true), @TraitConfigurationField(fieldName = CHANNELING_PATH, classToExpect = Double.class, optional = true), }) @Override public void setConfiguration(TraitConfiguration configMap) throws TraitConfigurationFailedException { super.setConfiguration(configMap); cost = configMap.getAsDouble(COST_PATH, 0); if(configMap.containsKey(COST_TYPE_PATH)){ String costTypeName = configMap.getAsString(COST_TYPE_PATH); costType = CostType.tryParse(costTypeName); if(costType == null){ throw new TraitConfigurationFailedException(getName() + " is incorrect configured. costType could not be read."); } if(costType == CostType.ITEM){ if(!configMap.containsKey(ITEM_TYPE_PATH)){ throw new TraitConfigurationFailedException(getName() + " is incorrect configured. 'costType' was ITEM but no Item is specified at 'item'."); } materialForCasting = configMap.getAsMaterial(ITEM_TYPE_PATH); if(materialForCasting == null){ throw new TraitConfigurationFailedException(getName() + " is incorrect configured." + " 'costType' was ITEM but the item read is not an Item. Items are CAPITAL. " + "See 'https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html' for all Materials. " + "Alternative use an ItemID."); } materialDamageForCasting = (byte) configMap.getAsInt(ITEM_DAMAGE_PATH, 0); materialNameForCasting = configMap.getAsString(ITEM_NAME_PATH, null); } } channelingTime = configMap.getAsDouble(CHANNELING_PATH, 0); } @Override public double getCost(RaCPlayer player){ int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastCostForLevel(level, modifyToPlayer(player, cost, "cost")); } @Override public CostType getCostType(){ return costType; } @Override public Material getCastMaterialType(RaCPlayer player) { int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastMaterialForLevel(level, this.materialForCasting); } @Override public short getCastMaterialDamage(RaCPlayer player) { int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastMaterialDamageForLevel(level, this.materialDamageForCasting); } @Override public String getCastMaterialName(RaCPlayer player) { int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastMaterialNameForLevel(level, this.materialNameForCasting); } @Override public boolean isStackable(){ return true; } @Override public boolean needsCostCheck(EventWrapper wrapper){ return wrapper.getEvent() instanceof PlayerInteractEvent; } @Override public void triggerButHasRestriction(TraitRestriction restriction, EventWrapper wrapper) { if(restriction == TraitRestriction.Costs){ if(!checkWandInHand(wrapper.getPlayer())) return; triggerButDoesNotHaveEnoghCostType(wrapper); }else{ if(wrapper.getPlayerAction() == PlayerAction.CHANGE_SPELL){ changeMagicSpell(wrapper.getPlayer()); } } } @Override public boolean isBindable() { return true; } @Override public double getChannelingTime() { return channelingTime; } @Override public TraitResults trigger(final RaCPlayer player) { final TraitResults result = TraitResults.False(); if(channelingTime > 0){ BukkitTask task = Bukkit.getScheduler().runTaskLater(plugin, new Runnable() { @Override public void run() { if(channelingMap.containsKey(player.getUniqueId())){ //Notifies that the Channeling ends: RacesAndClasses.getPlugin().getSilenceAndKickManager().endChannel(player.getUniqueId(), AbstractMagicSpellTrait.this); //check restrictions for the New Trigger. if(checkRestrictions(EventWrapperFactory.buildOnlyWithplayer(player)) != TraitRestriction.None) return; magicSpellTriggered(player, result); if(result.isTriggered() && result.isRemoveCostsAfterTrigger()){ player.getSpellManager().removeCost(AbstractMagicSpellTrait.this); } if(result.isTriggered() && result.isSetCooldownOnPositiveTrigger()){ setCooldownIfNeeded(player); } } } }, (long) (channelingTime * 20)); //Notifies that the Channeling ends: RacesAndClasses.getPlugin().getSilenceAndKickManager().startsChannel(player.getUniqueId(), this); channelingMap.put(player.getUniqueId(), new ChannelingContainer(player.getLocation(), task)); result.setTriggered(false); }else{ magicSpellTriggered(player, result); if(result.isTriggered() && result.isRemoveCostsAfterTrigger()){ player.getSpellManager().removeCost(this); if(onUseParticles != null){ Vollotile.get().sendOwnParticleEffectToAll( onUseParticles, player.getLocation().add(0, 1, 0), 0, 10); } } } return result; } @Override protected void evaluateIntern(RaCPlayer player, TraitResults result) { if(result.isTriggered() && result.isRemoveCostsAfterTrigger()){ //The spell was triggered! So start removing stuff! player.getSpellManager().removeCost(this); } } @Override public void gotKicked(UUID player) { ChannelingContainer container = channelingMap.remove(player); if(container != null && isKickable()){ container.task.cancel(); RaCPlayer racPlayer = RaCPlayerManager.get().getPlayer(player); if(racPlayer.isOnline()){ ParticleHelper.sendXParticleEffectToAllWithRandWidth( ParticleEffects.CRIT, racPlayer.getPlayer().getEyeLocation(), 0, 10); racPlayer.sendTranslatedMessage(Keys.trait_kicked, "name", getName()); } } } @Override public boolean isKickable() { return true; } @Override protected TraitRestriction checkForFurtherRestrictions(EventWrapper wrapper) { RaCPlayer player = wrapper.getPlayer(); if(!wrapper.getPlayer().getSpellManager().canCastSpell(this)){ return TraitRestriction.Costs; } if(SilenceAndKickAPI.isSilenced(player.getUniqueId())) { return TraitRestriction.Silenced; } return super.checkForFurtherRestrictions(wrapper); } protected static class ChannelingContainer{ final Location loc; final BukkitTask task; public ChannelingContainer(Location loc, BukkitTask task) { this.loc = loc; this.task = task; } } }