Asset class representing a card game definition (.cggame). Stores presentation, deck and prefab references, player limits, stats layout, betting defaults, and helper for deriving a display name from the resource path. Also validates required references during publishing.
namespace CardGames;
/// <summary>
/// A game asset (<c>.cggame</c>): the deck, table rules and presentation for one card game, plus a
/// reference to the prefab that holds the rules (a <see cref="CardGame"/> component). Player-facing
/// identity - title, description, author, icon - comes from the <see cref="Sandbox.Package"/> this was
/// published in, not from the asset, so it isn't duplicated here.
/// </summary>
[AssetType( Name = "Game", Extension = "cggame", Category = "Card Games" )]
public class GameDefinition : GameResource
{
/// <summary>
/// Rich-text "How to play" write-up shown in the table-setup screen and the in-game HUD.
/// Supports inline html (e.g. <b> for section headers, <br> for line breaks). Hidden when empty.
/// </summary>
[Property, TextArea] public string Tutorial { get; set; } = "";
/// <summary>
/// Prefab containing the rules component (a <see cref="CardGame"/>). Cloned when the game starts.
/// </summary>
[Property] public PrefabFile Prefab { get; set; }
/// <summary>
/// The deck this game is played with.
/// </summary>
[Property] public DeckDefinition Deck { get; set; }
[Group( "Players" ), Property] public int MinPlayers { get; set; } = 1;
[Group( "Players" ), Property] public int MaxPlayers { get; set; } = 4;
[Group( "Players" ), Property] public bool Multiplayer { get; set; } = true;
/// <summary>
/// Which stats this game's hero panel shows, and in what order - any stat key the game itself records
/// via <see cref="GameStats.Increment"/>/<see cref="GameStats.SetValue"/>, not just the framework's own
/// (games_played, win_rate, best_time, win_streak, time_played). A key gets a readable label for free
/// (e.g. "biggest_pot" → "Biggest Pot") unless <see cref="StatDisplay.Label"/> overrides it. Empty (the
/// default) falls back to <see cref="GameStats.DefaultDisplay"/>, the generic set every game gets for free.
/// </summary>
[Group( "Stats" ), Property] public List<StatDisplay> Stats { get; set; } = new();
/// <summary>
/// Chips each seat starts with. Games that don't use betting just ignore it.
/// </summary>
[Group( "Betting" ), Property] public long StartingStack { get; set; } = 1000;
/// <summary>
/// Camera height for this game (0 = use the <see cref="TableAnchor"/> default).
/// </summary>
[Group( "Presentation" ), Property] public float CameraHeight { get; set; } = 0f;
/// <summary>
/// Card size as a fraction of normal (1 = default).
/// </summary>
[Group( "Presentation" ), Property] public float CardScale { get; set; } = 1f;
/// <summary>
/// A readable name derived from the asset path, for local games that have no owning package to name them.
/// Games pulled from the cloud use their <see cref="Sandbox.Package"/> title instead. e.g. "games/texas-holdem.cggame" → "Texas Holdem".
/// </summary>
public string DisplayName => NiceName( ResourcePath );
/// <summary>
/// Turn a <c>.cggame</c> asset path into a readable title: the file name, sans folders and extension,
/// with separators spaced out and each word capitalised. Empty in, empty out.
/// </summary>
public static string NiceName( string resourcePath )
{
if ( string.IsNullOrEmpty( resourcePath ) ) return "";
var name = System.IO.Path.GetFileNameWithoutExtension( resourcePath ).Replace( '-', ' ' ).Replace( '_', ' ' );
return System.Globalization.CultureInfo.InvariantCulture.TextInfo.ToTitleCase( name );
}
public override void ConfigurePublishing( ResourcePublishContext context )
{
if ( Prefab is null )
{
context.SetPublishingDisabled( "Invalid: missing a prefab" );
return;
}
if ( Deck is null )
{
context.SetPublishingDisabled( "Invalid: missing a deck" );
}
context.IncludeCode = true;
}
}