Skip to main content

API Guide

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:

Hunter demo battle right after StartBattle, with a five-card opening hand, 3/3 energy, and the enemy intent icon already visible

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);
Update state through the public mutation APIs

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

Victory is checked by the phase machinery

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.