package jp.jias.bukkit.bossmonster; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.logging.Logger; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.configuration.Configuration; import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Arrow; import org.bukkit.entity.Creature; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.Player; import org.bukkit.entity.Skeleton; import org.bukkit.entity.Skeleton.SkeletonType; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageByBlockEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.entity.EntityRegainHealthEvent; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.util.Vector; /** * ボスモンスタープラグイン本体 * * @author i_shi_ba_shi ( jias.jp ) * */ public class BossMonster extends JavaPlugin implements Listener{ /** * 乱数ジェネレータ */ static private Random rand=new Random(); /** * ボスの残りHPゲージの表示方法 */ static final private String HEALTHGAUGE="||||||||||"; /** * ボスのドロップアイテムの設定キー */ static final private String CONF_DROPITEM="dropItem"; /** * ボスモンスター */ private Creature boss=null; /** * 直前に実行したスキル */ private Skill skill=null; /** * ボスのレベル */ private int level; /** * ログ出力用 */ private Logger log; /** * 設定ファイルの内容 */ private Configuration conf; /** * プラグインが開始するとき呼び出される */ public void onEnable(){ log=this.getLogger(); // 独自のシリアライズ・デシリアライズ可能クラスを登録する ConfigurationSerialization.registerClass(DropItem.class); // デフォルトの設定ファイルを用意する this.saveDefaultConfig(); // 設定ファイルを読み込む conf=this.getConfig(); // イベントリスナーの登録 getServer().getPluginManager().registerEvents((Listener) this,this); } /** * プラグインが終了するとき呼び出される */ public void onDisable(){ // ボスがまだ生きていたら削除する if(boss != null && !boss.isDead()){ Bukkit.broadcastMessage(ChatColor.GOLD + "邪悪な存在は姿を消した…"); boss.remove(); boss=null; } } /** * コマンドが実行されたら呼び出される */ public boolean onCommand(CommandSender sender,Command cmd, String commandLabel,String[] args){ // コマンドを実行したプレイヤーを特定する if(!(sender instanceof Player)){ log.warning("このコマンドはゲームにログインしたキャラクターが実行してください。"); return true; } // ボスモンスターを呼び出すコマンドか if(commandLabel.equalsIgnoreCase("boss")){ // 既にボスがいるか if(boss != null){ // 既にいるボスをコマンド実行者の場所に移動させる boss.teleport(((Player) sender).getLocation()); // 効果音としてワープポータルに入ったときの音を再生する boss.getWorld().playSound(boss.getLocation(),Sound.PORTAL,100,1f); sender.sendMessage(ChatColor.GOLD + "ボスをあなたの場所に移動させました。"); return true; } // 難易度のデフォルトは1 level=1; // コマンドの引数で難易度が指定されているか if(args.length == 1){ // 指定された難易度を数値として解釈する try{ level=Integer.parseInt(args[0]); }catch(NumberFormatException e){ sender.sendMessage(ChatColor.GOLD + "難易度は数値で指定してください。"); return false; } // 難易度を1以上50以下に制限する if(level < 1){ level=1; } else if(level > 50){ level=50; } } // ボスモンスターを配置する boss=(Creature) ((Entity) sender).getWorld().spawnEntity(((Entity) sender).getLocation(),EntityType.SKELETON); // スケルトンをスポーンさせた場合、ネザーではウィザースケルトンが出る可能性があるので、通常のスケルトンにする if(boss.getType() == EntityType.SKELETON){ ((Skeleton) boss).setSkeletonType(SkeletonType.NORMAL); } // ボスに持たせる武器を作る ItemStack weapon=new ItemStack(Material.BOW); // ボスの武器にダメージ増加エンチャントをかける weapon.addEnchantment(Enchantment.ARROW_DAMAGE,2); // ボスに武器を持たせる boss.getEquipment().setItemInHand(weapon); // ボスに鉄の防具4点セット(ヘルメット・チェストプレート・レギンス・ブーツ)を装備させる boss.getEquipment().setHelmet(new ItemStack(Material.IRON_HELMET)); boss.getEquipment().setChestplate(new ItemStack(Material.IRON_CHESTPLATE)); boss.getEquipment().setLeggings(new ItemStack(Material.IRON_LEGGINGS)); boss.getEquipment().setBoots(new ItemStack(Material.IRON_BOOTS)); // ボスに名前を付ける updateBossHealthGauge(); // ボスの名前を常に表示する boss.setCustomNameVisible(true); // ボスのHPを多めに設定 boss.setMaxHealth(100 * level); boss.setHealth(boss.getMaxHealth()); // ボスの移動速度を、レベル20以下ではレベル2、それ以上ではレベル3にする int speed=level <= 20?2:3; // ボスに1時間(3600秒)のエンチャント(移動速度増加・攻撃力上昇・再生能力・耐性・火炎耐性)をかける boss.addPotionEffect(new PotionEffect(PotionEffectType.SPEED,3600 * 20,speed,false)); boss.addPotionEffect(new PotionEffect(PotionEffectType.INCREASE_DAMAGE,3600 * 20,1,false)); boss.addPotionEffect(new PotionEffect(PotionEffectType.REGENERATION,3600 * 20,1,false)); boss.addPotionEffect(new PotionEffect(PotionEffectType.DAMAGE_RESISTANCE,3600 * 20,1,false)); boss.addPotionEffect(new PotionEffect(PotionEffectType.FIRE_RESISTANCE,3600 * 20,1,false)); // ボスがデスポーンするのを防止する boss.setRemoveWhenFarAway(false); // ボスが配置されたことをサーバー全体メッセージで報告する getServer().broadcastMessage(ChatColor.YELLOW + "邪悪な存在がこの地に舞い降りた…"); // 効果音としてワープポータルに入ったときの音を再生する boss.getWorld().playSound(boss.getLocation(),Sound.PORTAL_TRAVEL,100,1f); // ボスにエフェクトをつける new BossEffect(boss).runTaskTimer(this,0,5); return true; } // ドロップアイテムを設定するコマンドか else if(commandLabel.equalsIgnoreCase("bossdrop")){ // ホットバーに登録されているアイテムのリストを作る List items=new ArrayList<>(); Inventory inv=((Player) sender).getInventory(); for(int i=0;i < 9;i++){ // nullまたは空気アイテムは無視 if(inv.getItem(i) == null || inv.getItem(i).getType() == Material.AIR){ continue; } // アイテム個数は所持数の0.5倍から2倍の範囲、レアリティは100%でドロップアイテムのデータを作る ItemStack item=inv.getItem(i).clone(); DropItem drop=new DropItem(); drop.setAmountMin(item.getAmount() / 2d); drop.setAmountMax(item.getAmount() * 2); drop.setRarity(100); // アイテム個数のデータはamountMin、amountMaxで表現されるので、ItemStackのアイテム個数は1にしておく item.setAmount(1); drop.setItem(item); // リストに追加 items.add(drop); } // ドロップアイテムがないときはエラー if(items.isEmpty()){ sender.sendMessage(ChatColor.GOLD + "ドロップアイテムを設定するには、あなたのホットバーにドロップしたいアイテムを配置し、このコマンドをもう一度実行してください。"); return true; } // ドロップアイテムの設定を上書きして保存 conf.set(CONF_DROPITEM,items); sender.sendMessage(ChatColor.GOLD + "ドロップアイテムを設定しました。ドロップの数量や確率を調整するには、以下のファイルを直接編集して下さい。"); sender.sendMessage(ChatColor.GOLD + this.getDataFolder().getAbsolutePath() + File.separator + "conf.yml"); // パスをコピペできるよう、サーバーコンソールにも同じメッセージを出力する Bukkit.getConsoleSender() .sendMessage( ChatColor.GOLD + "ドロップアイテムを設定しました。ドロップの数量や確率を調整するには、以下のファイルを直接編集して下さい。"); Bukkit.getConsoleSender().sendMessage( ChatColor.GOLD + this.getDataFolder().getAbsolutePath() + File.separator + "conf.yml"); this.saveConfig(); return true; } return false; } /** * エンティティがエンティティからダメージを受けるとき呼び出される * * @param e */ @EventHandler public void onEntityDamageByEntityEvent(EntityDamageByEntityEvent e){ // ダメージを受けたエンティティはボスか if(e.getEntity().equals(boss)){ // ダメージ原因が矢で、かつその矢がボス自身の放ったものならダメージをキャンセルする if(e.getDamager() instanceof Arrow && boss.equals(((Arrow) e.getDamager()).getShooter())){ e.setCancelled(true); return; } // 1/3の確率で、効果音としてウィザーの攻撃音を再生速度50%で再生する if(rand.nextInt(3) == 0){ boss.getWorld().playSound(boss.getLocation(),Sound.WITHER_SHOOT,100,(float) (0.5+rand.nextFloat())); } // ボスのHP表示を更新する updateBossHealthGauge(); // スキルを実行中ではないか if(skill == null || !skill.isRunning()){ // 低確率でスキルを実行する switch(rand.nextInt(20)){ case 0: // 乱数で0が出たらトルネードショット skill=new TornadoShot(this,level); break; case 1: // 乱数で1が出たらマシンガンショット skill=new MachinegunShot(this,level); break; case 2: // 乱数で2が出たらシャワーショット skill=new ShowerShot(this,level); break; case 3: // 乱数で3が出てボスのHPが半分以下ならヒールポーション if(boss.getHealth() / (float) boss.getMaxHealth() < 0.5){ skill=new ThrowPotion(this,level); } break; default: // それ以外はスキルを実行しない break; } } // ボスがスケルトンかつレベル30以上かつ体力が30%以下の場合はウィザースケルトンに変身させる if(boss.getType() == EntityType.SKELETON && ((Skeleton) boss).getSkeletonType() == SkeletonType.NORMAL && level >= 30 && boss.getHealth() / (float) boss.getMaxHealth() < 0.3f){ getServer().broadcastMessage( ChatColor.YELLOW + "邪悪な存在はその正体を現した!"); // スケルトンタイプをウィザースケルトンに変更 ((Skeleton) boss).setSkeletonType(SkeletonType.WITHER); // 効果音を再生 boss.getWorld().playSound(boss.getLocation(), Sound.ZOMBIE_REMEDY,100,0.5f); } } } /** * エンティティがブロックからダメージを受けるとき呼び出される * * @param e */ @EventHandler public void onEntityDamageByBlockEvent(EntityDamageByBlockEvent e){ // ダメージを受けたエンティティはボスか if(e.getEntity().equals(boss)){ // ボスのHP表示を更新する updateBossHealthGauge(); } } /** * エンティティの体力が回復するとき呼び出される * * @param e */ @EventHandler public void onEntityRegainHealthEvent(EntityRegainHealthEvent e){ // エンティティはボスか if(e.getEntity().equals(boss)){ // ボスのHP表示を更新する updateBossHealthGauge(); } } /** * ボスの名前と残りHPゲージを表示する */ private void updateBossHealthGauge(){ // ボスの残りHPが最大HPの何割か調べる int remaining=(int) (boss.getHealth() / (double) boss.getMaxHealth() * HEALTHGAUGE .length()); // ボスの名前と残りHPを表示する boss.setCustomName("ゴルゴ・サーティワン " + ChatColor.GREEN + HEALTHGAUGE.substring(0,remaining) + ChatColor.DARK_RED + HEALTHGAUGE.substring(remaining) + ChatColor.RESET); } /** * エンティティが死亡するとき呼び出される * * @param e */ @SuppressWarnings("unchecked") @EventHandler public void onEntityDeathEvent(EntityDeathEvent e){ // 死亡したのはボスか if(e.getEntity().equals(boss)){ // 効果音としてウィザーの死亡音を再生する boss.getWorld().playSound(boss.getLocation(),Sound.WITHER_DEATH,100,1f); // スキルを実行中なら停止する if(skill != null && skill.isRunning()){ skill.cancel(); } // ボスが死んだことをサーバー全体メッセージで報告する getServer().broadcastMessage(ChatColor.YELLOW + "邪悪な存在は勇者により討伐された…"); // 設定ファイルからドロップアイテムの一覧と経験値の最小・最大値を取得する int dropExpMin,dropExpMax; List dropItems; try{ dropExpMin=conf.getInt("dropExp.amountMin"); dropExpMax=conf.getInt("dropExp.amountMax"); dropItems=(List) conf.getList(CONF_DROPITEM); }catch(Exception ex){ // エラー発生時は何もドロップしない log.warning("[ボスモンスタープラグイン] ボス討伐時のドロップ経験値とアイテムの設定を読み込めません。"); dropExpMin=0; dropExpMax=0; dropItems=new ArrayList(); } // ボスの座標を取得 Location bossLoc=boss.getLocation(); // ボスのレベル数だけドロップ計算する for(int i=0;i < level;i++){ // ドロップアイテムの一覧を処理 for(DropItem dropItem:dropItems){ // ドロップアイテムが集約してしまわないよう、周辺5m以内のランダムな座標を生成する dropItem.fire(getCircumferenceLocation(bossLoc,5)); } } // ボスのレベル数だけ経験値オーブを作る // Spigotサーバではオーブは近くのオーブと合算されてしまうので、別の場所にスポーンさせてからボスのもとへ移動させる Location spawnLoc=bossLoc.clone(); spawnLoc.setY(-1); for(int i=0;i < level;i++){ // 経験値を計算する int exp=dropExpMin + rand.nextInt(dropExpMax - dropExpMin); // 経験値がゼロだったらオーブをスポーンしない if(exp == 0){ continue; } // 経験値オーブをスポーンする ExperienceOrb orb=spawnLoc.getWorld().spawn(spawnLoc,ExperienceOrb.class); orb.setExperience(orb.getExperience() + exp); orb.teleport(bossLoc); orb.setVelocity(Vector.getRandom().multiply(0.3f)); } // 経験値オーブは別途スポーンさせているのでボスからは経験値オーブをスポーンさせない e.setDroppedExp(0); // ボスが死んだので変数を初期化する boss=null; } } /** * 指定した座標を中心に水平方向の周囲の座標を計算して返す(ただし計算量削減のため"周囲"の形は円ではなく短形) * * @param loc * 中央の座標 * @param width * 半径 * * @return 周囲の座標 */ public Location getCircumferenceLocation(Location loc,int width){ // 座標をコピー Location cirLoc=loc.clone(); // XとZ方向にランダムな値を加算 cirLoc.add(rand.nextDouble() * width * 2 - width,0,rand.nextDouble() * width * 2 - width); return cirLoc; } /** * チャンクがアンロードされるとき呼び出される * * @param e */ @EventHandler public void onChunkUnloadEvent(ChunkUnloadEvent e){ // ボスは生きているか if(boss != null && !boss.isDead()){ // アンロードされるチャンクはボスがいるチャンクか if(e.getChunk().equals(boss.getLocation().getBlock().getChunk())){ // アンロードをキャンセル e.setCancelled(true); } } } /** * 乱数ジェネレータを返す * * @return 乱数ジェネレータ */ static public Random getRandom(){ return rand; } /** * ボスを返す * * @return ボス */ public Creature getBoss(){ return boss; } }