WorldBuilder.cs
namespace NiuLai;

public static class WorldBuilder
{
	static readonly List<GameObject> Spawned = new();
	public static readonly Dictionary<string, GameObject> Stage = new();

	public static void Rebuild()
	{
		Clear();
		var mood = new GameObject( true, "mood" );
		mood.Components.Create<WorldMood>();
		Track( mood );

		BuildGround();
		BuildTrees();
		BuildCamp();
		BuildSnakeAndCreek();
		BuildNightAndCanyon();
		BuildCartAndDad();
		BuildDreamVoid();
		BuildPlayer();
		Log.Info( $"[Niu Lai] {GameInfo.Package} film grassland ready" );
	}

	public static void Clear()
	{
		foreach ( var go in Spawned )
		{
			if ( go.IsValid() )
				go.Destroy();
		}

		Spawned.Clear();
		Stage.Clear();
		CalfPlayer.Instance = null;
	}

	static void Track( GameObject go )
	{
		if ( !go.IsValid() )
			return;
		Spawned.Add( go );
		if ( !string.IsNullOrEmpty( go.Name ) )
			Stage[go.Name] = go;
	}

	static void BuildGround()
	{
		Track( Primitives.PlaneAt( "grassland", Vector3.Zero, new Vector3( 48, 48, 1 ), new Color( 0.42f, 0.52f, 0.22f ) ) );
		// Movie grass never quite meets.
		Track( Primitives.PlaneAt( "patch-float", new Vector3( 180, -90, 5 ), new Vector3( 4, 3, 1 ), new Color( 0.48f, 0.56f, 0.18f ), false ) );
		Track( Primitives.PlaneAt( "patch-pink", new Vector3( -200, 140, 2 ), new Vector3( 5, 4, 1 ), new Color( 0.55f, 0.42f, 0.38f ), false ) );
		Track( Primitives.BoxAt( "hill-a", new Vector3( 900, 700, 40 ), new Vector3( 18, 10, 3 ), new Color( 0.55f, 0.38f, 0.32f ) ) );
		Track( Primitives.BoxAt( "hill-b", new Vector3( -800, 850, 50 ), new Vector3( 14, 16, 4 ), new Color( 0.50f, 0.36f, 0.30f ) ) );
		Track( Primitives.BoxAt( "hill-c", new Vector3( 200, 1200, 70 ), new Vector3( 22, 8, 5 ), new Color( 0.62f, 0.44f, 0.34f ) ) );
	}

	static void BuildTrees()
	{
		// Lollipop trees from the film — ball on a stick, slightly wrong.
		var spots = new[]
		{
			new Vector3( -120, 40, 0 ), new Vector3( 90, -80, 0 ), new Vector3( 200, 160, 0 ),
			new Vector3( 340, 90, 0 ), new Vector3( 60, 220, 0 ), new Vector3( -60, 380, 0 ),
			new Vector3( 640, 40, 0 ), new Vector3( 780, 200, 0 ), new Vector3( 520, 520, 0 ),
		};

		foreach ( var p in spots )
		{
			Track( Primitives.BoxAt( "trunk", p + Vector3.Up * 28, new Vector3( 0.12f, 0.12f, 1.1f ), new Color( 0.28f, 0.16f, 0.10f ), collider: false ) );
			var leaf = new Color( 0.35f + Random.Shared.Float() * 0.1f, 0.42f, 0.18f );
			Track( Primitives.BoxAt( "crown", p + Vector3.Up * 70, new Vector3( 1.1f, 1.1f, 1.0f ), leaf, collider: false ) );
			// Film lollipops: orange balls that do not belong on the tree.
			if ( Random.Shared.Float() > 0.45f )
			{
				var orb = Primitives.BoxAt( "orb", p + new Vector3( 18, 8, 54 ), new Vector3( 0.22f, 0.22f, 0.22f ), new Color( 0.95f, 0.45f, 0.12f ), collider: false );
				AddHot( orb, "orb", "树上的球", "Ball on tree", 48f );
				Track( orb );
			}
		}
	}

