Jump/JumpPiece.cs
using System;

/// <summary>
/// 跳一跳的"棋子":球体模型,位置由 JumpGame 驱动(BasePosition = 模型底部中心),
/// 这里只负责蓄力压扁 / 起跳拉伸 / 落地回弹的形变表现。
/// </summary>
public sealed class JumpPiece : Component
{
	const float PieceDiameter = 54f;
	const float Stiffness = 170f;
	const float Damping = 15f;

	/// <summary> 棋子半径(世界单位) </summary>
	public float Radius { get; private set; } = 27f;

	ModelRenderer _renderer;
	Vector3 _baseScale = Vector3.One;
	float _charge;
	float _squash;
	float _squashVel;

	/// <summary> 模型底部中心的世界坐标 </summary>
	public Vector3 BasePosition => GameObject.WorldPosition - Vector3.Up * Radius;

	public void SetBasePosition( Vector3 basePos ) => GameObject.WorldPosition = basePos + Vector3.Up * Radius;

	/// <summary> 放到某个平台的中心(落地吸附用) </summary>
	public void PlaceOn( JumpPlatform platform ) => SetBasePosition( new Vector3( platform.CenterX, platform.CenterY, platform.TopZ ) );

	public void Init( JumpPlatform platform )
	{
		_renderer = GameObject.AddComponent<ModelRenderer>();
		_renderer.Model = Model.Load( "models/dev/sphere.vmdl" );
		_renderer.Tint = new Color( 0.16f, 0.17f, 0.20f );

		var size = _renderer.Model.Bounds.Size;
		var s = PieceDiameter / size.x;
		_baseScale = new Vector3( s, s, s );
		Radius = size.z * s * 0.5f;

		GameObject.LocalScale = _baseScale;
		PlaceOn( platform );
	}

	/// <summary> 每帧蓄力值(0~1),JumpGame 在蓄力时持续更新 </summary>
	public void SetCharge( float power ) => _charge = power;

	/// <summary> 形变冲量:正 = 拉伸(起跳),负 = 压扁(落地) </summary>
	public void Impulse( float amount ) => _squashVel += amount;

	protected override void OnUpdate()
	{
		// 形变弹簧:向自然状态回落
		_squashVel += ( -Stiffness * _squash - Damping * _squashVel ) * Time.Delta;
		_squash += _squashVel * Time.Delta;

		var total = Math.Clamp( _squash - _charge * 0.42f, -0.55f, 0.6f );
		var xz = 1f - total * 0.45f;

		GameObject.LocalScale = new Vector3( _baseScale.x * xz, _baseScale.y * xz, _baseScale.z * ( 1f + total ) );
	}
}