マイクラで鬼ごっこがしたい！

この記事は [Minecraft Command Advent Calendar 2024](https://qiita.com/advent-calendar/2024/mccmd) 9 日目の記事です。

## １．はじめに
はじめまして、ノフィーと申します。去年は見る専だったですが、今年は書いてみることにしました。対戦よろしくお願いします。

この記事は、**マルチ**で鬼ごっこがしたい！ついでにアイテムを追加してエキサイティングにしたい！そのための最低限の機構を**1から全部作ります**。そういう記事です。時折ごり押しで実装してますが、そこは温かい目で見てください。~~許せ~~

なお **ver1.21.1** で作成しています。（コンポーネント `lock` を直せば ver1.21.2 以降でも動きます。）

## ２．システムを作っていく
機構を小分けにしてみると、「鬼交代のシステム」・「時間管理」・「アイテム」が必要そうです。これを順番に作っていきます。

#### 2-1 鬼交代のシステム（メイン）
このデータパックでは「鬼が逃げてる人を殴ると交代する」という機構を作ります。

この条件は、「ダメージを与えたのは鬼」「ダメージを受けたのは逃げ」という２条件が同時（同ティック内）に満たされることと同じと捉えられます。ここで、プレイヤーに鬼なら `oni` 、逃げなら `nige` とタグをつけて区別することにします。
このもとでスコアボードを用いて実装したいと思います。

```mcfunction:まずはスコアボードを作る
# 与えたダメージ量を測るスコアボード
scoreboard objectives add GiveDamage custom:damage_dealt "give damage"

# 受けたダメージ量を測るスコアボード
scoreboard objectives add GetDamage custom:damage_taken "get damage"
```

```mcfunction: 鬼が交代したときを検知する
# 鬼がダメージを与える・逃がダメージを受けるが同時に起こったら鬼を交代する
execute as @a[tag=nige] if score @s GetDamage > _ GetDamage run scoreboard players set _ GetDamage 5
execute as @a[tag=oni] if score @s GiveDamage > _ GiveDamage run scoreboard players set _ GiveDamage 5

execute if score _ GetDamage matches 2.. if score _ GiveDamage matches 2.. run function <交代function>

# リセット
scoreboard players set @a GetDamage 0
scoreboard players set @a GiveDamage 0
scoreboard players set _ GetDamage 0
scoreboard players set _ GiveDamage 0
```

この条件を満たしたら、交代の処理を行います。
交代した時、逃げになった人には移動速度上昇を、鬼になった人には3秒間スタンさせましょう。あと次鬼になった人の通知も。

```mcfunction:鬼交代
# タグ入れかえ
execute at @a[tag=oni] as @p[tag=nige,scores={GetDamage=1..}] run function <oni->nige>
execute as @a[tag=oni,scores={GiveDamage=1..},tag=!triggered] run function <nige->oni>

# 新しい鬼の人を通知
tellraw @a [{"text": "次の鬼 "},{"text": "≫","color": "dark_green","bold": true},{"selector": "@a[tag=oni]","color": "red","bold": false}]

# タグを取り除く
tag @a remove triggered
```

```mcfunction:oni->nige
tag @s remove oni
tag @s add nige

# 再び鬼にならないようにタグをつける
tag @s add triggered

# バフ付与
effect give @s speed 3 4
```

```mcfunction:nige->oni
tag @s remove nige
tag @s add oni

# スタン(交代不可)
effect give @s slowness 3 255
effect give @s weakness 3 255
effect give @s blind 3 255
```

ここで、逃げから鬼の対象を１人にしないと複数人になってしまう事例が発生してしまうので注意が必要です。また、交代が発生した時に残り時間を伸ばすなどもここの処理に入れることもできます。

ということでメインの機構は終わりです。


#### 2-2 時間管理
残り時間をボスバーで表示するようにします。

自由に時間を設定できるように、実際にタイマーとして使う値(`_`)と設定用の値(`setting`)を分けて用意します。

```mcfunction:scoreboard作成
scoreboard objectives add timer.t dummy "タイマー(tick)"
scoreboard objectives add timer.s dummy "タイマー(秒)"
```

設定するときは `timer.s` のスコアボードの中に `setting` で値を追加します。
（例 → `scoreboard players set setting timer.s 360`）

そして、ここで設定した数値をゲーム開始時に `scoreboard players operation _ timer.s = setting timer.s` を実行して設定します。
さらに、ボスバーの最大値を `execute store result bossbar minecrft:timer max run scoreboard players get setting timer.s` で設定します。

ここでボスバーを作成しておきます。ついでに装飾も。プレイヤー全員に見えるようにしますが、ゲーム中以外は非表示にしておきます。

```mcfunction:ボスバー作成
bossbar add minecraft:timer [{"text": "残り時間 : "},{"score":{"name": "_","objective": "timer.s"}}]
bossbar modify minecraft:timer add @a
bossbar modify minecraft:timer visible false

# 以下装飾
bossbar modify minecraft:timer style notched12
bossbar modify minecraft:timer color green
```

ということで時間設定とボスバーの作成が出来たら時間を減らしていきます。
(おそらく)1番簡単な「毎ティックスコアを1増やし、20になったら処理を行う」方法で1秒を計測します。ちなみにこれで測れるのは「ゲーム内の1秒」なので、ラグがある場合は実際より長くなります。

また、ボスバーに表示する場合、**スコアに変更が起きた場合はその都度更新する必要がある**ので、その処理も入れてあげます。

```mcfunction:時間計測
scoreboard players add _ timer.t 1
execute if score _ timer.t matches 20.. run function <1秒おきに実行するfunction>
```

```mcfunction:1秒おきに実行するfunction
# 時間を1秒減らす
scoreboard players remove _ timer.s 1

# ボスバー更新
bossbar set minecraft:timer name [{"text": "残り時間 : "},{"score": {"name": "_", "objective": "timer.s"}}]

#
# ここにいろいろな処理
#

# リセット
scoreboard players remove _ timer.t 20
```

のちにアイテムを追加しますが、そのアイテムが時間制限ギリギリに使われてしまうと圧倒的に鬼の人が不利になってしまいます。ので、残り60秒までは画面に表示し、60秒を切ったら表示を消すようにします。

そのためにまずは新たにスコアボードを作成します。

```mcfunction:残り60秒用のスコアボード
scoreboard objectives add Last60sec dummy "残り60秒以下検知"
scoreboard players set _ Last60sec 0
```

そうしたら、残り60秒以下**かつフラグが立っていないとき**に専用のfunctionを実行します。フラグ処理にすることで、もし時間が伸びた時にその処理が繰り返さないようにできます。

```mcfunction:1秒おきに実行するfunction・改
# 時間を1秒減らす
scoreboard players remove _ timer.s 1

# ボスバー更新
bossbar set minecraft:timer name [{"text": "残り時間 : "},{"score": {"name": "_", "objective": "timer.s"}}]

# 残り60秒処理
execute if score _ timer.s matches ..60 if score _ Last60sec matches ..0 run function <last60>

#
# ここにいろいろな処理
#

# リセット
scoreboard players remove _ timer.t 20
```

```mcfunction:last60
# ボスバー非表示
bossbar modify minecraft:timer visible false

# ついでに全体に通知
tellraw @a [{"text": "> "},{"text": "残り60秒！","bold": true}]
execute at @a run playsound minecraft:blocks.bell.ring master @a ~ ~ ~ 0.8 1

#
# ここにいろんな処理
#

# フラグを立てる
scoreboard players set _ Last60sec 1
```

最後に残り時間が０秒以下になったらゲーム終了処理を行います。
そして、ゲームの結果を表示した７秒後にリセット処理を行うようにします。

**｜ゲーム結果表示等**

実装したい処理は
- ゲーム終了のアナウンス系統
- 鬼交代できなくする
- ７秒後にリセットする

という処理をコマンドにすると、

```mcfunction:ゲーム終了処理
# 画面に表示したり
title @a title [{"text": "ゲーム終了！"}]
title @a subtitle [{"text": "最後の鬼 →→→ "},{"selector": "@a[tag=oni]"}]
tellraw @a [{"text": "> "},{"text": "最後の鬼は "},{"selector": "@a[tag=oni]"},{"text": " でした！"}]

# 鬼交代不可＝殴れなくする
effect give @a weakness 60 255 true

# 残り時間を極端に減らして繰り返されないようにする
scoreboard players set _ timer.s -1000

# リセット処理
schedule function <リセットfunction> 7s
```

となります。

**｜リセット処理**

実装したい処理は
- エフェクトを消す
- アイテム（後述）を消す
- 「ゲーム中」のフラグをなくす
- 残り60秒のフラグを折る

ということでこれもコマンドにすると

```mcfunction:リセットfunction
# エフェクト消す
effect clear @a

# アイテム消す
clear @a *[lock=item]

# フラグ折る（スタート処理でやりますが"phase"というスコアボードで1ならゲーム中とします）
scoreboard players set _ phase 0
scoreboard players set _ Last60sec 0
```

そしてこれも1秒おきのfunctionに加えます。

```mcfunction:1秒おきに実行するfunction（完成版）
# 時間を1秒減らす
scoreboard players remove _ timer.s 1

# ボスバー更新
bossbar set minecraft:timer name [{"text": "残り時間 : "},{"score": {"name": "_", "objective": "timer.s"}}]

# 残り60秒処理
execute if score _ timer.s matches ..60 if score _ Last60sec matches ..0 run function <last60>

# ゲーム終了処理
execute if score _ timer.s matches -120..0 run function <ゲーム終了function>

#
# ここにいろいろな処理
#

# リセット
scoreboard players remove _ timer.t 20
```

そしたら最後にスタート用のfunctionを作ります。

```mcfunction:スタート
# 全員を自分のもとにtp
tp @a @s

# エフェクトを削除しておく（不正防止）
effect clear @a

# 残り時間を設定する
scoreboard players operation _ timer.s = setting timer.s

# ゲーム中フラグを建てる
scoreboard players set _ phase 1

# 鬼と逃げのタグをつける
tag @r add oni
tag @a[tag=!oni] add nige

# 全体に通知する
tellraw @a [{"text": "最初の鬼は "},{"selector": "@a[tag=oni]","color": "red"},{"text": " です"}]
```
あとは、`phase` が `1` のときにのみ時間経過がおこるようにすればOKです。
これでゲームの一連の流れが完成しました！長かった......

一応完全版を折りたたみに書いておきます。

<details><summary>完全版コード</summary>

```mcfunction:常時実行
execute if score _ phase matches 1.. run function <時間計測>
```

```mcfunction:時間計測
scoreboard players add _ timer.t 1
execute if score _ timer.t matches 20.. run function <1秒おきに実行するfunction>
```

```mcfunction:1秒おきに実行するfunction（完成版）
# 時間を1秒減らす
scoreboard players remove _ timer.s 1

# ボスバー更新
bossbar set minecraft:timer name [{"text": "残り時間 : "},{"score": {"name": "_", "objective": "timer.s"}}]

# 残り60秒処理
execute if score _ timer.s matches ..60 if score _ Last60sec matches ..0 run function <last60>

# ゲーム終了処理
execute if score _ timer.s matches -120..0 run function <ゲーム終了function>

#
# ここにいろいろな処理
#

# リセット
scoreboard players remove _ timer.t 20
```

```mcfunction:ゲーム終了処理
# 画面に表示したり
title @a title [{"text": "ゲーム終了！"}]
title @a subtitle [{"text": "最後の鬼 →→→ "},{"selector": "@a[tag=oni]"}]
tellraw @a [{"text": "> "},{"text": "最後の鬼は "},{"selector": "@a[tag=oni]"},{"text": " でした！"}]

# 鬼交代不可＝殴れなくする
effect give @a weakness 60 255 true

# 残り時間を極端に減らして繰り返されないようにする
scoreboard players set _ timer.s -1000

# リセット処理
schedule function <リセットfunction> 7s
```

```mcfunction:リセットfunction
# エフェクト消す
effect clear @a

# アイテム消す
clear @a *[lock=item]

# フラグ折る
scoreboard players set _ phase 0
scoreboard players set _ Last60sec 0
```

**↓ ゲームスタート時に実行するfunction ↓**
```mcfunction:スタート
# 全員を自分のもとにtp
tp @a @s

# エフェクトを削除しておく（不正防止）
effect clear @a

# 残り時間を設定する
scoreboard players operation _ timer.s = setting timer.s

# ゲーム中フラグを建てる
scoreboard players set _ phase 1

# 鬼と逃げのタグをつける
tag @r add oni
tag @a[tag=!oni] add nige

# 全体に通知する
tellraw @a [{"text": "最初の鬼は "},{"selector": "@a[tag=oni]","color": "red"},{"text": " です"}]
```

* * *

</details>

#### 2-3 アイテム
「足が速くなる」「透明になる」というようなアイテムを作ります。
ただ、アイテムの効果の部分は異なりますが検知の部分はほぼ同じなので、今回は１つだけ単純な「使うと透明になる」アイテムを作ります。

まず、アイテムを無限に簡単に増やせるように以下の規則を考えます。

1. 「アイテム」と識別できるタグを有する
2. `アイテムのid` によらずに識別できるようにする

・１つ目
リセット時など、とにかくアイテムを一括で消したい場面が来るときに備えます。一つ一つ消してもいいですが、数が増えたときに書く量も負荷も増えるので対策します。

今回は `lock` のコンポーネントを使います。
**ver1.21.1以前**であれば、`lock="<文字列>"` で指定ができます。
**ver1.21.2以降**では、`lock={}` で指定をします。もし複数種類の判定をしたい場合、`lock={components:{damage:3}}` `lock={count:2}` の数値を変更するなどで指定します。
が、今回はver1.21.1で進めます（確固たる意志）

・２つ目
アイテムの見た目として `minecraft:diamond` などを指定しますが、これをそのまま指定してしまうと同じidが使えない、idを変更すると訂正箇所が増える、`custom_model_data` が同一アイテムに対して複数指定できない(致命的)というデメリットがあります。
なのでコンポーネントの `repair_cost` を使って識別できるようにします。（アイテムに限らず、エンダーチェストUIなど広く活用できます。）

今回は 1000 + (通し番号) という規則で付けていきます。
アイテムを与えるだけならgiveでもいいのですが、訂正のしやすさや見やすさを考えてloottableで書こうと思います。
全文は折り畳みに書くとして、コンポーネントの部分を抜き出します。

```json:共通のコンポーネント
            {
                "function": "minecraft:set_components",
                "components": {
                "minecraft:enchantment_glint_override": true,
                "minecraft:lock": "item",
                "minecraft:repair_cost": 1001,
                "minecraft:max_stack_size": 2
                }
            },
```

書いていることは、上から順に
- 気分でエンチャントのモヤを乗っける
- アイテムだよ～ということを `lock` で書く
- 通し番号を `repair_cost` で指定する
- 個数制限を `max_stack_size` で書く

矢・武器の場合は、attributeやpotion_effectなどをここに追記するだけ！簡単！

<details><summary>参考：自分が作ったアイテム</summary>

```json:
{
    "pools": [
    {
        "rolls": 1,
        "entries": [
        {
            "type": "minecraft:item",
            "name": "minecraft:tipped_arrow",
            "functions": [
            {
                "function": "minecraft:set_name",
                "name": [
                    {
                        "text": "芋成敗機 MK-II",
                        "color": "#bfe493",
                        "italic": false,
                        "bold": true
                    }
                ]
            },
            {
                "function": "minecraft:set_lore",
                "lore": [
                {
                    "text": ""
                },
                {
                    "text": "< 効果 >",
                    "color": "aqua",
                    "italic": false,
                    "bold": true
                },
                {
                    "text": "炎上・ウィザーを与えるウィザースケルトンを",
                    "color": "#ffffff",
                    "italic": false
                },
                {
                    "text": "自分以外の誰かのもとに召喚します。",
                    "color": "#ffffff",
                    "italic": false
                },
                {
                    "text": ""
                },
                {
                    "text": "< 対象 >",
                    "color": "green",
                    "italic": false,
                    "bold": true
                },
                {
                    "text": "狙われた人",
                    "color": "#ffffff",
                    "italic": false
                }
                ],
                "mode": "replace_all"
            },
            {
                "function": "minecraft:set_components",
                "components": {
                "minecraft:enchantment_glint_override": true,
                "minecraft:lock": "item",
                "minecraft:repair_cost": 1001,
                "minecraft:max_stack_size": 2,
                "potion_contents": {
                    "custom_effects": [
                        {
                            "id": "minecraft:wither"
                        }
                    ]
                },
                "hide_additional_tooltip": {}
                }
            },
            {
                "function": "minecraft:set_count",
                "count": 2
            }
            ]
        }
        ]
    }
    ]
}
```

* * *

</details>

通し番号を付けるのは `damage` や `custom_model_data` などでも代替できると思います。

ということでアイテムができたのでそれを**検知**していきます。
今回は「オフハンドに持つと発動する」ようにします。

ただ、常時オフハンドに「`repair_cost` が `10xx` のアイテム持ってる？」と全アイテム分検知するのは無駄＆重くなるなので、まずは「**オフハンドにアイテムを持っているか**」を検知します。
さらに、`nbt` でもいいですが、負荷的に軽い `predicate` を使います。

```mcfunction:アイテム持ってる？
execeute as @a[predicate=sample:has_item] at @s run function <アイテム効果発動function>
```

```json:[predicate] sample:has_item
{
    "condition": "minecraft:entity_properties",
    "entity": "this",
    "predicate": {
    "nbt": "{Inventory:[{Slot:-106b,components:{\"minecraft:lock\": \"item\"}}]}"
    }
}
```

そしたら、オフハンドに持っているアイテムの `repair_cost` をスコアボード `SelectItem` に保存し、それと一致するもののfunctionを実行します。
（**オフハンドにアイテムがある場合**はInventoryの-1番目として値が取得できます。）
おそらく `storage` とか使えたらもっと楽な処理できるかもしれませんが、この人は一切使えないのでごり押しで実装していきます。使えるならマクロが1番楽だと思います。

ここでは `execute + score` で検知してますが、
・全アイテム分のオフハンド＆通し番号の `predicate` を作る
・二分探索で処理を軽減する
という方法も考えられます。

全アイテムのファイルを作るのはほぼ同じコードなのでコピペで済むのですが、さすがに数が多すぎると処理も重くなり、それよりもファイル数がアイテムと同じ量だけ増えるので、場合によってはエディタ等が見にくくなる場合があります（まあファイル分けを適切にすればいいですが）。

二分探索では、アイテムの数が傾き1の関数 $ x $ で増えるのに対し、高々 $ log_2{~x} $ の処理しか行われないのでアイテムの数が増えるだけ恩恵が大きくなります。が、その分作成しなければならないファイル数が膨大になるほか、一度作成したら総数の変更がとても面倒になるというデメリットもあります。

どちらにせよ根気とやる気が必要です......

```mcfunction:アイテム効果発動function
execute store result score @s SelectItem run data get entity @s Inventory[-1].components."minecraft:repair_cost"

execute if score @s SelectedItem matches 1001 run function <item1001>
execute if score @s SelectedItem matches 1002 run function <item1002>
...
execute if score @s SelectedItem matches n run function <item n>
```

これでアイテムの検知＆実行ができるようになったので、最後に効果を発動させます。
移動速度上昇などのエフェクトをつけるなどをするのは当然ですが、アイテムを使ったと分かりやすくするために効果音とパーティクルもつけます。
全アイテムに共通しますが、「アイテムの**消費**」も一緒に行います。
仕様としては、今オフハンドに持っている状態なので「メインハンドにアイテムをコピー」→「オフハンドのアイテムを消す」→「該当アイテムを消費する」で実装します。

ということで透明化するアイテムを通し番号 `1001` で作ってみます。
個人的な趣味として周囲を巻き込んで透明になるようにします。また、使用者に高レベルの耐性を付けて殴られても鬼にならないようにします。

```mcfunction:item1001
# エフェクト効果
effect give @a[distance=..3.5] invisibility 5 0
effect give @s resistance 1 255

# 演出
particle campfire_cosy_smoke ~ ~ ~ 2 2 2 0.01 7500
playsound block.amethyst_block.break master @a ~ ~ ~ 1.8 1

# アイテム消費
item replace entity @s mainhand from entity @s offfhand
item replace entity @s offhand with air
clear @s *[repair_cost:1001] 1
```

これでアイテムも完成です！アイテムを得るときは `/loot give @s loot ~` で。


## ３．追加要素案
今まで作ったものだけでも十分遊べます！が、物足りないだろうので（決めつけ）

エンダーチェストでアイテムが取れるようにしたり、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616358/f09beecc-bfe7-d6ae-b556-f8f591ba0ef4.png)

