# 游戏拍卖场与玩家市集

> 每个客户端实时看到上架、出价和倒计时，最后几秒出价自动延长防狙击，支持一口价与成交价走势；每次出价由你的服务器决定，游戏币和道具仍由你的游戏管理。

Source: https://hibira.io/zh-cn/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-CN.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 步：四个接口

玩家身份要从你自己的登录状态获取，不要从请求内容读取：

```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` 频道并调用同样的接口。房间和匹配请看[多人游戏房间与大厅](https://hibira.io/zh-cn/use-cases/game-lobbies)。
