World/Stage.cs
using System;

// The building blocks of every place in the game. They're plain GameObjects in scenes/arena.scene under
// "World", so you can move, add or delete them in the s&box editor and see the result straight away.

/// <summary>
/// A flat S&amp;S2 picture standing in the world, lit by the scene's lights. Height follows the picture.
/// </summary>
[Title( "Cutout" ), Category( "Shoes and Daggers" ), Icon( "image" )]
public sealed class Cutout : Component, Component.ExecuteInEditor
{
	/// <summary>Picture under Assets/Resources/sad/, e.g. ss/p/2102.png</summary>
	[Property] public string Image { get; set; } = "ss/p/2102.png";
	/// <summary>Width in world units (a gladiator is about 70 tall).</summary>
	[Property] public float Width { get; set; } = 200f;
	[Property] public Color Tint { get; set; } = Color.White;
	/// <summary>Lit by the scene's lights (off = always full brightness, e.g. the sky).</summary>
	[Property] public bool Lit { get; set; } = true;
	[Property] public bool CastShadows { get; set; }
	/// <summary>Glows: added on top, unlit (fire, sun, stained glass light).</summary>
	[Property] public bool Glow { get; set; }
	/// <summary>The object's position is the bottom edge (stands on the floor) instead of the middle.</summary>
	[Property] public bool StandOnBottom { get; set; } = true;
	[Property] public bool FlipX { get; set; }
	/// <summary>Turn the picture on the spot (degrees, clockwise), e.g. for chains.</summary>
	[Property] public float Spin { get; set; }

	GameObject spriteGo;
	SpriteRenderer sr;
	int built;

	int Key => HashCode.Combine( HashCode.Combine( Image, Width, Tint, Lit, CastShadows, Glow, StandOnBottom ), FlipX, Spin, World3D.TurnSprites );

	protected override void OnEnabled() => Rebuild();

	protected override void OnDisabled()
	{
		if ( spriteGo.IsValid() ) spriteGo.Destroy();
		spriteGo = null;
		sr = null;
	}

	protected override void OnUpdate()
	{
		if ( Key != built || !spriteGo.IsValid() ) Rebuild();
	}

	void Rebuild()
	{
		built = Key;
		if ( !spriteGo.IsValid() )
		{
			spriteGo = new GameObject( GameObject, true, "picture" );
			spriteGo.Flags |= GameObjectFlags.NotSaved;
			sr = spriteGo.AddComponent<SpriteRenderer>();
		}

		var tex = string.IsNullOrEmpty( Image ) ? null : SpritePuppet.Tex( Image );
		if ( tex is null || tex.Width <= 0 )
		{
			sr.Enabled = false;
			return;
		}

		float h = Width * tex.Height / (float)tex.Width;
		sr.Enabled = true;
		sr.Sprite = SpritePuppet.SpriteFor( Image );
		sr.Size = new Vector2( Width, h );
		sr.Billboard = SpriteRenderer.BillboardMode.None;
		sr.Lighting = Lit && !Glow;
		sr.Additive = Glow;
		sr.Shadows = CastShadows && !Glow;
		sr.Opaque = !Glow;
		sr.AlphaCutoff = 0.5f;
		sr.Color = Tint;
		sr.FlipHorizontal = FlipX;
		spriteGo.LocalPosition = StandOnBottom ? Vector3.Up * (h * 0.5f) : Vector3.Zero;
		var face = World3D.TurnSprites ? Rotation.FromYaw( 180 ) : Rotation.Identity;
		spriteGo.LocalRotation = face * Rotation.FromAxis( Vector3.Forward, World3D.TurnSprites ? Spin : -Spin );
	}
}

/// <summary>A plain lit box: floors, ledges, steps. The object's position is the middle of the TOP face.</summary>
[Title( "Box" ), Category( "Shoes and Daggers" ), Icon( "check_box_outline_blank" )]
public sealed class StageBox : Component, Component.ExecuteInEditor
{
	[Property] public Vector3 BoxSize { get; set; } = new( 400, 1200, 20 );
	[Property] public Color Tint { get; set; } = new( 0.85f, 0.7f, 0.46f );

	GameObject boxGo;
	ModelRenderer mr;
	int built;

	int Key => HashCode.Combine( BoxSize, Tint );

	protected override void OnEnabled() => Rebuild();

	protected override void OnDisabled()
	{
		if ( boxGo.IsValid() ) boxGo.Destroy();
		boxGo = null;
	}

	protected override void OnUpdate()
	{
		if ( Key != built || !boxGo.IsValid() ) Rebuild();
	}

	void Rebuild()
	{
		built = Key;
		if ( !boxGo.IsValid() )
		{
			boxGo = new GameObject( GameObject, true, "box" );
			boxGo.Flags |= GameObjectFlags.NotSaved;
			mr = boxGo.AddComponent<ModelRenderer>();
			mr.Model = Model.Cube;
		}

		var b = Model.Cube.Bounds.Size;
		boxGo.LocalPosition = new Vector3( 0, 0, -BoxSize.z * 0.5f );
		boxGo.LocalRotation = Rotation.Identity;
		boxGo.LocalScale = new Vector3( BoxSize.x / MathF.Max( b.x, 0.01f ), BoxSize.y / MathF.Max( b.y, 0.01f ), BoxSize.z / MathF.Max( b.z, 0.01f ) );
		mr.Tint = Tint;
	}
}

public enum StageLightKind { Sun, Point, Spot, Ambient }

