Core/RuinVisualFactory.cs
namespace Prototype;

public static class RuinVisualFactory
{
	private static readonly Color Basalt = new( .12f, .16f, .18f );
	private static readonly Color Bronze = new( .42f, .29f, .14f );

	public static bool UseImportedModel( GameObject root, string assetPath, Color tint, float scale = 1f )
	{
		var model = Model.Load( assetPath );
		if ( model is null ) return false;
		var renderer = root.Components.GetOrCreate<ModelRenderer>();
		renderer.Model = model;
		var metallic = assetPath.Contains( "relic_" ) || assetPath.Contains( "chest" ) || assetPath.Contains( "portal" );
		renderer.MaterialOverride = Material.Load( metallic ? "materials/prototype/relic_metal.vmat" : "materials/prototype/ruin_stone.vmat" )
			?? Material.Load( "materials/dev/primary_white.vmat" );
		renderer.Tint = Color.Lerp( tint, Color.White, .42f );
		root.WorldScale = new Vector3( scale );
		return true;
	}

	private static GameObject CreateProductionModel( string name, string assetPath, Vector3 position, Rotation rotation, Vector3 scale, GameObject parent )
	{
		var visual = new GameObject( true, name );
		visual.WorldPosition = position;
		visual.WorldRotation = rotation;
		visual.WorldScale = scale;
		visual.SetParent( parent, true );
		var renderer = visual.Components.Create<ModelRenderer>();
		renderer.Model = Model.Load( assetPath );
		renderer.MaterialOverride = null;
		renderer.Tint = Color.White;
		return visual;
	}

	public static bool CreateEnemyModel( GameObject root, EnemyRole role, Color tint, float groundOffset )
	{
		// Animated Blender assets remain quarantined until their ModelDoc bind pose,
		// centimetre conversion and bounds pass in-engine validation. Loading them
		// here previously expanded rig parts across the entire room.
		var file = role switch { EnemyRole.Chaser => "enemy_chaser_v2", EnemyRole.Gunner => "enemy_gunner_v2", EnemyRole.Charger => "enemy_charger_v2", _ => "enemy_boss_v2" };
		var visual = new GameObject( true, $"{role} Visual" );
		visual.WorldPosition = root.WorldPosition + Vector3.Down * groundOffset;
		// OBJ source is authored Z-up but ModelDoc's OBJ conversion presents that
		// height axis inverted around pitch in-game. The negative quarter-turn keeps
		// the feet at the root and the torso above the generated floor.
		visual.WorldRotation = root.WorldRotation * Rotation.FromPitch( -90f );
		visual.SetParent( root, true );
		if ( UseImportedModel( visual, $"models/prototype/{file}.vmdl", tint ) )
		{
			var animator = visual.Components.Create<EnemyVisualAnimator>();
			animator.Root = root;
			animator.GroundOffset = groundOffset;
			return true;
		}
		visual.Destroy();
		return false;
	}

	public static void SetWeaponModel( GameObject root, WeaponSpec spec )
	{
		var productionRifle = spec.Id != "scatter";
		if ( productionRifle )
		{
			var productionRenderer = root.Components.GetOrCreate<ModelRenderer>();
			productionRenderer.Model = Model.Load( "models/production/pulse_rifle.vmdl" );
			productionRenderer.MaterialOverride = null;
			productionRenderer.Tint = Color.White;
			root.WorldScale = new Vector3( .76f );
		}
		else
		{
			var shotgun = root.Components.GetOrCreate<ModelRenderer>();
			shotgun.Model = Model.Load( "models/production/scatter.vmdl" );
			shotgun.MaterialOverride = null;
			shotgun.Tint = Color.White;
			root.WorldScale = new Vector3( .76f );
		}
		var renderer = root.Components.Get<ModelRenderer>();
		if ( renderer is not null )
		{
			// A first-person model must not disappear into world geometry or inherit
			// the room's hard shadow volume.
			renderer.RenderOptions.Game = false;
			renderer.RenderOptions.Overlay = true;
		}
		var fill = root.Components.GetOrCreate<PointLight>();
		fill.Radius = 72f;
		fill.LightColor = Color.Lerp( spec.Color, Color.White, .45f ) * .42f;
		fill.Shadows = false;
	}