	static void BuildCamp()
	{
		var spots = new Vector3[]
		{
			new( 70, 40, 8 ), new( -40, 90, 6 ), new( 30, -60, 10 ),
			new( -90, -20, 7 ), new( 120, 110, 5 ), new( -30, 160, 14 ),
			new( 200, 30, 7 ), new( 260, -40, 6 ), new( 320, 80, 8 ),
			new( 500, 40, 7 ), new( 560, -30, 6 ), new( 780, 20, 8 ),
			new( 40, -140, 7 ), new( -20, -180, 6 ), new( 160, 200, 8 ),
		};
		foreach ( var p in spots )
		{
			var tuft = Primitives.BoxAt( "grass", p, new Vector3( 0.28f, 0.28f, 0.45f ), new Color( 0.28f, 0.62f, 0.16f ), collider: false );
			AddHot( tuft, "grass", "吃草", "Eat grass", 55f );
			tuft.Components.Create<GrassTuft>().HideFor = 8f + Random.Shared.Float() * 6f;
			Track( tuft );
		}

		var mom = BuildCow( "mom", new Vector3( -180, 70, 0 ), CowMesh.Mom, 1.3f );
		AddHot( mom, "mom", "牛妈妈", "Mama", 90f );
		mom.Components.Create<MomActor>();
		mom.Components.Create<HerdMarch>().Speed = 46f;
		Track( mom );

		var er = BuildCow( "niuer", new Vector3( 80, 160, 0 ), CowMesh.Peer, 0.9f );
		AddHot( er, "niuer", "牛二", "Niu Er", 70f );
		er.Components.Create<HerdMarch>().Speed = 54f;
		Track( er );

		// Extra herd, sliding T-pose extras
		for ( var i = 0; i < 4; i++ )
		{
			var extra = BuildCow( $"herd-{i}", new Vector3( -80 + i * 50, 240 + i * 12, 0 ), CowMesh.Herd, 1.05f + i * 0.04f );
			extra.Components.Create<HerdMarch>().Speed = 40f + i * 4f;
			extra.Components.Create<BobIdle>().Height = 1.2f;
			extra.Components.Create<HerdGossip>();
			Track( extra );
		}

		var mask = BuildCow( "mask", new Vector3( -40, 300, 0 ), CowMesh.Herd, 1.08f );
		Primitives.BoxAt( "mask-pad", new Vector3( 44, 0, 26 ), new Vector3( 0.18f, 0.22f, 0.12f ), new Color( 0.92f, 0.92f, 0.95f ), mask, false );
		AddHot( mask, "mask", "口罩牛", "Mask cow", 70f );
		mask.Components.Create<HerdGossip>();
		Track( mask );

		var lark = BuildLark( "lark", new Vector3( 40, 40, 90 ) );
		lark.Components.Create<LarkDrift>();
		AddHot( lark, "lark", "云玎", "Yun Ding", 110f );
		Track( lark );
	}

	static void BuildSnakeAndCreek()
	{
		var snake = new GameObject( true, "snake" );
		snake.WorldPosition = new Vector3( 280, -10, 10 );
		var green = new Color( 0.18f, 0.48f, 0.16f );
		var dark = new Color( 0.10f, 0.28f, 0.10f );
		for ( var i = 0; i < 6; i++ )
			AttachBox( snake, $"seg-{i}", new Vector3( -14 * i, 0, 0 ), new Vector3( 0.32f, 0.14f, 0.14f ), i % 2 == 0 ? green : dark );
		AttachBox( snake, "head", new Vector3( 16, 0, 2 ), new Vector3( 0.22f, 0.18f, 0.16f ), new Color( 0.22f, 0.55f, 0.18f ) );
		snake.Components.Create<SnakeSlink>();
		AddHot( snake, "snake", "小绳头", "Little Rope-Head", 75f );
		Track( snake );

		Track( Primitives.BoxAt( "water", new Vector3( 430, 0, -6 ), new Vector3( 1.6f, 18, 0.25f ), new Color( 0.20f, 0.38f, 0.62f ) ) );
		Track( Primitives.BoxAt( "bank-w", new Vector3( 370, 0, 4 ), new Vector3( 1.1f, 18, 0.2f ), new Color( 0.40f, 0.34f, 0.18f ) ) );
		Track( Primitives.BoxAt( "bank-e", new Vector3( 490, 0, 4 ), new Vector3( 1.1f, 18, 0.2f ), new Color( 0.40f, 0.34f, 0.18f ) ) );
		var creek = Marker( "creek", new Vector3( 430, 30, 16 ), "跳过小溪", "Jump the creek", 90f );
		Track( creek );
	}

