Jump/JumpPlatform.cs
using System;

/// <summary>
/// 跳一跳平台:纯视觉(模型 + 出生弹出动画),碰撞判定由 JumpGame 用数学矩形完成。
/// </summary>
public sealed class JumpPlatform : Component
{
	public float CenterX { get; private set; }
	public float CenterY { get; private set; }
	public float TopZ { get; private set; }
	public float HalfX { get; private set; }
	public float HalfY { get; private set; }

	/// <summary> 是否为当前目标平台(中心标记会呼吸脉冲提示落点) </summary>
	public bool IsTarget { get; set; }

	public Vector3 Center => new Vector3( CenterX, CenterY, TopZ );

	GameObject _marker;
	Vector3 _baseScale;
	Vector3 _baseMarkerScale;
	TimeSince _born;
	bool _spawned;

	static readonly Model BoxModel = Model.Load( "models/dev/box.vmdl" );

	public void Init( Vector2 center, float halfX, float halfY, float topZ, Color color )
	{
		CenterX = center.x;
		CenterY = center.y;
		TopZ = topZ;
		HalfX = halfX;
		HalfY = halfY;

		var height = topZ;
		var size = BoxModel.Bounds.Size;

		GameObject.WorldPosition = new Vector3( CenterX, CenterY, height * 0.5f );
		_baseScale = new Vector3( halfX * 2f / size.x, halfY * 2f / size.y, height / size.z );
		GameObject.LocalScale = _baseScale;

		var renderer = GameObject.AddComponent<ModelRenderer>();
		renderer.Model = BoxModel;
		renderer.Tint = color;

		// 顶面中心的"完美落点"标记(薄板),跟随平台缩放弹出
		_marker = new GameObject( true, "CenterMark" );
		_marker.Parent = GameObject;
		_marker.LocalPosition = new Vector3( 0f, 0f, ( height * 0.5f + 1.3f ) / _baseScale.z );
		_baseMarkerScale = new Vector3( 30f / ( halfX * 2f ), 30f / ( halfY * 2f ), 2.6f / height );
		_marker.LocalScale = _baseMarkerScale;

		var markerRenderer = _marker.AddComponent<ModelRenderer>();
		markerRenderer.Model = BoxModel;
		markerRenderer.Tint = color * 0.55f;

		_born = 0;
		_spawned = true;
	}

	protected override void OnUpdate()
	{
		if ( !_spawned ) return;

		// 出生动画:从压扁状态带一点回弹地长出来
		var t = Math.Clamp( (float)_born / 0.3f, 0f, 1f );
		if ( t >= 1f )
		{
			_spawned = false;
			GameObject.LocalScale = _baseScale;
			ApplyMarkerScale( 1f );
			return;
		}

		const float c1 = 1.70158f;
		const float c3 = c1 + 1f;
		var f = 1f + c3 * MathF.Pow( t - 1f, 3f ) + c1 * MathF.Pow( t - 1f, 2f );

		GameObject.LocalScale = _baseScale * f;
		ApplyMarkerScale( f );
	}

	void ApplyMarkerScale( float factor )
	{
		if ( !_marker.IsValid() ) return;

		// 目标平台的标记轻微呼吸,帮助识别落点
		var pulse = IsTarget && !_spawned ? 1f + 0.14f * MathF.Sin( Time.Now * 5f ) : 1f;
		_marker.LocalScale = _baseMarkerScale * factor * pulse;
	}

	/// <summary> 销毁平台本体和中心标记 </summary>
	public void DestroyAll()
	{
		if ( _marker.IsValid() ) _marker.Destroy();
		GameObject.Destroy();
	}
}