６. 撲殺バットMODの作成（独自ギミック追加１）

# 概要
前回まででアイテムやレシピの追加、多言語対応など、一通りの実装が終わりました。
ただ、MOD開発の一番楽しい所であるLuaでの実装がほとんど出てきませんでした。
そこで今回は、Lua実装サンプルとして独自ギミックを追加します。[^1]

※ 今回はLuaによる実装の解説になりますので、読み飛ばして頂いても構いません。
　 LuaによるMOD開発を進めたい方は、参考までにご参照ください。

# １. 独自ギミックについて
今回のMOD開発のメインである撲殺バットですが、レシピを使って武器のグレードを実装しました。そのグレードアップとは別に、「撲殺バットを使えば使うほど効果が上がる」という「熟練度」という仕様＋αを実装したいと思います。

## 熟練度の仕様
熟練度の仕様は下記とし、仕様詳細は各実装で補足します。
* 撲殺バットで得た経験値を保持しておき、経験値に応じて熟練度が上がっていく
* 熟練度が上がると、撲殺バットの攻撃力とクリティカル性能が向上する
* プレイヤーが現在の熟練度を確認できる機能を具備する

## その他の仕様
* 撲殺バットは耐久値の自己修復機能を保持する
* 撲殺バットを使用中は空腹度の上昇が加速する

# ２. 独自ギミックのファイル作成
独自ギミックはLuaファイルに実装します。今回の実装内容は全てクライアント側の処理になるので、下記のファイルを用意し、そこに各機能を追加していきます。
```
\media\lua\client\P4BokusatsuBat.lua
```
:::note info
「client」フォルダ以下のフォルダ・ファイル構成は任意になります。
:::
また、実装準備として下記をひな型とします。
```lua:P4BokusatsuBat.lua
local P4BokusatsuBat = {}
```
バニラ実装や他MODとのコンフリクトを防ぐため、グローバル変数化が必要な場合以外はローカル変数として宣言することをお薦めします。上記のようにローカル変数（テーブル）を用意しておき、全ての関数や変数は、このローカル変数に格納します。

# ３. 装備中かどうかの判定
今回の仕様では「撲殺バットでの経験値を保持」「撲殺バットの性能向上」といったように、「撲殺バットを装備しているかどうか」という判定が必要になります。ゲームキャラが装備しているアイテムについては、下記の方法で確認が可能です。

1. ゲームキャラのプレイヤーオブジェクトを取得する
1. プレイヤーオブジェクトのメイン装備品を取得する
1. 取得したメイン装備品が撲殺バットかどうか確認する
1. プライヤーオブジェクトのサブ装備品を取得する
1. 取得したサブ装備品が撲殺バットかどうか確認する

判定が必要な度に上記方法で判定しても良いのですが、実行コストが気になります。Luaは非常に高速な言語かつ、上記程度の処理であればそこまで性能劣化は発生しないと思いますが、性能劣化は極力避ける実装が望ましいと考えます。そこで「撲殺バットを装備したら、装備中フラグをONにし、判定には装備中フラグを用いる」こととします。

## 着脱時の対応
装備中フラグの変更タイミングは下記が考えられます。

* 撲殺バットがメインに装備された場合、フラグをONにする
  * 例）撲殺バットを両手に装備する
  * 例）撲殺バットをメインに装備する
* 撲殺バットがメイン装備から外れた場合、フラグをOFFにする
  * 例）撲殺バットを装備から外す
  * 例）撲殺バット以外のアイテムを両手もしくはメインに装備する
  * 例）転倒などで撲殺バットの装備が外れる

上記の実現は、下記のように「イベントトリガー」を使って実装できます。