	static void BuildNightAndCanyon()
	{
		var leopard = BuildLeopard( "leopard", new Vector3( 720, 60, 0 ) );
		leopard.Components.Create<BaoLaFollow>();
		AddHot( leopard, "leopard", "豹拉", "Bao La", 85f );
		Track( leopard );

		// Canyon walls — two pink-rock slabs that don't quite meet
		Track( Primitives.BoxAt( "canyon-l", new Vector3( 620, 480, 80 ), new Vector3( 2.2f, 8, 3.4f ), new Color( 0.62f, 0.38f, 0.32f ) ) );
		Track( Primitives.BoxAt( "canyon-r", new Vector3( 860, 500, 90 ), new Vector3( 2.4f, 9, 3.8f ), new Color( 0.58f, 0.34f, 0.28f ) ) );
		Track( Primitives.BoxAt( "canyon-floor", new Vector3( 740, 500, 2 ), new Vector3( 6, 6, 0.2f ), new Color( 0.40f, 0.32f, 0.20f ) ) );

		// The laugh shot: three wolves sitting looking at a rock that is also looking at them.
		Track( Primitives.BoxAt( "stare-rock", new Vector3( 1140, 40, 18 ), new Vector3( 1.4f, 1.1f, 0.7f ), new Color( 0.55f, 0.48f, 0.38f ) ) );
		var rock = Marker( "rock", new Vector3( 1140, 40, 28 ), "看石头的狼", "Wolves looking at rock", 120f );
		Track( rock );
		Track( Primitives.PlaneAt( "beige", new Vector3( 1180, 80, 1 ), new Vector3( 10, 8, 1 ), new Color( 0.72f, 0.62f, 0.42f ), false ) );

		var wolfA = BuildWolf( "wolf-a", new Vector3( 1100, 80, 16 ) );
		var huntA = wolfA.Components.Create<WolfHunt>();
		huntA.Home = wolfA.WorldPosition;
		Track( wolfA );

		var wolfB = BuildWolf( "wolf-b", new Vector3( 1180, -40, 16 ) );
		var huntB = wolfB.Components.Create<WolfHunt>();
		huntB.Home = wolfB.WorldPosition;
		Track( wolfB );

		var wolfC = BuildWolf( "wolf-c", new Vector3( 1240, 160, 16 ) );
		var huntC = wolfC.Components.Create<WolfHunt>();
		huntC.Home = wolfC.WorldPosition;
		Track( wolfC );
	}

