> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Economy v2 中的掉落表和隨機戰利品

> 使用 Title Data、Azure Functions 和 ExecuteInventoryOperations，在 PlayFab Economy v2 中建置加權隨機戰利品掉落，以不可部分完成的方式授與物品。

<Info>
  Economy v2 現已正式推出。如需支援和意見反應，請前往 [PlayFab 論壇](https://community.playfab.com)。
</Info>

Economy v1 (舊版) 具有內建的[掉落表](/zh-TW/services/playfab/economy-monetization/economy/tutorials/drop-tables)功能，可讓您直接在 Game Manager 中定義加權隨機物品分配。Economy v2 不包含原生的掉落表功能，但您可以結合使用 [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) 和 [ExecuteInventoryOperations](https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/execute-inventory-operations) API 來實作相同的行為，而且更具彈性。

本教學課程說明如何建置完整的隨機戰利品系統，該系統可以：

* 在 [Title Data](/zh-TW/services/playfab/live-service-management/game-configuration/titledata) 中定義具有加權物品池的戰利品表。
* 使用 Azure Function 從戰利品表中隨機抽出物品。
* 在單一呼叫中，以不可部分完成的方式扣除容器權杖並授與隨機選取的物品。

## 運作方式

1. **在 Title Data 中定義戰利品表**：將您的掉落表設定 (物品 ID、權重、數量) 以 JSON 格式儲存在 Title Data 中，這樣您就能在不重新部署程式碼的情況下更新它們。
2. **以虛擬貨幣表示容器**：使用與固定內容容器相同的[容器權杖模式](/zh-TW/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents)：以虛擬貨幣作為未開啟的戰利品箱。
3. **呼叫 Azure Function 以開啟容器**：函式會讀取戰利品表、執行加權隨機選取，然後呼叫 `ExecuteInventoryOperations`，以不可部分完成的方式扣除 1 個容器權杖並新增隨機選取的物品。

## 必要條件

* [PlayFab 開發人員帳戶](https://developer.playfab.com/en-us/sign-up)。
* 在 [Game Manager](https://developer.playfab.com/) 中建立的遊戲。
* 可作為戰利品獎勵授與的已發佈目錄物品。
* 代表容器的虛擬貨幣 (如需設定指示，請參閱[具有固定內容的容器](/zh-TW/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents#step-1-create-a-virtual-currency-for-the-container))。
* 已連線至 PlayFab 遊戲的 [Azure Functions](/zh-TW/services/playfab/live-service-management/service-gateway/automation/cloudscript-af/quickstart) 專案。

## 步驟 1：在 Title Data 中定義戰利品表

將您的戰利品表定義儲存在 [Title Data](/zh-TW/services/playfab/live-service-management/game-configuration/titledata) 中，讓設計人員不需要變更程式碼就能更新掉落率。

### 戰利品表結構範例

建立名為 `LootTables` 的 Title Data 索引鍵，並使用 JSON 值：

```json theme={null}
{
  "TreasureChest": {
    "Rolls": 1,
    "Items": [
      { "ItemId": "{{CommonSwordID}}", "Weight": 1000, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{UncommonSwordID}}", "Weight": 100, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{RareSwordID}}", "Weight": 10, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{LegendarySwordID}}", "Weight": 1, "MinQuantity": 1, "MaxQuantity": 1 }
    ]
  },
  "SupplyCrate": {
    "Rolls": 3,
    "Items": [
      { "ItemId": "{{HealthPotionID}}", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 5 },
      { "ItemId": "{{ManaPotionID}}", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 3 },
      { "ItemId": "{{GoldCurrencyID}}", "Weight": 80, "MinQuantity": 10, "MaxQuantity": 100 },
      { "ItemId": "{{RareGemID}}", "Weight": 5, "MinQuantity": 1, "MaxQuantity": 1 }
    ]
  }
}
```

每個戰利品表都包含下列屬性：

| 屬性 | 描述 |
| - | - |
| **Rolls** | 開啟容器時要在表上抽選的次數。 |
| **Items** | 可能獎勵的物品池，每個物品都有權重和數量範圍。 |
| **Weight** | 相對機率。權重越高，表示物品越可能掉落。 |
| **MinQuantity / MaxQuantity** | 每次抽選要授與該物品的數量範圍。 |

### 設定 Title Data

#### Game Manager

1. 前往 **Content** > **Title Data**。
2. 建立名為 `LootTables` 的索引鍵。
3. 貼上您的 JSON 戰利品表設定作為值。
4. 選取 **Save**。

#### API

使用 [SetTitleData](https://learn.microsoft.com/en-us/rest/api/playfab/server/title-wide-data-management/set-title-data)：

```json theme={null}
{
  "Key": "LootTables",
  "Value": "{\"TreasureChest\":{\"Rolls\":1,\"Items\":[...]}}"
}
```

***

## 步驟 2：建立 Azure Function

建立一個 Azure Function，讓您的遊戲用戶端在玩家想要開啟容器時呼叫。此函式會：

1. 確認玩家至少擁有一個容器權杖。
2. 從 Title Data 讀取戰利品表。
3. 執行加權隨機選取。
4. 呼叫 `ExecuteInventoryOperations`，以不可部分完成的方式扣除權杖並授與獎勵。

### 資料模型

```csharp theme={null}
public class OpenContainerRequest
{
    public string LootTableId { get; set; }  // e.g., "TreasureChest"
}

public class LootTable
{
    public int Rolls { get; set; }
    public List<LootTableEntry> Items { get; set; }
}

public class LootTableEntry
{
    public string ItemId { get; set; }
    public int Weight { get; set; }
    public int MinQuantity { get; set; }
    public int MaxQuantity { get; set; }
}
```

### 加權隨機選取

```csharp theme={null}
public static LootTableEntry RollOnTable(LootTable table, Random rng)
{
    int totalWeight = table.Items.Sum(e => e.Weight);
    int roll = rng.Next(0, totalWeight);

    int cumulative = 0;
    foreach (var entry in table.Items)
    {
        cumulative += entry.Weight;
        if (roll < cumulative)
            return entry;
    }

    // Fallback (shouldn't reach here)
    return table.Items.Last();
}
```

### 完整的 Azure Function

```csharp theme={null}
[FunctionName("OpenLootContainer")]
public static async Task<dynamic> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    // 1. Parse the PlayFab context and request
    var context = JsonConvert.DeserializeObject<FunctionExecutionContext<dynamic>>(
        await req.ReadAsStringAsync());
    var args = context.FunctionArgument != null
        ? JsonConvert.DeserializeObject<OpenContainerRequest>(
            context.FunctionArgument.ToString())
        : null;

    if (args == null || string.IsNullOrEmpty(args.LootTableId))
        return new { Error = "LootTableId is required." };

    var playerEntity = context.CallerEntityProfile.Entity;
    var settings = new PlayFabApiSettings
    {
        TitleId = context.TitleAuthenticationContext.Id,
        DeveloperSecretKey = Environment.GetEnvironmentVariable("PLAYFAB_DEV_SECRET_KEY")
    };
    var authContext = new PlayFabAuthenticationContext
    {
        EntityToken = context.TitleAuthenticationContext.EntityToken
    };

    var serverApi = new PlayFabServerInstanceAPI(settings);
    var economyApi = new PlayFabEconomyInstanceAPI(settings, authContext);

    // 2. Read the loot table from Title Data
    var titleDataResult = await serverApi.GetTitleDataAsync(new GetTitleDataRequest
    {
        Keys = new List<string> { "LootTables" }
    });

    if (!titleDataResult.Result.Data.ContainsKey("LootTables"))
        return new { Error = "LootTables not found in Title Data." };

    var allTables = JsonConvert.DeserializeObject<Dictionary<string, LootTable>>(
        titleDataResult.Result.Data["LootTables"]);

    if (!allTables.ContainsKey(args.LootTableId))
        return new { Error = $"Loot table '{args.LootTableId}' not found." };

    var lootTable = allTables[args.LootTableId];

    // 3. Roll for rewards
    var rng = new Random();
    var rewards = new Dictionary<string, int>(); // ItemId -> total quantity

    for (int i = 0; i < lootTable.Rolls; i++)
    {
        var entry = RollOnTable(lootTable, rng);
        int quantity = rng.Next(entry.MinQuantity, entry.MaxQuantity + 1);

        if (rewards.ContainsKey(entry.ItemId))
            rewards[entry.ItemId] += quantity;
        else
            rewards[entry.ItemId] = quantity;
    }

    // 4. Build the atomic operation: subtract 1 container token + add rewards
    var operations = new List<InventoryOperation>();

    // Subtract the container token
    // The container currency ID must match the LootTableId mapping
    // Store this mapping in Title Data or use a naming convention
    operations.Add(new InventoryOperation
    {
        Subtract = new SubtractInventoryItemsOperation
        {
            Item = new InventoryItemReference
            {
                Id = GetContainerCurrencyId(args.LootTableId) // Your mapping logic
            },
            Amount = 1
        }
    });

    // Add each reward
    foreach (var reward in rewards)
    {
        operations.Add(new InventoryOperation
        {
            Add = new AddInventoryItemsOperation
            {
                Item = new InventoryItemReference { Id = reward.Key },
                Amount = reward.Value
            }
        });
    }

    // 5. Execute atomically
    var executeResult = await economyApi.ExecuteInventoryOperationsAsync(
        new ExecuteInventoryOperationsRequest
        {
            Entity = new EntityKey
            {
                Id = playerEntity.Id,
                Type = playerEntity.Type
            },
            Operations = operations,
            IdempotencyId = Guid.NewGuid().ToString()
        });

    if (executeResult.Error != null)
    {
        log.LogError($"Failed to execute operations: {executeResult.Error.ErrorMessage}");
        return new { Error = executeResult.Error.ErrorMessage };
    }

    log.LogInformation($"Player {playerEntity.Id} opened {args.LootTableId}, received: " +
        string.Join(", ", rewards.Select(r => $"{r.Value}x {r.Key}")));

    return new { Rewards = rewards, TransactionIds = executeResult.Result.TransactionIds };
}
```

<Note>
  `GetContainerCurrencyId` 協助程式函式會將戰利品表名稱對應至其對應的容器貨幣 ID。您可以將此對應與戰利品表一起儲存在 Title Data 中，或使用命名慣例。
</Note>

## 步驟 3：將容器權杖授與玩家

透過遊戲過程、商店購買或獎勵將容器權杖授與玩家。請使用[具有固定內容的容器](/zh-TW/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents#step-3-grant-container-tokens-to-players)中所述的相同方法。

## 步驟 4：從遊戲用戶端呼叫函式

當玩家想要開啟容器時，請使用 [ExecuteFunction](https://learn.microsoft.com/en-us/rest/api/playfab/cloudscript/server-side-cloud-script/execute-function) 呼叫 Azure Function：

```json theme={null}
{
    "FunctionName": "OpenLootContainer",
    "FunctionParameter": {
        "LootTableId": "TreasureChest"
    },
    "GeneratePlayStreamEvent": true
}
```

回應包含隨機選取的獎勵：

```json theme={null}
{
    "FunctionResult": {
        "Rewards": {
            "{{UncommonSwordID}}": 1
        },
        "TransactionIds": ["txn-abc-123"]
    }
}
```

## 戰利品表設計模式

### 稀有度層級

權重呈指數遞減的經典稀有度系統：

```json theme={null}
{
  "Items": [
    { "ItemId": "CommonItem",     "Weight": 1000, "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "UncommonItem",   "Weight": 100,  "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "RareItem",       "Weight": 10,   "MinQuantity": 1, "MaxQuantity": 1 },
    { "ItemId": "LegendaryItem",  "Weight": 1,    "MinQuantity": 1, "MaxQuantity": 1 }
  ]
}
```

掉落機率：

* 普通：約 90.0%
* 罕見：約 9.0%
* 稀有：約 0.9%
* 傳奇：約 0.09%

### 多次抽選搭配混合獎勵

從混合物品池中提供三個隨機物品的補給箱：

```json theme={null}
{
  "Rolls": 3,
  "Items": [
    { "ItemId": "{{HealthPotionID}}", "Weight": 40, "MinQuantity": 1, "MaxQuantity": 3 },
    { "ItemId": "{{ManaPotionID}}",   "Weight": 40, "MinQuantity": 1, "MaxQuantity": 3 },
    { "ItemId": "{{GoldCurrencyID}}", "Weight": 30, "MinQuantity": 10, "MaxQuantity": 50 },
    { "ItemId": "{{RareGemID}}",      "Weight": 5,  "MinQuantity": 1, "MaxQuantity": 1 }
  ]
}
```

### 保底 + 隨機 (保底系統)

若要實作「每開啟 10 次保證獲得稀有或更高等級物品」的模式，請在[顯示屬性](/zh-TW/services/playfab/economy-monetization/economy-v2/inventory#display-properties)或[玩家資料](/zh-TW/services/playfab/player-progression/player-data/quickstart)中追蹤玩家的開啟次數，並在達到閾值時於 Azure Function 中覆寫隨機選取。

### 巢狀掉落表

戰利品表可以依 ID 參照另一個戰利品表。您的 Azure Function 會以遞迴方式解析參照：

```json theme={null}
{
  "BossDropTable": {
    "Rolls": 2,
    "Items": [
      { "ItemId": "TABLE:WeaponDropTable", "Weight": 50, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "TABLE:ArmorDropTable",  "Weight": 30, "MinQuantity": 1, "MaxQuantity": 1 },
      { "ItemId": "{{GoldCurrencyID}}",    "Weight": 80, "MinQuantity": 50, "MaxQuantity": 200 }
    ]
  }
}
```

函式會檢查 `ItemId` 是否以 `TABLE:` 開頭，若是，則會在參照的表上抽選，而不是直接授與該物品。

## 安全性考量

* **一律在伺服器端執行戰利品邏輯** (在 Azure Function 中)。永遠不要信任用戶端來決定抽到哪些物品。此方法可防止竄改。
* 在 `ExecuteInventoryOperations` 呼叫上**使用 `IdempotencyId`**，以防止函式重試時重複授與。如需詳細資訊，請參閱[等冪交易和重試](/zh-TW/services/playfab/economy-monetization/economy-v2/tutorials/idempotent-transactions-and-retries)。
* 在扣除之前檢查玩家的庫存，以**驗證容器擁有權**。如果玩家沒有足夠的權杖，`ExecuteInventoryOperations` 中的 `Subtract` 作業會失敗，這會復原整個批次。
* **記錄所有抽選**以供稽核。函式的傳回值和 PlayStream 事件可提供每位玩家所獲得內容的記錄。

## 另請參閱

* [具有固定內容的容器](/zh-TW/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents)
* [ExecuteInventoryOperations API 參考](https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/execute-inventory-operations)
* [等冪交易和重試](/zh-TW/services/playfab/economy-monetization/economy-v2/tutorials/idempotent-transactions-and-retries)
* [Title Data](/zh-TW/services/playfab/live-service-management/game-configuration/titledata)
* [Azure Functions 快速入門](/zh-TW/services/playfab/live-service-management/service-gateway/automation/cloudscript-af/quickstart)
* [掉落表 (舊版 Economy)](/zh-TW/services/playfab/economy-monetization/economy/tutorials/drop-tables)
* [Economy v2 概觀](/zh-TW/services/playfab/economy-monetization/economy-v2/overview)


## Related topics

- [Economy (舊版) 中的掉落表](/zh-TW/services/playfab/economy-monetization/economy/tutorials/drop-tables.md)
- [Economy v2 中具有固定內容的容器](/zh-TW/services/playfab/economy-monetization/economy-v2/tutorials/containers-with-fixed-contents.md)
- [Economy v2 概觀](/zh-TW/services/playfab/economy-monetization/economy-v2/overview.md)
- [目錄 (舊版 Economy)](/zh-TW/services/playfab/economy-monetization/economy/items/catalogs.md)
- [各國家/地區的年齡分級指引](/zh-TW/publishing/game-publishing/concepts/age-ratings-guidance.md)
