バニラのクールダウンを自分の武器に実装したい！

##### はじめに
1つで1スタックとなるツール系統を右クリックで使用したときに、エンダーパールのようなクールダウンをつける機能についてです。バージョン1.21.3時点の情報でお話しします。

# 結論
前置きとか要らんからさっさと仕様を見せろ、という方のために結論からです。
解説などは後ほど。
### Loottableファイル
```json:<font color="yellow">namespace:item</font>
{
  "pools": [
    {
      "rolls": 1,
      "entries": [
        {
          "type": "item",
          "name": "wooden_sword",
          "functions": [
            {
              "function": "set_custom_data",
              "tag": "{\"weapon\":1b}"
            },
            {
              "function": "set_components",
              "components": {
                "use_cooldown": {
                  "seconds": 1,
                  "cooldown_group": "tool"
                },
                "consumable": {
                  "consume_seconds": 0,
                  "sound": "intentionally_empty",
                  "has_consume_particles": false
                },
                "use_remainder": {
                  "id": "wooden_sword",
                  "components": {
                    "custom_data": "{\"Used\":1b,Loottable:\"namespace:item\"}"
                  }
                }
              }
            }
          ]
        }
      ]
    }
  ]
}
```
まずこんな感じのloottableファイルを用意します。`namespace`など各データ名はご自身のデータパックに合うように変更してください。
### Advancementファイル
```json:<font color="yellow">namespace:consume</font>
{
  "criteria": {
    "weapon": {
      "trigger": "consume_item",
      "conditions": {
        "item": {
          "predicates": {
            "custom_data": "{\"weapon\":1b}"
          }
        }
      }
    }
  },
  "rewards": {
    "function": "namespace:use_item"
  }
}
```
```json:<font color="yellow">namespace:inventory</font>
{
  "criteria": {
    "used": {
      "trigger": "inventory_changed",
      "conditions": {
        "items": [
          {
            "predicates": {
              "custom_data": "{\"Used\":1b}"
            }
          }
        ]
      }
    }
  },
  "rewards": {
    "function": "namespace:get_used_item"
  }
}
```
次に２つのadvancementファイルも作りましょう。
### Functionファイル
```ruby:<font color="yellow">namespace:use_item</font>
# 実行させたいコマンド
function namespace:player
advancement revoke @s only namespace:consume
```
```ruby:<font color="yellow">namespace:get_used_item</font>
function namespace:return_item with entity @s SelectedItem.components.minecraft:custom_data
advancement revoke @s only namespace:inventory
```
```ruby:<font color="yellow">namespace:return_item</font>
$loot replace entity @s weapon.mainhand loot $(Loottable)
```
最後に３つのfunctionファイルを作ります。`使用したときに実行させたいファイル`には、右クリックしたときに実行させたいコマンドを入れてください。
### 使い方
1.`loot`コマンドでアイテムを出します。
2.アイテムを右クリックします。

以上で実装は終了です。お疲れ様でした。

## 追記
https://x.com/Hirobao1/status/1867518896728944976
@Hirobao1氏により、カスタムエンチャントで安定してアイテムを復帰する機構が発信されました。
こちらもご確認ください:information_desk_person:

# 挨拶
どうもおはしあ。けいらぎっていいます。
最近はコマンドが便利になってきて、右クリックの検知方法が豊富になりましたね。
自分はこれまで`consumable`の時間を最大にして`using_item`で検知してました。
しかし最近になって、これにアイテムクールダウンをつけたいなぁとなりました。エンダーパールとかコーラスフルーツ使ったときのやつね。その方が視覚的にクールダウンの終わりまでがわかりやすいですから。
しかし、このクールダウンはアイテムを使い切らないと発生しません。なので「使っている途中」を検知しているこの手法では上手く実装できませんでした。

てことで新しく考えてきました。協力していただいたaatomuさんと友人(匿名希望)に心から感謝します。

# 解説
まず今回の仕様をざっくり解説すると、
1.アイテムを0秒で消費する
2.変化したアイテムがインベントリにあることを検知
3.手に残ったアイテムの情報からプレイヤーのアイテムを戻す
となっています。順を追って解説します。

