OMRRoundRuleProfile.cs
using Sandbox;
using System;
using System.Collections.Generic;

/// <summary>
/// How a round rule obtains the weapon used by both players.
///
/// CompetitiveShuffle consumes the shared competitive shuffle bag.
/// FilteredShuffle keeps a separate bag restricted by AllowedWeaponIds and/or
/// AllowedArchetypes. Fixed always uses FixedWeapon.
/// </summary>
public enum OMRRoundWeaponPolicy
{
	CompetitiveShuffle,
	FilteredShuffle,
	Fixed
}

/// <summary>
/// Optional specialized runtime flow owned by RoundManager.
/// Most rules use None and only contribute data-driven arena/player/weapon
/// rules. High Noon keeps its latency-aware draw sequence isolated here.
/// </summary>
public enum OMRRoundSpecialFlow
{
	None,
	HighNoon
}

/// <summary>
/// Data-driven rules for one OMR round modifier.
///
/// The profile is intentionally declarative. It does not mutate source weapon
/// definitions or player prefabs. RoundManager/PlayerState resolve it at round
/// start, apply the temporary runtime state, then restore defaults at round end.
/// </summary>
[AssetType(
	Name = "OMR Round Rule",
	Extension = "omrrule",
	Category = "One More Round/Rounds"
)]
public sealed partial class OMRRoundRuleProfile : GameResource
{
	[Group( "Identity" )]
	public RoundModifier Modifier { get; set; } =
		RoundModifier.Standard;

	[Group( "Identity" )]
	public string DisplayName { get; set; } =
		"STANDARD";

	[Group( "Identity" )]
	public string Description { get; set; } =
		"Standard arena rules.";

	[Group( "Modifier Offer" ), Title( "Offer In Modifier Decision" )]
	[Description( "If enabled, this rule can appear as one of the loser's ONE MORE ROUND modifier choices." )]
	public bool OfferInModifierDecision { get; set; } =
		true;

	[Group( "Arena" )]
	[Description( "Arena layout category requested for this round. Standard is used by normal modular rules; HighNoon requests the empty duel layout." )]
	public ArenaLayoutUsage ArenaUsage { get; set; } =
		ArenaLayoutUsage.Standard;

	[Group( "Weapon Selection" )]
	public OMRRoundWeaponPolicy WeaponPolicy { get; set; } =
		OMRRoundWeaponPolicy.CompetitiveShuffle;

	[Group( "Weapon Selection" )]
	public PrefabFile FixedWeapon { get; set; }

	[Group( "Weapon Selection" )]
	[Description( "Exact weapon IDs allowed by a FilteredShuffle. Matching is case-insensitive. Leave both filters empty to allow the whole competitive pool." )]
	public List<string> AllowedWeaponIds { get; set; } =
		new();

	[Group( "Weapon Selection" )]
	[Description( "Weapon archetypes allowed by a FilteredShuffle. A weapon is eligible when it matches either an allowed ID or an allowed archetype." )]
	public List<OMRWeaponArchetype> AllowedArchetypes { get; set; } =
		new();

	[Group( "Player Rules" ), Range( 0.05f, 3f )]
	[Description( "Temporary owner-side Rigidbody gravity scale while this round is active. 1 = normal gravity." )]
	public float GravityScale { get; set; } =
		1f;

	[Group( "Player Rules" ), Range( 0, 3 )]
	[Description( "Additional jumps available after leaving the ground. 0 = standard single jump; 1 = double jump." )]
	public int AdditionalAirJumps { get; set; } =
		0;

	[Group( "Weapon Rules" )]
	[Description( "If disabled, owner-side ADS input is suppressed for the duration of this round without mutating the weapon definition." )]
	public bool AllowAim { get; set; } =
		true;

	[Group( "Round Timing" ), Range( 0f, 120f )]
	[Description( "Optional fight-duration override in seconds. 0 keeps RoundManager's normal Fight Duration." )]
	public float FightDurationOverride { get; set; } =
		0f;

	[Group( "Damage Rules" )]
	[Description( "When enabled, weapon hits outside the resolved head hitgroup deal no damage." )]
	public bool HeadshotOnly { get; set; } =
		false;

	[Group( "Weapon Runtime Modifiers" )]
	[Description( "Optional reusable .omrmod assets applied only to weapons in this round. They are removed without clearing attachment/perk modifiers." )]
	public List<OMRWeaponModifierDefinition> WeaponModifiers { get; set; } =
		new();

	[Group( "Special Flow" )]
	public OMRRoundSpecialFlow SpecialFlow { get; set; } =
		OMRRoundSpecialFlow.None;

	public string GetDisplayName()
	{
		if ( !string.IsNullOrWhiteSpace( DisplayName ) )
			return DisplayName.Trim();

		return Modifier.ToString().ToUpperInvariant();
	}

	public string GetDescription()
	{
		return string.IsNullOrWhiteSpace( Description )
			? "Special round rules."
			: Description.Trim();
	}

	/// <summary>
	/// Resolve an authored fixed PrefabFile into the loaded prefab template used
	/// by BaseInventoryComponent.Pickup. GameResources should store PrefabFile
	/// rather than scene GameObject references.
	/// </summary>
	public GameObject ResolveFixedWeaponPrefab()
	{
		if (
			FixedWeapon is null ||
			!FixedWeapon.IsValid
		)
			return null;

		string path =
			FixedWeapon.ResourcePath;

		GameObject template =
			string.IsNullOrWhiteSpace( path )
				? null
				: GameObject.GetPrefab( path );

		if ( template is null || !template.IsValid )
		{
			template =
				SceneUtility.GetPrefabScene(
					FixedWeapon
				);
		}

		return
			template is not null &&
			template.IsValid
				? template
				: null;
	}

	/// <summary>
	/// Returns whether one competitive prefab belongs to this profile's filtered
	/// pool. Empty filters intentionally mean "all competitive weapons" so a typo
	/// in authoring cannot silently create a round with no weapon at all.
	/// </summary>
	public bool AllowsWeapon(
		GameObject prefab
	)
	{
		if ( WeaponPolicy != OMRRoundWeaponPolicy.FilteredShuffle )
			return true;

		bool hasIdFilters =
			AllowedWeaponIds is not null &&
			AllowedWeaponIds.Exists(
				x => !string.IsNullOrWhiteSpace( x )
			);

		bool hasArchetypeFilters =
			AllowedArchetypes is not null &&
			AllowedArchetypes.Count > 0;

		if ( !hasIdFilters && !hasArchetypeFilters )
			return true;

		if ( hasIdFilters )
		{
			string id =
				OMRWeaponUtility.GetWeaponId(
					prefab
				);

			foreach ( string allowedId in AllowedWeaponIds )
			{
				if ( string.IsNullOrWhiteSpace( allowedId ) )
					continue;

				if (
					string.Equals(
						id,
						allowedId.Trim(),
						StringComparison.OrdinalIgnoreCase
					)
				)
				{
					return true;
				}
			}
		}

		if ( hasArchetypeFilters )
		{
			foreach ( OMRWeaponArchetype archetype in AllowedArchetypes )
			{
				if (
					OMRWeaponUtility.MatchesArchetype(
						prefab,
						archetype
					)
				)
				{
					return true;
				}
			}
		}

		return false;
	}
}