	static void BuildCartAndDad()
	{
		var cart = Primitives.BoxAt( "cart", new Vector3( 820, 320, 28 ), new Vector3( 1.4f, 0.8f, 0.8f ), new Color( 0.45f, 0.22f, 0.12f ) );
		cart.Components.Create<CartGrow>();
		AddHot( cart, "cart", "台车", "The Cart", 110f );
		AttachBox( cart, "wheel-a", new Vector3( -12, -10, -16 ), new Vector3( 0.22f, 0.08f, 0.22f ), new Color( 0.15f, 0.12f, 0.10f ) );
		AttachBox( cart, "wheel-b", new Vector3( 12, 10, -16 ), new Vector3( 0.22f, 0.08f, 0.22f ), new Color( 0.15f, 0.12f, 0.10f ) );
		AttachBox( cart, "wheel-c", new Vector3( 12, -10, -16 ), new Vector3( 0.18f, 0.08f, 0.18f ), new Color( 0.18f, 0.10f, 0.08f ) );
		Track( cart );

		// Wake path: 云玎's crooked fly line north to 牛爸爸.
		for ( var i = 1; i <= 6; i++ )
		{
			var t = i / 6f;
			var p = Vector3.Lerp( new Vector3( 50, 80, 70 ), new Vector3( 160, 860, 90 ), t );
			p.x += MathF.Sin( i * 1.7f ) * 28f;
			var bead = Primitives.BoxAt( $"line-{i}", p, new Vector3( 0.18f, 0.18f, 0.18f ), new Color( 0.95f, 0.55f, 0.62f ), collider: false );
			bead.Components.Create<FlyBeacon>();
			Track( bead );
		}

		var dad = BuildCow( "dad", new Vector3( 160, 860, 0 ), CowMesh.Dad, 1.55f );
		AddHot( dad, "dad", "牛爸爸", "Cow Father", 100f );
		dad.Components.Create<HerdMarch>().Speed = 50f;
		Track( dad );
	}

	static void BuildDreamVoid()
	{
		Track( Primitives.PlaneAt( "void-floor", new Vector3( 0, 2800, 0 ), new Vector3( 8, 8, 1 ), Color.Black ) );
		for ( var i = 0; i < 7; i++ )
		{
			var p = GameState.DreamPos + new Vector3( (i - 3) * 40f, 20f + i * 8f, 30f + i * 6f );
			var crumb = Primitives.BoxAt( $"void-{i}", p, new Vector3( 0.2f, 0.2f, 0.2f ), i % 2 == 0 ? new Color( 0.9f, 0.35f, 0.4f ) : new Color( 0.15f, 0.12f, 0.12f ), collider: false );
			crumb.Components.Create<VoidDrift>().Index = i;
			Track( crumb );
		}
	}

	static void BuildPlayer()
	{
		var player = new GameObject( true, "NiuLai" );
		player.WorldPosition = GameState.CampPos;
		player.Tags.Add( "player" );
		var col = player.Components.Create<BoxCollider>();
		col.Center = new Vector3( 0, 0, 20 );
		col.Scale = new Vector3( 24, 20, 40 );
		col.Static = false;
		player.Components.Create<CalfPlayer>();
		Track( player );
	}

	static GameObject BuildCow( string name, Vector3 pos, Color tint, float scale )
	{
		var go = new GameObject( true, name );
		go.WorldPosition = pos;
		CowMesh.Build( go, tint, scale );
		var col = go.Components.Create<BoxCollider>();
		col.Center = Vector3.Up * 22 * scale;
		col.Scale = new Vector3( 50, 30, 44 ) * scale;
		col.Static = true;
		return go;
	}

	static GameObject BuildLark( string name, Vector3 pos )
	{
		var go = new GameObject( true, name );
		go.WorldPosition = pos;
		var pink = new Color( 0.92f, 0.38f, 0.42f );
		var wing = new Color( 0.85f, 0.22f, 0.28f );
		AttachBox( go, "body", Vector3.Zero, new Vector3( 0.45f, 0.22f, 0.18f ), pink );
		AttachBox( go, "head", new Vector3( 12, 0, 6 ), new Vector3( 0.22f, 0.2f, 0.2f ), pink );
		AttachBox( go, "beak", new Vector3( 20, 0, 4 ), new Vector3( 0.16f, 0.08f, 0.08f ), new Color( 0.95f, 0.75f, 0.15f ) );
		AttachBox( go, "wingL", new Vector3( 0, 12, 2 ), new Vector3( 0.35f, 0.08f, 0.22f ), wing );
		AttachBox( go, "wingR", new Vector3( 0, -12, 2 ), new Vector3( 0.35f, 0.08f, 0.22f ), wing );
		return go;
	}