## 1.アイテムを0秒で消費する
最近追加された`consumable`というコンポーネントには、アイテムをどれだけ右クリックしたら消費するかを決められます。そこで0秒にして右クリックを押した瞬間に消費することで"アイテムを消費した"という進捗を達成しましょうというお話です。

しかしこれについて知っている方はお気づきになることでしょう、
「一瞬で消費しても『ｼｬｸｯ』という音が鳴る」と。
そうなんです。消費する時にどうしても食べる音が鳴ってしまう。
`sound`というタグで食べるときの音を決定できますが、厄介なことに、リソパで追加した音を指定することができませんでした。ついでに存在しない文字列を入れてもダメ、何も入れなくてもダメ。「なるべく静かな音を設定する」という方法くらいしかありませんでした。

が！友人がネットの海からこんなものを発掘してきてくれました。
```
"sound": "intentionally_empty"
```
これはMinecraftに存在する、完全に無音の音声データです。`playsound`コマンドには出てこない、ハードコーディング(基礎コードに直接記入)されたものだそうです。今回のことで初めて知りました。これを使うことで、使用時に音が鳴ってしまう問題をクリアできます！
ついでにアイテム使用中はパーティクルが出るので、`has_consume_particles`でパーティクルが出ないように設定します。これでアイテムを一瞬で消費する部分は終わりです。
(追記)タラやサケ、フグなど魚系統の環境音も音が出ないファイルだそうです。どっちを使うかはお好みで。

## 2.変化したアイテムがインベントリにあることを検知
アイテムのタグ`use_remainder`を用いると、アイテムを消費したとき手元に残るアイテムを指定できます。バニラで言うなら、ポーションを飲んだ後に空のビンが手に入る挙動ですね。今回はここに専用のタグをつけて、`inventory_changed`の進捗で使用後のアイテムが手に入ったことを検知します。

ここちょっと大事なポイントで、`inventory_changed`の進捗は、アイテムを使用してから"1tick後"に達成されます。

進捗を２つに分け、一連の処理を２つのファンクションに分けていること。ここがこの仕様の唯一の欠点であり、バグの発生地点となっています。
進捗が達成された瞬間に、手に持っているアイテムを使う前のアイテムに置き換えれば良いじゃないかという話なんですが、ここの挙動が思ったより複雑なんです。
#### ①アイテムを消費したtick
まず、内部の情報的に「アイテムを消費した」という進捗を達成したtickでは、「アイテムはまだインベントリにある」状態になっているようです。
ここで`tellraw`コマンドの`"nbt":"SelectedItem"`で見ると、消費前のアイテムの情報が出てきます。
#### ②消費したtickの次のtick
ここでアイテムがインベントリから「消失または変化」します。今回は`use_remainder`タグで使った後に残るアイテムを設定するので、設定したアイテムに変化して手元に残ります。ここが今回の厄介なポイントです。

この「アイテムの消失または変化」が思ったより優先度が強く、前のtickで別のアイテムを手元に置き換えていたとしてもここで「消失または変化」が発生します。そのため、1tick遅らせてから達成される進捗を使ってアイテムを設定し直さなければ、アイテムの操作ができませんでした。

### 懸念点
進捗を２つに分け、1tick遅らせて実行しているということは、もちろんその間にアイテムを持ち変えればアイテムを増殖させることが可能です。ただ`tick rate`で時間の進みを1/20にしてもかなり難しかったです。これが意図せず起こることはあまりないと思います。

modを入れて遊ぶときに何かの操作(描画)の拍子にクラッシュしたこと、あるんじゃないでしょうか。それが右クリックの関数の中に紛れ込んでしまった場合、マルチで参加者がクラッシュして退出し、進捗が変に達成されないなどの挙動も考えられます。このあたりの仕様は詳しくないですが、こっちは割とありそうですよね。

### 対策
自分が制作しているマップでは残った時のアイテムは使用出来ないように、`attribute`などが0になるように設定しています。そして一定の行動タイミングでアイテムチェックを行い、残った状態のアイテムがあればそれを元に戻すという方法を取っています。残さないことは難しいので、残ったアイテムを後で処理する方針ですね。~~配布マップ"Saharass" 2024年内配信予定！~~
あ、一応アイテムチェックについても話しておきます？けどまだ途中なんで後で！

