API Guide
One static facade over the battle runtime: GCSApi starts battles, plays cards, reads live state, mutates it safely, and hands out event subscriptions
Guard before you call
GCSApi.IsReady is true once a GameCardManager exists in the scene; most calls are safe without one: they return empty lists, false, zero, or null instead of throwing; UI code should still guard the paths that only make sense during an active battle
if (!GCSApi.IsReady)
{
return;
}
if (!GCSApi.IsBattleActive)
{
GCSApi.StartBattle(encounter);
}
Start a battle
StartBattle creates a live battle from a GameEncounter asset; any battle already in progress is disposed first, the opening hand is drawn, and OnBattleStarted is raised before the call returns, with enemy intents already resolved for the first turn
public GameEncounter Encounter;
public void Begin()
{
GCSApi.StartBattle(Encounter);
}
After StartBattle returns, the opening hand, player energy, enemy lineup, and first resolved Intents are available to the presentation layer:

Once the battle exists, let events drive the UI instead of polling:
private IDisposable _battleStarted;
void OnEnable()
{
_battleStarted = GCSApi.OnBattleStarted(RefreshAll);
}
void OnDisable()
{
_battleStarted?.Dispose();
}
Play a card
Read the hand, test playability, then call TryPlayCard
public void TryPlayFirstCard(UnitState target)
{
foreach (var card in GCSApi.Hand())
{
if (!GCSApi.CanPlayCard(card)) continue;
GCSApi.TryPlayCard(card, target);
return;
}
}
Not every card needs a target from the player. RequiresTarget returns true only for cards that target a single enemy unit; self, all-enemy, and untargeted cards resolve their targets automatically, so you can play them without opening a picker
var activeCard = GCSApi.GetActiveCard(card);
if (GCSApi.RequiresTarget(activeCard))
{
ShowTargetPicker(card);
}
else
{
GCSApi.TryPlayCard(card);
}
TryPlayCard returns false when the play is rejected: no active battle, the card is not in the hand, energy is short, the card carries the Unplayable keyword, or a play hook cancelled it
End the player turn
public void EndTurnButton()
{
if (GCSApi.Phase == BattlePhase.PlayerPhase)
{
GCSApi.EndPlayerTurn();
}
}
EndPlayerTurn advances to the enemy phase and does nothing outside PlayerPhase, so a stray click on a stale button cannot break the turn order
Read state for UI
The common UI reads are named helpers:
turnLabel.text = GCSApi.TurnNumber.ToString();
energyLabel.text = $"{GCSApi.PlayerEnergy}/{GCSApi.MaxEnergy}";
var player = GCSApi.Player();
hpLabel.text = $"{GCSApi.Hp(player)}/{GCSApi.MaxHp(player)}";
foreach (var enemy in GCSApi.AliveEnemies())
{
RenderEnemy(enemy, GCSApi.CurrentIntentTag(enemy), GCSApi.CurrentIntentValue(enemy));
}
When an event fires, refresh from API state instead of trusting a cached copy; the runtime owns the truth; your UI is a projection of it
Mutate battle state safely
External systems mutate state through GCSApi helpers; the calls route into the runtime controller, so hooks still run and events still fire
var player = GCSApi.Player();
GCSApi.GainArmor(player, 5);
GCSApi.DrawCards(1);
GCSApi.ApplyStatus(player, shieldedStatus, 2, player);
Writing to UnitState.CurrentHp, UnitState.Statuses, or the pile lists directly skips hooks and events, so statuses stop reacting and the UI never receives the change; use the GCSApi mutation helpers or controller instead
Win and loss are evaluated on phase transitions and after card plays, not on every HP write; dropping an enemy to zero HP through a mutation helper registers at the next check rather than ending the battle on the spot
Subscribe to events
Shortcut methods cover the built-in events and return IDisposable handles:
private readonly List<IDisposable> _subs = new();
void OnEnable()
{
_subs.Add(GCSApi.OnEnergyChanged(_ => RefreshEnergy()));
_subs.Add(GCSApi.OnUnitHpChanged(_ => RefreshUnits()));
_subs.Add(GCSApi.OnCardDrawn(_ => RefreshHand()));
}
void OnDisable()
{
foreach (var sub in _subs) sub.Dispose();
_subs.Clear();
}
For custom event names, or when you want the channel directly, use Subscribe<TArgs>:
_subs.Add(GCSApi.Subscribe<DamageEventArgs>(
GCSEventNames.OnDamageDealt,
args => LogDamage(args.SourceUnitId, args.TargetUnitId, args.Amount)));
Parameterless events use Subscribe(string, Action); every built-in name is a constant on GCSEventNames, and the payload classes live in GCSEventArgs; custom events raised through RaiseGameEvent deliver their payload as IReadOnlyDictionary<string, object>, so subscribe with that as TArgs
Discover content
GCSApi enumerates the active databases and their entities
foreach (var card in GCSApi.Cards())
{
AddCardToBrowser(card);
}
var poison = GCSApi.FindStatus(poisonGuid);
Enumeration methods build fresh lists and can allocate; cache the result for menus that refresh every frame
Maintain the master deck
The master deck is the run-level deck future battles are built from. StartBattle seeds the draw pile from it
GCSApi.ClearMasterDeck();
GCSApi.AddCardToMasterDeck(strike);
GCSApi.AddCardToMasterDeck(defend, isUpgraded: true);
GCSApi.StartBattle(encounter);
Changing the master deck does not rewrite the runtime piles of a battle that has already started
Extension registries
For tools and debug panels, the four reflection registries are exposed as helpers:
var nodes = GCSApi.NodeTypes();
var cardTypes = GCSApi.CardTypes();
var tags = GCSApi.CardTags();
var tokens = GCSApi.DescriptionTokens();
The results include custom nodes, card types, tags, and description tokens discovered from every loaded assembly that references TinyGiants.GCS.Runtime
Use API Reference for the complete member signatures and return types.