Player Party
Run several complete player units in one battle while every card and effect remains bound to the correct character
A GameEncounter can create one primary player and additional party members. Every member keeps independent HP, armor, statuses, hooks, content definition, and presentation, while the default policy gives the party one player phase and one shared set of card piles and energy.
The diagram connects encounter authoring to the final runtime result: the encounter establishes stable unit identities and slots, the party shares cards and energy, and each accepted play records its immutable card origin, actual action source, resolved targets, FlowGraph context, events, and presentation.
Party support covers multiple complete local player-side units. Activation and card-play policies provide gameplay extension boundaries, but GCS does not implement networking, replication, rollback, rooms, or accounts.
Build the initial party
Player Units presents the complete starting roster as one ordered list. Row 1 remains the primary compatibility entry, while rows 2 and 3 store additional party members. Add, remove, or drag rows directly; both authored and runtime player rosters are limited to three members.
| Setting | Runtime result |
|---|---|
Player Units[0] | Creates the primary member in slot 0; legacy PlayerUnit and GCSApi.Player() continue to resolve this member |
Player Units[1..2] | Creates additional independent PlayerUnitState members in roster order |
| Row order | Maps members to presentation slots 0, 1, and 2 without additional slot configuration |
Workbench validation reports missing members, more than three players, out-of-range slots, and occupied slots before Play Mode.
Keep identity explicit
Every live unit carries UnitId, Team, TeamId, SlotIndex, and JoinOrder. GCSApi.Player() is the compatibility primary, ActivePlayer() is the selected member, PlayerParty() preserves roster order including dead members, and AlivePlayers() and DeadPlayers() separate action and revival queries.
The default shared party turn prepares armor, status lifecycle, resources, and draws for the full party, then opens one player action window. A skipped member is excluded without skipping other members, and the default loss condition requires every player-side member to be dead. The default win condition becomes eligible only after HasEncounteredEnemy is true, so an encounter may begin with an empty enemy roster and spawn its first enemy later.
Pass a BattlePolicySet to GCSApi.StartBattle when a project needs a different action, activation, reaction, targeting, or result rule. GCS resolves every omitted policy before units and cards are created, then freezes that resolved set for the battle. Configure a new set for the next battle instead of mutating ActivePolicies during play.
| Policy | Controls |
|---|---|
IPartyActivationPolicy | Free selection or ordered member activation |
IAutomaticPartyActivationPolicy | Companion-controlled activations inside an ordered party turn |
IActionWindowPolicy | The phases in which a player card request may be accepted |
IReactionWindowPolicy | A pausable reaction window before an enemy action |
ICardPlayPolicy | Project-specific source or card permission |
IUnitTargetPolicy | The actor-relative default opponent, independent of presentation selection |
IBattleResultPolicy | The terminal win or loss phase |
Share cards and energy
All player members use one Hand, Draw pile, Discard pile, Exhaust pile, and energy value. Initialization, turn draws, reshuffles, hand limits, energy changes, and card movement therefore settle once for the party rather than once per member.
When the persistent Master Deck is empty, Player Unit 1 supplies the Starting Deck used to create the shared Draw pile. Starting Decks on later rows are not merged into this battle; those Player assets can still use their own decks when assigned as Player Unit 1 in another Encounter. Cards created for the battle retain the primary member's OwnerUnitId as immutable origin metadata, but the selected member remains free to act as the source of a later play.
The legacy TryPlayCard(card, target) overload uses the selected ActivePlayer() as the action source. That source drives Self, source-relative hooks, FlowGraph execution, and card-play events without changing OwnerUnitId. Ordered or automatic party logic can submit an explicit eligible source through CardPlayRequest.
PlayerUnitState member = GCSApi.AlivePlayers()[1] as PlayerUnitState;
GCSApi.TrySetActivePlayerUnit(member);
foreach (CardInstance card in GCSApi.Hand(member))
{
var request = new CardPlayRequest(card, member, GCSApi.FirstAliveEnemy());
if (GCSApi.GetEffectiveEnergyCost(card) <= GCSApi.Energy(member)
&& GCSApi.CanPlayCard(request))
{
GCSApi.TryPlayCard(request);
}
}
An accepted play stores an immutable CardPlayContext on CardInstance.LastPlayContext. It contains the origin zone, original owner, actual source, and complete resolved target list. OnCardPlayed exposes the same distinctions, so UI and external systems do not need to infer execution from ownership.
Target allies and relative teams
Cards can use Single Ally, Other Ally, All Allies, Random Ally, Random Opponent, and Any Unit. Single-target ally and any-unit modes use the Demo targeting arrow and legal-unit highlight, while all and random modes resolve automatically. Random Opponent submits exactly one living unit from the opposing side; Any Unit accepts any living player or enemy unit. FlowGraph All Allies and All Opponents resolve relative to the current source team, Active Player returns the selected living member, and All Dead Allies supplies revival targets.
Change the roster at runtime
Spawn Unit can create player or enemy units by Team. Player reinforcements stop at the fixed three-player limit, take a stable slot, and publish OnPlayerUnitAdded without creating another deck or energy value. ReplacePlayer removes one roster identity and creates a replacement in the same slot; the shared cards and energy remain unchanged, while Active Player and current turn roles follow the replacement when applicable. Despawn Unit removes either team and publishes OnUnitRemoved; ordinary death publishes OnUnitDied, keeps the identity in the roster, and a successful Revive Unit publishes OnUnitRevived.
PlayerUnitState joined = GCSApi.SummonPlayer(playerDefinition, 2, GCSApi.ActivePlayer());
if (joined != null)
GCSApi.SetEnergy(joined, GCSApi.MaxEnergyFor(joined));
PlayerUnitState replacement = GCSApi.ReplacePlayer(joined, replacementDefinition);
GCSApi.RemoveUnit(replacement);
A removed identity cannot be revived, and repeated death processing does not publish duplicate death events. Removing or replacing a member does not split, transfer, or rebuild the party's shared cards and energy.
The Demo BattleBoard follows add, remove, death, revival, and active-member changes. Bundled single-character scenes keep one visible player slot; adding a second or third runtime member expands the children under the same Player Area root instead of adding sibling area objects. Party Showcase authors the three child slots for inspection, while Dashboard Initialize System installs the same three-slot starting layout into the active scene.
Verify the complete party in Party Showcase
Open Demo/Scenes/Multi-Character/Party Showcase and enter Play Mode. The battle board should show Scout, Squire, and Apprentice together against Plague Stalker, Spore Brute, and Mire Reaper, with one shared hand and energy display.

Player Unit 1, Scout, supplies Party Tactics for the shared eight-card pool. Field Mend, Interpose, Shared Resolve, and Emergency Aid verify direct, other-only, all-party, and random ally targets. Select a different member before playing an attack: shared energy pays the cost, the selected member becomes SourceUnit, and the card retains its original OwnerUnitId. Game Card Monitor exposes each member's identity, slot, active state, shared energy, and statuses; single-character encounters remain compatible with one Player Units row.