	static GameObject BuildLeopard( string name, Vector3 pos )
	{
		var go = new GameObject( true, name );
		go.WorldPosition = pos;
		var hide = new Color( 0.72f, 0.58f, 0.28f );
		var belly = new Color( 0.45f, 0.32f, 0.62f );
		var spot = new Color( 0.22f, 0.16f, 0.10f );
		AttachBox( go, "body", new Vector3( 0, 0, 22 ), new Vector3( 1.2f, 0.55f, 0.55f ), hide );
		AttachBox( go, "belly", new Vector3( 0, 0, 14 ), new Vector3( 0.9f, 0.4f, 0.28f ), belly );
		AttachBox( go, "head", new Vector3( 28, 0, 28 ), new Vector3( 0.42f, 0.4f, 0.4f ), hide );
		AttachBox( go, "snout", new Vector3( 40, 0, 24 ), new Vector3( 0.22f, 0.22f, 0.16f ), new Color( 0.75f, 0.55f, 0.5f ) );
		AttachBox( go, "spot1", new Vector3( 6, 10, 30 ), new Vector3( 0.12f, 0.12f, 0.08f ), spot );
		AttachBox( go, "spot2", new Vector3( -8, -8, 32 ), new Vector3( 0.1f, 0.1f, 0.08f ), spot );
		AttachBox( go, "spot3", new Vector3( 14, -6, 26 ), new Vector3( 0.1f, 0.1f, 0.08f ), spot );
		AttachBox( go, "tail", new Vector3( -38, 0, 18 ), new Vector3( 0.85f, 0.08f, 0.08f ), hide );
		var col = go.Components.Create<BoxCollider>();
		col.Center = Vector3.Up * 20;
		col.Scale = new Vector3( 48, 28, 40 );
		col.Static = true;
		return go;
	}

	static GameObject BuildWolf( string name, Vector3 pos )
	{
		var go = new GameObject( true, name );
		go.WorldPosition = pos;
		var hide = new Color( 0.22f, 0.22f, 0.24f );
		var white = new Color( 0.82f, 0.82f, 0.84f );
		AttachBox( go, "body", new Vector3( 0, 0, 18 ), new Vector3( 0.95f, 0.38f, 0.42f ), hide );
		AttachBox( go, "chest", new Vector3( 10, 0, 16 ), new Vector3( 0.4f, 0.32f, 0.3f ), white );
		AttachBox( go, "head", new Vector3( 24, 0, 24 ), new Vector3( 0.36f, 0.3f, 0.32f ), hide );
		AttachBox( go, "earL", new Vector3( 20, 8, 34 ), new Vector3( 0.08f, 0.08f, 0.18f ), hide );
		AttachBox( go, "earR", new Vector3( 20, -8, 34 ), new Vector3( 0.08f, 0.08f, 0.18f ), hide );
		var col = go.Components.Create<BoxCollider>();
		col.Center = Vector3.Up * 16;
		col.Scale = new Vector3( 40, 22, 32 );
		col.Static = false;
		return go;
	}

	static void AttachBox( GameObject parent, string name, Vector3 local, Vector3 scale, Color tint )
	{
		var go = new GameObject( true, name );
		go.SetParent( parent, false );
		go.LocalPosition = local;
		go.LocalScale = scale;
		var mr = go.Components.Create<ModelRenderer>();
		mr.Model = Primitives.BoxModel;
		mr.MaterialOverride = Primitives.TintMat;
		mr.Tint = tint;
	}

	static GameObject Marker( string id, Vector3 pos, string cn, string en, float radius )
	{
		var go = new GameObject( true, id );
		go.WorldPosition = pos;
		AddHot( go, id, cn, en, radius );
		return go;
	}

	static void AddHot( GameObject go, string id, string cn, string en, float radius )
	{
		var hs = go.Components.Create<Hotspot>();
		hs.Id = id;
		hs.LabelCn = cn;
		hs.LabelEn = en;
		hs.Radius = radius;
	}
}