/// <summary>A light for a place. Sun and Spot shine along the object's forward (blue arrow).</summary>
[Title( "Stage Light" ), Category( "Shoes and Daggers" ), Icon( "light_mode" )]
public sealed class StageLight : Component, Component.ExecuteInEditor
{
	[Property] public StageLightKind Kind { get; set; } = StageLightKind.Point;
	[Property] public Color Colour { get; set; } = Color.White;
	[Property] public float Brightness { get; set; } = 1f;
	/// <summary>How far a point/spot light reaches.</summary>
	[Property] public float Radius { get; set; } = 400f;
	[Property] public bool Shadows { get; set; } = true;
	/// <summary>Torch/candle flicker.</summary>
	[Property] public bool Flicker { get; set; }
	/// <summary>Sun only: the colour the sky adds to shadows.</summary>
	[Property] public Color SkyColour { get; set; } = new( 0.35f, 0.45f, 0.6f );

	GameObject lightGo;
	int built;

	int Key => HashCode.Combine( Kind, Colour, Brightness, Radius, Shadows, Flicker, SkyColour );

	protected override void OnEnabled() => Rebuild();

	protected override void OnDisabled()
	{
		if ( lightGo.IsValid() ) lightGo.Destroy();
		lightGo = null;
	}

	protected override void OnUpdate()
	{
		if ( Key != built || !lightGo.IsValid() ) Rebuild();
	}

	void Rebuild()
	{
		built = Key;
		if ( lightGo.IsValid() ) lightGo.Destroy();
		lightGo = new GameObject( GameObject, true, "light" );
		lightGo.Flags |= GameObjectFlags.NotSaved;

		switch ( Kind )
		{
			case StageLightKind.Sun:
				{
					var l = lightGo.AddComponent<DirectionalLight>();
					l.LightColor = Colour;
					l.Intensity = Brightness;
					l.SkyColor = SkyColour;
					l.Shadows = Shadows;
					break;
				}
			case StageLightKind.Point:
				{
					var l = lightGo.AddComponent<PointLight>();
					l.LightColor = Colour;
					l.Intensity = Brightness;
					l.Radius = Radius;
					l.Shadows = Shadows;
					if ( Flicker )
					{
						l.AnimationEnabled = true;
						l.AnimationPattern = LightAnimationPattern.Flicker;
					}
					break;
				}
			case StageLightKind.Spot:
				{
					var l = lightGo.AddComponent<SpotLight>();
					l.LightColor = Colour;
					l.Intensity = Brightness;
					l.Radius = Radius;
					l.Shadows = Shadows;
					if ( Flicker )
					{
						l.AnimationEnabled = true;
						l.AnimationPattern = LightAnimationPattern.Candle;
					}
					break;
				}
			default:
				{
					var a = lightGo.AddComponent<AmbientLight>();
					a.Color = new Color( Colour.r * Brightness, Colour.g * Brightness, Colour.b * Brightness );
					break;
				}
		}
	}
}

/// <summary>
/// One place in the game (the town, a shop, the dungeon, the arena...). Only the place for the current
/// screen is switched on. Put StageSpots inside it for the camera and where people stand.
/// </summary>
[Title( "Stage Set" ), Category( "Shoes and Daggers" ), Icon( "theaters" )]
public sealed class StageSet : Component
{
	/// <summary>Which screens show this place, comma separated: Title, Create, Intro, Town, Weaponsmith,
	/// Armoury, Magic, Church, Stats, Fitting, Arena (the arena fights), Duel (the dungeon fights).</summary>
	[Property] public string Screens { get; set; } = "Town";
	/// <summary>Colour behind everything (where no picture covers the view).</summary>
	[Property] public Color Background { get; set; } = Color.Black;

	public bool Shows( string screen ) =>
		(Screens ?? "").Split( ',' ).Any( x => x.Trim().Equals( screen, StringComparison.OrdinalIgnoreCase ) );

	public StageSpot Spot( string role ) =>
		GameObject.GetComponentsInChildren<StageSpot>( true, true ).FirstOrDefault( x => string.Equals( x.Role, role, StringComparison.OrdinalIgnoreCase ) );

	public IEnumerable<StageHotspot> Hotspots => GameObject.GetComponentsInChildren<StageHotspot>( true, true );

	/// <summary>Switches everything in the place on or off (the set itself stays on so it can be found).</summary>
	public void Show( bool on )
	{
		foreach ( var child in GameObject.Children )
			if ( child.Enabled != on ) child.Enabled = on;
	}
}

/// <summary>
/// A named spot in a place. Roles: "Camera" (or "Camera:Title", "Camera:Versus"... for one screen),
/// "Player", "Hero", "Chained", "Fight" (middle of the fighting ground).
/// Camera spots look along their forward (blue arrow).
/// </summary>
[Title( "Stage Spot" ), Category( "Shoes and Daggers" ), Icon( "place" )]
public sealed class StageSpot : Component
{
	[Property] public string Role { get; set; } = "Camera";
	/// <summary>Camera spots: field of view (degrees, across the screen).</summary>
	[Property] public float FieldOfView { get; set; } = 60f;
	/// <summary>People spots: how big the gladiator is drawn (1 = normal).</summary>
	[Property] public float Size { get; set; } = 1f;
}

/// <summary>A clickable label in the town that takes you somewhere (sits where this object is).</summary>
[Title( "Stage Hotspot" ), Category( "Shoes and Daggers" ), Icon( "ads_click" )]
public sealed class StageHotspot : Component
{
	[Property] public GameScreen Target { get; set; } = GameScreen.Weaponsmith;
	[Property] public string Label { get; set; } = "Weaponsmith";
}