	public static GameObject CreateHero( GameObject owner, HeroSpec hero, Vector3 origin )
	{
		var root = new GameObject( true, $"{hero.Name} Visual" ); root.WorldPosition = origin; root.SetParent( owner, true );
		var imported = new GameObject( true, $"{hero.Name} Armour" );
		imported.WorldPosition = origin;
		imported.WorldRotation = root.WorldRotation * Rotation.FromPitch( -90f );
		imported.SetParent( root, true );
		if ( UseImportedModel( imported, $"models/prototype/hero_{hero.Id}.vmdl", hero.Color ) ) return root;
		imported.Destroy();
		PrototypeFactory.Box( "Torso", origin + Vector3.Up * 36f, new Vector3( 32f, 24f, 46f ), Basalt, false, root );
		PrototypeFactory.Box( "Core", origin + Vector3.Up * 42f + Vector3.Forward * 13f, new Vector3( 11f, 5f, 14f ), hero.Color, false, root );
		PrototypeFactory.Box( "Head", origin + Vector3.Up * 68f, new Vector3( 20f, 20f, 20f ), Bronze, false, root );
		var shoulder = hero.Id == "warden" ? 19f : 14f;
		PrototypeFactory.Box( "Left Shoulder", origin + Vector3.Up * 52f + Vector3.Left * shoulder, new Vector3( hero.Id == "warden" ? 22f : 13f, 25f, 13f ), hero.Color.Darken( .35f ), false, root );
		PrototypeFactory.Box( "Right Shoulder", origin + Vector3.Up * 52f + Vector3.Right * shoulder, new Vector3( hero.Id == "warden" ? 22f : 13f, 25f, 13f ), hero.Color.Darken( .35f ), false, root );
		PrototypeFactory.Box( "Left Leg", origin + Vector3.Up * 13f + Vector3.Left * 9f, new Vector3( 12f, 14f, 28f ), Basalt, false, root );
		PrototypeFactory.Box( "Right Leg", origin + Vector3.Up * 13f + Vector3.Right * 9f, new Vector3( 12f, 14f, 28f ), Basalt, false, root );
		if ( hero.Id is "seer" or "nomad" ) PrototypeFactory.Box( "Mantle", origin + Vector3.Up * 37f + Vector3.Backward * 14f, new Vector3( 35f, 5f, 48f ), hero.Color.Darken( .55f ), false, root );
		return root;
	}

	public static void DecorateFloor( GameObject floor, Vector3 size, int variant )
	{
		var p = floor.WorldPosition + Vector3.Up * 18f;
		var stone = variant % 2 == 0 ? new Color( .19f, .22f, .25f ) : new Color( .16f, .19f, .23f );
		// Large inset flagstones break the single flat room silhouette without
		// introducing additional collision seams beneath the controller.
		for ( var i = -2; i <= 2; i++ )
		{
			var strip = PrototypeFactory.Box( "Inlaid Flagstone", p + Vector3.Right * i * (size.y / 5f), new Vector3( size.x - 90f, 8f, 3f ), stone, false, floor );
			strip.WorldRotation = Rotation.Identity;
		}
		PrototypeFactory.Disc( "Ruin Floor Seal", p + Vector3.Up * 1f, 118f, 3f, new Color( .18f, .34f, .38f ), floor );
	}

