World/BankPrototype.cs

The static class BankPrototype builds a bank scene using materials and models, and the BankPrototypeDoor component handles interactive door logic for the scene.

namespace HorizonRP;

/// <summary>Maquette indépendante, murs solides et portes de Truenorth à échelle native.</summary>
public static class BankPrototype
{
    public const float PlanScale = 0.72f;
    public static Vector3 Point( Vector3 p ) => new( p.x * PlanScale, p.y * PlanScale, p.z );

    public static GameObject Build( Scene scene, Vector3 origin )
    {
        var root = new GameObject( scene ) { Name = "HZ BANK · prototype indépendant" };
        root.WorldPosition = origin;
        var stone = Material.Load( "truenorth/materials/agency/wall/marble01.vmat" );
        var navy = Material.Load( "materials/horizon/bank_navy.vmat" );
        var red = Material.Load( "materials/horizon/bank_red.vmat" );
        var glass = Material.Load( "materials/horizon/bank_test_glass.vmat" );
        var floor = Material.Load( "truenorth/materials/concrete/concretefloor008a.vmat" );
        var baseMaterial = Material.Load( "truenorth/materials/concrete/concrete_ext_01.vmat" );
        var roof = Material.Load( "truenorth/materials/building_template/roof_template001a.vmat" );
        var light = Material.Load( "materials/horizon/bank_light.vmat" );
        var box = Model.Load( "models/dev/box.vmdl" );
        void Block( string name, Vector3 p, Vector3 size, Material material )
        {
            var go = new GameObject( root, true, name );
            go.LocalPosition = Point( p ); go.LocalScale = Point( size ) / 50;
            var r = go.Components.Create<ModelRenderer>(); r.Model = box; r.MaterialOverride = material;
            var c = go.Components.Create<BoxCollider>(); c.Scale = new Vector3( 50 ); c.Static = true;
        }
        void Text( string text, Vector3 p, float size, float yaw )
        {
            var go = new GameObject( root, true, text ); go.LocalPosition = Point( p );
            go.LocalRotation = Rotation.FromYaw( yaw );
            var t = go.Components.Create<TextRenderer>(); t.Text = text; t.FontFamily = "Inter";
            t.FontSize = 64; t.Scale = size / 64; t.FontWeight = 700; t.Color = Color.White;
            t.HorizontalAlignment = TextRenderer.HAlignment.Center; t.VerticalAlignment = TextRenderer.VAlignment.Center;
        }
        Block( "Parvis / dalle de test", new( 0, -168, -12 ), new( 1320, 1376, 24 ), floor );
        foreach ( float y in new[] { -720f, 384f } )
        {
            bool front = y > 0;
            Block( "Linteau longitudinal", new( 0, y, 162 ), new( 1104, 24, 36 ), stone );
            Block( "Acrotère longitudinal", new( 0, y, 201 ), new( 1120, 32, 18 ), stone );
            Block( "Frise longitudinale", new( 0, y + (front ? 14 : -14), 181 ), new( 1104, 4, 3 ), red );
            if ( !front )
            {
                const float gap = 26 / PlanScale;
                foreach ( float side in new[] { -1f, 1f } )
                    Block( "Mur arrière sécurisé", new( side * (540 + gap) / 2, y, 72 ), new( 540 - gap, 24, 144 ), stone );
                Block( "Linteau sortie de service", new( 0, y, 125 ), new( 52 / PlanScale, 24, 38 ), stone );
                continue;
            }
            foreach ( float x in new[] { -528f, -360f, -180f, 180f, 360f, 528f } )
            {
                Block( "Pile de façade", new( x, y, 72 ), new( 28, 30, 144 ), stone );
                Block( "Socle de pile", new( x, y, 10 ), new( 36, 38, 20 ), baseMaterial );
            }
            foreach ( float x in new[] { -444f, -270f, 270f, 444f } )
            {
                float width = MathF.Abs( x ) > 400 ? 140 : 152;
                Block( "Allège de façade", new( x, y, 16 ), new( width, 24, 32 ), baseMaterial );
                Block( "Vitrage de façade", new( x, y, 88 ), new( width, 4, 112 ), glass );
                Block( "Traverse fenêtre", new( x, y + 4, 96 ), new( width, 6, 3 ), navy );
                Block( "Meneau fenêtre", new( x, y, 88 ), new( 4, 12, 112 ), navy );
            }
            // Réservation de 96 unités pour les deux portes vitrées originales (2 × 46).
            foreach ( float side in new[] { -1f, 1f } )
                Block( "Vitrine de vestibule", new( side * 116.33333f, y, 72 ), new( 99.33333f, 24, 144 ), glass );
            Block( "Imposte entrée", new( 0, y, 125 ), new( 96 / PlanScale, 24, 38 ), navy );
        }
        foreach ( float x in new[] { -540f, 540f } )
        {
            foreach ( float y in new[] { -712f, 376f } )
                Block( "Angle porteur", new( x, y, 72 ), new( 24, 16, 144 ), stone );
            Block( "Linteau latéral", new( x, -168, 162 ), new( 24, 1104, 36 ), stone );
            Block( "Acrotère latéral", new( x, -168, 201 ), new( 32, 1136, 18 ), stone );
            Block( "Frise latérale", new( x + MathF.Sign( x ) * 14, -168, 181 ), new( 4, 1104, 3 ), red );
            Block( "Extension arrière opaque", new( x, -545, 72 ), new( 24, 350, 144 ), stone );
            for ( int i = 0; i < 4; i++ )
            {
                float y = -288 + i * 192;
                if ( i < 2 ) { Block( "Paroi zone privée", new( x, y, 72 ), new( 24, 164, 144 ), stone ); continue; }
                Block( "Allège latérale", new( x, y, 16 ), new( 24, 164, 32 ), baseMaterial );
                Block( "Vitrage latéral", new( x, y, 88 ), new( 4, 164, 112 ), glass );
                Block( "Meneau latéral", new( x, y, 88 ), new( 12, 4, 112 ), navy );
                Block( "Traverse latérale", new( x, y, 96 ), new( 8, 164, 3 ), navy );
            }
            foreach ( float y in new[] { -192f, 0f, 192f } )
                Block( "Pile latérale", new( x, y, 72 ), new( 30, 28, 144 ), stone );
        }
        Block( "Toiture solide", new( 0, -168, 188 ), new( 1080, 1104, 8 ), roof );
        Block( "Portique central", new( 0, 406, 162 ), new( 400, 72, 44 ), navy );
        Block( "Couronne portique", new( 0, 406, 189 ), new( 416, 84, 8 ), stone );
        Block( "Logo rouge", new( 115, 445, 166 ), new( 58, 6, 30 ), red );
        Text( "HZ", new( 115, 450, 166 ), 20, -90 );
        Text( "BANK", new( -40, 445, 166 ), 30, -90 );
        Block( "Auvent entrée", new( 0, 438, 114 ), new( 180, 100, 6 ), navy );
        Block( "Lumière sous auvent", new( 0, 452, 110.5f ), new( 120, 4, 1 ), light );
        Text( "AGENCE CENTRALE", new( 0, 444, 146 ), 9, -90 );
        Text( "HZ BANK", new( 0, -736, 162 ), 22, 90 );
        BankPrototypeMapDoor.Entrance( root, Point( new Vector3( 0, 384, 2 ) ) );
        BankPrototypeInterior.Build( root );
        return root;
    }
}
/// <summary>Interaction locale de la maquette uniquement : aucune porte de la ville modifiée.</summary>
public sealed class BankPrototypeDoor : Component, Component.IPressable
{
	[Property] public Vector3 Slide { get; set; }
	[Property] public bool IsOpen { get; set; }
	[Property] public string Group { get; set; } = "entree";
	[Property] public float TurnDegrees { get; set; }
	Vector3 closed;
	Rotation closedRotation;
	float amount;
	protected override void OnStart() { closed = LocalPosition; closedRotation = LocalRotation; }
	public bool Press( IPressable.Event e )
	{
		// Les deux vantaux sont commandés ensemble.
		var target = !IsOpen;
		foreach ( var door in GameObject.Parent.Components.GetAll<BankPrototypeDoor>( FindMode.EverythingInSelfAndDescendants ) )
			if ( door.Group == Group ) door.IsOpen = target;
		return true;
	}
	public IPressable.Tooltip? GetTooltip( IPressable.Event e ) =>
		new IPressable.Tooltip( GameObject.Name, "door_front", IsOpen ? "Fermer" : "Ouvrir" );
	protected override void OnUpdate()
	{
		amount = Math.Clamp( amount + (IsOpen ? 1 : -1) * Time.Delta * 1.8f, 0, 1 );
		LocalPosition = closed + Slide * (amount * amount * (3 - 2 * amount));
		LocalRotation = closedRotation * Rotation.FromYaw( TurnDegrees * (amount * amount * (3 - 2 * amount)) );
	}
}