Skip to main content

GCSApi Guide

GCS ยท Runtime API

One static facade over the battle runtime: GCSApi starts battles, plays cards, reads live state, mutates it safely, and hands out event subscriptions.

For developers building their own UI or scene flow on top of GCS instead of the demo presentation.

GCSApi is the public runtime entry point for Game Card System. Everything external code needs goes through it: starting a battle, playing cards, reading live state, mutating that state without bypassing hooks, enumerating content databases, maintaining the master deck, subscribing to events, and discovering the extension registries. The class lives in Runtime/GCS.API.cs, and its XML docs double as the full reference, so IntelliSense shows the same documentation you read here.

The snippets on this page are component excerpts, not complete files. Drop them into your own MonoBehaviour, presenter, or service class and add using System;, using System.Collections.Generic;, and using TinyGiants.GCS.Runtime; as needed.

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);
}

With the demo presentation in the scene, that single call is the whole distance from an encounter asset to this:

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);
Never poke the state objects directly

Do not write to UnitState.CurrentHp, UnitState.Statuses, or the pile lists yourself. Direct writes skip hooks and events, so statuses stop reacting and the UI never hears about the change.

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โ€‹

Building tools or 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.


For a custom battle screen:

  1. Place a GameCardManager in the scene and assign databases.
  2. Start the battle with GCSApi.StartBattle.
  3. Subscribe to events in OnEnable; dispose the handles in OnDisable.
  4. Refresh UI from GCSApi state after each event.
  5. Wire card buttons through CanPlayCard, RequiresTarget, and TryPlayCard.
  6. Route player decisions through EndPlayerTurn, ApplyReward, and SkipReward.
  7. Keep presentation state out of gameplay state.

This keeps your UI decoupled from the demo while reusing the same battle engine underneath.