	public static void DecorateWall( GameObject wall, Vector3 size, System.Action<Vector3, Vector3> registerObstacle = null )
	{
		var p = wall.WorldPosition;
		var alongX = size.x > size.y;
		var longAxis = alongX ? Vector3.Forward : Vector3.Left;
		var length = alongX ? size.x : size.y;
		var depth = alongX ? size.y : size.x;
		var capSize = alongX ? new Vector3( length + 18f, depth + 22f, 18f ) : new Vector3( depth + 22f, length + 18f, 18f );
		PrototypeFactory.Box( "Weathered Wall Cap", p + Vector3.Up * (size.z * .5f - 10f), capSize, new Color(.30f,.29f,.27f), false, wall );
		// Split doorway segments are deliberately left without freestanding
		// buttresses, so no decorative column can occupy the entrance lane.
		if ( length < 1000f ) return;
		var count = System.Math.Clamp( (int)(length / 620f), 2, 4 );
		for ( var i = 0; i < count; i++ )
		{
			var t = count == 1 ? 0f : -length * .42f + length * .84f * i / (count - 1f);
			var buttressSize = alongX ? new Vector3( 58f, depth + 34f, size.z * .56f ) : new Vector3( depth + 34f, 58f, size.z * .56f );
			var pier = PrototypeFactory.Box( "Ancient Buttress", p + longAxis * t - Vector3.Up * size.z * .21f, buttressSize, new Color(.17f,.20f,.22f), true, wall );
			registerObstacle?.Invoke( pier.WorldPosition, buttressSize );
			PrototypeFactory.Box( "Buttress Capital", pier.WorldPosition + Vector3.Up * buttressSize.z*.5f, buttressSize.WithZ(18f) * new Vector3(1.35f,1.35f,1f), Bronze.Darken(.2f), false, wall );
		}
	}

	public static void DecorateObstacle( GameObject obstacle, Vector3 size, bool isWall, int variant, System.Action<Vector3, Vector3> registerObstacle = null )
	{
		var p = obstacle.WorldPosition;
		PrototypeFactory.Box( "Broken Stone Cap", p + Vector3.Up * (size.z * .5f + 5f), new Vector3(size.x + 14f,size.y + 14f,10f), new Color(.31f,.29f,.25f), false, obstacle );
		if ( isWall )
		{
			var alongX = size.x > size.y;
			var axis = alongX ? Vector3.Forward : Vector3.Left;
			var end = (alongX ? size.x : size.y) * .38f;
			for ( var sign = -1; sign <= 1; sign += 2 )
            {
                var pierPosition = p + axis * end * sign;
                var pierSize = new Vector3(52f,52f,size.z + 22f);
                PrototypeFactory.Box( "Carved Wall Pier", pierPosition, pierSize, new Color(.20f,.22f,.22f), true, obstacle );
                registerObstacle?.Invoke( pierPosition, pierSize );
            }
		}
		else
		{
			PrototypeFactory.Box( "Relief Band", p + Vector3.Up * (size.z * .12f), new Vector3(size.x + 18f,size.y + 18f,14f), variant % 2 == 0 ? Bronze : new Color(.16f,.32f,.34f), false, obstacle );
		}
	}

	public static void DecorateGate( GameObject gate, Vector3 size )
	{
		var p = gate.WorldPosition;
		var thinX = size.x < size.y;
		var side = thinX ? Vector3.Forward : Vector3.Right;
		var span = thinX ? size.y : size.x;
		var pillarSize = thinX ? new Vector3(82f,74f,size.z + 70f) : new Vector3(74f,82f,size.z + 70f);
		foreach ( var sign in new[] {-1, 1} )
		{
			// The energy slab is the authoritative gate collider. Decorative pillars
			// must never materialize under a player when a room locks behind them.
			PrototypeFactory.Box( "Gate Pillar", p + side * sign * (span * .5f + 34f), pillarSize, new Color(.11f,.14f,.16f), false, gate );
			PrototypeFactory.Box( "Gate Capital", p + side * sign * (span * .5f + 34f) + Vector3.Up * (size.z*.5f+22f), pillarSize.WithZ(34f) * new Vector3(1.35f,1.35f,1f), Bronze, false, gate );
		}
		var lintel = thinX ? new Vector3(74f,span+180f,54f) : new Vector3(span+180f,74f,54f);
		PrototypeFactory.Box( "Gate Lintel", p + Vector3.Up * (size.z*.5f+35f), lintel, new Color(.17f,.18f,.18f), false, gate );
	}

