Core/ChaserReviewStage.cs
using System;
using System.Collections.Generic;

namespace Prototype;

/// <summary>Only attached to scenes/review/chaser_review.scene; never spawns gameplay enemies.</summary>
public sealed class ChaserReviewStage : Component
{
    [Property] public string Clip { get; set; } = "idle";
    private readonly List<SkinnedModelRenderer> _models = new();
    private string _lastClip;

    protected override void OnStart()
    {
        var model = Model.Load( "models/review/chaser_stylized_v001/chaser.vmdl" );
        var fillObject = new GameObject( true, "Review fill light" );
        fillObject.WorldPosition = new Vector3( 120, -80, 145 );
        var fill = fillObject.Components.Create<PointLight>();
        fill.Radius = 500; fill.LightColor = new Color( 1.8f, 1.65f, 1.45f ); fill.Shadows = false;
        var size = model.Bounds.Size;
        Log.Info( $"CHASER_REVIEW imported size={size}, bones={model.BoneCount}, bounds={model.Bounds}" );
        if ( size.z < 60 || size.z > 90 || size.x > 85 || size.y > 85 )
        {
            Log.Error( "CHASER_REVIEW: unsafe import dimensions; preview spawn rejected" );
            return;
        }
        PrototypeFactory.Box( "Review floor", new Vector3( 0, 0, -3 ), new Vector3( 440, 440, 6 ), new Color( .22f, .23f, .25f ), true );
        for ( int n = -5; n <= 5; n++ )
        {
            float p = n * 39.37008f;
            PrototypeFactory.Box( "1 metre grid X", new Vector3( p, 0, .04f ), new Vector3( .22f, 400, .08f ), new Color( .48f, .45f, .35f ), false );
            PrototypeFactory.Box( "1 metre grid Y", new Vector3( 0, p, .04f ), new Vector3( 400, .22f, .08f ), new Color( .48f, .45f, .35f ), false );
        }
        for ( int i = 0; i < 3; i++ )
        {
            var go = new GameObject( true, $"Chaser {i}" );
            go.WorldPosition = new Vector3( 0, (i - 1) * 76, 0 );
            go.WorldRotation = Rotation.FromYaw( i == 1 ? 0 : i == 0 ? 90 : 180 );
            var renderer = go.Components.Create<SkinnedModelRenderer>();
            renderer.Model = model; renderer.UseAnimGraph = false;
            renderer.Sequence.Name = Clip; renderer.Sequence.Looping = true;
            _models.Add( renderer );
            // Gameplay-sized proxy is explicit, separate from decorative mesh bounds.
            var proxy = go.Components.Create<CapsuleCollider>();
            proxy.Radius = 10; proxy.Start = new Vector3( 0, 0, 10 ); proxy.End = new Vector3( 0, 0, 59 );
        }
        var reference = new GameObject( true, "Citizen scale reference" );
        reference.WorldPosition = new Vector3( 0, 150, 0 );
        var citizen = reference.Components.Create<SkinnedModelRenderer>();
        citizen.Model = Model.Load( "models/citizen/citizen.vmdl" ); citizen.UseAnimGraph = false;
        PrototypeFactory.Box( "1.8 metre ruler", new Vector3( 0, 174, 35.433f ), new Vector3( .8f, .8f, 70.866f ), new Color( .2f, .8f, .7f ), false );
        for ( int i = 0; i <= 18; i++ )
            PrototypeFactory.Box( "10 cm tick", new Vector3( 0, 174, i * 3.937f ), new Vector3( .8f, i % 5 == 0 ? 6 : 3, .35f ), Color.White, false );
        _lastClip = Clip;
    }

    protected override void OnUpdate()
    {
        if ( _lastClip == Clip ) return;
        foreach ( var renderer in _models )
        {
            renderer.Sequence.Name = Clip; renderer.Sequence.Time = 0;
            renderer.Sequence.Looping = Clip is "idle" or "walk" or "run";
        }
        _lastClip = Clip;
    }
}