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