	public static void DecorateBridge( GameObject bridge, Vector3 size )
	{
		// Keep one continuous rendered deck. Separate coplanar slab meshes caused
		// depth-buffer fighting on every camera step.
	}

	public static void CreateSkyBackdrop( GameObject parent, Vector3 center, float radius )
	{
		var material = Material.Load( "materials/prototype/ruins_sky.vmat" );
		if ( material is null ) return;
		void Panel( string name, Vector3 position, Vector3 size )
		{
			var panel = PrototypeFactory.Box( name, position, size, Color.White, false, parent );
			panel.Components.Get<ModelRenderer>().MaterialOverride = material;
		}
		// A single remote vault avoids the obvious boxed-panorama corners and the
		// moving streak walls seen when the player crossed the former side panels.
		Panel( "Storm Sky Vault", center + Vector3.Up * 1850f, new Vector3(radius*2.4f,radius*2.4f,12f) );
	}

	public static void CreateRoomTorches( GameObject parent, Vector3 center, float halfSize, int variant )
	{
		var roomFill = new GameObject( true, "Room Ambient Bounce" );
		roomFill.WorldPosition = center + Vector3.Up * 150f;
		roomFill.SetParent( parent, true );
		var fill = roomFill.Components.Create<PointLight>();
		fill.Radius = halfSize * 1.35f;
		fill.LightColor = (variant % 3) switch
		{
			0 => new Color( .20f, .30f, .46f ) * 1.35f,
			1 => new Color( .29f, .20f, .40f ) * 1.28f,
			_ => new Color( .18f, .36f, .38f ) * 1.25f
		};
		fill.Shadows = false;
		var inset = halfSize - 28f;
		CreateWallTorch( parent, center + new Vector3(-520f, inset, 185f), Rotation.FromYaw(-90f), variant );
		CreateWallTorch( parent, center + new Vector3(520f, -inset, 185f), Rotation.FromYaw(90f), variant + 1 );
		CreateWallTorch( parent, center + new Vector3(inset, 520f, 185f), Rotation.FromYaw(180f), variant + 2 );
		CreateWallTorch( parent, center + new Vector3(-inset, -520f, 185f), Rotation.Identity, variant + 3 );
	}

	private static void CreateWallTorch( GameObject parent, Vector3 position, Rotation rotation, int variant )
	{
		var root = new GameObject( true, "Ancient Wall Torch" );
		root.WorldPosition = position;
		root.WorldRotation = rotation;
		root.SetParent( parent, true );
        root.NetworkMode = NetworkMode.Never;
        var renderer = root.Components.Create<ModelRenderer>();
        renderer.Model = Model.Load("models/production/torch_v001/torch.vmdl");
        root.Components.Create<BrazierFire>().Phase = variant * 1.73f;
	}

	public static void DecorateEnemy( GameObject root, EnemyRole role, Color accent, float scale )
	{
		var origin = root.WorldPosition;
		PrototypeFactory.Box( "Temporary Head", origin + Vector3.Up * 70f * scale, new Vector3(30f,28f,30f) * scale, Bronze.Darken(.18f), false, root );
		PrototypeFactory.Box( "Left Leg", origin + Vector3.Up * 15f * scale + Vector3.Left * 17f * scale, new Vector3( 18f, 24f, 34f ) * scale, Basalt, false, root );
		PrototypeFactory.Box( "Right Leg", origin + Vector3.Up * 15f * scale + Vector3.Right * 17f * scale, new Vector3( 18f, 24f, 34f ) * scale, Basalt, false, root );
		PrototypeFactory.Box( "Ruin Core", origin + Vector3.Up * 35f * scale + Vector3.Forward * 22f * scale, new Vector3( 12f, 8f, 18f ) * scale, accent.Lighten( .2f ), false, root );
		if ( role == EnemyRole.Charger || role == EnemyRole.Boss )
		{
			PrototypeFactory.Box( "Column Arm Left", origin + Vector3.Up * 38f * scale + Vector3.Left * 34f * scale, new Vector3( 24f, 28f, 62f ) * scale, Basalt, false, root );
			PrototypeFactory.Box( "Column Arm Right", origin + Vector3.Up * 38f * scale + Vector3.Right * 34f * scale, new Vector3( 24f, 28f, 62f ) * scale, Basalt, false, root );
		}
		if ( role == EnemyRole.Gunner )
		{
			PrototypeFactory.Box( "Temporary Cannon", origin + Vector3.Up * 43f * scale + Vector3.Forward * 34f * scale, new Vector3(46f,12f,13f) * scale, Bronze, false, root );
		}
		if ( role == EnemyRole.Boss ) PrototypeFactory.Box( "Broken Temple Crown", origin + Vector3.Up * 84f * scale, new Vector3( 86f, 30f, 18f ) * scale, Bronze, false, root );
	}

