FiveM（QBCore）でガチャスクリプトを開発してみた

# はじめに
FiveMの既存スクリプト（猫カフェuwu）にランダムでアイテムが排出されるガチャBOXがあったが、同じようなガチャBOXを使用したいという話をサーバ運営者と村人の会話があり、luaは初めて触るが作ってみた。備忘録も兼ねて記載する。

## 仕様
【初回作成時点】
・本スクリプトで複数種類のガチャを扱えること
・排出するアイテム、確率は、外部ファイルで管理できるようにすること
・機能としては、アイテムが１つ出るガチャと複数種類のアイテムがでるガチャを作ること
　（イメージとしては、通常のガチャと福袋的なイメージ）

【機能追加】
・お金をだせるようにも改修（スクラッチとして活用）
・あたりが出た際に、音声を鳴動させるように改修

## 実際の画面
アイテムを使うとプログレスバーの表示と箱を開けているようなモーションをして、ガチャアイテムが削除されて、排出されるアイテムが取得されます。

![gacha.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/88a70d95-1d9c-ed61-41be-e03364e351c3.gif)

上記で動作させているアイテムは、２つアイテムがランダムで出るように設定している。
※スクリプトの中で通常の１種類しか出ないものと、２種類出るものと使い分けています。
※Gifのアップロード容量上限の関係で早送りみたいに見えています。

## ソースコード（一部紹介）
初めてのScript開発だったのと継ぎ足しで開発したりしたので、コードは汚い状態です。
（時間があるときにリファクタリング予定）
また、Scriptの配布やソースコードの公開については現在検討中。

QBCoreの公式ドキュメントに色々と載っているのでコーディングで便利です。

https://docs.qbcore.org/qbcore-documentation/guides/script-optimization

また、よく使うパターンなどをまとめていますので、そちらも良ければ見てみてください。

https://qiita.com/KumaRider/items/3ee5435487f746fa29e6

■fxmanifest.lua
スクリプトの構成は以下のluaの通りです。
ui_pageとfilesは音声を鳴動させるために、必要で追加したものです。

```lua:fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

author 'KumaRider'
description 'Gacha System with Sound for QBCore'
version '1.1.0'

-- サーバースクリプト
server_scripts {
    'server/gacha.lua',
    'shared/shared.lua',
}

-- クライアントスクリプト
client_scripts {
    'client/client.lua',
}

-- QBCoreと依存関係があることを宣言
dependencies {
    'qb-core',
}

-- NUIファイル
ui_page 'html/index.html'

-- 必要なファイル
files {
    'html/index.html',
    'html/script.js',
    'html/sounds/*.ogg'
}
```

■ガチャの出現アイテムと確率
ガチャの出現アイテムと確率は、shard.luaで定義しています。chanceがアイテムの出現確率になります。
合計で100を超えると、100以上のアイテムは出てきません。ただ、実際の画面で映していたように２種類排出されるものについては、１種類目を除外するようにスクリプトで記載しているため、以下は合計112.5としています。

```lua:shard.lua
RandomItemCatCafeBox = {
    { item = "kira_kira_currye", count = 30, chance = 12.5 },
    { item = "strawberry_shortcake", count = 30, chance = 12.5 },
    { item = "doki_doki_pancakes", count = 30, chance = 12.5 },
    { item = "rice_balls", count = 30, chance = 12.5 },
    { item = "cat_macaroon_green", count = 30, chance = 12.5 },
    { item = "hot_chocolate", count = 30, chance = 12.5 },
    { item = "booba_milk_tea_1", count = 30, chance = 12.5 },
    { item = "sweet_herbal_tea", count = 30, chance = 12.5 },
    { item = "cat_cafe_box", count = 1, chance = 11.5 },
    { item = "cat_cafe_box", count = 5, chance = 1 }
}
```

■排出されるアイテムを選定する部分
shard.luaのchanceを足しこみながらヒットしたところで止めるような処理です。
```lua:gacha.lua
local randomNumber = math.random() * 100
local accumulatedChance = 0
local selectedItem = nil

for _, v in ipairs(itemList) do
    accumulatedChance = accumulatedChance + v.chance
    if randomNumber <= accumulatedChance then
        selectedItem = v
        break
    end
end
```

■アイテム付与部分
アイテムの付与はサーバ側の処理で実施するため、gacha.luaに記述しています。

```lua:gacha.lua
if selectedItem1 then
    Player.Functions.AddItem(selectedItem1.item, selectedItem1.count)
    TriggerClientEvent('inventory:client:ItemBox', src, QBCore.Shared.Items[selectedItem1.item], "add")
    TriggerClientEvent('QBCore:Notify', src, 'You have received ' .. QBCore.Shared.Items[selectedItem1.item].label, 'success')
end
```

■開けるモーション部分など
TriggerClientEvent('gacha:openGacha', source, ticketType)で client.luaのgacha:openGachaを発火させています。

```lua:gacha.lua
-- ガチャアイテムを使用すると呼ばれるイベント
local function UseGachaTicket(ticketType)
    QBCore.Functions.CreateUseableItem(ticketType, function(source, item)
        local Player = QBCore.Functions.GetPlayer(source)
        
        -- チケットが1枚以上あるか確認
        if Player.Functions.GetItemByName(ticketType) then
            -- ガチャを回すイベントをトリガー
            TriggerClientEvent('gacha:openGacha', source, ticketType)
        else
            TriggerClientEvent('QBCore:Notify', source, 'You do not have a ' .. ticketType .. '.', 'error')
        end
    end)
end

--ガチャ（アイテム）を使用できるように登録（ここを増やせばガチャの種類を増やせる）
UseGachaTicket('gacha_ticket_basic')
UseGachaTicket('gacha_ticket_premium')
UseGachaTicket('cat_cafe_box')
```

```lua:client.lua
-- ガチャを開くイベント
RegisterNetEvent('gacha:openGacha')
AddEventHandler('gacha:openGacha', function(ticketType)
    local playerPed = PlayerPedId()
    local label = "ガチャを回しています..."

    -- 特定のチケットの場合、特別なメッセージを設定
    if ticketType == 'cat_cafe_box' then
        label = "ボックスをあけてるにゃ"
    end

    -- アニメーションをリクエスト
    RequestAnimDict("amb@prop_human_bum_bin@base")

    while not HasAnimDictLoaded("amb@prop_human_bum_bin@base") do
        Citizen.Wait(100)
    end
```

# 苦労した点
最初に構築したテストサーバは、QBCoreのバニラ環境であったが、実際に導入する本番サーバはox_lib,ox_inventoryを利用しているため、最初本番サーバでは動作しない状態だった。
ox_lib,ox_inventory用に書き換えても、アイテムの使用トリガーはQBCoreを利用しないと動かないなどがあり、自身のサーバをある程度本番サーバと同じような環境に作り上げて、検証を実施して、なんとか動作させることができた。

# 少し宣伝
FiveMの35VILLAGE RPというサーバで遊んでいます！

https://discord.gg/N2E63F48zq

犯罪の種類が多かったり、白ジョブ、フリージョブもたくさんあるサーバです。
私が作った自作Scriptもいつくか入っているので、ぜひ遊びに来てください！