## 3.手に残ったアイテムの情報からプレイヤーのアイテムを戻す
1tick後にアイテムを戻すコマンドを実行するので、検知するアイテムは使用後のものとなります。
そこで使用後に残るアイテムを設定する`use_remainder`で、loottableのパスを指定した`custom_data`が入ったアイテムを残します。このアイテムのデータを見てマクロ関数を実行することで、手にアイテムを戻しています。この部分はaatomuさんから教えていただきました。以上で全体の処理が完了します。

### クールダウングループについて
`use_cooldown`というタグを用いて、アイテムを使用したときのクールダウンを設定しているわけですが、ここの`cooldown_group`という項目でクールダウンを共有するアイテムを指定できます。逆に言えば、これをそれぞれ指定すれば別のクールダウンが実装できるわけですね。

### 思わぬ副産物
アイテムを0秒で消費しているため、使用しても足が遅くなりません！
`using_item`を使っていた頃は、どうやってもアイテム使用中に足が遅くなっていましたからね。作っているときは全く意図していませんでしたが、嬉しい誤算でした。


## 余談：アイテムチェック
「持っているアイテムを1つ1つチェックし、指定したデータがあればそのアイテムをピンポイントで編集する」という機能が、マクロ関数を用いて可能となりました。海外の方を参考にしたものですが、これを見つけて来てくれたのも友人です。すごいですね。

### 概要
```ruby:<font color="yellow">namespace:item_check</font>
#インベントリデータをStorageに保存
data modify storage stuff: Inventory set from entity @s Inventory
#最後のデータからチェック
execute if data storage stuff: Inventory[-1] run function namespace:each_item
```
```ruby:<font color="yellow">namespace:each_item</font>
#最後のアイテムに検知したいアイテムがあったら関数で編集する
execute if data storage stuff: Inventory[-1].components.minecraft:custom_data.detect 
    run function namespace:update with storage stuff: Inventory[-1]
#リストの最後のデータを消す
data remove storage stuff: Inventory[-1]
#まだデータが残っていれば最初に戻ってもう一度
execute if data storage stuff: Inventory[-1] 
    run function namespace:each_item with storage stuff: Inventory[-1]
```
```ruby:<font color="yellow">namespace:update</font>
#検知したアイテムのデータから元のアイテムに戻す
$loot replace entity @s container.$(Slot) loot $(Loottable)
```
いきなり簡易的になってしまいましたが、こんな感じのファイルを3つ作ります。これも順に解説します。
#### 1.インベントリデータをStorageのリストに保存
`data storage`でStorageにインベントリの情報を保存します。このときリストにはアイテムの名前の他に、「それがあったインベントリの位置`slot`」や、個数、カスタム情報`components`なども入ってくれるんですって。便利～
#### 2. ~~最初~~ 最後のデータからチェック
このデータの中身は一つの長い列のようになっています。データの位置を`Inventory[-1]`と指定し、ifコマンドを実行します。これが後で効いてくるんですよ。
(追記) リストの最後のデータを見る方が軽いそうです。情報ありがとうございます！
#### 3. ~~1番目~~ 最後のアイテムに検知したいアイテムがあったら関数で編集する
`execute if`で、アイテムに検知したいデータがあることを確認したらマクロ関数を組み立て、インベントリの位置のところで`loot replace`を実行します。
#### 5.リストの ~~最初~~ 最後のデータを消す
 ~~リストになっているデータの一番最初を消すと、全体が一つ前にズレます。一つ次のデータは先頭のデータになるわけですね。~~
 最後から見るため、ここの操作で全体をズラす必要が無くなりました。
#### 6.まだデータが残っていれば最初に戻ってもう一度
ここでまた`Inventory[-1]`を指定するんです。新しいデータが最後になっているので！
綺麗なプロセスですよね。ここ好きポイント。
これによってリスト内のデータが全部なくなるまで繰り返されます。

以上が概要となります。結構いろいろなものに応用が効くと思います。

## おわり
てことで今回はクールダウンをツール系アイテムでも使いたい！というお話でした。
質問や改善点などあればお気軽にどうぞ！できれば優しく、フランクな感じで。
初心者かつ昨日思いついた機構なので荒があるかもしれません。ぜひこれを基に改良され、システムが広まり、コマンドの世界がより良くなることを願っています。(他力本願)
それでは、ご覧いただきありがとうございました。
おつなも！