```lua:P4BokusatsuBat.lua
P4BokusatsuBat.currentEquipped = nil

P4BokusatsuBat.OnEquipPrimary = function(player, item)
	if P4BokusatsuBat.currentEquipped then
		P4BokusatsuBat.currentEquipped = nil
	end
	if P4BokusatsuBat.isBokusatsuBat(item) then
		P4BokusatsuBat.currentEquipped = item
	end
end
Events.OnEquipPrimary.Add(P4BokusatsuBat.OnEquipPrimary)
```
「currentEquipped」変数は現在装備している撲殺バットの武器オブジェクトを保持する変数です。今までの説明では装備中フラグと呼んでいましたが、装備中かどうかの判定をした後に装備中の武器オブジェクトを使った処理が発生するため、フラグだけでは「装備している武器オブジェクトを取得する」必要が発生し、冗長な処理となります。そこで、フラグの代わりに「現在装備中の撲殺バットの武器オブジェクト」を保持するようにしました。（以降の説明では、引き続きフラグの体で記載します）

「OnEquipPrimary 」関数の部分がイベントトリガーとなります。イベントトリガーは様々なイベント発生時にフックできる仕組みで、コールバック関数を登録することができます。ここでは、バニラ側の「プライマリ（＝メイン）装備変更イベント」（Events.OnEquipPrimary）が発生した際に、MOD側の「OnEquipPrimary」関数がコールバックされるようにしています。

このイベントはメイン装備に変更があった際に発生するもので、装備した場合も装備を外した場合も該当します。そして、コールバックの際は「プレイヤーオブジェクト」（player）と「装備品オブジェクト」（item）が指定されます。そこで、現在撲殺バットを装備している場合は、装備の変更が発生したため一旦フラグOFFとし、新たに装備されたアイテムが撲殺バットだった場合にフラグONとしています。

