---
# System prepended metadata

title: Mythic Mob 自訂怪物掉落物動畫
tags: [Minecraft Plugin]

---

---
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>
