OMRCombatTuning.cs
using Sandbox;
using System;
// Kept for backward compatibility with Revision 10 resources. The new damage
// feedback presentation no longer uses coarse Gradient/Solid edge panels.
public enum OMRDamageIndicatorStyle
{
Gradient,
Solid
}
/// <summary>
/// Live-edit friendly gameplay/presentation tuning for combat.
///
/// Keep values that designers are expected to iterate on here instead of on
/// the spawned network player prefab. Editing the network prefab itself while
/// a multiplayer session is active can rebuild/refresh its hierarchy and break
/// ownership; editing this resource does not change the network object shape.
/// </summary>
[AssetType(
Name = "OMR Combat Tuning",
Extension = "omrcbt",
Category = "One More Round/Gameplay"
)]
public sealed partial class OMRCombatTuning : GameResource
{
// =========================================================
// HEALTH / DAMAGE
// =========================================================
[Group( "Health / Damage" ), Range( 1f, 500f ), Title( "Maximum Health" )]
public float MaxHealth { get; set; } = 180f;
[Group( "Health / Damage" ), Range( 1f, 3f ), Title( "Headshot Damage Multiplier" )]
public float HeadshotDamageMultiplier { get; set; } = 1.5f;
[Group( "Health / Damage" ), Title( "Enable Headshot Height Fallback" )]
public bool EnableHeadshotHeightFallback { get; set; } = true;
[Group( "Health / Damage" ), Range( 0.60f, 0.95f ), Title( "Headshot Height Fraction" )]
public float HeadshotHeightFraction { get; set; } = 0.78f;
// =========================================================
// HIT CONFIRMATION
// =========================================================
[Group( "Hit Confirmation" ), Title( "Show Hitmarker" )]
public bool ShowHitmarker { get; set; } = true;
[Group( "Hit Confirmation" ), Range( 0.03f, 0.5f ), Title( "Hitmarker Duration" )]
public float HitmarkerDuration { get; set; } = 0.16f;
[Group( "Hit Confirmation" ), Range( 0.03f, 0.75f ), Title( "Kill Hitmarker Duration" )]
public float KillHitmarkerDuration { get; set; } = 0.26f;
[Group( "Hit Confirmation" ), Range( 0f, 1f ), Title( "Maximum Opacity" )]
public float HitmarkerMaxOpacity { get; set; } = 1f;
[Group( "Hit Confirmation" ), Range( 20f, 80f ), Title( "Overall Size" )]
public float HitmarkerSize { get; set; } = 38f;
[Group( "Hit Confirmation" ), Range( 4f, 24f ), Title( "Line Length" )]
public float HitmarkerLineLength { get; set; } = 11f;
[Group( "Hit Confirmation" ), Range( 1f, 6f ), Title( "Line Thickness" )]
public float HitmarkerLineThickness { get; set; } = 2f;
[Group( "Hit Confirmation" ), Range( 0f, 14f ), Title( "Center Gap" )]
public float HitmarkerCenterGap { get; set; } = 4f;
[Group( "Hit Confirmation" ), Range( 0.75f, 1.75f ), Title( "Headshot Scale" )]
public float HeadshotHitmarkerScale { get; set; } = 1.08f;
[Group( "Hit Confirmation" ), Range( 0.75f, 2f ), Title( "Kill Scale" )]
public float KillHitmarkerScale { get; set; } = 1.18f;
[Group( "Hit Confirmation" ), Title( "Override Theme Colors" )]
public bool OverrideHitmarkerColors { get; set; } = false;
[Group( "Hit Confirmation" ), ShowIf( nameof( OverrideHitmarkerColors ), true ), Title( "Normal Hit Color" )]
[ColorUsage( true, false )]
public Color HitmarkerColor { get; set; } = Color.White;
[Group( "Hit Confirmation" ), ShowIf( nameof( OverrideHitmarkerColors ), true ), Title( "Headshot Color" )]
[ColorUsage( true, false )]
public Color HeadshotHitmarkerColor { get; set; } = new Color( 0.68f, 0.78f, 1f, 1f );
[Group( "Hit Confirmation" ), ShowIf( nameof( OverrideHitmarkerColors ), true ), Title( "Kill Color" )]
[ColorUsage( true, false )]
public Color KillHitmarkerColor { get; set; } = new Color( 1f, 0.49f, 0.45f, 1f );
// =========================================================
// DAMAGE INDICATOR - TIMING / INTENSITY
// =========================================================
[Group( "Damage Indicator - General" ), Title( "Show Damage Indicator" )]
public bool ShowDamageIndicator { get; set; } = true;
[Group( "Damage Indicator - General" ), Range( 0.08f, 1.25f ), Title( "Duration" )]
public float DamageIndicatorDuration { get; set; } = 0.42f;
[Group( "Damage Indicator - General" ), Range( 0f, 0.75f ), Title( "Hold Before Fade (%)" )]
public float DamageIndicatorHoldFraction { get; set; } = 0.24f;
[Group( "Damage Indicator - General" ), Range( 0f, 1f ), Title( "Minimum Visual Strength" )]
public float MinimumDamageIndicatorStrength { get; set; } = 0.65f;
[Group( "Damage Indicator - General" ), Range( 0f, 1f ), Title( "Damage Amount Influence" )]
public float DamageIndicatorDamageInfluence { get; set; } = 0.35f;
[Group( "Damage Indicator - General" ), Title( "Override Theme Danger Color" )]
public bool OverrideDamageIndicatorColor { get; set; } = false;
[Group( "Damage Indicator - General" ), ShowIf( nameof( OverrideDamageIndicatorColor ), true ), Title( "Damage Indicator Color" )]
[ColorUsage( true, false )]
public Color DamageIndicatorColor { get; set; } = new Color( 1f, 0.20f, 0.16f, 1f );
// =========================================================
// DAMAGE INDICATOR - DIRECTIONAL ARC
// =========================================================
//
// Revision 16 deliberately does NOT depend on a raw PNG for the default
// damage arc. Raw images are not automatically shipped to connected game
// instances unless they are listed under Project Settings -> Other ->
// Resource Files. The default arc is therefore generated once from SVG
// source on each client and cached as a Texture. A custom compiled Texture
// can still be supplied when a designer wants a bespoke shape.
[Group( "Damage Indicator - Directional Arc" ), Title( "Show Directional Arc" )]
public bool ShowDirectionalDamageArc { get; set; } = true;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDirectionalDamageArc ), true ), Title( "Use Custom Arc Texture" )]
public bool UseCustomDamageIndicatorArcTexture { get; set; } = false;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( UseCustomDamageIndicatorArcTexture ), true ), Title( "Custom Arc Texture" )]
public Texture CustomDamageIndicatorArcTexture { get; set; }
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( UseCustomDamageIndicatorArcTexture ), false ), Range( 45f, 170f ), Title( "Procedural Arc Span" )]
public float ProceduralDamageArcSpanDegrees { get; set; } = 100f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( UseCustomDamageIndicatorArcTexture ), false ), Range( 20f, 46f ), Title( "Procedural Arc Curve Radius (%)" )]
public float ProceduralDamageArcRadiusPercent { get; set; } = 36f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( UseCustomDamageIndicatorArcTexture ), false ), Range( 1.5f, 12f ), Title( "Procedural Arc Thickness (%)" )]
public float ProceduralDamageArcThicknessPercent { get; set; } = 5.5f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( UseCustomDamageIndicatorArcTexture ), false ), Range( 0f, 1f ), Title( "Procedural Edge Softness" )]
public float ProceduralDamageArcEdgeSoftness { get; set; } = 0.70f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDirectionalDamageArc ), true ), Range( 220f, 900f ), Title( "Arc Diameter" )]
public float DamageIndicatorArcDiameter { get; set; } = 520f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDirectionalDamageArc ), true ), Range( -180f, 320f ), Title( "Arc Radial Offset" )]
public float DamageIndicatorArcRadialOffset { get; set; } = 0f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDirectionalDamageArc ), true ), Range( 0f, 1f ), Title( "Arc Peak Opacity" )]
public float DamageIndicatorArcOpacity { get; set; } = 0.90f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDirectionalDamageArc ), true ), Range( 1f, 1.35f ), Title( "Impact Scale" )]
public float DamageIndicatorArcImpactScale { get; set; } = 1.06f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDirectionalDamageArc ), true ), Range( -180f, 180f ), Title( "Rotation Offset" )]
public float DamageIndicatorArcRotationOffset { get; set; } = 0f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDirectionalDamageArc ), true ), Title( "Show Arc Glow" )]
public bool ShowDamageIndicatorArcGlow { get; set; } = true;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDamageIndicatorArcGlow ), true ), Range( 0f, 1f ), Title( "Glow Opacity" )]
public float DamageIndicatorArcGlowOpacity { get; set; } = 0.34f;
[Group( "Damage Indicator - Directional Arc" ), ShowIf( nameof( ShowDamageIndicatorArcGlow ), true ), Range( 0f, 20f ), Title( "Glow Blur" )]
public float DamageIndicatorArcGlowBlur { get; set; } = 5f;
// Revision 11-15 compatibility only. Raw source-image paths are intentionally
// ignored by the runtime now; keeping the fields allows older .omrcbt files
// to deserialize without errors.
[Hide]
public string DamageIndicatorArcImagePath { get; set; } = "";
[Hide]
public Texture DamageIndicatorArcTexture { get; set; }
// =========================================================
// DAMAGE INDICATOR - IMPACT VIGNETTE
// =========================================================
[Group( "Damage Indicator - Impact Vignette" ), Title( "Show Impact Vignette" )]
public bool ShowDamageImpactVignette { get; set; } = true;
[Group( "Damage Indicator - Impact Vignette" ), ShowIf( nameof( ShowDamageImpactVignette ), true ), Range( 0f, 0.7f ), Title( "Vignette Peak Opacity" )]
public float DamageImpactVignetteOpacity { get; set; } = 0.13f;
// =========================================================
// DAMAGE INDICATOR - LEGACY REVISION 10 DATA
// =========================================================
// Keep these serialized so an older default.omrcbt can still be loaded and
// hot-replaced without asset parse failures. Revision 11 no longer consumes
// them; the new arc/vignette presentation above owns the look.
[Hide]
public OMRDamageIndicatorStyle DamageIndicatorStyle { get; set; } = OMRDamageIndicatorStyle.Gradient;
[Hide]
public float DamageIndicatorMaxOpacity { get; set; } = 0.22f;
[Hide]
public float DamageIndicatorCoveragePercent { get; set; } = 34f;
[Hide]
public float FrontDamageOpacityScale { get; set; } = 0.68f;
[Hide]
public float SideDamageOpacityScale { get; set; } = 1f;
[Hide]
public float VerticalDamageOpacityScale { get; set; } = 0.92f;
[Hide]
public bool ShowDamageIndicatorRim { get; set; } = true;
[Hide]
public float DamageIndicatorRimThickness { get; set; } = 3f;
[Hide]
public float DamageIndicatorRimOpacity { get; set; } = 0.55f;
[Hide]
public float AboveDirectionThreshold { get; set; } = 0.42f;
[Hide]
public float FrontDirectionThreshold { get; set; } = 0.58f;
[Hide]
public float BackDirectionThreshold { get; set; } = -0.48f;
// =========================================================
// DIAGNOSTICS
// =========================================================
[Group( "Diagnostics" ), Title( "Log Damage Resolution" )]
public bool LogDamageResolution { get; set; } = false;
[Group( "Diagnostics" ), Title( "Log Combat Feedback Network" )]
public bool LogCombatFeedbackRpc { get; set; } = false;
private const int ProceduralArcTextureResolution = 512;
private Texture _cachedProceduralArcTexture;
private int _cachedProceduralArcHash;
/// <summary>
/// Returns the texture used by the directional damage cue.
///
/// The default texture is generated locally from vector/SVG source and is
/// therefore available to host, client instances and published builds
/// without requiring a loose PNG to be distributed. Custom textures are
/// optional; an invalid/missing custom texture automatically falls back to
/// the generated arc instead of making the cue disappear.
/// </summary>
public Texture GetDamageIndicatorArcTexture()
{
if (
UseCustomDamageIndicatorArcTexture &&
CustomDamageIndicatorArcTexture is not null &&
CustomDamageIndicatorArcTexture.IsValid &&
!CustomDamageIndicatorArcTexture.IsError
)
{
return CustomDamageIndicatorArcTexture;
}
return GetProceduralDamageIndicatorArcTexture();
}
private Texture GetProceduralDamageIndicatorArcTexture()
{
int shapeHash =
HashCode.Combine(
MathF.Round( ProceduralDamageArcSpanDegrees, 2 ),
MathF.Round( ProceduralDamageArcRadiusPercent, 2 ),
MathF.Round( ProceduralDamageArcThicknessPercent, 2 ),
MathF.Round( ProceduralDamageArcEdgeSoftness, 3 )
);
if (
_cachedProceduralArcTexture is not null &&
_cachedProceduralArcTexture.IsValid &&
!_cachedProceduralArcTexture.IsError &&
_cachedProceduralArcHash == shapeHash
)
{
return _cachedProceduralArcTexture;
}
string svg =
BuildProceduralDamageArcSvg();
Texture generated =
Texture.CreateFromSvgSource(
svg,
ProceduralArcTextureResolution,
ProceduralArcTextureResolution,
Color.White
);
if (
generated is null ||
!generated.IsValid ||
generated.IsError
)
{
// Keep the failure quiet and deterministic. The HUD can continue
// rendering all other feedback, and the caller never starts a loop
// of Texture.Load() attempts against a missing loose file.
return Texture.Transparent;
}
_cachedProceduralArcTexture =
generated;
_cachedProceduralArcHash =
shapeHash;
return _cachedProceduralArcTexture;
}
private string BuildProceduralDamageArcSvg()
{
const float size =
ProceduralArcTextureResolution;
const float center =
size * 0.5f;
float radius =
size *
MathX.Clamp(
ProceduralDamageArcRadiusPercent / 100f,
0.20f,
0.46f
);
float span =
MathX.Clamp(
ProceduralDamageArcSpanDegrees,
45f,
170f
);
float halfSpanRadians =
(span * 0.5f) *
(MathF.PI / 180f);
float startX =
center - MathF.Sin( halfSpanRadians ) * radius;
float startY =
center - MathF.Cos( halfSpanRadians ) * radius;
float endX =
center + MathF.Sin( halfSpanRadians ) * radius;
float endY =
startY;
float coreWidth =
size *
MathX.Clamp(
ProceduralDamageArcThicknessPercent / 100f,
0.015f,
0.12f
);
float softness =
MathX.Clamp(
ProceduralDamageArcEdgeSoftness,
0f,
1f
);
// Feathering is built from two wider, progressively fainter vector
// strokes around the solid core. This reproduces the soft PNG look while
// staying resolution-independent and fully local to each game instance.
float softWidth =
coreWidth * (1f + 1.10f * softness);
float outerWidth =
coreWidth * (1f + 2.25f * softness);
float softOpacity =
0.24f * softness;
float outerOpacity =
0.10f * softness;
string path =
$"M {SvgNumber( startX )} {SvgNumber( startY )} " +
$"A {SvgNumber( radius )} {SvgNumber( radius )} 0 0 1 " +
$"{SvgNumber( endX )} {SvgNumber( endY )}";
return
$"<svg xmlns=\"http://www.w3.org/2000/svg\" " +
$"width=\"{ProceduralArcTextureResolution}\" " +
$"height=\"{ProceduralArcTextureResolution}\" " +
$"viewBox=\"0 0 {ProceduralArcTextureResolution} {ProceduralArcTextureResolution}\">" +
$"<path d=\"{path}\" fill=\"none\" stroke=\"#ffffff\" " +
$"stroke-width=\"{SvgNumber( outerWidth )}\" " +
$"stroke-opacity=\"{SvgNumber( outerOpacity )}\" stroke-linecap=\"round\"/>" +
$"<path d=\"{path}\" fill=\"none\" stroke=\"#ffffff\" " +
$"stroke-width=\"{SvgNumber( softWidth )}\" " +
$"stroke-opacity=\"{SvgNumber( softOpacity )}\" stroke-linecap=\"round\"/>" +
$"<path d=\"{path}\" fill=\"none\" stroke=\"#ffffff\" " +
$"stroke-width=\"{SvgNumber( coreWidth )}\" " +
$"stroke-opacity=\"1\" stroke-linecap=\"round\"/>" +
$"</svg>";
}
private static string SvgNumber( float value )
{
// SVG requires "." as the decimal separator regardless of the operating
// system locale.
return value.ToString( "0.###", System.Globalization.CultureInfo.InvariantCulture );
}
public int ContentHash()
{
HashCode hash = new();
hash.Add( MaxHealth );
hash.Add( HeadshotDamageMultiplier );
hash.Add( EnableHeadshotHeightFallback );
hash.Add( HeadshotHeightFraction );
hash.Add( ShowHitmarker );
hash.Add( HitmarkerDuration );
hash.Add( KillHitmarkerDuration );
hash.Add( HitmarkerMaxOpacity );
hash.Add( HitmarkerSize );
hash.Add( HitmarkerLineLength );
hash.Add( HitmarkerLineThickness );
hash.Add( HitmarkerCenterGap );
hash.Add( HeadshotHitmarkerScale );
hash.Add( KillHitmarkerScale );
hash.Add( OverrideHitmarkerColors );
hash.Add( HitmarkerColor );
hash.Add( HeadshotHitmarkerColor );
hash.Add( KillHitmarkerColor );
hash.Add( ShowDamageIndicator );
hash.Add( DamageIndicatorDuration );
hash.Add( DamageIndicatorHoldFraction );
hash.Add( MinimumDamageIndicatorStrength );
hash.Add( DamageIndicatorDamageInfluence );
hash.Add( OverrideDamageIndicatorColor );
hash.Add( DamageIndicatorColor );
hash.Add( ShowDirectionalDamageArc );
hash.Add( UseCustomDamageIndicatorArcTexture );
hash.Add( CustomDamageIndicatorArcTexture );
hash.Add( ProceduralDamageArcSpanDegrees );
hash.Add( ProceduralDamageArcRadiusPercent );
hash.Add( ProceduralDamageArcThicknessPercent );
hash.Add( ProceduralDamageArcEdgeSoftness );
hash.Add( DamageIndicatorArcDiameter );
hash.Add( DamageIndicatorArcRadialOffset );
hash.Add( DamageIndicatorArcOpacity );
hash.Add( DamageIndicatorArcImpactScale );
hash.Add( DamageIndicatorArcRotationOffset );
hash.Add( ShowDamageIndicatorArcGlow );
hash.Add( DamageIndicatorArcGlowOpacity );
hash.Add( DamageIndicatorArcGlowBlur );
hash.Add( ShowDamageImpactVignette );
hash.Add( DamageImpactVignetteOpacity );
hash.Add( LogDamageResolution );
hash.Add( LogCombatFeedbackRpc );
return hash.ToHashCode();
}
}
/// <summary>
/// Central fallback path for combat tuning.
/// </summary>
public static class OMRCombatResources
{
public const string DefaultTuningPath =
"gameplay/default.omrcbt";
public static OMRCombatTuning DefaultTuning =>
ResourceLibrary.Get<OMRCombatTuning>( DefaultTuningPath );
}