:::note info
イベントトリガーの詳細については、下記URLを参照してください。
[PZwiki : Lua Events](https://pzwiki.net/wiki/Lua_Events)
:::

また、「アイテムオブジェクトが撲殺バットかどうか」は下記の関数で判定しています。撲殺バットにはグレードが３つあるので、アイテム名の前方一致で判定しています。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.isBokusatsuBat = function(item)
	if item and string.find(item:getType(), "P4BokusatsuBatT") then
		return true
	else
		return false
	end
end
```
## 考慮漏れの対応
装備中かどうかの判定は前述しましたが、実は１ケース考慮漏れがあります。メイン装備の着脱については対応できていますが、「撲殺バットを装備したままゲームを終了し、再開したケース」に対応できていません。この場合、前述のプライマリ装備変更イベントが発生しないため、装備中フラグは初期値のOFFのままになります。かと言って、初期値をONにするわけにもいきません。そこで、このケースもイベントトリガーで対応します。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.OnCreatePlayer = function(index, player)
	local pItem = player:getPrimaryHandItem()
	if P4BokusatsuBat.isBokusatsuBat(pItem) then
		P4BokusatsuBat.currentEquipped = pItem
	end
end
Events.OnCreatePlayer.Add(P4BokusatsuBat.OnCreatePlayer)
```
「Events.OnCreatePlayer」はゲーム開始時にプレイヤーオブジェクトが作成された際に発生するイベントになります。プレイヤーに関する初期化処理は、このイベントトリガーでフックするのが良いでしょう。コールバック時にプレイヤーオブジェクトが渡されるので、メイン装備品を取得し、取得した装備品が撲殺バットだった場合に装備中フラグをONにしています。

これで「撲殺バットを装備中かどうか」については、本スクリプト内ではどこからでも参照できる「currentEquipped」変数を確認することで判定できるようになりました。

# ４. スクリプト変数の用意
先程の「currentEquipped」変数のように、スクリプト内で適宜参照したいデータが幾つかあります。例えば、プレイヤーオブジェクトやインベントリーオブジェクト、ステータスオブジェクトなどは至る所で使用する可能性が高く、その都度引数で渡したり、コア機能を使ってオブジェクトを取得するのは冗長です。そこで、「currentEquipped」変数のように、スクリプト内のどこからでもアクセスできるような「スクリプト変数」として保持することとします。
```diff_lua:P4BokusatsuBat.lua
+ P4BokusatsuBat.player = nil
+ P4BokusatsuBat.inventory = nil
+ P4BokusatsuBat.stats = nil
  P4BokusatsuBat.currentEquipped = nil

  P4BokusatsuBat.OnCreatePlayer = function(index, player)
+	  P4BokusatsuBat.player = player
+	  P4BokusatsuBat.inventory = player:getInventory()
+	  P4BokusatsuBat.stats = player:getStats()
	  local pItem = player:getPrimaryHandItem()
	  if P4BokusatsuBat.isBokusatsuBat(pItem) then
		  P4BokusatsuBat.currentEquipped = pItem
	  end
  end
  Events.OnCreatePlayer.Add(P4BokusatsuBat.OnCreatePlayer)
```
実装としては、スクリプト変数として定義し、「OnCreatePlayer」イベントで値をセットしています。ちなみに、これらのデータの実態はJava側のオブジェクトであり、現在の値を保持するというよりはオブジェクトのアドレスを保持するイメージになります。実際の最新状態を取得する際は、このJavaオブジェクトに対して都度取得処理を実施することになるので、このような方式が可能となります。

また、Javaオブジェクトに対してメソッドを呼び出す場合は、「player:getInventory()」のようにコロン（":"）で記載します。Luaオブジェクトの場合はテーブル参照となるためにピリオド（"."）となるので、「現在処理しているオブジェクトがJava側なのかLua側なのか」を意識する必要があるのでご注意ください。

# ５. 熟練度のレベリング実装
熟練度は撲殺バットでの経験値でレベリングします。具体的には、バニラ実装の「鈍器(大型)の経験値を獲得した際、撲殺バットを装備していた場合は、熟練度経験値として別途保持する」こととします。そして、保持した経験値を元にレベルを計算します。

## 経験値の保持
撲殺バットでの経験値についても、イベントトリガーを活用します。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.AddXP = function(player, perk, xp)
	if perk:getType() == Perks.Blunt then
		local pItem = player:getPrimaryHandItem()
		if P4BokusatsuBat.isBokusatsuBat(pItem) then
			P4BokusatsuBat.addXP(xp)
		end
	end
end
Events.AddXP.Add(P4BokusatsuBat.AddXP)
```
「Events.AddXP」は経験値を獲得した際に発生するイベントになります。どのスキルに対して経験値が増えたかは「perk」引数で判定することができるので、対象スキルが鈍器(大型)かどうかの判定をしています。また、鈍器(大型)の経験値の内、撲殺バットでの経験値に絞るため、撲殺バット判定関数でも判定しています。これらの判定が通ったものが撲殺バットでの経験値となるので、その加算量（「xp」引数）を保持します。

経験値の保持先については、Luaではファイル入出力も可能なのでファイルでの永続化でも良いのですが、「MODデータ」で永続化するのが通例のようです。MODデータは、MOD内のステータスなどを永続化するための機能で、プレイヤー単位に保持するMODデータと、セーブデータ単位に保持するグローバルMODデータがあります。今回の経験値はプレイヤー毎に保持すべきデータのため、通常のMODデータを利用します。

```lua:P4BokusatsuBat.lua
P4BokusatsuBat.getModData = function()
	local modData = P4BokusatsuBat.player:getModData()
	if not modData.BokusatsuBat then
		modData.BokusatsuBat = {}
	end
	return modData
end

P4BokusatsuBat.getXP = function()
	local modData = P4BokusatsuBat.getModData()
	if not modData.BokusatsuBat.xp then
		modData.BokusatsuBat.xp = 0
	end
	return modData.BokusatsuBat.xp
end

P4BokusatsuBat.addXP = function(xp)
	local modData = P4BokusatsuBat.getModData()
	if not modData.BokusatsuBat.xp then
		modData.BokusatsuBat.xp = 0
	end
	modData.BokusatsuBat.xp = modData.BokusatsuBat.xp + xp
end
```
「getModData」関数は、プレイヤー毎のMODデータを取得する内部的な関数です。MODデータの中身はLuaのテーブルオブジェクトとなっており、撲殺バットMOD用のテーブルが無い場合は初期化してMODデータを返却します。

「getXP」関数は、MODデータに保持している経験値の現在の値を返します。こちらも、経験値データがない場合は初期化をしています。

「addXP」関数は、MODデータに経験値を加算します。この関数が呼ばれるタイミングで経験値データがないことはあり得ない実装ですが、念のため初期化処理を入れています。こちらは、単純に現在の経験値データに、引数で渡された値を加算する関数で、前述の「AddXP」イベント内で呼び出しています。

## レベルの計算
撲殺バットでの経験値を保持できるようになったので、その保持した値を使ってレベルを計算する処理を実装します。まずは、熟練度のレベルは全部で５段階（レベル１始まり、レベル５が最大）とし、レベルアップに必要な経験値を以下のように設定しました。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.requiredToLv2 = 3000
P4BokusatsuBat.requiredToLv3 = 6000
P4BokusatsuBat.requiredToLv4 = 9000
P4BokusatsuBat.requiredToLv5 = 12000
```
次に、現在の経験値からレベルを計算する関数を実装します。レベルアップに必要な経験値からレベルを求めるだけの簡単な処理ですが、「熟練度のレベルがカンストする前に、鈍器(大型)のレベルがカンストしたケース」を考慮しています。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.calcLevel = function()
	if P4BokusatsuBat.player:getPerkLevel(Perks.Blunt) >= 10 then
		-- If XP is no longer gained, return as max level.
		return 5
	end
	local xp = P4BokusatsuBat.getXP()
	if xp >= P4BokusatsuBat.requiredToLv5 then
		return 5
	elseif xp >= P4BokusatsuBat.requiredToLv4 then
		return 4
	elseif xp >= P4BokusatsuBat.requiredToLv3 then
		return 3
	elseif xp >= P4BokusatsuBat.requiredToLv2 then
		return 2
	else
		return 1
	end
end
```
では、レベル計算関数を組み込みます。組み込み先は前述の「AddXP」イベントとし、経験値を獲得した際に都度最新のレベルを計算し、レベルアップした場合は現在のレベルを保持するスクリプト変数「currentLevel」を更新します。
```diff_lua:P4BokusatsuBat.lua
+ P4BokusatsuBat.currentLevel = 1

  P4BokusatsuBat.AddXP = function(player, perk, xp)
	  if perk:getType() == Perks.Blunt then
		  local pItem = player:getPrimaryHandItem()
		  if P4BokusatsuBat.isBokusatsuBat(pItem) then
			  P4BokusatsuBat.addXP(xp)
+			  local previousLevel = P4BokusatsuBat.currentLevel
+			  local currentLevel = P4BokusatsuBat.calcLevel()
+			  if previousLevel < currentLevel then
+				  P4BokusatsuBat.currentLevel = currentLevel
+			  end
		  end
	  end
  end
  Events.AddXP.Add(P4BokusatsuBat.AddXP)
```
また、「currentLevel」変数の初期化は、「OnCreatePlayer」イベントで実施します。
```diff_lua:P4BokusatsuBat.lua
  P4BokusatsuBat.OnCreatePlayer = function(index, player)
	  P4BokusatsuBat.player = player
	  P4BokusatsuBat.inventory = player:getInventory()
	  P4BokusatsuBat.stats = player:getStats()
+	  P4BokusatsuBat.currentLevel = P4BokusatsuBat.calcLevel()
	  local pItem = player:getPrimaryHandItem()
	  if P4BokusatsuBat.isBokusatsuBat(pItem) then
		  P4BokusatsuBat.currentEquipped = pItem
	  end
  end
  Events.OnCreatePlayer.Add(P4BokusatsuBat.OnCreatePlayer)
```
これで、撲殺バットでの経験値を保持し、現在のレベルについては「currentLevel」変数を確認することで判定できるようになりました。

# ６. ブーストモードの実装
熟練度が上がると、撲殺バットの攻撃力とクリティカル性能が向上する「ブーストモード」を実装します。武器の攻撃力やクリティカル性能は、アイテム定義やグレードで設定しましたが、武器オブジェクト単位で個別に設定することもできます。そこで、「普段は定義通りの性能だが、熟練度の高いゲームキャラが装備すると、その時だけ性能が向上する」という仕様にしたいと思います。

## ベース値の保持
ブースト時に性能を向上させた後、装備から外れた場合は武器オブジェクトの性能を元に戻す必要があります。その為、ブースト前のベース値を保持しておかなければなりません。これらの値は、アイテム定義で設定した値となるため、下記のようにゲーム内オブジェクトから値を取得して、スクリプト変数に定数として保持しておきます。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.T1 = {}
P4BokusatsuBat.T2 = {}
P4BokusatsuBat.T3 = {}

P4BokusatsuBat.initBaseProperty = function(src, dst)
	dst.minDamage = src:getMinDamage()
	dst.maxDamage = src:getMaxDamage()
	dst.criticalChance = src:getCriticalChance()
	dst.critDmgMultiplier = src:getCritDmgMultiplier()
end

P4BokusatsuBat.OnGameStart = function()
	P4BokusatsuBat.initBaseProperty(InventoryItemFactory.CreateItem("P4BokusatsuBat.P4BokusatsuBatT1"), P4BokusatsuBat.T1)
	P4BokusatsuBat.initBaseProperty(InventoryItemFactory.CreateItem("P4BokusatsuBat.P4BokusatsuBatT2"), P4BokusatsuBat.T2)
	P4BokusatsuBat.initBaseProperty(InventoryItemFactory.CreateItem("P4BokusatsuBat.P4BokusatsuBatT3"), P4BokusatsuBat.T3)
end
Events.OnGameStart.Add(P4BokusatsuBat.OnGameStart)
```
スクリプト変数にティア１～３のベース値を保持する用の変数を用意します。「initBaseProperty」関数は、ゲーム内オブジェクトを参照元とし、そこから定義されている攻撃力やクリティカル性能を取得して、スクリプト変数に設定します。この「initBaseProperty」関数を、ゲーム開始時の「OnGameStart」イベントでティア１～３それぞれについて呼び出すことで、ベース値を初期化しています。

## ブースト・ブースト解除処理
ブースト・ブースト解除処理は下記のように実装しました。ブースト時の性能は、熟練度のレベルに応じてベース値に上昇率を積算する方法としました。また、ブースト解除時はスクリプト変数のベース値をそのまま反映させています。ブーストとブースト解除は、「isBoost」引数で指定します。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.setProperty = function(dst, isBoost)
	local src = nil
	if dst:getType() == "P4BokusatsuBatT1" then
		src = P4BokusatsuBat.T1
	elseif dst:getType() == "P4BokusatsuBatT2" then
		src = P4BokusatsuBat.T2
	elseif dst:getType() == "P4BokusatsuBatT3" then
		src = P4BokusatsuBat.T3
	end
	local adjustRate = 1.0
	if isBoost then
		if P4BokusatsuBat.currentLevel >= 5 then
			adjustRate = 1.5
		elseif P4BokusatsuBat.currentLevel >= 4 then
			adjustRate = 1.3
		elseif P4BokusatsuBat.currentLevel >= 3 then
			adjustRate = 1.15
		elseif P4BokusatsuBat.currentLevel >= 2 then
			adjustRate = 1.05
		end
	end
	if src then
		dst:setMinDamage(src.minDamage * adjustRate)
		dst:setMaxDamage(src.maxDamage * adjustRate)
		dst:setCriticalChance(src.criticalChance * adjustRate)
		dst:setCritDmgMultiplier(src.critDmgMultiplier * adjustRate)
	end
end
```

## ブースト切替
ブーストが切り替わるタイミングで、前述の「setProperty」関数を呼び出すようにします。まずは装備が切り替わるタイミングのため、「OnEquipPrimary」イベント内で、撲殺バットを装備から外す場合は引数falseで、撲殺バットを装備する場合は引数trueで呼び出します。
```diff_lua:P4BokusatuBat.lua
  P4BokusatsuBat.OnEquipPrimary = function(player, item)
	  if P4BokusatsuBat.currentEquipped then
+		  P4BokusatsuBat.setProperty(P4BokusatsuBat.currentEquipped, false)
		  P4BokusatsuBat.currentEquipped = nil
	  end
	  if P4BokusatsuBat.isBokusatsuBat(item) then
+		  P4BokusatsuBat.setProperty(item, true)
		  P4BokusatsuBat.currentEquipped = item
	  end
  end
  Events.OnEquipPrimary.Add(P4BokusatsuBat.OnEquipPrimary)
```
次に、熟練度のレベルが上がり、ブースト値が変わる場合も呼び出しが必要となります。こちらは「AddXP」イベント内で、レベルが上がった際に引数trueで呼び出します。
```diff_lua:P4BokusatuBat.lua
  P4BokusatsuBat.AddXP = function(player, perk, xp)
	  if perk:getType() == Perks.Blunt then
		  local pItem = player:getPrimaryHandItem()
		  if P4BokusatsuBat.isBokusatsuBat(pItem) then
			  P4BokusatsuBat.addXP(xp)
			  local previousLevel = P4BokusatsuBat.currentLevel
			  local currentLevel = P4BokusatsuBat.calcLevel()
			  if previousLevel < currentLevel then
				  P4BokusatsuBat.currentLevel = currentLevel
+				  P4BokusatsuBat.setProperty(pItem, true)
			  end
		  end
	  end
  end
  Events.AddXP.Add(P4BokusatsuBat.AddXP)
```
これで、現在の装備状態やレベルに応じて、ブースト・ブースト解除ができるようになりました。

# ７. 確認機能の実装
プレイヤーが現在の熟練度を確認できる機能を追加します。具体的には、撲殺バットのコンテキストメニューに確認コマンドを追加し、コマンドが選択されたら現在のレベルを調べて結果を返す仕様とします。

## コマンド追加
下記のようにして撲殺バットのコンテキストメニューにコマンドを追加します。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.OnFillInventoryObjectContextMenu = function(player, table, items)
	for i,v in ipairs(items) do
		if not instanceof(v, "InventoryItem") then
			if P4BokusatsuBat.isBokusatsuBat(v.items[1]) then
				table:addOption(getText("ContextMenu_P4BokusatsuBat_CheckBokusatsuBat"), v, P4BokusatsuBat.checkBokusatsuBat)
			end
		end
	end
end
Events.OnFillInventoryObjectContextMenu.Add(P4BokusatsuBat.OnFillInventoryObjectContextMenu)
```
「Events.OnFillInventoryObjectContextMenu」はアイテムのコンテキストメニューを表示する際に発生するイベントになります。対象のアイテムが撲殺バットの場合に、コマンド表示名とコマンド選択時に実行される関数（checkBokusatsuBat）を指定します。なお、コマンド表示名は下記の通り翻訳を追加し、バニラ機能の「getText」関数で翻訳後の文字列に変換した値を指定しています。
```text:\media\lua\shared\Translate\JP\ContextMenu_JP.txt
ContextMenu_JP = {
	ContextMenu_P4BokusatsuBat_CheckBokusatsuBat = "撲殺バットを確認する",
}
```
```text:\media\lua\shared\Translate\EN\ContextMenu_EN.txt
ContextMenu_EN = {
	ContextMenu_P4BokusatsuBat_CheckBokusatsuBat = "Check the Bludgeoning Bat",
}
```

## レベル確認処理
コンテキストメニューから呼ばれる「checkBokusatsuBat」関数は下記の通りです。事前にレベルに応じた表示メッセージを用意しておき、翻訳後の値をスクリプト変数に定数として保持しておきます。そして、関数内では現在のレベルに応じたメッセージをゲームキャラに発言させることで、プレイヤーへの通知としました。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.messages = {}
P4BokusatsuBat.messages.CheckBokusatsuBat = {}
table.insert(P4BokusatsuBat.messages.CheckBokusatsuBat, getText("UI_P4BokusatsuBat_CheckBokusatsuBat_Lv1"))
table.insert(P4BokusatsuBat.messages.CheckBokusatsuBat, getText("UI_P4BokusatsuBat_CheckBokusatsuBat_Lv2"))
table.insert(P4BokusatsuBat.messages.CheckBokusatsuBat, getText("UI_P4BokusatsuBat_CheckBokusatsuBat_Lv3"))
table.insert(P4BokusatsuBat.messages.CheckBokusatsuBat, getText("UI_P4BokusatsuBat_CheckBokusatsuBat_Lv4"))
table.insert(P4BokusatsuBat.messages.CheckBokusatsuBat, getText("UI_P4BokusatsuBat_CheckBokusatsuBat_Lv5"))

P4BokusatsuBat.checkBokusatsuBat = function()
	P4BokusatsuBat.showInfo(P4BokusatsuBat.messages.CheckBokusatsuBat[P4BokusatsuBat.currentLevel])
end

P4BokusatsuBat.showInfo = function(message)
	P4BokusatsuBat.player:Say(message, 0.607, 0.717, 1.000, UIFont.Dialogue, 15, "radio")
end
```
こちらも、下記のように翻訳を追加しています。ここでは、経験値や次の必要経験値の具体的な値を通知するのではなく、感覚的なメッセージで通知することで没入感を演出しました。
```text:\media\lua\shared\Translate\JP\UI_JP.txt
UI_JP = {
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv1 = "特に何も感じられなかった…",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv2 = "手に馴染んできた気がする…",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv3 = "とても使い慣れてきた気がする！",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv4 = "まるで手足の一部のように感じる！",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv5 = "この感じは…撲殺天使！？",
}
```
```text:\media\lua\shared\Translate\EN\UI_EN.txt
UI_EN = {
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv1 = "I didn't feel any particular...",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv2 = "I feel more comfortable in my hands...",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv3 = "I feel very comfortable using it!",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv4 = "It feels as if it is part of the limb!",
	UI_P4BokusatsuBat_CheckBokusatsuBat_Lv5 = "This feels like... a Bludgeoning Angel!?",
}
```
また、今回の「checkBokusatsuBat」関数は、熟練度のレベルアップの際の通知として流用できるため、下記の通り「AddXP」イベント内でも呼び出しを追加しました。
```diff_lua:P4BokusatsuBat.lua
  P4BokusatsuBat.AddXP = function(player, perk, xp)
	  if perk:getType() == Perks.Blunt then
		  local pItem = player:getPrimaryHandItem()
		  if P4BokusatsuBat.isBokusatsuBat(pItem) then
			  P4BokusatsuBat.addXP(xp)
			  local previousLevel = P4BokusatsuBat.currentLevel
			  local currentLevel = P4BokusatsuBat.calcLevel()
			  if previousLevel < currentLevel then
				  P4BokusatsuBat.currentLevel = currentLevel
				  P4BokusatsuBat.setProperty(pItem, true)
+				  P4BokusatsuBat.checkBokusatsuBat()
			  end
		  end
	  end
  end
  Events.AddXP.Add(P4BokusatsuBat.AddXP)
```
これで、熟練度に関する仕様の実装は完了となります。

# ８. その他の仕様の実装
引き続き、その他の仕様も実装します。

## 自己修復機能
撲殺バットは修理不可のアイテム設定としているので、耐久の回復手段がありません。ただ、世界に１つだけ湧く特殊なアイテムの位置づけのため、耐久の自己修復機能を実装します。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.EveryHours = function()
	local targets = P4BokusatsuBat.inventory:getItemsFromType("P4BokusatsuBatT1", true)
	targets:addAll(P4BokusatsuBat.inventory:getItemsFromType("P4BokusatsuBatT2", true))
	targets:addAll(P4BokusatsuBat.inventory:getItemsFromType("P4BokusatsuBatT3", true))
	for i = 0, targets:size() - 1 do
		local target = targets:get(i)
		local condition = target:getCondition()
		if condition <= 0 then
			if ZombRand(20) == 0 then
				target:setCondition(1)
			end
		elseif condition < target:getConditionMax() then
			target:setCondition(condition + 1)
		end
	end
end
Events.EveryHours.Add(P4BokusatsuBat.EveryHours)
```
「Events.EveryHours」はゲーム内時間で１時間毎に発生するイベントとなります。このイベント内で、インベントリ内にある撲殺バットを調べ、発見した撲殺バットに対して、耐久値を回復させます。もし破損状態の場合は、低確率で回復させるようにしました。

この「EveryHours」イベントに関連したもので「EveryOneMinute」イベントなど、さらに発生間隔の狭いイベントもあります。最小間隔としては「OnTick」イベントの各チック毎に発生するイベントもあり、時間的に細かい操作が可能となりますが、特に「OnTick」イベントは発生回数が非常に多いので、性能劣化が顕著に現れます。周期イベントについては、実行頻度と負荷の見極めにご注意ください。

## 空腹度の上昇
撲殺バットについては、バニラ武器より強力なアイテム設定やブースト機能など、このままでは「ただただ強いだけの武器」となってしまうので、デメリットも追加したいと思います。そこで、撲殺バットでの攻撃時はお腹が減り易くなるという仕組みを追加します。実装に際しては今までの応用です。
```lua:P4BokusatsuBat.lua
P4BokusatsuBat.OnWeaponSwing = function(player, handWeapon)
	if P4BokusatsuBat.isBokusatsuBat(handWeapon) then
		P4BokusatsuBat.stats:setHunger(P4BokusatsuBat.stats:getHunger() + 0.0005)
	end
end
Events.OnWeaponSwing.Add(P4BokusatsuBat.OnWeaponSwing)
```
「Events.OnWeaponSwing」は武器をスイングした際に発生するイベントになります。このイベント内で、現在撲殺バットを装備していた場合は、プレイヤーオブジェクトの空腹度の値を加算するようにしています。

非常に長くなりましたが、これで全ての独自ギミックの追加が完了しました。

# ９. 独自ギミックの確認
今回追加した独自ギミックの全ての動作確認は割愛しますが、熟練度が適用されていることが確認できます。
![06-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2481343/2bb8be47-b75f-2ca9-2c92-5fb1062eb613.png)

また、熟練度のレベルアップや、それに合わせてブースト機能が有効になることも確認できました。
![06-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2481343/6c391fec-3d61-e592-4392-b0cf78c352de.png)

# あとがき
ここでは、Lua実装での独自ギミックを追加しました。
Lua実装は他にも沢山ポイントがあるので、バニラ実装などを参考にすることをお薦めします。
次回はやり残していたもう１つの独自ギミックを追加します。

---
索引：[Project ZomboidのMODを開発する](https://qiita.com/PePePePePeil/items/5db632d902eb91847541)
前：[５. 撲殺バットMODの作成（翻訳追加）](https://qiita.com/PePePePePeil/items/48fb6cbae1aa5b2b8952) 
次：[７. 撲殺バットMODの作成（独自ギミック追加２）](https://qiita.com/PePePePePeil/items/2877ece62d151c5ecf51) 

[^1]: 今回の内容を反映したMODファイルは下記。
💾 [P4BokusatsuBat_06.zip](https://www.pepepepe.com/zomboid/Qiita/P4BokusatsuBat_06.zip)（MD5チェックサム：63AE8095F666BFB2E1B45CC7CBDED9A8）