卡牌、手牌、卡组、弃牌与消耗
运行时卡牌区域如何工作、抽牌堆如何排序,以及哪些 API 能安全地移动卡牌。
卡牌进了错误的牌堆、生成的卡没出现在手牌里、UI 显示的手牌内容过期时,来查这一页。运行时卡牌区域是 CardInstance 列表。卡牌实例是 GameCard 资产的一份战斗副本,拥有自己的 instance id、升级标记和 master deck cost delta。Game Card Monitor 的 Pile 页签会实时列出所有区域的内容。

卡牌定义与卡牌实例
| 概念 | 类型 | 含义 |
|---|---|---|
| Card definition | GameCard | 创作资产:名称、费用、tags、稀有度、文本、目标选择和行为。 |
| Card instance | CardInstance | 战斗牌堆中的运行时副本。它指向一个 GameCard,并追踪战斗专属状态。 |
| Active card | CardInstance.GetActiveCard() 或 GCSApi.GetActiveCard(instance) | 实例已升级时为升级变体,否则为基础卡。 |
| Effective cost | GCSApi.GetEffectiveEnergyCost(instance) | 经过卡牌定义、master deck delta、临时修正和 cost hooks 后的费用。 |
运行时动作用实例。内容浏览、卡组构建和数据库查询用定义。
运行时牌堆
| 牌堆 | API | 含义 |
|---|---|---|
| Hand | GCSApi.Hand() 或 GCSApi.CurrentHand | 玩家手牌中当前可用的卡牌。 |
| Draw pile | GCSApi.DrawPile() | 等待被抽到的卡牌。最后一个元素下一张被抽。 |
| Discard pile | GCSApi.DiscardPile() | 本场战斗中被弃掉、可被洗回的卡牌。 |
| Exhaust pile | GCSApi.ExhaustPile() | 本场战斗剩余时间内移出游戏的卡牌。 |
| Played this turn | GCSApi.PlayedThisTurn() | 当前回合成功打出的卡牌,按打出顺序排列。 |
| Drawn this turn | GCSApi.DrawnThisTurn() | 当前回合抽到的卡牌,按抽牌顺序排列。 |
Hand()、DrawPile()、DiscardPile()、ExhaustPile()、PlayedThisTurn() 和 DrawnThisTurn() 永远不返回 null。没有活动战斗时返回空列表。CurrentHand 镜像 manager,战斗外返回 null。
卡牌在四个区域之间的流转路径很少:
抽牌堆顺序
抽牌堆的顺序约定是最后一个元素位于堆顶。文档里说"抽牌堆顶部"时,指的就是下一张会被抽到的那个元素。
添加卡牌时用 DrawPilePosition:
| 位置 | 含义 |
|---|---|
| Top | 先于当前牌堆内容被抽到。 |
| Bottom | 后于当前牌堆内容被抽到。 |
| Random | 插入随机位置。 |
开局手牌与每回合抽牌
战斗开始时:
- GCS 从 run master deck 或玩家初始卡组构建抽牌堆。
- 洗抽牌堆。
- 优先把 Innate 卡放入开局手牌。
- 抽起始手牌。
之后的玩家回合:
- 清空
DrawnThisTurn。 - 触发(Raise)
OnDrawPhase。 - 运行 draw-phase 状态行为。
- GCS 按
BattleRules.DrawPerTurn抽牌。
抽牌时如果抽牌堆已空,弃牌堆会自动洗回抽牌堆并触发 OnDrawPileShuffled。两个堆都空时,抽牌就此停止。
手牌上限是 BattleRules.MaxHandSize(默认 10;战斗中 Hand Size Hook 可以改这个上限)。手牌已满时,每回合抽牌会把多出来的牌从抽牌堆顶直接翻进弃牌堆,并逐张触发 OnCardDiscarded;直接调用 GCSApi.DrawCards 则提前停止。AddCardToHand 或往已满手牌移动卡牌,同样会把牌送进弃牌堆。
DrawnThisTurn 只覆盖当前回合。之前回合抽到的牌不包含在内,即使它们还留在手牌里。
打出卡牌
GCSApi.TryPlayCard(card, target) 成功时:
- 卡牌必须在当前手牌中。
- 解析 active card definition。
- 检查有效费用。
- 运行 playability hooks。
- 消耗能量。
- 把卡牌实例加入
PlayedThisTurn。 - 触发
OnCardPlayed。 - 运行卡牌的 on-play FlowGraph。
- 注册卡牌 hooks。
- 卡牌离开手牌。
- 如果 active card 带有
Exhaustkeyword,移入消耗堆并触发OnCardExhausted;否则移入弃牌堆。
卡牌在行为结算之后才离开手牌。自定义 UI 不要手动移除它。
回合结束的手牌处理
玩家回合结束时,GCS 处理剩余手牌:
| 卡牌行为 | 运行时结果 |
|---|---|
| 普通手牌 | 移入弃牌堆。 |
带 Retain keyword 的卡牌 | 留在手牌中,并触发 OnCardRetained。 |
带 Ethereal keyword 的卡牌 | 移入消耗堆,并触发 OnCardExhausted。 |
| 已打出且会消耗的卡牌 | 在打出时就移入消耗堆,早于回合结束。 |
上表基于默认的 HandDiscardRule.DiscardOnTurnEnd。遭遇战改用 Persist 时会跳过弃牌步骤:留下的每张牌都触发 OnCardRetained,Ethereal 卡牌仍会消耗。
如果你的 UI 想播放手牌清理动画,订阅卡牌生命周期事件,然后从 GCSApi.Hand() 刷新。
创建和移动卡牌
卡牌移动请使用 controller 背书的 API。
| 任务 | API |
|---|---|
| 把一张卡加入手牌 | GCSApi.AddCardToHand(card) |
| 移动已有实例 | GCSApi.MoveCardToPile(instance, PileScope.Discard) |
| 洗抽牌堆 | GCSApi.ShuffleDrawPile() |
| 抽牌 | GCSApi.DrawCards(count) |
| 读取 active card definition | GCSApi.GetActiveCard(instance) |
| 读取有效费用 | GCSApi.GetEffectiveEnergyCost(instance) |
不要直接编辑 CardPile.Cards。直接编辑会跳过事件、牌堆规则,以及你的 UI 依赖的刷新假设。
Master deck 与战斗牌堆
Run master deck 是用于启动战斗的持久卡组列表,运行时牌堆是战斗状态。
| 需求 | API |
|---|---|
| 查看 run deck | GCSApi.GetMasterDeck() |
| 替换 run deck | GCSApi.SetMasterDeck(entries) |
| 向 run deck 添加卡牌 | GCSApi.AddCardToMasterDeck(card, isUpgraded) |
| 从 run deck 移除 | GCSApi.RemoveCardFromMasterDeck(cardGuid, removeFirstOnly) |
| 在 run deck 中升级 | GCSApi.UpgradeCardInMasterDeck(cardGuid) |
| 清空 run deck | GCSApi.ClearMasterDeck() |
修改 master deck 不会追溯改变已经在实时战斗中的卡牌,除非你同时修改运行时牌堆。
卡牌事件
| 事件 | Payload | 用于 |
|---|---|---|
| OnCardPlayed | CardPlayedEventArgs | 出牌 UI、统计、连击计数。 |
| OnCardModified | CardModifiedEventArgs | 费用或升级刷新。 |
| OnCardDrawn | CardLifecycleEventArgs | 抽牌动画和手牌 widget 创建。 |
| OnCardDiscarded | CardLifecycleEventArgs | 弃牌动画。 |
| OnCardExhausted | CardLifecycleEventArgs | 消耗动画。 |
| OnCardRetained | CardLifecycleEventArgs | 保留反馈。 |
| OnCardAddedToHand | CardLifecycleEventArgs | 生成卡牌的 UI。 |
| OnCardAddedToDeck | CardLifecycleEventArgs | Run deck 更新。 |
| OnDrawPileShuffled | 无 payload | 洗牌反馈。 |
处理完事件后始终从当前牌堆刷新。事件告诉你什么变了,API 告诉你当前的事实。