着弾地点で大爆発を起こすようにしたり、
![sample1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616358/4e879e0a-cb15-538e-7bd0-5afa03fed95a.gif)

透明の人の位置が見えるようにしたり、
![sample2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616358/d0031480-54f7-3cd9-3c53-b7481ca56d77.gif)

ランダムイベントを発生させたり、
![2024-12-06_20.17.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616358/3007d307-9a55-167b-f74b-86cccb2b2c59.png)

直線状にビームを飛ばしてみたり、
![sample3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616358/deaf721a-6ef3-3af0-ec80-6be28a619d9e.gif)

パッシブ効果のアイテムを作ったり、大量のデバフを与えたり、残り時間を減らしたり、鬼交代の時に時間を延ばしたり、逆に減らしたり、オートエイム（合法）させたり、タイマン勝負をできるようにしたり、アイテム効果を無効化できるようにしたり、ほかのゲームのアイテムを~~パクったり~~オマージュしたり、......

処理面で言えば、`advancement` を使って処理を高速にしたり、コマンドの数を減らしたり......

と、いろいろ独創性を追加しやすいです！素晴らしい！！

この記事で挙げたコマンド等は（良識の範囲内で）自由に利用してもらって構わないので好きにいじって遊んでください。

また、`predicate` などはツールを使うと楽です。
> このツールは `predicate` `loottable` をはじめとしたたくさんのツールがあります。
> 
> Loot Table Generator - Minecraft 1.19, 1.20, 1.21 
> https://misode.github.io/loot-table/


## ４．終わりに
全部機構を書いたらくそ長くなりましたが、これで完成です。
事細かに書いたので何かしらの助けや暇つぶしになればいいな～と思います。

「アイテム」という名目でいろんなコマンドの実験もできるので一度作ってみてはどうでしょうか。マルチ対応の練習にもなっていいです（バグ修正に苦しんでますが）。
筆者自身、このデタパ制作でnbtの取り方やbossbarなどを学んだので教育効果はあると思います。

そして完成したら友達と一緒に遊んでみましょう！
なお、マイクラには友達は付属しておりません。ご自身でご用意ください。