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

<div style="text-align:center; font-size:20px;">
打死Boss向外噴散物品
</div>

<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`` 為此物品的導航,用來導航到最終位置(稍微偏差)

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

<div style="text-align:center; font-size:20px;">
打死怪物者可以看到掉落物
</div>

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