Framework/GameDefinition.cs

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.

File Access
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. &lt;b&gt; for section headers, &lt;br&gt; 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;
	}
}