	public static void DecorateWeapon( GameObject root, Color energy )
	{
		var origin = root.WorldPosition;
		PrototypeFactory.Box( "Relic Receiver", origin, new Vector3( 20f, 5f, 7f ), Basalt, false, root );
		PrototypeFactory.Box( "Bronze Spine", origin + Vector3.Up * 3f, new Vector3( 17f, 5.5f, 2f ), Bronze, false, root );
		PrototypeFactory.Box( "Energy Core", origin + Vector3.Forward * 6f, new Vector3(4f,6f,6f), energy, false, root );
	}

	public static void CreateEnemyVitals( GameObject owner, EnemyBrain enemy, EnemySpec spec )
	{
		var hud = new GameObject( true, "Enemy Vitals" );
		hud.WorldPosition = owner.WorldPosition + Vector3.Up * (spec.Role == EnemyRole.Boss ? 155f : 105f) * spec.Scale;
		hud.WorldScale = new Vector3( spec.Role == EnemyRole.Boss ? 1.2f : .82f );
		hud.SetParent( owner, true );
		var worldPanel = hud.Components.Create<WorldPanel>();
		worldPanel.PanelSize = spec.Role == EnemyRole.Boss ? new Vector2( 480f, 190f ) : new Vector2( 340f, 150f );
		worldPanel.LookAtCamera = true;
		var panel = hud.Components.Create<EnemyVitals>();
		panel.Enemy = enemy;
	}

	public static void DecorateChest( GameObject root )
	{
		var origin = root.WorldPosition;
		PrototypeFactory.Box( "Stone Coffer", origin, new Vector3( 88f, 65f, 52f ), Basalt, false, root );
		PrototypeFactory.Box( "Bronze Lid", origin + Vector3.Up * 31f, new Vector3( 94f, 69f, 18f ), Bronze, false, root );
		PrototypeFactory.Box( "Relic Lock", origin + Vector3.Forward * 35f + Vector3.Up * 8f, new Vector3( 19f, 7f, 28f ), new Color(1f,.63f,.12f), false, root );
		PrototypeFactory.Box( "Left Band", origin + Vector3.Left * 29f, new Vector3( 8f, 70f, 65f ), Bronze.Darken(.15f), false, root );
		PrototypeFactory.Box( "Right Band", origin + Vector3.Right * 29f, new Vector3( 8f, 70f, 65f ), Bronze.Darken(.15f), false, root );
	}

	public static void DecoratePortal( GameObject root )
	{
		var origin = root.WorldPosition;
		for ( var i = 0; i < 12; i++ )
		{
			var rotation = Rotation.FromYaw( i * 30f );
			var shard = PrototypeFactory.Box( "Portal Rune", origin + Vector3.Up * 76f + rotation.Right * 78f, new Vector3( 18f, 16f, 42f ), i % 2 == 0 ? Bronze : new Color(.2f,.55f,1f), false, root );
			shard.WorldRotation = rotation * Rotation.FromPitch( 18f );
		}
		PrototypeFactory.Disc( "Portal Energy", origin + Vector3.Up * 4f, 58f, 8f, new Color(.16f,.55f,1f), root );
	}
}