--- title: Mythic Mob 自訂怪物掉落物動畫 tags: Minecraft Plugin --- ## Mythic Mob 自訂怪物掉落物 ### 插件期望效果 :::success 對[MythicMob](https://mythiccraft.io/index.php?ewr-porta/)怪物掉落的[DropTable](https://www.mythicmobs.net/mmoitems/manual/doku.php/items/droptables)使打怪掉落物品的動畫更改 例如[新楓之谷](https://maplestory.beanfun.com/main) ![](https://i.imgur.com/Zn1njtG.gif) <div style="text-align:center; font-size:20px;"> 打死Boss向外噴散物品 </div> ![](https://i.imgur.com/5txt3oF.gif) <div style="text-align:center; font-size:20px;"> 打死小怪垂直向上掉落物品 </div> ::: ### 如何達到 :::success 1. 先透過抓取 MythicMob 怪物DropTable得知產生何物 2. 利用nms發送假的物品產生封包給玩家 3. 玩家靠近該物品撿起物品 ::: ### 優點 :::success 由於物品是用封包產生,故可以做到 <u>僅限</u> 特定玩家可以撿起 例如: :::info 設定限制只有打過該怪物的玩家可以撿起掉落物品 相對的沒有對該怪物進行戰鬥的玩家無法撿起物品(包括看到物品) ::: ### 版本 :::success 遊戲版本為 1.17 [MythicMob](https://www.mythiccraft.io/downloads/mythicmobs/free/MythicMobs-5.0.0-alpha1.jar) 版本為 5.0.0-alpha1 [Spigot](https://www.spigotmc.org/wiki/buildtools/) 版本為 1.17.1-R0.1-SNAPSHOT ::: ## 前置設定 ### 設定 repo :::info ``` //pom.xml <repositories> <repository> <id>spigotmc-repo</id> <url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url> </repository> <repository> <id>sonatype</id> <url>https://oss.sonatype.org/content/groups/public/</url> </repository> </repositories> ``` ::: ### 設定 depend :::info ``` //pom.xml <dependencies> <dependency> <groupId>org.spigotmc</groupId> <artifactId>spigot</artifactId> <version>1.17.1-R0.1-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.lumine.xikage</groupId> <artifactId>MythicMobs</artifactId> <version>5.0.0-SNAPSHOT-5145f853</version> <systemPath>${project.basedir}/src/main/resources/MythicMobs-5.0.0-alpha1.jar</systemPath> <scope>system</scope> </dependency> </dependencies> ``` ::: ### 設定 plugin.yml :::info ```yml ... api-version: 1.17 depend: [MythicMobs] ... ``` ::: ## 代碼解釋 ### MMdrops.java :::info 為此輔助插件的主要class ```java public HashMap<UUID, PlayerDrops> cache = new HashMap<>(); private Event event; private BackgroundTimer backgroundTimer; ``` 1. ``HashMap<UUID, PlayerDrops> cache`` 紀錄玩家上線至下線時的對應 PlayerDrops 類別 2. ``Event event`` 實例(instance)方法,在此用來刪除在``Event.java``裡的``trans`` 3. ``private BackgroundTimer backgroundTimer``此插件唯一背景執行Runnable介面 ::: ### BackgroundTimer.java ```java= public class BackgroundTimer { int taskID; public BackgroundTimer(MMdrops plugin){//導入主要class this.taskID = Bukkit.getScheduler().scheduleSyncRepeatingTask(MMdrops.getPlugin(MMdrops.class),new Runnable() { @Override public void run() { // //每 1/20 秒 偵測物品是否需要消失, 在此順便確定 玩家是否撿起物品 // for (Map.Entry<UUID, PlayerDrops> entry : plugin.cache.entrySet()){ entry.getValue().check();//確認物品生命週期與是否被撿起 } } }, 0L, 1L); } public void onDisable(){ Bukkit.getScheduler().cancelTask(this.taskID);//取消task } } ``` :::info 在 line 10 可見 ``plugin.cache.entrySet`` 抓取目前在線上的所有玩家所對應的所有物品 ::: ### Event.java :::info ```java private HashMap<UUID,UUID> trans = new HashMap<>(); private HashMap<UUID, Set<UUID>> killer = new HashMap<>(); private final MMdrops plugin; ``` 1. ``HashMap<UUID,UUID> trans`` 為一個 Falling Block 對應到 該物品(導航)的持有者(Player)的UUID 2. ``HashMap<UUID, Set<UUID>> killer`` 用來記錄 MythicMob 的UUID 對應到所有打過此怪物的玩家UUID 3. ``final MMdrops plugin`` 主要 class ::: #### 登入登出 ```java= @EventHandler public void onPlayerJoin(PlayerJoinEvent event){ Player player = event.getPlayer(); this.plugin.createPlayerDrops(player); //建立新的 player Drop } @EventHandler public void onPlayerQuit(PlayerQuitEvent event){ Player player = event.getPlayer(); this.plugin.removePlayerDrops(player.getUniqueId()); //刪除 player drop } ``` #### MythicMob 怪物生成 ```java= @EventHandler public void onMMspawned(MythicMobSpawnEvent event){ this.killer.put(event.getEntity().getUniqueId(),new HashSet<>()); //產生新的 集合 用來儲存對怪物進行過攻擊的玩家 } ``` #### MythicMob 怪物被擊殺 ```java= @EventHandler public void onMMDeath(MythicMobDeathEvent event){ MythicMob mob = event.getMobType();//導入怪物 List<ItemStack> drops = event.getDrops();//導入掉落物 for (UUID uuid: this.killer.get(event.getEntity().getUniqueId())){//對每個有進行過攻擊的玩家 List<UUID> fallingBlockUUIDs = plugin.getPlayerDrops(uuid).addItems(drops,event.getEntity().getLocation(),mob); //每個物品對應到的 Falling Block UUID for (UUID fallingblockuuid:fallingBlockUUIDs) { this.trans.put(fallingblockuuid, uuid); } //加入到 trans 裡面 } drops.clear();// 清除掉落物 event.setDrops(drops); this.killer.remove(event.getEntity().getUniqueId());//取消 killer 計數 } ``` #### MythicMob 怪物消失 ```java= @EventHandler public void onMMDespawned(MythicMobDespawnEvent event){ this.killer.remove(event.getEntity().getUniqueId());//取消 killer 計數 } ``` #### FallingBlock 放置方塊時 ```java= @EventHandler public void onFallingBlockPlaced(EntityChangeBlockEvent event){ if (event.getEntity() instanceof FallingBlock){ FallingBlock fallingBlock = (FallingBlock) event.getEntity(); if (trans.containsKey(fallingBlock.getUniqueId())){//確認是否為導航物品專用的 Falling Block plugin.getPlayerDrops(trans.get(fallingBlock.getUniqueId())).setFinalLocation(fallingBlock);//更改最後物品的 location trans.remove(fallingBlock.getUniqueId());//刪除對應ㄋ event.setCancelled(true);//取消放置 } } } ``` #### MythicMob 怪物被攻擊時 (needed to be updated somedays) ```java= @EventHandler public void onMMDamaged(EntityDamageByEntityEvent event){ if (event.getDamager() instanceof Player && MythicMobs.inst().getMobManager().isActiveMob(event.getEntity().getUniqueId())){ this.killer.get(event.getEntity().getUniqueId()).add(event.getDamager().getUniqueId()); }else if (MythicMobs.inst().getMobManager().isActiveMob(event.getEntity().getUniqueId())){ if (event.getDamager() instanceof Arrow){ Arrow arrow = (Arrow) event.getDamager(); if (arrow.getShooter() instanceof Player){ Player player = (Player) arrow.getShooter(); this.killer.get(event.getEntity().getUniqueId()).add(player.getUniqueId()); } } } } ``` ### CustomItem.java :::info ```java private FallingBlock block; private EntityItem item; private ItemStack itemStack; private net.minecraft.world.item.ItemStack nmsItem; private Location finalLocation; private Long Age; private MMdrops plugin; private MythicMob mythicMob; ``` :warning: 此CustomeItem類別 被當作為一個不存在的實體物品,也就是以封包方式發送 1. ``FallingBlock block`` 為此物品的導航,用來導航到最終位置(稍微偏差) ![](https://i.imgur.com/vy9coET.png) 由於Falling Block 與 Items 擁有相同的性質,故採用Falling Block 為其導航,導航至最終位置(又因Falling Block實體較大,故最終稍微有些偏差) 2. ``EntityItem item`` 以封包方式,使對應玩家可看到 3. ``ItemStack itemStack`` 怪物的掉落物 4. ``net.minecraft.world.item.ItemStack nmsItem`` 由itemStack轉為nmsItem 5. ``Location finalLocation`` 最終位置,一開始的設定為怪物死亡位置 6. ``Long Age`` 物品消失時間 7. ``MMdrops plugin`` 主要class 8. ``MythicMob mythicMob`` 怪物導入,用於讀取config值 ::: #### 建構子 在建構子裡可看到直接產生對應封包給玩家 ```java= PlayerConnection connection = ((CraftPlayer)player).getHandle().b; //建立連線 connection.sendPacket(new PacketPlayOutSpawnEntity(item)); //產生物品 pMobEffect mbeffect = new MobEffect(new MobEffect(MobEffectList.fromId(24),Integer.MAX_VALUE,0,false,false)); this.item.setFlag(6,true); //給予發光效果 connection.sendPacket(new PacketPlayOutEntityEffect(item.getId(),mbeffect)); //給予效果封包 connection.sendPacket(new PacketPlayOutEntityMetadata(item.getId(), item.getDataWatcher(), true)); //給予物品外觀 connection.sendPacket(new PacketPlayOutEntityVelocity(item.getId(),motion)); //給予物品motion ``` #### 設置最後位址以及校正 :::info ```java= public void setLocation(Location location,Player player){ //respawn item PlayerConnection connection = ((CraftPlayer)player).getHandle().b; connection.sendPacket(new PacketPlayOutEntityDestroy(item.getId())); this.item = new EntityItem(((CraftWorld)location.getWorld()).getHandle(), location.getX(), location.getY(), location.getZ(), this.nmsItem); connection.sendPacket(new PacketPlayOutSpawnEntity(item)); MobEffect mbeffect = new MobEffect(new MobEffect(MobEffectList.fromId(24),Integer.MAX_VALUE,0,false,false)); this.item.setFlag(6,true); this.item.setNoGravity(true); connection.sendPacket(new PacketPlayOutEntityEffect(item.getId(),mbeffect)); connection.sendPacket(new PacketPlayOutEntityMetadata(item.getId(), item.getDataWatcher(), true)); this.finalLocation = location; } ``` 簡單來說就是使用到此 method 代表已經是最後確定的位置了,故給物品多加上 noGravity 並且重新召喚 ::: #### 玩家撿起物品 :::info ```java= public boolean pickupItem(Player player){ if (player.getInventory().firstEmpty() == -1) return false; //確認玩家是否有空間 player.getInventory().addItem(itemStack); player.sendMessage("You pickup "+(itemStack.getItemMeta().hasDisplayName() ? itemStack.getItemMeta().getDisplayName() : itemStack.getType().name())+" x"+itemStack.getAmount()); player.playSound(player.getLocation(), Sound.ENTITY_ITEM_PICKUP,10,1); return true; } ``` ::: ### PlayerDrops.java :::info ```java private HashMap<UUID,CustomItem> customItem = new HashMap<>(); private final Player player; private final MMdrops plugin; ``` 1. ``HashMap<UUID,CustomItem> customItem`` 用來記錄物品導航的Falling Block UUID 對應到的 CustomItem 2. ``final Player player`` 目前PlayerDrops的對應玩家 3. ``final MMdrops plugin`` 主要class ::: #### 加入物品 :::info ```java= public List<UUID> addItems(@NotNull List<ItemStack> items, Location location, MythicMob mythicMob){ List<UUID> FallingBlockUUIDs = new ArrayList<>(); for (ItemStack item : items){ CustomItem customItem = new CustomItem(item,location,player,plugin,mythicMob); this.customItem.put(customItem.getFallingBlockUUID(),customItem); FallingBlockUUIDs.add(customItem.getFallingBlockUUID()); } return FallingBlockUUIDs; } ``` 會回傳一個UUID list 用來記錄每個Falling Block 的 UUID 回傳到 Event.class 用於紀錄 ``trans`` ::: #### 設置最後位置 :::info ```java= public void setFinalLocation(@NotNull FallingBlock fallingBlock){ if (customItem.containsKey(fallingBlock.getUniqueId())){ Location finalLocation = fallingBlock.getLocation(); if (isWaterLogged(finalLocation)){ //校正 finalLocation = finalLocation.add(0,1-(finalLocation.getY()-Math.floor(finalLocation.getY())),0); } // 累加到 final Location 為非 water Block while(isWaterLogged(finalLocation)){ finalLocation = finalLocation.add(0,1,0); } customItem.get(fallingBlock.getUniqueId()).setLocation(finalLocation,player); } } ``` 在 ``line 5-8`` 可看到有一格用來校正當 最後位置是在辦專上且 辦磚上有水等類似情況時 用來矯正其位置 ```java= /** * 偵測目前位置的方塊 是否包含水 * @param blockLocation 對應位置 * @return true 為真 */ private boolean isWaterLogged(Location blockLocation){ Block block = blockLocation.getBlock(); BlockData blockData = block.getBlockData(); if (isWaterBlocks(block.getType())) return true; if (blockData instanceof Waterlogged){ return ((Waterlogged) blockData).isWaterlogged(); } return false; } /** * * @param block 材料 * @return 是否為 水方塊 */ private boolean isWaterBlocks(Material block){ return block.equals(Material.WATER) | block.equals(Material.KELP) | block.equals(Material.SEAGRASS) | block.equals(Material.TALL_SEAGRASS) | block.equals(Material.KELP_PLANT); } ``` 確認是否目前方塊 為水方塊 ::: #### 超時與撿起物品 :::info ```java= public void check(){ if (this.customItem.isEmpty()) return; Location location = player.getLocation(); for (CustomItem item:this.customItem.values()){ if (location.distance(item.getFinalLocation()) < 1.5){ if (item.pickupItem(this.player)){ item.DestroyItem(this.player); this.customItem.remove(item.getFallingBlockUUID()); break; // Debug } }else if (item.isOvered()){ item.DestroyItem(this.player); this.customItem.remove(item.getFallingBlockUUID()); break; } } } ``` 在此可以注意到 ``line 10、15`` 各有一行 ``break`` 用來防止刪除物品後 因為List 抓不到下一個位置的錯誤問題 ::: ## 成品 :::success ![](https://i.imgur.com/evyk8fI.gif) <div style="text-align:center; font-size:20px;"> 打死怪物者可以看到掉落物 </div> ![](https://i.imgur.com/8D0OvQA.gif) <div style="text-align:center; font-size:20px;"> 尚未打怪物的玩家無法看到 </div> ::: ### Github [GuoJTim/MMdrops](https://github.com/GuoJTim/MMdrops) ### Contributes <div valign="middle"> <image width="20px" src="https://crafatar.com/avatars/69920481e8a846a0a4d00acc25cc2143"><a href="https://namemc.com/profile/69920481e8a846a0a4d00acc25cc2143">Bnpig</a><br> <image width="20px" src="https://crafatar.com/avatars/eb6eb960ef2e4c3ead9dfeaa98c656b7"><a href="https://namemc.com/profile/eb6eb960ef2e4c3ead9dfeaa98c656b7">BugTea</a> </div>