# 遊戲拍賣場與玩家市集

> 每個用戶端即時看到上架、出價和倒數，最後幾秒出價自動延長防狙擊，支援直接購買與成交價走勢；每筆出價由你的伺服器決定，遊戲幣和道具仍由你的遊戲管理。

Source: https://hibira.io/zh-tw/use-cases/game-auction-house

拍賣場、交易所或玩家市集能讓遊戲經濟持續運轉，但只有在每個人同時看到同樣的東西時才會讓人覺得公平：有人出價的瞬間就看到新價格、每個畫面上的倒數都一樣，最後一秒也不會被人偷走。開源套件 [@hibira-io/auction](https://www.npmjs.com/package/@hibira-io/auction) 在 Hibira 上提供這些功能，而每一筆出價都由你自己的伺服器決定。

## 玩家會得到什麼

- 拍賣品的上架、變價和下架會出現在每個打開的用戶端，不用重新整理。
- 倒數和伺服器時鐘一致，最後幾秒出價會延長時間，而不是被人狙擊。
- 被超過的當下就收到通知，可以直接購買，也看得到每件物品最近的成交價。

![拍賣場範例：玩家對獵人之弓出價，另一個視窗隨即出價超過，第一位玩家立刻看到新價格和被超過的提示。](https://hibira.io/portal/articles/demo-game-auction-house-zh-TW.webp)

## 使用的功能

- **每個拍賣場一個公開或私有頻道**（`auction-main`），只有一種事件 `auction-update`，帶著完整的拍賣品和版本號。
- **從你的伺服器呼叫 HTTP API**（`trigger`），每次變化只發布一次，不管有多少玩家在看。
- **[@hibira-io/auction](https://github.com/hibira-io/auction-js)**：伺服器端負責檢查出價、延長最後時刻的拍賣並結標；用戶端保留每件拍賣品的最新版本，斷線重連後自動補齊。

```text
sequence: 出價送到你的伺服器，由 Hibira 通知每位玩家
玩家 A | 你的伺服器 | *Hibira | 玩家 B
1 > 2: POST /auction/:id/bid | 120 金幣
note: house.bid() 檢查出價並凍結金幣
2 > 3: auction-update | 價格 120，A 領先
3 > 1: auction-update
3 > 4: auction-update | 「出價 126」
```

## 步驟 1：在伺服器上建立拍賣場

道具和遊戲幣仍然由你的遊戲管理。拍賣場會請你凍結出價者的幣、在有人出更高價時退回，並在結標時轉移道具和幣：

```js
import Pusher from 'pusher';
import { createAuctionHouse } from '@hibira-io/auction/server';

const hibira = new Pusher({ appId, key, secret, host: 'ws.hibira.io', useTLS: true });

const house = createAuctionHouse({
  pusher: hibira,
  name: 'main',
  extendWithin: 30000, // a bid in the last 30 s adds time
  reserve: async ({ userId, amount }) => {
    if (!(await wallet.hold(userId, amount))) throw new Error('not enough gold');
  },
  release: async ({ userId, amount }) => wallet.unhold(userId, amount),
  settle: async ({ listing }) => game.deliver(listing), // idempotent by listing.id
});
house.start(); // closes and settles ended auctions
```

## 步驟 2：四個 API

玩家身分要從你自己的登入狀態取得，不要從請求內容讀取：

```js
app.get('/auction', async (req, res) => res.json(await house.snapshot()));

app.post('/auction/:id/bid', requireLogin, async (req, res) => {
  try {
    res.json(await house.bid({ listingId: req.params.id, bidderId: req.user.id, bidder: { name: req.user.name }, amount: req.body.amount }));
  } catch (err) {
    res.status(err.status ?? 500).json({ error: err.code });
  }
});
// The same for house.buyout() and house.list()
```

## 步驟 3：在遊戲中顯示拍賣場

```js
import { AuctionHouse } from '@hibira-io/auction';

const house = AuctionHouse.open(pusher, 'main', {
  load: () => fetch('/auction').then((r) => r.json()),
});

house.on('change', ({ listings }) => render(listings));
setInterval(() => listings.forEach((l) => showTimeLeft(l, house.timeLeft(l))), 250);

bidButton.onclick = () => fetch(`/auction/${l.id}/bid`, { method: 'POST', body: JSON.stringify({ amount: l.minBid }) });
```

`house.prices(type)` 會回傳某種物品最近的成交價，可以畫成價格走勢圖。斷線重連後拍賣場會重新載入快照，不會漏掉任何變化。

## 試試看

[拍賣場範例](https://github.com/hibira-io/examples/tree/main/nextjs-auction) 是一個完整的 Next.js 遊戲市集：NPC 商人、背包、直接購買、被超過通知和成交價，支援七種語言。在兩個視窗打開，在其中一個出價看看。

## 小提醒

- 每次變化算一則訊息，不管有多少玩家在看。大型拍賣場可以按分類或地區拆開（`name: 'weapons'`），讓每個用戶端只載入它要顯示的部分。
- 有多台遊戲伺服器時，給拍賣場一個存在你資料庫上的 `store`；寫入是比較後再存，兩台伺服器不可能同時接受同一筆出價。
- 它轉移的是遊戲自己的幣和道具，不處理真錢。
- Unity、Godot 或任何 Pusher SDK 的用戶端都能訂閱同一個 `auction-main` 頻道並呼叫同樣的 API。房間和配對請看[多人遊戲房間與大廳](https://hibira.io/zh-tw/use-cases/game-lobbies)。
