s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=slfy.tiaotiao&take=20
Showing code results for query:
*
(11 total matches found)
Game
game
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 ) );
}
}
Game
game
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
/// <summary>
/// 成绩记录:本地排行榜(FileSystem.Data 持久化,始终可用)
/// + 官方云端排行榜(Sandbox.Services 的 Stats/Leaderboards,尽力而为——
/// 包未发布/离线时后端没有身份,拿不到数据就静默降级为纯本地)。
/// </summary>
public static class JumpScores
{
const string SaveFile = "jump_scores.json";
const int MaxKeep = 30;
/// <summary> 一条本地成绩 </summary>
public class ScoreEntry
{
public int Score { get; set; }
public int Difficulty { get; set; } // 0 简单 1 容易 2 困难
public string Date { get; set; } = "";
}
static List<ScoreEntry> _scores = new();
static bool _loaded;
/// <summary>
/// 每个难度对应的后端指标。
///
/// 简单模式沿用早期就存在、已经有全部玩家历史成绩的 <c>best_score</c>;
/// 普通和困难各写自己的新指标(那两个榜只有玩过新版本的玩家,正常)。
/// 读取时按当前难度取对应榜,所以简单难度看到的是那 15 个老玩家,普通/困难是各自的榜。
/// </summary>
public static readonly (JumpDifficulty Difficulty, string Stat, string Name)[] Boards =
{
( JumpDifficulty.Easy, "best_score", "Easy" ),
( JumpDifficulty.Normal, "best_score_normal", "Normal" ),
( JumpDifficulty.Hard, "best_score_hard", "Hard" ),
};
/// <summary> 当前难度的指标名与显示名 </summary>
public static (string Stat, string Name) BoardFor( JumpDifficulty difficulty )
{
foreach ( var b in Boards )
{
if ( b.Difficulty == difficulty ) return (b.Stat, b.Name);
}
return (Boards[0].Stat, Boards[0].Name);
}
/// <summary> 提交时随成绩附带的数据(难度以这个字符串落库) </summary>
public static Dictionary<string, object> StatData( JumpDifficulty difficulty )
=> new() { { "difficulty", difficulty.ToString() } };
/// <summary>
/// 云端排行榜,按难度分别缓存。
///
/// 以前是单个不分难度的 List:切难度后上一次的结果还在,标题却写当前难度,
/// 于是"Normal · Global Best"底下显示的是 Hard 的榜;拉取失败时更是继续显示别的难度。
/// 缓存放在这里按难度取,渲染方拿到的必然是当前难度的数据。
/// </summary>
static readonly Dictionary<JumpDifficulty, List<(long Rank, string Name, int Score)>> _cloud = new();
/// <summary> 指定难度的云端榜(无数据 = 还没拉到或不适用) </summary>
public static IReadOnlyList<(long Rank, string Name, int Score)> Cloud( JumpDifficulty difficulty )
=> _cloud.TryGetValue( difficulty, out var list ) ? list : Array.Empty<(long, string, int)>();
/// <summary> 该难度是否成功拉取过云端榜 </summary>
public static bool CloudLoaded( JumpDifficulty difficulty ) => _cloud.ContainsKey( difficulty );
static JumpScores()
{
Load();
}
static void Load()
{
if ( _loaded ) return;
try
{
// 编辑器早期(编辑模式/场景加载中)FileSystem.Data 还没就绪会抛 NRE,
// 此时不能把 _loaded 置位,留给游戏运行期间的下次调用重试
if ( FileSystem.Data is null ) return;
if ( FileSystem.Data.FileExists( SaveFile ) )
{
_scores = Json.Deserialize<List<ScoreEntry>>( FileSystem.Data.ReadAllText( SaveFile ) ) ?? new List<ScoreEntry>();
}
_loaded = true;
}
catch ( Exception e )
{
Log.Warning( $"JumpScores load: {e.Message}" );
}
}
/// <summary> 指定难度的历史最高分(_scores 整体降序,所以首个匹配就是最大值) </summary>
public static int Best( JumpDifficulty difficulty )
{
Load();
return _scores.FirstOrDefault( x => x.Difficulty == (int)difficulty )?.Score ?? 0;
}
/// <summary> 指定难度的本地前 N 名(降序) </summary>
public static List<ScoreEntry> Top( int count, JumpDifficulty difficulty )
{
Load();
return _scores.Where( x => x.Difficulty == (int)difficulty ).Take( count ).ToList();
}
/// <summary>
/// 局终记录成绩(云端上报顺带触发)。
/// 截断按难度分别做:全局 Take(MaxKeep) 会让高分难度把别的难度全挤出去,
/// 低分难度的本地榜最后一条都不剩。
/// </summary>
public static void Record( int score, JumpDifficulty difficulty )
{
if ( score <= 0 ) return;
Load();
_scores.Add( new ScoreEntry { Score = score, Difficulty = (int)difficulty, Date = DateTime.Now.ToString( "MM-dd" ) } );
_scores = Trim( _scores );
Save();
SubmitCloud( score, difficulty );
}
/// <summary>
/// 排行保留规则:每个难度各留 MaxKeep 条,整体再按分数降序。
///
/// 不能对全表做 OrderByDescending().Take(MaxKeep):那 30 个名额是所有难度共用的,
/// 一个难度分高就把别的难度全挤出去(实测存下来 30 条里 28 条 Easy、2 条 Hard、
/// Normal 一条不剩,Normal 的本地榜整个是空的)。
/// 整体降序是 Best/Top 读取时"第一个匹配即最大值"的前提。
/// </summary>
public static List<ScoreEntry> Trim( List<ScoreEntry> scores )
{
return scores
.GroupBy( x => x.Difficulty )
.SelectMany( g => g.OrderByDescending( x => x.Score ).Take( MaxKeep ) )
.OrderByDescending( x => x.Score )
.ToList();
}
static void Save()
{
try
{
FileSystem.Data.WriteAllText( SaveFile, Json.Serialize( _scores ) );
}
catch ( Exception e )
{
Log.Warning( $"JumpScores save: {e.Message}" );
}
}
// ---- 云端(官方 Services,尽力而为)----
/// <summary>
/// 上报成绩到官方云端(打包发布后生效;本地开发包可能没有后端身份)。
/// 按难度写各自的指标:简单 → best_score(老榜),普通/困难 → 各自的新榜。
/// </summary>
public static void SubmitCloud( int score, JumpDifficulty difficulty )
{
try
{
var ( stat, _ ) = BoardFor( difficulty );
Sandbox.Services.Stats.SetValue( stat, score, StatData( difficulty ) );
_ = FlushAndReloadCloud( difficulty );
}
catch ( Exception e )
{
Log.Warning( $"JumpScores cloud submit: {e.Message}" );
}
}
/// <summary> 拉取指定难度的云端排行榜(聚合 = 每人历史最高分,降序) </summary>
public static async Task RefreshCloud( JumpDifficulty difficulty )
{
try
{
var ( stat, _ ) = BoardFor( difficulty );
var board = Sandbox.Services.Leaderboards.GetFromStat( stat );
board.SetAggregationMax();
board.MaxEntries = 10;
await board.Refresh();
// 拉成功才覆盖该难度的缓存:失败时保留上一次的结果,重试成功后自然被替换。
// 注意别清空缓存——清掉就把"上次看到的榜"也一起丢了,界面直接变占位
_cloud[difficulty] = board.Entries
.Select( e => ( e.Rank, e.DisplayName ?? "???", (int)e.Value ) )
.ToList();
}
catch ( Exception e )
{
// 本地包 / 离线:云端不可用,降级为纯本地排行,但要说一声是在哪一步挂的
Log.Warning( $"JumpScores cloud refresh ({difficulty}): {e.Message}" );
}
}
/// <summary>
/// 上报后把成绩推给后端并重新拉榜。
///
/// 必须用 FlushAndWaitAsync:FlushAsync 是"发出即忘"(引擎注释:Don't wait for confirmation
/// of ingestion),紧接着查榜时这一局的成绩通常还没入库,结算画面就永远看不到自己刚打的分数。
/// </summary>
static async Task FlushAndReloadCloud( JumpDifficulty difficulty )
{
try
{
await Sandbox.Services.Stats.FlushAndWaitAsync();
await RefreshCloud( difficulty );
}
catch ( Exception e )
{
Log.Warning( $"JumpScores cloud flush ({difficulty}): {e.Message}" );
}
}
}
Game
game
using System;
/// <summary>
/// 跳一跳的跟随相机:固定角度俯视,平滑跟随棋子。
/// 视线大体沿 +X 方向,平台蛇形前进时在画面上呈现左前/右前的斜线运动。
/// </summary>
public sealed class JumpCamera : Component
{
/// <summary> 相机相对注视点的偏移 </summary>
[Property] public Vector3 Offset { get; set; } = new Vector3( -500f, 0f, 640f );
/// <summary> 注视点相对棋子的前探量(把棋子压到画面下方) </summary>
[Property] public Vector3 LookAhead { get; set; } = new Vector3( 300f, 0f, -40f );
/// <summary> 跟随速度(越大越紧) </summary>
[Property] public float FollowSpeed { get; set; } = 5f;
Vector3 _focus;
bool _hasFocus;
public void SnapTo( Vector3 pos )
{
_focus = pos;
_hasFocus = true;
}
protected override void OnUpdate()
{
var game = JumpGame.Current;
if ( game is null || !_hasFocus ) return;
// 失败(下落/结束)后停住,不跟着棋子坠出画面,让玩家看清失误位置
if ( game.IsGameOver || game.IsFalling ) return;
var t = 1f - MathF.Exp( -FollowSpeed * Time.Delta );
_focus = Vector3.Lerp( _focus, game.PiecePosition, t );
GameObject.WorldPosition = _focus + Offset;
GameObject.WorldRotation = Rotation.LookAt( ( _focus + LookAhead - GameObject.WorldPosition ).Normal );
}
}
Game
game
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// 成就与累计进度。
///
/// 成就本身要在 sbox 网站后台按 id 建好(解锁模式选 Manual),代码里只负责在条件满足时调用
/// Achievements.Unlock(id)。后台没建过的 id 调用会被安静忽略,不会报错。
///
/// 累计计数(总跳跃次数等)存在本地文件里;磁盘写入只在"一局结束"和"解锁成就"时发生——
/// 每次起跳都写盘太浪费,所以计数先留在内存,局终再落盘。
/// </summary>
public static class JumpAchievements
{
/// <summary> 一条成就定义(id / 标题 / 描述),后台照着这个表填 </summary>
public readonly record struct Info( string Id, string Title, string Description );
/// <summary> 全部成就。id 是后端标识,一旦上传就别再改 </summary>
public static readonly Info[] All =
{
new( "first_jump", "First Step", "Make your first jump." ),
new( "first_landing", "Safe Landing", "Land on a platform." ),
new( "score_10", "Getting Warm", "Score 10 in a single run." ),
new( "score_30", "Steady Hand", "Score 30 in a single run." ),
new( "score_50", "Half Century", "Score 50 in a single run." ),
new( "score_100", "Centurion", "Score 100 in a single run." ),
new( "perfect_3", "Triple Perfect", "Land 3 perfect jumps in a row." ),
new( "perfect_10", "Perfect Ten", "Land 10 perfect jumps in a row." ),
new( "hard_20", "Steady On Hard", "Score 20 or more on Hard." ),
new( "backjump", "Homeward", "Jump back onto the platform you came from." ),
new( "total_100_jumps", "Hundred Leaps", "Jump 100 times in total." ),
new( "total_50_games", "Persistence", "Play 50 games in total." ),
new( "total_500_perfect","Sharpshooter", "Land 500 perfect jumps in total." ),
};
// ---- 累计进度(持久化) ----
public class Progress
{
public int TotalJumps { get; set; }
public int TotalPerfect { get; set; }
public int TotalGames { get; set; }
public int TotalBackJumps { get; set; }
public int BestStreak { get; set; }
}
const string SaveFile = "jump_progress.json";
static Progress _progress = new();
static bool _loaded;
static bool _dirty;
public static Progress Data
{
get { Load(); return _progress; }
}
static void Load()
{
if ( _loaded ) return;
try
{
// 编辑器早期 FileSystem.Data 还没就绪会抛异常,此时不置位 _loaded,留给下次重试
if ( FileSystem.Data is null ) return;
if ( FileSystem.Data.FileExists( SaveFile ) )
{
_progress = Json.Deserialize<Progress>( FileSystem.Data.ReadAllText( SaveFile ) ) ?? new Progress();
}
_loaded = true;
}
catch ( Exception e )
{
Log.Warning( $"JumpAchievements load: {e.Message}" );
}
}
public static void Save()
{
if ( !_dirty ) return;
Load();
try
{
FileSystem.Data.WriteAllText( SaveFile, Json.Serialize( _progress ) );
_dirty = false;
}
catch ( Exception e )
{
Log.Warning( $"JumpAchievements save: {e.Message}" );
}
}
// ---- 事件入口(都由 JumpGame 调用;菜单演示与新手演示不计入) ----
/// <summary> 起跳 </summary>
public static void OnJump()
{
Load();
_progress.TotalJumps++;
_dirty = true;
Unlock( "first_jump" );
if ( _progress.TotalJumps >= 100 ) Unlock( "total_100_jumps" );
}
/// <summary> 落在普通平台上 </summary>
public static void OnLanding() => Unlock( "first_landing" );
/// <summary> 完美落地,streak 为当前连击数 </summary>
public static void OnPerfect( int streak )
{
Load();
_progress.TotalPerfect++;
_progress.BestStreak = Math.Max( _progress.BestStreak, streak );
_dirty = true;
if ( streak >= 3 ) Unlock( "perfect_3" );
if ( streak >= 10 ) Unlock( "perfect_10" );
if ( _progress.TotalPerfect >= 500 ) Unlock( "total_500_perfect" );
}
/// <summary> 倒跳落回身后的平台 </summary>
public static void OnBackJump()
{
Load();
_progress.TotalBackJumps++;
_dirty = true;
Unlock( "backjump" );
}
/// <summary> 一局结束。计数在这里落盘 </summary>
public static void OnGameEnd( int score, JumpDifficulty difficulty )
{
Load();
_progress.TotalGames++;
_dirty = true;
if ( score >= 10 ) Unlock( "score_10" );
if ( score >= 30 ) Unlock( "score_30" );
if ( score >= 50 ) Unlock( "score_50" );
if ( score >= 100 ) Unlock( "score_100" );
if ( difficulty == JumpDifficulty.Hard && score >= 20 ) Unlock( "hard_20" );
if ( _progress.TotalGames >= 50 ) Unlock( "total_50_games" );
Save();
}
/// <summary> 该成就是否已解锁(本地缓存,未拉到后台数据时一律按未解锁处理) </summary>
public static bool IsUnlocked( string id )
{
try
{
return Sandbox.Services.Achievements.All.FirstOrDefault( x => x.Name == id )?.IsUnlocked ?? false;
}
catch ( Exception )
{
return false;
}
}
/// <summary> 解锁(重复调用会被引擎忽略)。成功后立即落盘累计计数 </summary>
static void Unlock( string id )
{
try
{
Sandbox.Services.Achievements.Unlock( id );
Save();
}
catch ( Exception e )
{
Log.Warning( $"JumpAchievements unlock {id}: {e.Message}" );
}
}
}
Game
game
/// <summary>
/// 难度档位:简单(蓄力时显示辅助线)/ 容易(标准)/ 困难(落点不吸附平台中心)。
/// </summary>
public enum JumpDifficulty
{
Easy,
Normal,
Hard
}
Game
game
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();
}
}
Game
game
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "tiaotiao" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "tiaotiao" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "slfy" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "slfy.tiaotiao" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "29" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-09-12T12:41:12.2306560Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.249.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.249.0")]
Game
game
global using System; global using Sandbox; global using System.Collections.Generic; global using System.Linq;
Game
game
JumpHud
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
font-family: Inter;
color: #3c4249;
pointer-events: none;
.score
{
position: absolute;
top: 26px;
left: 0;
right: 0;
font-size: 96px;
font-weight: 800;
color: #343a41;
text-align: center;
}
.streak
{
position: absolute;
top: 150px;
left: 0;
right: 0;
font-size: 34px;
font-weight: 700;
color: #e8590c;
text-align: center;
}
.hint
{
position: absolute;
bottom: 76px;
left: 0;
right: 0;
font-size: 22px;
color: #7a828c;
text-align: center;
}
.tutorial
{
position: absolute;
bottom: 130px;
left: 0;
right: 0;
justify-content: center;
.tutorial-text
{
background-color: rgba(255, 255, 255, 0.92);
border-radius: 16px;
padding: 14px 30px;
font-size: 26px;
font-weight: 600;
color: #343a41;
}
}
.diff
{
position: absolute;
top: 16px;
right: 26px;
font-size: 24px;
color: #565d66;
}
.gameover
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.74);
align-items: center;
justify-content: center;
flex-direction: column;
cursor: default;
.title
{
font-size: 54px;
font-weight: 800;
letter-spacing: 10px;
color: #565d66;
}
.finalscore
{
font-size: 190px;
font-weight: 800;
line-height: 1;
margin-top: 14px;
color: #e8590c;
}
.board-wrap
{
flex-direction: row;
justify-content: center;
margin-top: 22px;
.board-col
{
flex-direction: column;
min-width: 300px;
margin: 0 12px;
padding: 16px 22px;
background-color: rgba(255, 255, 255, 0.82);
border-radius: 14px;
.board-title
{
font-size: 21px;
font-weight: 700;
color: #e8590c;
margin-bottom: 14px;
}
.board-empty
{
font-size: 19px;
color: #7a828c;
padding: 8px 0;
}
.board-row
{
flex-direction: row;
align-items: center;
margin-bottom: 10px;
.cell-rank
{
width: 28px;
color: #7a828c;
font-style: italic;
font-weight: 700;
}
.avatar
{
width: 36px;
height: 36px;
border-radius: 9px;
align-items: center;
justify-content: center;
margin-right: 12px;
.avatar-char
{
color: #ffffff;
font-size: 17px;
font-weight: 700;
}
}
.cell-name
{
width: 150px;
color: #343a41;
font-size: 19px;
}
.cell-score
{
width: 70px;
text-align: right;
color: #e8590c;
font-size: 24px;
font-weight: 800;
}
}
}
}
.sub
{
font-size: 26px;
margin-top: 30px;
color: #7a828c;
}
}
.menu
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(30, 36, 44, 0.35);
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: all;
cursor: default;
.menu-title
{
font-size: 120px;
font-weight: 800;
letter-spacing: 12px;
color: #2f343a;
}
.menu-sub
{
font-size: 28px;
color: #5c636b;
margin-top: 4px;
margin-bottom: 44px;
}
.menu-btn-row
{
flex-direction: row;
.menu-btn
{
padding: 18px 46px;
margin: 0 14px;
border-radius: 14px;
background-color: rgba(255, 255, 255, 0.94);
color: #343a41;
font-size: 30px;
font-weight: 700;
cursor: pointer;
}
}
.menu-best
{
font-size: 26px;
color: #c8ced6;
margin-top: 34px;
}
}
}
Game
game
using System;
using Sandbox.UI;
// board
/// <summary>
/// 跳一跳 HUD:开始菜单(AI 背景演示)、计分、连击提示、操作提示、新手演示字幕、结束画面、难度角标。
/// 用纯 C# Panel 构建(编辑器热重载不会为新加的 .razor 重新编译,C# 面板可以即改即生效),
/// 样式在同名样式表 JumpHud.cs.scss 里(PanelComponent 会按"类名.cs.scss"自动加载)。
/// </summary>
public sealed class JumpHud : PanelComponent
{
Label _score;
Label _streak;
Label _hint;
Panel _tutorialWrap;
Label _tutorialText;
Panel _gameOver;
Label _gameOverScore;
Panel _menuWrap;
Label _menuBest;
Label _difficultyLabel;
Panel _boardWrap;
Panel[] _localRows;
Label[] _localRank, _localAvatarChar, _localNameLabels, _localScore;
Panel[] _localAvatar;
Panel[] _cloudRows;
Label[] _cloudRank, _cloudAvatarChar, _cloudName, _cloudScore;
Panel[] _cloudAvatar;
Label _localEmpty;
Label _cloudEmpty;
const int BoardRows = 5;
static string _localName;
/// <summary> 本地玩家名字(拿不到就显示"你") </summary>
static string LocalName()
{
if ( _localName is not null ) return _localName;
try { _localName = string.IsNullOrWhiteSpace( Connection.Local?.DisplayName ) ? "You" : Connection.Local.DisplayName; }
catch { _localName = "You"; }
return _localName;
}
static readonly Color[] AvatarColors =
{
new Color( 0.86f, 0.47f, 0.28f ),
new Color( 0.36f, 0.62f, 0.86f ),
new Color( 0.44f, 0.72f, 0.50f ),
new Color( 0.72f, 0.50f, 0.86f ),
new Color( 0.90f, 0.72f, 0.30f ),
};
Label _localTitle;
Label _cloudTitle;
string _menuBoardRequested;
bool _built;
protected override void OnStart()
{
base.OnStart();
BuildUi();
}
void BuildUi()
{
var root = Panel;
if ( root is null ) return;
// 已建好且所有引用有效 → 不重复建。
// 热重载可能保留旧实例(老字段有效但新增字段为 null),或构建中途被打断
// (数组半满),任何一项失效都整体清空重建
var healthy = _built && _score.IsValid() && _menuBest.IsValid() && _menuWrap.IsValid()
&& _tutorialWrap.IsValid() && _localTitle.IsValid() && _cloudTitle.IsValid()
&& _localEmpty.IsValid() && _cloudEmpty.IsValid()
&& ( _localAvatarChar?.All( x => x is not null ) ?? false )
&& ( _localNameLabels?.All( x => x is not null ) ?? false )
&& ( _cloudName?.All( x => x is not null ) ?? false )
&& root.ChildrenCount > 0;
if ( healthy ) return;
root.DeleteChildren( true );
root.Style.Position = PositionMode.Absolute;
root.Style.Left = 0f;
root.Style.Top = 0f;
root.Style.Right = 0f;
root.Style.Bottom = 0f;
root.Style.PointerEvents = PointerEvents.None;
_score = new Label() { Classes = "score" };
_streak = new Label() { Classes = "streak" };
_streak.Style.Display = DisplayMode.None;
_hint = new Label() { Classes = "hint" };
_hint.Text = "Hold Left Mouse to charge · release to jump · hold S to jump back · Q — Menu";
_hint.Style.Display = DisplayMode.None;
_tutorialWrap = new Panel() { Classes = "tutorial" };
_tutorialText = new Label() { Classes = "tutorial-text" };
_tutorialWrap.AddChild( _tutorialText );
_tutorialWrap.Style.Display = DisplayMode.None;
_gameOver = new Panel() { Classes = "gameover" };
_gameOver.Style.Display = DisplayMode.None;
var title = new Label() { Classes = "title" };
title.Text = "GAME OVER";
_gameOver.AddChild( title );
_gameOverScore = new Label() { Classes = "finalscore" };
_gameOver.AddChild( _gameOverScore );
_boardWrap = new Panel() { Classes = "board-wrap" };
var localCol = new Panel() { Classes = "board-col" };
_localTitle = new Label() { Classes = "board-title" };
localCol.AddChild( _localTitle );
_localRows = new Panel[BoardRows];
_localRank = new Label[BoardRows];
_localAvatar = new Panel[BoardRows];
_localAvatarChar = new Label[BoardRows];
_localNameLabels = new Label[BoardRows];
_localScore = new Label[BoardRows];
for ( int i = 0; i < BoardRows; i++ )
{
( _localRows[i], _localRank[i], _localAvatar[i], _localAvatarChar[i], _localNameLabels[i], _localScore[i] ) = MakeBoardRow( localCol );
}
_localEmpty = new Label() { Classes = "board-empty" };
_localEmpty.Style.Display = DisplayMode.None;
localCol.AddChild( _localEmpty );
_boardWrap.AddChild( localCol );
var cloudCol = new Panel() { Classes = "board-col" };
_cloudTitle = new Label() { Classes = "board-title" };
cloudCol.AddChild( _cloudTitle );
_cloudRows = new Panel[BoardRows];
_cloudRank = new Label[BoardRows];
_cloudAvatar = new Panel[BoardRows];
_cloudAvatarChar = new Label[BoardRows];
_cloudName = new Label[BoardRows];
_cloudScore = new Label[BoardRows];
for ( int i = 0; i < BoardRows; i++ )
{
( _cloudRows[i], _cloudRank[i], _cloudAvatar[i], _cloudAvatarChar[i], _cloudName[i], _cloudScore[i] ) = MakeBoardRow( cloudCol );
}
_cloudEmpty = new Label() { Classes = "board-empty" };
_cloudEmpty.Style.Display = DisplayMode.None;
cloudCol.AddChild( _cloudEmpty );
_boardWrap.AddChild( cloudCol );
_gameOver.AddChild( _boardWrap );
var goHint = new Label() { Classes = "sub" };
goHint.Text = "Click to restart · Q — Menu";
_gameOver.AddChild( goHint );
_menuWrap = new Panel() { Classes = "menu" };
var menuTitle = new Label() { Classes = "menu-title" };
menuTitle.Text = "Jump Jump";
_menuWrap.AddChild( menuTitle );
var menuSub = new Label() { Classes = "menu-sub" };
menuSub.Text = "Select Difficulty · press 1 / 2 / 3";
_menuWrap.AddChild( menuSub );
var menuBtnRow = new Panel() { Classes = "menu-btn-row" };
AddMenuButton( menuBtnRow, "Easy", JumpDifficulty.Easy );
AddMenuButton( menuBtnRow, "Normal", JumpDifficulty.Normal );
AddMenuButton( menuBtnRow, "Hard", JumpDifficulty.Hard );
_menuWrap.AddChild( menuBtnRow );
_menuBest = new Label() { Classes = "menu-best" };
_menuWrap.AddChild( _menuBest );
_difficultyLabel = new Label() { Classes = "diff" };
root.AddChild( _score );
root.AddChild( _streak );
root.AddChild( _hint );
root.AddChild( _tutorialWrap );
root.AddChild( _gameOver );
root.AddChild( _difficultyLabel );
root.AddChild( _menuWrap );
_built = true;
}
void AddMenuButton( Panel parent, string text, JumpDifficulty difficulty )
{
var btn = new Label() { Classes = "menu-btn" };
btn.Text = text;
btn.AddEventListener( "onclick", () =>
{
Sound.Play( "sounds/kenney/ui/ui.button.press.sound" );
JumpGame.Current?.StartGame( difficulty );
} );
parent.AddChild( btn );
}
static (Panel row, Label rank, Panel avatar, Label avatarChar, Label name, Label score) MakeBoardRow( Panel parent )
{
var row = new Panel() { Classes = "board-row" };
var rank = new Label() { Classes = "cell-rank" };
row.AddChild( rank );
var avatar = new Panel() { Classes = "avatar" };
var avatarChar = new Label() { Classes = "avatar-char" };
avatar.AddChild( avatarChar );
row.AddChild( avatar );
var name = new Label() { Classes = "cell-name" };
row.AddChild( name );
var score = new Label() { Classes = "cell-score" };
row.AddChild( score );
parent.AddChild( row );
return ( row, rank, avatar, avatarChar, name, score );
}
static void SetVisible( Panel panel, bool visible )
{
panel.Style.Display = visible ? DisplayMode.Flex : DisplayMode.None;
}
static string DifficultyName( JumpDifficulty difficulty ) => difficulty switch
{
JumpDifficulty.Easy => "Easy",
JumpDifficulty.Normal => "Normal",
_ => "Hard"
};
protected override void OnUpdate()
{
BuildUi();
var game = JumpGame.Current;
if ( game is null || !_built || _score is null || _tutorialText is null || _localAvatarChar is null ) return;
_score.Text = game.Score.ToString();
_streak.Text = $"PERFECT ×{game.Streak}";
_gameOverScore.Text = game.Score.ToString();
_tutorialText.Text = game.TutorialText ?? "";
var inMenu = game.InMenu;
_difficultyLabel.Text = DifficultyName( game.Difficulty ) + " · Q — Menu";
// 回到菜单时拉一次云端榜(异步、尽力而为);键上带难度,换难度就重拉
var boardKey = $"{inMenu}:{game.Difficulty}";
if ( inMenu && _menuBoardRequested != boardKey )
{
_menuBoardRequested = boardKey;
_ = JumpScores.RefreshCloud( game.Difficulty );
}
else if ( !inMenu )
{
_menuBoardRequested = null;
}
// 菜单里的"最佳"跟当前难度(难度按钮/数字键立即改写 game.Difficulty)。
// 原来用的是不分难度的 _scores[0],显示的是别的难度的最高分
var best = JumpScores.Best( game.Difficulty );
_menuBest.Text = best > 0 ? $"{DifficultyName( game.Difficulty )} Best {best}" : "Set your first record!";
// 开始菜单盖住游戏 HUD,只保留背景演示
SetVisible( _menuWrap, inMenu );
SetVisible( _score, !game.IsGameOver && !inMenu );
SetVisible( _streak, !game.IsGameOver && game.Streak >= 2 && !inMenu );
SetVisible( _hint, !game.HasJumped && !game.IsGameOver && !game.IsTutorial && !inMenu );
SetVisible( _tutorialWrap, !inMenu && !string.IsNullOrEmpty( game.TutorialText ) );
SetVisible( _gameOver, game.IsGameOver && !inMenu );
SetVisible( _difficultyLabel, !inMenu );
// 结算画面显示排行榜:左本地 / 右平台,两边都按当前难度取
var diffName = DifficultyName( game.Difficulty );
_localTitle.Text = $"{diffName} · Local Best";
_cloudTitle.Text = $"{diffName} · Global Best";
// 只显示当前难度的本地排行(头像块 = 本地玩家)
try
{
var top = JumpScores.Top( BoardRows, game.Difficulty );
for ( int i = 0; i < BoardRows; i++ )
{
var row = _localRows[i];
if ( i < top.Count )
{
var e = top[i];
var name = LocalName();
row.Style.Display = DisplayMode.Flex;
_localRank[i].Text = $"{i + 1}.";
_localAvatarChar[i].Text = name.Substring( 0, 1 );
_localAvatar[i].Style.BackgroundColor = AvatarColors[Math.Abs( name.GetHashCode() ) % AvatarColors.Length];
_localNameLabels[i].Text = name;
_localScore[i].Text = e.Score.ToString();
}
else
{
row.Style.Display = DisplayMode.None;
}
}
_localEmpty.Text = "No records yet";
_localEmpty.Style.Display = top.Count == 0 ? DisplayMode.Flex : DisplayMode.None;
}
catch ( Exception )
{
_built = false; // 结构异常 → 下帧整体重建
}
// 平台排行:本地包没有云端身份、或该难度还没拉到时显示占位。
// 取的是按难度缓存的列表,不会串到别的难度
var cloud = JumpScores.Cloud( game.Difficulty );
if ( cloud.Count == 0 )
{
for ( int i = 0; i < BoardRows; i++ ) _cloudRows[i].Style.Display = DisplayMode.None;
_cloudEmpty.Text = "No records yet";
_cloudEmpty.Style.Display = DisplayMode.Flex;
}
else
{
_cloudEmpty.Style.Display = DisplayMode.None;
for ( int i = 0; i < BoardRows; i++ )
{
var row = _cloudRows[i];
if ( i < cloud.Count )
{
var (rank, name, score) = cloud[i];
row.Style.Display = DisplayMode.Flex;
_cloudRank[i].Text = $"{rank}.";
_cloudAvatarChar[i].Text = string.IsNullOrEmpty( name ) ? "?" : name.Substring( 0, 1 );
_cloudAvatar[i].Style.BackgroundColor = AvatarColors[Math.Abs( name.GetHashCode() ) % AvatarColors.Length];
_cloudName[i].Text = name;
_cloudScore[i].Text = score.ToString();
}
else
{
row.Style.Display = DisplayMode.None;
}
}
}
}
}
Game
game
using System;
/// <summary>
/// 跳一跳主逻辑:平台生成、蓄力跳跃、落点判定与计分。
/// GameObjectSystem 会在场景加载时自动实例化(无需挂载到场景),
/// 但编辑器的编辑模式下也会 tick,所以所有逻辑都用 Game.IsPlaying 拦截。
/// 整个世界在运行时生成,不依赖场景里已有的内容(模板对象会被屏蔽)。
/// </summary>
public sealed class JumpGame : GameObjectSystem<JumpGame>
{
// ---- 手感参数(可在 项目设置 → Systems 里覆盖)----
/// <summary> 蓄力从 0 到 1 需要的秒数 </summary>
[Property] public float ChargeTime { get; set; } = 2.0f;
/// <summary> 轻点时的跳跃距离 </summary>
[Property] public float MinJumpDistance { get; set; } = 130f;
/// <summary>
/// 蓄满时的跳跃距离。必须贴着 CenterDistance.y(平台中心距上限)取值:
/// 平台最远只生成到 470,若蓄满能飞 780,则蓄力条有近一半的量程永远不该被用到,
/// 而多蓄一点点就直接飞过头。取 470 + 平台半宽 + 判定余量 ≈ 560,
/// 蓄满正好是"最远的平台擦边够到",整条蓄力条都有用。
/// </summary>
[Property] public float MaxJumpDistance { get; set; } = 560f;
/// <summary> 落点距平台中心小于该值算"完美",触发连击加分 </summary>
[Property] public float PerfectRadius { get; set; } = 20f;
/// <summary> 平台顶面半宽的随机范围 </summary>
[Property] public Vector2 PlatformHalfSize { get; set; } = new Vector2( 48, 90 );
/// <summary> 相邻平台中心距离的随机范围 </summary>
[Property] public Vector2 CenterDistance { get; set; } = new Vector2( 250, 470 );
/// <summary> 每一步方向的随机偏转角(度,左右交替施加) </summary>
[Property] public Vector2 TurnAngle { get; set; } = new Vector2( 14, 34 );
// ---- 常量 ----
const float KillDepth = 420f; // 低于附近平台顶面多少算坠落出局
const float Gravity = 2400f; // 自由落体阶段的重力(仅用于坠落表现)
static readonly Color[] PlatformColors =
{
new Color( 0.98f, 0.74f, 0.62f ),
new Color( 0.64f, 0.86f, 0.74f ),
new Color( 0.99f, 0.88f, 0.58f ),
new Color( 0.66f, 0.80f, 0.95f ),
new Color( 0.93f, 0.77f, 0.90f ),
new Color( 0.96f, 0.96f, 0.94f ),
};
// ---- 对外状态(HUD / 相机读取)----
public int Score { get; private set; }
public int Streak { get; private set; }
public bool HasJumped { get; private set; }
public bool IsGameOver { get; private set; }
public bool IsFalling => _falling;
public bool IsTutorial => _tutorialActive;
/// <summary> 当前难度(开始菜单选择) </summary>
public JumpDifficulty Difficulty { get; private set; } = JumpDifficulty.Normal;
/// <summary> 是否在开始菜单(背景由 AI 自动演示跳跃) </summary>
public bool InMenu => _menuMode;
/// <summary> 新手演示字幕(非空时 HUD 显示) </summary>
public string TutorialText { get; private set; }
public Vector3 PiecePosition { get; private set; }
// ---- 内部状态 ----
GameObject _root;
JumpPiece _piece;
JumpCamera _camera;
readonly List<JumpPlatform> _platforms = new();
readonly List<(GameObject Go, TimeSince Born, Vector3 Scale)> _dots = new();
static GameObject _sharedRoot; // 热重载/重建时复用同一个世界根,避免堆叠多套场景
JumpPlatform _current;
JumpPlatform _target;
JumpPlatform _previous; // 身后那一块(倒跳目标)
bool _worldReady;
bool _wasDown;
bool _charging;
float _power;
float _dirAngle;
float _turnSide = 1f;
bool _menuMode = true; // 开局进入开始菜单,背景 AI 自动跳
// 简单模式辅助线(蓄力时显示预测弹道与落点环)
const int AssistDotCount = 14;
static readonly Color AssistHitColor = new Color( 0.30f, 0.78f, 0.42f );
static readonly Color AssistMissColor = new Color( 0.92f, 0.32f, 0.28f );
readonly List<GameObject> _assistDots = new();
GameObject _assistRing;
ModelRenderer _assistRingRenderer;
bool _assistVisible;
static readonly Model BoxModel = Model.Load( "models/dev/box.vmdl" );
// 新手演示
bool _tutorialActive;
bool _tutorialDone; // 每个 play 会话只演示一次,重开不再播
bool _waitingForInput; // 开局等待期:玩家 3 秒内按下则跳过演示,闲置则播放
TimeSince _sinceSpawn;
float _demoPower;
TimeSince _tutorialT;
TimeSince _sinceTutorialEnd;
TimeSince _dotTimer;
static readonly Model SphereModel = Model.Load( "models/dev/sphere.vmdl" );
// 飞行状态
bool _flying;
bool _falling;
Vector3 _jumpFrom;
Vector3 _jumpTo;
float _jumpTime;
float _jumpDuration;
float _jumpHeight;
float _prevBaseZ;
float _killZ;
Vector3 _fallVelocity;
public JumpGame( Scene scene ) : base( scene )
{
Listen( Stage.UpdateBones, 0, Tick, "JumpGame.Tick" );
}
void Tick()
{
if ( !Game.IsPlaying ) return;
EnsureWorld();
var down = Input.Down( "Attack1" ) || Input.Down( "Jump" );
// Q:从游戏 / 结算画面返回开始菜单
if ( !InMenu && Input.Pressed( "Menu" ) )
{
EnterMenu();
return;
}
if ( IsGameOver )
{
if ( down && !_wasDown ) ResetGame();
_wasDown = down;
return;
}
if ( _piece.IsValid() ) PiecePosition = _piece.BasePosition;
TickTrailDots();
UpdateAssistLine();
if ( _flying )
{
TickFlight( Time.Delta );
_wasDown = down;
return;
}
// 开始菜单背景 / 新手演示:AI 自动表演"蓄力→起跳→落点"
if ( _menuMode || _tutorialActive )
{
// 菜单里支持键盘选难度(数字键 1/2/3)
if ( _menuMode )
{
if ( Input.Pressed( "Slot1" ) ) { StartGame( JumpDifficulty.Easy ); return; }
if ( Input.Pressed( "Slot2" ) ) { StartGame( JumpDifficulty.Normal ); return; }
if ( Input.Pressed( "Slot3" ) ) { StartGame( JumpDifficulty.Hard ); return; }
}
TickAutoJump( Time.Delta );
_wasDown = false;
return;
}
if ( TutorialText != null && _sinceTutorialEnd > 2.4f ) TutorialText = null;
// 开局 3 秒观察窗:玩家自己按下 → 跳过演示;一直闲置 → 播放演示
if ( _waitingForInput )
{
if ( down && !_wasDown )
{
_waitingForInput = false;
_tutorialDone = true;
}
else if ( _sinceSpawn > 3f )
{
_waitingForInput = false;
StartTutorial();
return;
}
}
// 地面状态:按下蓄力,松开起跳
if ( down && !_wasDown )
{
_charging = true;
_power = 0f;
}
if ( _charging )
{
if ( down )
{
_power = Math.Min( _power + Time.Delta / ChargeTime, 1f );
_piece?.SetCharge( _power );
}
else
{
_charging = false;
Launch();
}
}
_wasDown = down;
}
// ---- 世界搭建 ----
void EnsureWorld()
{
if ( _worldReady ) return;
_worldReady = true;
// 复用旧的世界根(热重载等场景下避免堆叠多套平台/相机),并清掉其动态子物体。
// 注意:Play 停止后 static 引用可能指向已卸载的旧场景对象(IsValid 仍为真),
// 必须校验场景归属,否则向旧场景根挂子物体直接断言失败
if ( _sharedRoot.IsValid() && _sharedRoot.Scene == Scene )
{
_root = _sharedRoot;
foreach ( var child in _root.Children.ToArray() )
{
if ( child.IsValid() ) child.Destroy();
}
}
else
{
_sharedRoot = null;
_root = new GameObject( true, "JumpGame" );
_sharedRoot = _root;
// 屏蔽场景里模板留下的所有对象(示例相机、方块等),只跑我们生成的世界。
// Scene 本身就是根 GameObject,顶层对象都在它的 Children 里
foreach ( var go in Scene.Children )
{
if ( go.IsValid() && go != _root ) go.Enabled = false;
}
}
CreateLight();
CreateCamera();
CreateHud();
BuildInitialWorld();
}
void CreateLight()
{
var sun = new GameObject( true, "Sun" );
sun.Parent = _root;
sun.WorldRotation = Rotation.LookAt( new Vector3( -0.5f, -0.6f, -1.1f ).Normal );
var light = sun.AddComponent<DirectionalLight>();
light.LightColor = new Color( 1f, 0.96f, 0.90f );
light.Shadows = true;
light.ShadowCascadeCount = 2;
var ambientGo = new GameObject( true, "Ambient" );
ambientGo.Parent = _root;
ambientGo.AddComponent<AmbientLight>().Color = new Color( 0.48f, 0.52f, 0.60f );
}
void CreateCamera()
{
var go = new GameObject( true, "JumpCamera" );
go.Parent = _root;
var cam = go.AddComponent<CameraComponent>();
cam.IsMainCamera = true;
cam.Priority = -100;
cam.FieldOfView = 55f;
cam.BackgroundColor = new Color( 0.80f, 0.81f, 0.83f );
_camera = go.AddComponent<JumpCamera>();
}
void CreateHud()
{
var go = new GameObject( true, "Hud" );
go.Parent = _root;
go.AddComponent<ScreenPanel>();
go.AddComponent<JumpHud>();
}
void BuildInitialWorld()
{
Score = 0;
Streak = 0;
HasJumped = false;
IsGameOver = false;
_charging = false;
_power = 0f;
_flying = false;
_falling = false;
_dirAngle = 0f;
foreach ( var p in _platforms ) p.DestroyAll();
_platforms.Clear();
_target = null;
_previous = null;
if ( _piece.IsValid() ) _piece.GameObject.Destroy();
var first = CreatePlatform( Vector2.Zero, 80f, 80f, 100f, PlatformColors[0] );
_current = first;
var pieceGo = new GameObject( true, "Piece" );
pieceGo.Parent = _root;
_piece = pieceGo.AddComponent<JumpPiece>();
_piece.Init( first );
// 开局:当前平台就是第一块,身后没有平台,所以没有倒跳目标
_previous = null;
SpawnNextPlatform();
_camera?.SnapTo( PiecePosition = _piece.BasePosition );
// 菜单背景演示 / 开局引导(本会话第一次进入时先等 3 秒:
// 玩家 3 秒内自己按下 → 跳过演示直接玩;一直闲置 → 自动播放演示)
if ( _menuMode )
{
_waitingForInput = false;
TutorialText = null;
_tutorialT = 0;
_demoPower = ComputeDemoPower();
}
else if ( _tutorialDone )
{
TutorialText = null;
_waitingForInput = false;
}
else
{
_waitingForInput = true;
_sinceSpawn = 0;
TutorialText = null;
}
}
/// <summary> 计算让棋子正好落在目标平台中心的蓄力值 </summary>
float ComputeDemoPower()
{
if ( !_target.IsValid() || !_piece.IsValid() ) return 0.35f;
var dx = _target.CenterX - _piece.BasePosition.x;
var dy = _target.CenterY - _piece.BasePosition.y;
var dist = MathF.Sqrt( dx * dx + dy * dy );
return Math.Clamp( ( dist - MinJumpDistance ) / ( MaxJumpDistance - MinJumpDistance ), 0.05f, 1f );
}
/// <summary> 进入开始菜单(背景 AI 演示) </summary>
public void EnterMenu()
{
_menuMode = true;
_tutorialActive = false;
ResetGame();
// 菜单遮罩会在下一帧 HUD 刷新时才显示,"返回菜单"这一下点击不该顺手把蓄力也开始了
if ( Input.Down( "Attack1" ) ) _wasDown = true;
}
/// <summary> 从开始菜单选择难度进入正式游戏 </summary>
public void StartGame( JumpDifficulty difficulty )
{
Difficulty = difficulty;
_menuMode = false;
_tutorialActive = false;
ResetGame();
// 按钮是在鼠标按下/松开那一刻被点中的,那一次点击用来选难度,
// 不置位的话松手会被当成"蓄力结束"立刻起跳一次
if ( Input.Down( "Attack1" ) ) _wasDown = true;
Sound.Play( "sounds/kenney/ui/ui.navigate.forward.sound", PiecePosition, 0f );
}
/// <summary> 开局演示:AI 自动表演一次蓄力跳跃,落点对准目标中心 </summary>
void StartTutorial()
{
_tutorialDone = true;
_tutorialActive = true;
_tutorialT = 0;
TutorialText = "Watch! Hold to charge, release to jump";
_demoPower = ComputeDemoPower();
}
void TickAutoJump( float dt )
{
var t = (float)_tutorialT;
if ( t < 0.9f )
{
// 先停顿一拍,让玩家看清开局
}
else if ( t < 1.7f )
{
// 模拟蓄力:力度爬升到"刚好能落在中心"的值
_power = Math.Min( ( t - 0.9f ) / 0.8f, 1f ) * _demoPower;
_piece?.SetCharge( _power );
}
else
{
Launch();
}
}
/// <summary> 演示跳跃的轨迹点:棋子飞过的地方留下小圆点并渐渐消散 </summary>
void SpawnTrailDot( Vector3 basePos )
{
var go = new GameObject( true, "TrailDot" );
go.Parent = _root;
var r = go.AddComponent<ModelRenderer>();
r.Model = SphereModel;
r.Tint = new Color( 0.25f, 0.28f, 0.34f );
var size = SphereModel.Bounds.Size.x;
var scale = 14f / size;
var dotScale = new Vector3( scale, scale, scale );
go.LocalScale = dotScale;
go.WorldPosition = basePos + Vector3.Up * ( SphereModel.Bounds.Size.z * scale * 0.5f + 2f );
_dots.Add( ( go, 0, dotScale ) );
}
void TickTrailDots()
{
for ( int i = _dots.Count - 1; i >= 0; i-- )
{
var dot = _dots[i];
var age = (float)dot.Born;
if ( age >= 0.9f || !dot.Go.IsValid() )
{
if ( dot.Go.IsValid() ) dot.Go.Destroy();
_dots.RemoveAt( i );
continue;
}
dot.Go.LocalScale = dot.Scale * ( 1f - age / 0.9f );
}
}
void ResetGame()
{
BuildInitialWorld();
}
JumpPlatform CreatePlatform( Vector2 center, float halfX, float halfY, float topZ, Color color )
{
var go = new GameObject( true, "Platform" );
go.Parent = _root;
var platform = go.AddComponent<JumpPlatform>();
platform.Init( center, halfX, halfY, topZ, color );
_platforms.Add( platform );
return platform;
}
/// <summary>
/// 在当前平台前方生成下一块。左右严格交替、相对主轴(+X)定角(不累加),
/// 保证路线像原版一样蛇形前进、下一块平台始终留在画面内。
/// </summary>
void SpawnNextPlatform()
{
var prev = _current;
_turnSide = -_turnSide;
_dirAngle = _turnSide * Game.Random.Float( TurnAngle.x, TurnAngle.y ) * MathF.PI / 180f;
var dist = Game.Random.Float( CenterDistance.x, CenterDistance.y );
var px = prev.CenterX + MathF.Cos( _dirAngle ) * dist;
var py = prev.CenterY + MathF.Sin( _dirAngle ) * dist;
var halfX = Game.Random.Float( PlatformHalfSize.x, PlatformHalfSize.y );
var halfY = Game.Random.Float( PlatformHalfSize.x, PlatformHalfSize.y );
var topZ = Math.Clamp( prev.TopZ + Game.Random.Float( -50f, 60f ), 40f, 210f );
if ( _target.IsValid() ) _target.IsTarget = false;
_target = CreatePlatform( new Vector2( px, py ), halfX, halfY, topZ,
Game.Random.FromArray( PlatformColors ) );
_target.IsTarget = true;
}
void PruneOldPlatforms()
{
// 从最旧的一块开始扫,删掉第一块"已经没用了"的平台。
// 仍在用的(当前位置 / 目标 / 身后那一块)只是跳过,不能挪到队尾——
// 一旦打乱"最旧在前"的顺序,垃圾就永远排不到队头,池子会无上限地长。
for ( int i = 0; i < _platforms.Count && _platforms.Count > 3; i++ )
{
var p = _platforms[i];
if ( p == _current || p == _target || p == _previous ) continue;
_platforms.RemoveAt( i );
p.DestroyAll();
i--;
}
}
// ---- 跳跃与判定 ----
/// <summary>
/// 玩家是否想要倒着跳:只按后退键(只按前进、两个都按、都不按,一律视为前进)。
/// 菜单背景演示和新手演示由 AI 驱动,必须始终往前跳,所以这里一并排除。
/// </summary>
bool WantsBackJump => !_menuMode && !_tutorialActive
&& Input.Down( "Backward" ) && !Input.Down( "Forward" );
/// <summary> 依据蓄力值计算一次跳跃的落点/时长/高度(起跳与辅助线共用) </summary>
(Vector3 to, float duration, float height) PlanJump( float power )
{
var from = _piece.BasePosition;
// 只按后退 = 在"来路方向"上按当前力度出手(力度合适就弹回上一块平台)
var origin = WantsBackJump && _previous.IsValid() ? _previous : _target;
var dir = new Vector2( origin.CenterX - from.x, origin.CenterY - from.y ).Normal;
var dist = Math.Min( MinJumpDistance + power * ( MaxJumpDistance - MinJumpDistance ), MaxJumpDistance );
return ( new Vector3( from.x + dir.x * dist, from.y + dir.y * dist, from.z ),
0.42f + 0.22f * power,
150f + 60f * power );
}
void Launch()
{
if ( _target is null || !_piece.IsValid() ) return;
HasJumped = true;
var (to, duration, height) = PlanJump( _power );
_jumpFrom = _piece.BasePosition;
_jumpTo = to;
_jumpDuration = duration;
_jumpHeight = height;
_jumpTime = 0f;
_prevBaseZ = _jumpFrom.z;
_killZ = Math.Min( _current.TopZ, _target.TopZ ) - KillDepth;
_flying = true;
_falling = false;
_piece.SetCharge( 0f );
_piece.Impulse( 3.4f );
Sound.Play( "sounds/footsteps/footstep-concrete-jump.sound", _jumpFrom, 0f );
if ( !_menuMode && !_tutorialActive ) JumpAchievements.OnJump();
}
void TickFlight( float dt )
{
if ( !_piece.IsValid() ) return;
if ( !_falling )
{
_jumpTime += dt;
var u = Math.Min( _jumpTime / _jumpDuration, 1f );
var x = MathX.Lerp( _jumpFrom.x, _jumpTo.x, u );
var y = MathX.Lerp( _jumpFrom.y, _jumpTo.y, u );
// 抛物线:起点 0、终点 0、顶点 jumpHeight
var z = _jumpFrom.z + 4f * _jumpHeight * u * ( 1f - u );
_piece.SetBasePosition( new Vector3( x, y, z ) );
// 演示跳跃时留下轨迹点,标出抛物线
if ( _tutorialActive && _dotTimer > 0.055f )
{
_dotTimer = 0;
SpawnTrailDot( new Vector3( x, y, z ) );
}
// 下降阶段做落点判定(帧间跨过平台顶面才算落上,任何帧率下都准确)
if ( z <= _prevBaseZ )
{
ResolveLanding( x, y, z, _prevBaseZ );
}
_prevBaseZ = z;
// 抛物线走完仍没落到任何平台 → 转自由落体
if ( u >= 1f && !_falling && _flying )
{
_falling = true;
_fallVelocity = new Vector3(
( _jumpTo.x - _jumpFrom.x ) / _jumpDuration,
( _jumpTo.y - _jumpFrom.y ) / _jumpDuration,
-4f * _jumpHeight / _jumpDuration );
}
}
if ( _falling )
{
_fallVelocity -= Vector3.Up * Gravity * dt;
var pos = _piece.BasePosition + _fallVelocity * dt;
_piece.SetBasePosition( pos );
ResolveLanding( pos.x, pos.y, pos.z, pos.z - _fallVelocity.z * dt );
if ( pos.z < _killZ )
{
if ( _menuMode )
{
ResetGame();
return;
}
OnGameOver();
}
}
}
void ResolveLanding( float x, float y, float baseZ, float prevBaseZ )
{
// 先判"正好跨过某个平台顶面"→ 成功落上
foreach ( var p in LandablePlatforms() )
{
if ( !InsidePlatform( p, x, y ) ) continue;
if ( prevBaseZ >= p.TopZ && baseZ <= p.TopZ )
{
LandOn( p, x, y );
return;
}
}
// 已经低于平台顶面还处在平台投影内 → 撞侧面,转坠落(游戏失败)
if ( _flying && !_falling )
{
foreach ( var p in LandablePlatforms() )
{
if ( !InsidePlatform( p, x, y ) ) continue;
if ( baseZ < p.TopZ )
{
_falling = true;
if ( !_menuMode )
Sound.Play( "sounds/player_use_fail.sound", new Vector3( x, y, baseZ ), 0f );
var horizontal = _jumpDuration > 0f
? new Vector3( ( _jumpTo.x - _jumpFrom.x ) / _jumpDuration, ( _jumpTo.y - _jumpFrom.y ) / _jumpDuration, 0 )
: Vector3.Zero;
_fallVelocity = horizontal + Vector3.Up * ( -4f * _jumpHeight * ( 2f * Math.Min( _jumpTime / _jumpDuration, 1f ) - 1f ) / _jumpDuration );
return;
}
}
}
}
IEnumerable<JumpPlatform> LandablePlatforms()
{
if ( _target.IsValid() ) yield return _target;
if ( _current.IsValid() && _current != _target ) yield return _current;
if ( _previous.IsValid() && _previous != _target && _previous != _current ) yield return _previous;
}
bool InsidePlatform( JumpPlatform p, float x, float y )
{
return MathF.Abs( x - p.CenterX ) <= p.HalfX + LandingMargin()
&& MathF.Abs( y - p.CenterY ) <= p.HalfY + LandingMargin();
}
float LandingMargin() => _piece.IsValid() ? _piece.Radius * 0.55f : 14f;
void LandOn( JumpPlatform p, float x, float y )
{
_flying = false;
_falling = false;
// 困难模式:落点不吸附平台中心,棋子停在真实落点
if ( Difficulty == JumpDifficulty.Hard )
_piece.SetBasePosition( new Vector3( x, y, p.TopZ ) );
else
_piece.PlaceOn( p );
_piece.Impulse( -3.4f );
if ( p == _target )
{
if ( _menuMode )
{
// 背景演示不计分;力度在新目标生成后重算(见下方 SpawnNextPlatform 之后)
_tutorialT = 0;
}
else if ( _tutorialActive )
{
// 演示落点不计分,把控制权交还玩家
_tutorialActive = false;
_sinceTutorialEnd = 0;
TutorialText = "Your turn! Hold & release";
}
else
{
var centerDist = MathF.Sqrt( ( x - p.CenterX ) * ( x - p.CenterX ) + ( y - p.CenterY ) * ( y - p.CenterY ) );
if ( centerDist <= PerfectRadius )
{
Streak++;
Score += Math.Min( Streak * 2, 16 );
var handle = Sound.Play( "sounds/kenney/ui/ui.upvote.sound", p.Center, 0f );
if ( handle.IsValid() ) handle.Pitch = 1f + 0.1f * Math.Min( Streak - 1, 8 );
JumpAchievements.OnPerfect( Streak );
}
else
{
Streak = 0;
Score += 1;
Sound.Play( "sounds/kenney/ui/ui.downvote.sound", p.Center, 0f );
JumpAchievements.OnLanding();
}
}
// 往前跳:刚离开的平台变成"身后那一块"(倒跳目标),更早的那块已无用。
// 顺序要紧:SpawnNextPlatform 是从 _current 往前生成,所以先记下旧值再改 _current
var origin = _current;
_previous = origin;
_current = p;
p.IsTarget = false;
_target = null;
SpawnNextPlatform();
PruneOldPlatforms();
// 新目标生成后再计算演示力度,保证背景 AI 每一跳都成功落在中心
if ( _menuMode )
{
_tutorialT = 0;
_demoPower = ComputeDemoPower();
}
}
else if ( p == _previous )
{
// 倒着弹回上一块平台:计分规则一致,然后把"来路"接回来当新的前进方向
Sound.Play( "sounds/kenney/ui/ui.navigate.back.sound", p.Center, 0f );
if ( !_menuMode && !_tutorialActive )
{
var centerDist = MathF.Sqrt( ( x - p.CenterX ) * ( x - p.CenterX ) + ( y - p.CenterY ) * ( y - p.CenterY ) );
if ( centerDist <= PerfectRadius )
{
Streak++;
Score += Math.Min( Streak * 2, 16 );
}
else
{
Streak = 0;
Score += 1;
}
JumpAchievements.OnBackJump();
}
// 正在 P2 站着、往前目标 P3,倒跳落回 P1:
// 新的前进方向是刚离开的 P2(正好还能再跳一次),P1 身后没有记录,倒跳到此为止。
// 不必重新生成平台——只是把当前所处的平台挪一下
var forward = _current;
if ( _target.IsValid() ) _target.IsTarget = false;
_previous = null;
_current = p;
_target = forward;
p.IsTarget = true;
}
else
{
// 力度不足落回当前所站的那一块:状态完全不变,只用回退音效区分
Sound.Play( "sounds/kenney/ui/ui.navigate.back.sound", p.Center, 0f );
}
}
// ---- 简单模式辅助线 ----
void UpdateAssistLine()
{
// 玩家蓄力(简单模式)或新手教学演示蓄力时显示;开始菜单背景不显示
var autoCharging = _tutorialActive && _power > 0.01f;
var want = ( _charging || autoCharging ) && !IsGameOver
&& ( Difficulty == JumpDifficulty.Easy || _tutorialActive )
&& _target.IsValid() && _piece.IsValid();
if ( !want )
{
if ( _assistVisible ) HideAssist();
return;
}
EnsureAssistObjects();
var (to, _, height) = PlanJump( _power );
var from = _piece.BasePosition;
for ( int i = 0; i < AssistDotCount; i++ )
{
var u = ( i + 1 ) / (float)AssistDotCount;
var x = MathX.Lerp( from.x, to.x, u );
var y = MathX.Lerp( from.y, to.y, u );
var z = from.z + 4f * height * u * ( 1f - u );
_assistDots[i].WorldPosition = new Vector3( x, y, z + SphereModel.Bounds.Size.z * 0.06f );
_assistDots[i].Enabled = true;
}
// 落点环:会落在平台上就贴其顶面并变绿,否则悬空变红
var surface = from.z;
var hit = false;
foreach ( var p in LandablePlatforms() )
{
if ( InsidePlatform( p, to.x, to.y ) )
{
surface = p.TopZ;
hit = true;
break;
}
}
_assistRing.WorldPosition = new Vector3( to.x, to.y, surface + 2f );
_assistRingRenderer.Tint = hit ? AssistHitColor : AssistMissColor;
_assistRing.Enabled = true;
_assistVisible = true;
}
void EnsureAssistObjects()
{
if ( _assistDots.Count == AssistDotCount && _assistDots.All( d => d.IsValid() ) && _assistRing.IsValid() ) return;
HideAssist();
_assistDots.Clear();
var dotScale = 9f / SphereModel.Bounds.Size.x;
for ( int i = 0; i < AssistDotCount; i++ )
{
var go = new GameObject( false, "AssistDot" );
go.Parent = _root;
go.LocalScale = new Vector3( dotScale, dotScale, dotScale );
var r = go.AddComponent<ModelRenderer>();
r.Model = SphereModel;
r.Tint = new Color( 0.30f, 0.34f, 0.42f );
_assistDots.Add( go );
}
_assistRing = new GameObject( false, "AssistRing" );
_assistRing.Parent = _root;
var bs = BoxModel.Bounds.Size;
_assistRing.LocalScale = new Vector3( 30f / bs.x, 30f / bs.y, 2.2f / bs.z );
_assistRingRenderer = _assistRing.AddComponent<ModelRenderer>();
_assistRingRenderer.Model = BoxModel;
}
void HideAssist()
{
foreach ( var dot in _assistDots )
{
if ( dot.IsValid() ) dot.Enabled = false;
}
if ( _assistRing.IsValid() ) _assistRing.Enabled = false;
_assistVisible = false;
}
void OnGameOver()
{
if ( IsGameOver ) return;
IsGameOver = true;
if ( !_menuMode )
{
Sound.Play( "sounds/kenney/ui/ui.button.deny.sound", PiecePosition, 0f );
JumpScores.Record( Score, Difficulty );
JumpAchievements.OnGameEnd( Score, Difficulty );
}
}
}
Debug: View Raw JSON Response
{
"TotalCount": 11,
"Files": [
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpPiece.cs",
"FileName": "JumpPiece.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "using System;\n\n/// <summary>\n/// \u8df3\u4e00\u8df3\u7684\"\u68cb\u5b50\"\uff1a\u7403\u4f53\u6a21\u578b\uff0c\u4f4d\u7f6e\u7531 JumpGame \u9a71\u52a8\uff08BasePosition = \u6a21\u578b\u5e95\u90e8\u4e2d\u5fc3\uff09\uff0c\n/// \u8fd9\u91cc\u53ea\u8d1f\u8d23\u84c4\u529b\u538b\u6241 / \u8d77\u8df3\u62c9\u4f38 / \u843d\u5730\u56de\u5f39\u7684\u5f62\u53d8\u8868\u73b0\u3002\n/// </summary>\npublic sealed class JumpPiece : Component\n{\n\tconst float PieceDiameter = 54f;\n\tconst float Stiffness = 170f;\n\tconst float Damping = 15f;\n\n\t/// <summary> \u68cb\u5b50\u534a\u5f84\uff08\u4e16\u754c\u5355\u4f4d\uff09 </summary>\n\tpublic float Radius { get; private set; } = 27f;\n\n\tModelRenderer _renderer;\n\tVector3 _baseScale = Vector3.One;\n\tfloat _charge;\n\tfloat _squash;\n\tfloat _squashVel;\n\n\t/// <summary> \u6a21\u578b\u5e95\u90e8\u4e2d\u5fc3\u7684\u4e16\u754c\u5750\u6807 </summary>\n\tpublic Vector3 BasePosition => GameObject.WorldPosition - Vector3.Up * Radius;\n\n\tpublic void SetBasePosition( Vector3 basePos ) => GameObject.WorldPosition = basePos + Vector3.Up * Radius;\n\n\t/// <summary> \u653e\u5230\u67d0\u4e2a\u5e73\u53f0\u7684\u4e2d\u5fc3\uff08\u843d\u5730\u5438\u9644\u7528\uff09 </summary>\n\tpublic void PlaceOn( JumpPlatform platform ) => SetBasePosition( new Vector3( platform.CenterX, platform.CenterY, platform.TopZ ) );\n\n\tpublic void Init( JumpPlatform platform )\n\t{\n\t\t_renderer = GameObject.AddComponent<ModelRenderer>();\n\t\t_renderer.Model = Model.Load( \"models/dev/sphere.vmdl\" );\n\t\t_renderer.Tint = new Color( 0.16f, 0.17f, 0.20f );\n\n\t\tvar size = _renderer.Model.Bounds.Size;\n\t\tvar s = PieceDiameter / size.x;\n\t\t_baseScale = new Vector3( s, s, s );\n\t\tRadius = size.z * s * 0.5f;\n\n\t\tGameObject.LocalScale = _baseScale;\n\t\tPlaceOn( platform );\n\t}\n\n\t/// <summary> \u6bcf\u5e27\u84c4\u529b\u503c\uff080~1\uff09\uff0cJumpGame \u5728\u84c4\u529b\u65f6\u6301\u7eed\u66f4\u65b0 </summary>\n\tpublic void SetCharge( float power ) => _charge = power;\n\n\t/// <summary> \u5f62\u53d8\u51b2\u91cf\uff1a\u6b63 = \u62c9\u4f38\uff08\u8d77\u8df3\uff09\uff0c\u8d1f = \u538b\u6241\uff08\u843d\u5730\uff09 </summary>\n\tpublic void Impulse( float amount ) => _squashVel += amount;\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// \u5f62\u53d8\u5f39\u7c27\uff1a\u5411\u81ea\u7136\u72b6\u6001\u56de\u843d\n\t\t_squashVel += ( -Stiffness * _squash - Damping * _squashVel ) * Time.Delta;\n\t\t_squash += _squashVel * Time.Delta;\n\n\t\tvar total = Math.Clamp( _squash - _charge * 0.42f, -0.55f, 0.6f );\n\t\tvar xz = 1f - total * 0.45f;\n\n\t\tGameObject.LocalScale = new Vector3( _baseScale.x * xz, _baseScale.y * xz, _baseScale.z * ( 1f + total ) );\n\t}\n}"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpScores.cs",
"FileName": "JumpScores.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\n\n/// <summary>\n/// \u6210\u7ee9\u8bb0\u5f55\uff1a\u672c\u5730\u6392\u884c\u699c\uff08FileSystem.Data \u6301\u4e45\u5316\uff0c\u59cb\u7ec8\u53ef\u7528\uff09\n/// + \u5b98\u65b9\u4e91\u7aef\u6392\u884c\u699c\uff08Sandbox.Services \u7684 Stats/Leaderboards\uff0c\u5c3d\u529b\u800c\u4e3a\u2014\u2014\n/// \u5305\u672a\u53d1\u5e03/\u79bb\u7ebf\u65f6\u540e\u7aef\u6ca1\u6709\u8eab\u4efd\uff0c\u62ff\u4e0d\u5230\u6570\u636e\u5c31\u9759\u9ed8\u964d\u7ea7\u4e3a\u7eaf\u672c\u5730\uff09\u3002\n/// </summary>\npublic static class JumpScores\n{\n\tconst string SaveFile = \"jump_scores.json\";\n\tconst int MaxKeep = 30;\n\n\t/// <summary> \u4e00\u6761\u672c\u5730\u6210\u7ee9 </summary>\n\tpublic class ScoreEntry\n\t{\n\t\tpublic int Score { get; set; }\n\t\tpublic int Difficulty { get; set; } // 0 \u7b80\u5355 1 \u5bb9\u6613 2 \u56f0\u96be\n\t\tpublic string Date { get; set; } = \"\";\n\t}\n\n\tstatic List<ScoreEntry> _scores = new();\n\tstatic bool _loaded;\n\n\t/// <summary>\n\t/// \u6bcf\u4e2a\u96be\u5ea6\u5bf9\u5e94\u7684\u540e\u7aef\u6307\u6807\u3002\n\t///\n\t/// \u7b80\u5355\u6a21\u5f0f\u6cbf\u7528\u65e9\u671f\u5c31\u5b58\u5728\u3001\u5df2\u7ecf\u6709\u5168\u90e8\u73a9\u5bb6\u5386\u53f2\u6210\u7ee9\u7684 <c>best_score</c>\uff1b\n\t/// \u666e\u901a\u548c\u56f0\u96be\u5404\u5199\u81ea\u5df1\u7684\u65b0\u6307\u6807\uff08\u90a3\u4e24\u4e2a\u699c\u53ea\u6709\u73a9\u8fc7\u65b0\u7248\u672c\u7684\u73a9\u5bb6\uff0c\u6b63\u5e38\uff09\u3002\n\t/// \u8bfb\u53d6\u65f6\u6309\u5f53\u524d\u96be\u5ea6\u53d6\u5bf9\u5e94\u699c\uff0c\u6240\u4ee5\u7b80\u5355\u96be\u5ea6\u770b\u5230\u7684\u662f\u90a3 15 \u4e2a\u8001\u73a9\u5bb6\uff0c\u666e\u901a/\u56f0\u96be\u662f\u5404\u81ea\u7684\u699c\u3002\n\t/// </summary>\n\tpublic static readonly (JumpDifficulty Difficulty, string Stat, string Name)[] Boards =\n\t{\n\t\t( JumpDifficulty.Easy, \"best_score\", \"Easy\" ),\n\t\t( JumpDifficulty.Normal, \"best_score_normal\", \"Normal\" ),\n\t\t( JumpDifficulty.Hard, \"best_score_hard\", \"Hard\" ),\n\t};\n\n\t/// <summary> \u5f53\u524d\u96be\u5ea6\u7684\u6307\u6807\u540d\u4e0e\u663e\u793a\u540d </summary>\n\tpublic static (string Stat, string Name) BoardFor( JumpDifficulty difficulty )\n\t{\n\t\tforeach ( var b in Boards )\n\t\t{\n\t\t\tif ( b.Difficulty == difficulty ) return (b.Stat, b.Name);\n\t\t}\n\n\t\treturn (Boards[0].Stat, Boards[0].Name);\n\t}\n\n\t/// <summary> \u63d0\u4ea4\u65f6\u968f\u6210\u7ee9\u9644\u5e26\u7684\u6570\u636e\uff08\u96be\u5ea6\u4ee5\u8fd9\u4e2a\u5b57\u7b26\u4e32\u843d\u5e93\uff09 </summary>\n\tpublic static Dictionary<string, object> StatData( JumpDifficulty difficulty )\n\t\t=> new() { { \"difficulty\", difficulty.ToString() } };\n\n\t/// <summary>\n\t/// \u4e91\u7aef\u6392\u884c\u699c\uff0c\u6309\u96be\u5ea6\u5206\u522b\u7f13\u5b58\u3002\n\t///\n\t/// \u4ee5\u524d\u662f\u5355\u4e2a\u4e0d\u5206\u96be\u5ea6\u7684 List\uff1a\u5207\u96be\u5ea6\u540e\u4e0a\u4e00\u6b21\u7684\u7ed3\u679c\u8fd8\u5728\uff0c\u6807\u9898\u5374\u5199\u5f53\u524d\u96be\u5ea6\uff0c\n\t/// \u4e8e\u662f\"Normal \u00b7 Global Best\"\u5e95\u4e0b\u663e\u793a\u7684\u662f Hard \u7684\u699c\uff1b\u62c9\u53d6\u5931\u8d25\u65f6\u66f4\u662f\u7ee7\u7eed\u663e\u793a\u522b\u7684\u96be\u5ea6\u3002\n\t/// \u7f13\u5b58\u653e\u5728\u8fd9\u91cc\u6309\u96be\u5ea6\u53d6\uff0c\u6e32\u67d3\u65b9\u62ff\u5230\u7684\u5fc5\u7136\u662f\u5f53\u524d\u96be\u5ea6\u7684\u6570\u636e\u3002\n\t/// </summary>\n\tstatic readonly Dictionary<JumpDifficulty, List<(long Rank, string Name, int Score)>> _cloud = new();\n\n\t/// <summary> \u6307\u5b9a\u96be\u5ea6\u7684\u4e91\u7aef\u699c\uff08\u65e0\u6570\u636e = \u8fd8\u6ca1\u62c9\u5230\u6216\u4e0d\u9002\u7528\uff09 </summary>\n\tpublic static IReadOnlyList<(long Rank, string Name, int Score)> Cloud( JumpDifficulty difficulty )\n\t\t=> _cloud.TryGetValue( difficulty, out var list ) ? list : Array.Empty<(long, string, int)>();\n\n\t/// <summary> \u8be5\u96be\u5ea6\u662f\u5426\u6210\u529f\u62c9\u53d6\u8fc7\u4e91\u7aef\u699c </summary>\n\tpublic static bool CloudLoaded( JumpDifficulty difficulty ) => _cloud.ContainsKey( difficulty );\n\n\tstatic JumpScores()\n\t{\n\t\tLoad();\n\t}\n\n\tstatic void Load()\n\t{\n\t\tif ( _loaded ) return;\n\n\t\ttry\n\t\t{\n\t\t\t// \u7f16\u8f91\u5668\u65e9\u671f\uff08\u7f16\u8f91\u6a21\u5f0f/\u573a\u666f\u52a0\u8f7d\u4e2d\uff09FileSystem.Data \u8fd8\u6ca1\u5c31\u7eea\u4f1a\u629b NRE\uff0c\n\t\t\t// \u6b64\u65f6\u4e0d\u80fd\u628a _loaded \u7f6e\u4f4d\uff0c\u7559\u7ed9\u6e38\u620f\u8fd0\u884c\u671f\u95f4\u7684\u4e0b\u6b21\u8c03\u7528\u91cd\u8bd5\n\t\t\tif ( FileSystem.Data is null ) return;\n\n\t\t\tif ( FileSystem.Data.FileExists( SaveFile ) )\n\t\t\t{\n\t\t\t\t_scores = Json.Deserialize<List<ScoreEntry>>( FileSystem.Data.ReadAllText( SaveFile ) ) ?? new List<ScoreEntry>();\n\t\t\t}\n\t\t\t_loaded = true;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"JumpScores load: {e.Message}\" );\n\t\t}\n\t}\n\n\t/// <summary> \u6307\u5b9a\u96be\u5ea6\u7684\u5386\u53f2\u6700\u9ad8\u5206\uff08_scores \u6574\u4f53\u964d\u5e8f\uff0c\u6240\u4ee5\u9996\u4e2a\u5339\u914d\u5c31\u662f\u6700\u5927\u503c\uff09 </summary>\n\tpublic static int Best( JumpDifficulty difficulty )\n\t{\n\t\tLoad();\n\t\treturn _scores.FirstOrDefault( x => x.Difficulty == (int)difficulty )?.Score ?? 0;\n\t}\n\n\t/// <summary> \u6307\u5b9a\u96be\u5ea6\u7684\u672c\u5730\u524d N \u540d\uff08\u964d\u5e8f\uff09 </summary>\n\tpublic static List<ScoreEntry> Top( int count, JumpDifficulty difficulty )\n\t{\n\t\tLoad();\n\t\treturn _scores.Where( x => x.Difficulty == (int)difficulty ).Take( count ).ToList();\n\t}\n\n\t/// <summary>\n\t/// \u5c40\u7ec8\u8bb0\u5f55\u6210\u7ee9\uff08\u4e91\u7aef\u4e0a\u62a5\u987a\u5e26\u89e6\u53d1\uff09\u3002\n\t/// \u622a\u65ad\u6309\u96be\u5ea6\u5206\u522b\u505a\uff1a\u5168\u5c40 Take(MaxKeep) \u4f1a\u8ba9\u9ad8\u5206\u96be\u5ea6\u628a\u522b\u7684\u96be\u5ea6\u5168\u6324\u51fa\u53bb\uff0c\n\t/// \u4f4e\u5206\u96be\u5ea6\u7684\u672c\u5730\u699c\u6700\u540e\u4e00\u6761\u90fd\u4e0d\u5269\u3002\n\t/// </summary>\n\tpublic static void Record( int score, JumpDifficulty difficulty )\n\t{\n\t\tif ( score <= 0 ) return;\n\t\tLoad();\n\n\t\t_scores.Add( new ScoreEntry { Score = score, Difficulty = (int)difficulty, Date = DateTime.Now.ToString( \"MM-dd\" ) } );\n\t\t_scores = Trim( _scores );\n\n\t\tSave();\n\n\t\tSubmitCloud( score, difficulty );\n\t}\n\n\t/// <summary>\n\t/// \u6392\u884c\u4fdd\u7559\u89c4\u5219\uff1a\u6bcf\u4e2a\u96be\u5ea6\u5404\u7559 MaxKeep \u6761\uff0c\u6574\u4f53\u518d\u6309\u5206\u6570\u964d\u5e8f\u3002\n\t///\n\t/// \u4e0d\u80fd\u5bf9\u5168\u8868\u505a OrderByDescending().Take(MaxKeep)\uff1a\u90a3 30 \u4e2a\u540d\u989d\u662f\u6240\u6709\u96be\u5ea6\u5171\u7528\u7684\uff0c\n\t/// \u4e00\u4e2a\u96be\u5ea6\u5206\u9ad8\u5c31\u628a\u522b\u7684\u96be\u5ea6\u5168\u6324\u51fa\u53bb\uff08\u5b9e\u6d4b\u5b58\u4e0b\u6765 30 \u6761\u91cc 28 \u6761 Easy\u30012 \u6761 Hard\u3001\n\t/// Normal \u4e00\u6761\u4e0d\u5269\uff0cNormal \u7684\u672c\u5730\u699c\u6574\u4e2a\u662f\u7a7a\u7684\uff09\u3002\n\t/// \u6574\u4f53\u964d\u5e8f\u662f Best/Top \u8bfb\u53d6\u65f6\"\u7b2c\u4e00\u4e2a\u5339\u914d\u5373\u6700\u5927\u503c\"\u7684\u524d\u63d0\u3002\n\t/// </summary>\n\tpublic static List<ScoreEntry> Trim( List<ScoreEntry> scores )\n\t{\n\t\treturn scores\n\t\t\t.GroupBy( x => x.Difficulty )\n\t\t\t.SelectMany( g => g.OrderByDescending( x => x.Score ).Take( MaxKeep ) )\n\t\t\t.OrderByDescending( x => x.Score )\n\t\t\t.ToList();\n\t}\n\n\tstatic void Save()\n\t{\n\t\ttry\n\t\t{\n\t\t\tFileSystem.Data.WriteAllText( SaveFile, Json.Serialize( _scores ) );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"JumpScores save: {e.Message}\" );\n\t\t}\n\t}\n\n\t// ---- \u4e91\u7aef\uff08\u5b98\u65b9 Services\uff0c\u5c3d\u529b\u800c\u4e3a\uff09----\n\n\t/// <summary>\n\t/// \u4e0a\u62a5\u6210\u7ee9\u5230\u5b98\u65b9\u4e91\u7aef\uff08\u6253\u5305\u53d1\u5e03\u540e\u751f\u6548\uff1b\u672c\u5730\u5f00\u53d1\u5305\u53ef\u80fd\u6ca1\u6709\u540e\u7aef\u8eab\u4efd\uff09\u3002\n\t/// \u6309\u96be\u5ea6\u5199\u5404\u81ea\u7684\u6307\u6807\uff1a\u7b80\u5355 \u2192 best_score\uff08\u8001\u699c\uff09\uff0c\u666e\u901a/\u56f0\u96be \u2192 \u5404\u81ea\u7684\u65b0\u699c\u3002\n\t/// </summary>\n\tpublic static void SubmitCloud( int score, JumpDifficulty difficulty )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar ( stat, _ ) = BoardFor( difficulty );\n\n\t\t\tSandbox.Services.Stats.SetValue( stat, score, StatData( difficulty ) );\n\t\t\t_ = FlushAndReloadCloud( difficulty );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"JumpScores cloud submit: {e.Message}\" );\n\t\t}\n\t}\n\n\t/// <summary> \u62c9\u53d6\u6307\u5b9a\u96be\u5ea6\u7684\u4e91\u7aef\u6392\u884c\u699c\uff08\u805a\u5408 = \u6bcf\u4eba\u5386\u53f2\u6700\u9ad8\u5206\uff0c\u964d\u5e8f\uff09 </summary>\n\tpublic static async Task RefreshCloud( JumpDifficulty difficulty )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar ( stat, _ ) = BoardFor( difficulty );\n\n\t\t\tvar board = Sandbox.Services.Leaderboards.GetFromStat( stat );\n\t\t\tboard.SetAggregationMax();\n\t\t\tboard.MaxEntries = 10;\n\n\t\t\tawait board.Refresh();\n\n\t\t\t// \u62c9\u6210\u529f\u624d\u8986\u76d6\u8be5\u96be\u5ea6\u7684\u7f13\u5b58\uff1a\u5931\u8d25\u65f6\u4fdd\u7559\u4e0a\u4e00\u6b21\u7684\u7ed3\u679c\uff0c\u91cd\u8bd5\u6210\u529f\u540e\u81ea\u7136\u88ab\u66ff\u6362\u3002\n\t\t\t// \u6ce8\u610f\u522b\u6e05\u7a7a\u7f13\u5b58\u2014\u2014\u6e05\u6389\u5c31\u628a\"\u4e0a\u6b21\u770b\u5230\u7684\u699c\"\u4e5f\u4e00\u8d77\u4e22\u4e86\uff0c\u754c\u9762\u76f4\u63a5\u53d8\u5360\u4f4d\n\t\t\t_cloud[difficulty] = board.Entries\n\t\t\t\t.Select( e => ( e.Rank, e.DisplayName ?? \"???\", (int)e.Value ) )\n\t\t\t\t.ToList();\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\t// \u672c\u5730\u5305 / \u79bb\u7ebf\uff1a\u4e91\u7aef\u4e0d\u53ef\u7528\uff0c\u964d\u7ea7\u4e3a\u7eaf\u672c\u5730\u6392\u884c\uff0c\u4f46\u8981\u8bf4\u4e00\u58f0\u662f\u5728\u54ea\u4e00\u6b65\u6302\u7684\n\t\t\tLog.Warning( $\"JumpScores cloud refresh ({difficulty}): {e.Message}\" );\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// \u4e0a\u62a5\u540e\u628a\u6210\u7ee9\u63a8\u7ed9\u540e\u7aef\u5e76\u91cd\u65b0\u62c9\u699c\u3002\n\t///\n\t/// \u5fc5\u987b\u7528 FlushAndWaitAsync\uff1aFlushAsync \u662f\"\u53d1\u51fa\u5373\u5fd8\"\uff08\u5f15\u64ce\u6ce8\u91ca\uff1aDon't wait for confirmation\n\t/// of ingestion\uff09\uff0c\u7d27\u63a5\u7740\u67e5\u699c\u65f6\u8fd9\u4e00\u5c40\u7684\u6210\u7ee9\u901a\u5e38\u8fd8\u6ca1\u5165\u5e93\uff0c\u7ed3\u7b97\u753b\u9762\u5c31\u6c38\u8fdc\u770b\u4e0d\u5230\u81ea\u5df1\u521a\u6253\u7684\u5206\u6570\u3002\n\t/// </summary>\n\tstatic async Task FlushAndReloadCloud( JumpDifficulty difficulty )\n\t{\n\t\ttry\n\t\t{\n\t\t\tawait Sandbox.Services.Stats.FlushAndWaitAsync();\n\t\t\tawait RefreshCloud( difficulty );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"JumpScores cloud flush ({difficulty}): {e.Message}\" );\n\t\t}\n\t}\n}\n"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpCamera.cs",
"FileName": "JumpCamera.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "using System;\r\n\r\n/// <summary>\r\n/// \u8df3\u4e00\u8df3\u7684\u8ddf\u968f\u76f8\u673a\uff1a\u56fa\u5b9a\u89d2\u5ea6\u4fef\u89c6\uff0c\u5e73\u6ed1\u8ddf\u968f\u68cb\u5b50\u3002\r\n/// \u89c6\u7ebf\u5927\u4f53\u6cbf +X \u65b9\u5411\uff0c\u5e73\u53f0\u86c7\u5f62\u524d\u8fdb\u65f6\u5728\u753b\u9762\u4e0a\u5448\u73b0\u5de6\u524d/\u53f3\u524d\u7684\u659c\u7ebf\u8fd0\u52a8\u3002\r\n/// </summary>\r\npublic sealed class JumpCamera : Component\r\n{\r\n\t/// <summary> \u76f8\u673a\u76f8\u5bf9\u6ce8\u89c6\u70b9\u7684\u504f\u79fb </summary>\r\n\t[Property] public Vector3 Offset { get; set; } = new Vector3( -500f, 0f, 640f );\r\n\r\n\t/// <summary> \u6ce8\u89c6\u70b9\u76f8\u5bf9\u68cb\u5b50\u7684\u524d\u63a2\u91cf\uff08\u628a\u68cb\u5b50\u538b\u5230\u753b\u9762\u4e0b\u65b9\uff09 </summary>\r\n\t[Property] public Vector3 LookAhead { get; set; } = new Vector3( 300f, 0f, -40f );\r\n\r\n\t/// <summary> \u8ddf\u968f\u901f\u5ea6\uff08\u8d8a\u5927\u8d8a\u7d27\uff09 </summary>\r\n\t[Property] public float FollowSpeed { get; set; } = 5f;\r\n\r\n\tVector3 _focus;\r\n\tbool _hasFocus;\r\n\r\n\tpublic void SnapTo( Vector3 pos )\r\n\t{\r\n\t\t_focus = pos;\r\n\t\t_hasFocus = true;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tvar game = JumpGame.Current;\r\n\t\tif ( game is null || !_hasFocus ) return;\r\n\r\n\t\t// \u5931\u8d25\uff08\u4e0b\u843d/\u7ed3\u675f\uff09\u540e\u505c\u4f4f\uff0c\u4e0d\u8ddf\u7740\u68cb\u5b50\u5760\u51fa\u753b\u9762\uff0c\u8ba9\u73a9\u5bb6\u770b\u6e05\u5931\u8bef\u4f4d\u7f6e\r\n\t\tif ( game.IsGameOver || game.IsFalling ) return;\r\n\r\n\t\tvar t = 1f - MathF.Exp( -FollowSpeed * Time.Delta );\r\n\t\t_focus = Vector3.Lerp( _focus, game.PiecePosition, t );\r\n\r\n\t\tGameObject.WorldPosition = _focus + Offset;\r\n\t\tGameObject.WorldRotation = Rotation.LookAt( ( _focus + LookAhead - GameObject.WorldPosition ).Normal );\r\n\t}\r\n}"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpAchievements.cs",
"FileName": "JumpAchievements.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\n/// <summary>\n/// \u6210\u5c31\u4e0e\u7d2f\u8ba1\u8fdb\u5ea6\u3002\n///\n/// \u6210\u5c31\u672c\u8eab\u8981\u5728 sbox \u7f51\u7ad9\u540e\u53f0\u6309 id \u5efa\u597d\uff08\u89e3\u9501\u6a21\u5f0f\u9009 Manual\uff09\uff0c\u4ee3\u7801\u91cc\u53ea\u8d1f\u8d23\u5728\u6761\u4ef6\u6ee1\u8db3\u65f6\u8c03\u7528\n/// Achievements.Unlock(id)\u3002\u540e\u53f0\u6ca1\u5efa\u8fc7\u7684 id \u8c03\u7528\u4f1a\u88ab\u5b89\u9759\u5ffd\u7565\uff0c\u4e0d\u4f1a\u62a5\u9519\u3002\n///\n/// \u7d2f\u8ba1\u8ba1\u6570\uff08\u603b\u8df3\u8dc3\u6b21\u6570\u7b49\uff09\u5b58\u5728\u672c\u5730\u6587\u4ef6\u91cc\uff1b\u78c1\u76d8\u5199\u5165\u53ea\u5728\"\u4e00\u5c40\u7ed3\u675f\"\u548c\"\u89e3\u9501\u6210\u5c31\"\u65f6\u53d1\u751f\u2014\u2014\n/// \u6bcf\u6b21\u8d77\u8df3\u90fd\u5199\u76d8\u592a\u6d6a\u8d39\uff0c\u6240\u4ee5\u8ba1\u6570\u5148\u7559\u5728\u5185\u5b58\uff0c\u5c40\u7ec8\u518d\u843d\u76d8\u3002\n/// </summary>\npublic static class JumpAchievements\n{\n\t/// <summary> \u4e00\u6761\u6210\u5c31\u5b9a\u4e49\uff08id / \u6807\u9898 / \u63cf\u8ff0\uff09\uff0c\u540e\u53f0\u7167\u7740\u8fd9\u4e2a\u8868\u586b </summary>\n\tpublic readonly record struct Info( string Id, string Title, string Description );\n\n\t/// <summary> \u5168\u90e8\u6210\u5c31\u3002id \u662f\u540e\u7aef\u6807\u8bc6\uff0c\u4e00\u65e6\u4e0a\u4f20\u5c31\u522b\u518d\u6539 </summary>\n\tpublic static readonly Info[] All =\n\t{\n\t\tnew( \"first_jump\", \"First Step\", \"Make your first jump.\" ),\n\t\tnew( \"first_landing\", \"Safe Landing\", \"Land on a platform.\" ),\n\t\tnew( \"score_10\", \"Getting Warm\", \"Score 10 in a single run.\" ),\n\t\tnew( \"score_30\", \"Steady Hand\", \"Score 30 in a single run.\" ),\n\t\tnew( \"score_50\", \"Half Century\", \"Score 50 in a single run.\" ),\n\t\tnew( \"score_100\", \"Centurion\", \"Score 100 in a single run.\" ),\n\t\tnew( \"perfect_3\", \"Triple Perfect\", \"Land 3 perfect jumps in a row.\" ),\n\t\tnew( \"perfect_10\", \"Perfect Ten\", \"Land 10 perfect jumps in a row.\" ),\n\t\tnew( \"hard_20\", \"Steady On Hard\", \"Score 20 or more on Hard.\" ),\n\t\tnew( \"backjump\", \"Homeward\", \"Jump back onto the platform you came from.\" ),\n\t\tnew( \"total_100_jumps\", \"Hundred Leaps\", \"Jump 100 times in total.\" ),\n\t\tnew( \"total_50_games\", \"Persistence\", \"Play 50 games in total.\" ),\n\t\tnew( \"total_500_perfect\",\"Sharpshooter\", \"Land 500 perfect jumps in total.\" ),\n\t};\n\n\t// ---- \u7d2f\u8ba1\u8fdb\u5ea6\uff08\u6301\u4e45\u5316\uff09 ----\n\n\tpublic class Progress\n\t{\n\t\tpublic int TotalJumps { get; set; }\n\t\tpublic int TotalPerfect { get; set; }\n\t\tpublic int TotalGames { get; set; }\n\t\tpublic int TotalBackJumps { get; set; }\n\t\tpublic int BestStreak { get; set; }\n\t}\n\n\tconst string SaveFile = \"jump_progress.json\";\n\n\tstatic Progress _progress = new();\n\tstatic bool _loaded;\n\tstatic bool _dirty;\n\n\tpublic static Progress Data\n\t{\n\t\tget { Load(); return _progress; }\n\t}\n\n\tstatic void Load()\n\t{\n\t\tif ( _loaded ) return;\n\n\t\ttry\n\t\t{\n\t\t\t// \u7f16\u8f91\u5668\u65e9\u671f FileSystem.Data \u8fd8\u6ca1\u5c31\u7eea\u4f1a\u629b\u5f02\u5e38\uff0c\u6b64\u65f6\u4e0d\u7f6e\u4f4d _loaded\uff0c\u7559\u7ed9\u4e0b\u6b21\u91cd\u8bd5\n\t\t\tif ( FileSystem.Data is null ) return;\n\n\t\t\tif ( FileSystem.Data.FileExists( SaveFile ) )\n\t\t\t{\n\t\t\t\t_progress = Json.Deserialize<Progress>( FileSystem.Data.ReadAllText( SaveFile ) ) ?? new Progress();\n\t\t\t}\n\n\t\t\t_loaded = true;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"JumpAchievements load: {e.Message}\" );\n\t\t}\n\t}\n\n\tpublic static void Save()\n\t{\n\t\tif ( !_dirty ) return;\n\t\tLoad();\n\n\t\ttry\n\t\t{\n\t\t\tFileSystem.Data.WriteAllText( SaveFile, Json.Serialize( _progress ) );\n\t\t\t_dirty = false;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"JumpAchievements save: {e.Message}\" );\n\t\t}\n\t}\n\n\t// ---- \u4e8b\u4ef6\u5165\u53e3\uff08\u90fd\u7531 JumpGame \u8c03\u7528\uff1b\u83dc\u5355\u6f14\u793a\u4e0e\u65b0\u624b\u6f14\u793a\u4e0d\u8ba1\u5165\uff09 ----\n\n\t/// <summary> \u8d77\u8df3 </summary>\n\tpublic static void OnJump()\n\t{\n\t\tLoad();\n\n\t\t_progress.TotalJumps++;\n\t\t_dirty = true;\n\n\t\tUnlock( \"first_jump\" );\n\n\t\tif ( _progress.TotalJumps >= 100 ) Unlock( \"total_100_jumps\" );\n\t}\n\n\t/// <summary> \u843d\u5728\u666e\u901a\u5e73\u53f0\u4e0a </summary>\n\tpublic static void OnLanding() => Unlock( \"first_landing\" );\n\n\t/// <summary> \u5b8c\u7f8e\u843d\u5730\uff0cstreak \u4e3a\u5f53\u524d\u8fde\u51fb\u6570 </summary>\n\tpublic static void OnPerfect( int streak )\n\t{\n\t\tLoad();\n\n\t\t_progress.TotalPerfect++;\n\t\t_progress.BestStreak = Math.Max( _progress.BestStreak, streak );\n\t\t_dirty = true;\n\n\t\tif ( streak >= 3 ) Unlock( \"perfect_3\" );\n\t\tif ( streak >= 10 ) Unlock( \"perfect_10\" );\n\t\tif ( _progress.TotalPerfect >= 500 ) Unlock( \"total_500_perfect\" );\n\t}\n\n\t/// <summary> \u5012\u8df3\u843d\u56de\u8eab\u540e\u7684\u5e73\u53f0 </summary>\n\tpublic static void OnBackJump()\n\t{\n\t\tLoad();\n\n\t\t_progress.TotalBackJumps++;\n\t\t_dirty = true;\n\n\t\tUnlock( \"backjump\" );\n\t}\n\n\t/// <summary> \u4e00\u5c40\u7ed3\u675f\u3002\u8ba1\u6570\u5728\u8fd9\u91cc\u843d\u76d8 </summary>\n\tpublic static void OnGameEnd( int score, JumpDifficulty difficulty )\n\t{\n\t\tLoad();\n\n\t\t_progress.TotalGames++;\n\t\t_dirty = true;\n\n\t\tif ( score >= 10 ) Unlock( \"score_10\" );\n\t\tif ( score >= 30 ) Unlock( \"score_30\" );\n\t\tif ( score >= 50 ) Unlock( \"score_50\" );\n\t\tif ( score >= 100 ) Unlock( \"score_100\" );\n\t\tif ( difficulty == JumpDifficulty.Hard && score >= 20 ) Unlock( \"hard_20\" );\n\n\t\tif ( _progress.TotalGames >= 50 ) Unlock( \"total_50_games\" );\n\n\t\tSave();\n\t}\n\n\t/// <summary> \u8be5\u6210\u5c31\u662f\u5426\u5df2\u89e3\u9501\uff08\u672c\u5730\u7f13\u5b58\uff0c\u672a\u62c9\u5230\u540e\u53f0\u6570\u636e\u65f6\u4e00\u5f8b\u6309\u672a\u89e3\u9501\u5904\u7406\uff09 </summary>\n\tpublic static bool IsUnlocked( string id )\n\t{\n\t\ttry\n\t\t{\n\t\t\treturn Sandbox.Services.Achievements.All.FirstOrDefault( x => x.Name == id )?.IsUnlocked ?? false;\n\t\t}\n\t\tcatch ( Exception )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/// <summary> \u89e3\u9501\uff08\u91cd\u590d\u8c03\u7528\u4f1a\u88ab\u5f15\u64ce\u5ffd\u7565\uff09\u3002\u6210\u529f\u540e\u7acb\u5373\u843d\u76d8\u7d2f\u8ba1\u8ba1\u6570 </summary>\n\tstatic void Unlock( string id )\n\t{\n\t\ttry\n\t\t{\n\t\t\tSandbox.Services.Achievements.Unlock( id );\n\t\t\tSave();\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"JumpAchievements unlock {id}: {e.Message}\" );\n\t\t}\n\t}\n}\n"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpDifficulty.cs",
"FileName": "JumpDifficulty.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "/// <summary>\n/// \u96be\u5ea6\u6863\u4f4d\uff1a\u7b80\u5355\uff08\u84c4\u529b\u65f6\u663e\u793a\u8f85\u52a9\u7ebf\uff09/ \u5bb9\u6613\uff08\u6807\u51c6\uff09/ \u56f0\u96be\uff08\u843d\u70b9\u4e0d\u5438\u9644\u5e73\u53f0\u4e2d\u5fc3\uff09\u3002\n/// </summary>\npublic enum JumpDifficulty\n{\n\tEasy,\n\tNormal,\n\tHard\n}"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpPlatform.cs",
"FileName": "JumpPlatform.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "using System;\r\n\r\n/// <summary>\r\n/// \u8df3\u4e00\u8df3\u5e73\u53f0\uff1a\u7eaf\u89c6\u89c9\uff08\u6a21\u578b + \u51fa\u751f\u5f39\u51fa\u52a8\u753b\uff09\uff0c\u78b0\u649e\u5224\u5b9a\u7531 JumpGame \u7528\u6570\u5b66\u77e9\u5f62\u5b8c\u6210\u3002\r\n/// </summary>\r\npublic sealed class JumpPlatform : Component\r\n{\r\n\tpublic float CenterX { get; private set; }\r\n\tpublic float CenterY { get; private set; }\r\n\tpublic float TopZ { get; private set; }\r\n\tpublic float HalfX { get; private set; }\r\n\tpublic float HalfY { get; private set; }\r\n\r\n\t/// <summary> \u662f\u5426\u4e3a\u5f53\u524d\u76ee\u6807\u5e73\u53f0\uff08\u4e2d\u5fc3\u6807\u8bb0\u4f1a\u547c\u5438\u8109\u51b2\u63d0\u793a\u843d\u70b9\uff09 </summary>\r\n\tpublic bool IsTarget { get; set; }\r\n\r\n\tpublic Vector3 Center => new Vector3( CenterX, CenterY, TopZ );\r\n\r\n\tGameObject _marker;\r\n\tVector3 _baseScale;\r\n\tVector3 _baseMarkerScale;\r\n\tTimeSince _born;\r\n\tbool _spawned;\r\n\r\n\tstatic readonly Model BoxModel = Model.Load( \"models/dev/box.vmdl\" );\r\n\r\n\tpublic void Init( Vector2 center, float halfX, float halfY, float topZ, Color color )\r\n\t{\r\n\t\tCenterX = center.x;\r\n\t\tCenterY = center.y;\r\n\t\tTopZ = topZ;\r\n\t\tHalfX = halfX;\r\n\t\tHalfY = halfY;\r\n\r\n\t\tvar height = topZ;\r\n\t\tvar size = BoxModel.Bounds.Size;\r\n\r\n\t\tGameObject.WorldPosition = new Vector3( CenterX, CenterY, height * 0.5f );\r\n\t\t_baseScale = new Vector3( halfX * 2f / size.x, halfY * 2f / size.y, height / size.z );\r\n\t\tGameObject.LocalScale = _baseScale;\r\n\r\n\t\tvar renderer = GameObject.AddComponent<ModelRenderer>();\r\n\t\trenderer.Model = BoxModel;\r\n\t\trenderer.Tint = color;\r\n\r\n\t\t// \u9876\u9762\u4e2d\u5fc3\u7684\"\u5b8c\u7f8e\u843d\u70b9\"\u6807\u8bb0\uff08\u8584\u677f\uff09\uff0c\u8ddf\u968f\u5e73\u53f0\u7f29\u653e\u5f39\u51fa\r\n\t\t_marker = new GameObject( true, \"CenterMark\" );\r\n\t\t_marker.Parent = GameObject;\r\n\t\t_marker.LocalPosition = new Vector3( 0f, 0f, ( height * 0.5f + 1.3f ) / _baseScale.z );\r\n\t\t_baseMarkerScale = new Vector3( 30f / ( halfX * 2f ), 30f / ( halfY * 2f ), 2.6f / height );\r\n\t\t_marker.LocalScale = _baseMarkerScale;\r\n\r\n\t\tvar markerRenderer = _marker.AddComponent<ModelRenderer>();\r\n\t\tmarkerRenderer.Model = BoxModel;\r\n\t\tmarkerRenderer.Tint = color * 0.55f;\r\n\r\n\t\t_born = 0;\r\n\t\t_spawned = true;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( !_spawned ) return;\r\n\r\n\t\t// \u51fa\u751f\u52a8\u753b\uff1a\u4ece\u538b\u6241\u72b6\u6001\u5e26\u4e00\u70b9\u56de\u5f39\u5730\u957f\u51fa\u6765\r\n\t\tvar t = Math.Clamp( (float)_born / 0.3f, 0f, 1f );\r\n\t\tif ( t >= 1f )\r\n\t\t{\r\n\t\t\t_spawned = false;\r\n\t\t\tGameObject.LocalScale = _baseScale;\r\n\t\t\tApplyMarkerScale( 1f );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tconst float c1 = 1.70158f;\r\n\t\tconst float c3 = c1 + 1f;\r\n\t\tvar f = 1f + c3 * MathF.Pow( t - 1f, 3f ) + c1 * MathF.Pow( t - 1f, 2f );\r\n\r\n\t\tGameObject.LocalScale = _baseScale * f;\r\n\t\tApplyMarkerScale( f );\r\n\t}\r\n\r\n\tvoid ApplyMarkerScale( float factor )\r\n\t{\r\n\t\tif ( !_marker.IsValid() ) return;\r\n\r\n\t\t// \u76ee\u6807\u5e73\u53f0\u7684\u6807\u8bb0\u8f7b\u5fae\u547c\u5438\uff0c\u5e2e\u52a9\u8bc6\u522b\u843d\u70b9\r\n\t\tvar pulse = IsTarget && !_spawned ? 1f + 0.14f * MathF.Sin( Time.Now * 5f ) : 1f;\r\n\t\t_marker.LocalScale = _baseMarkerScale * factor * pulse;\r\n\t}\r\n\r\n\t/// <summary> \u9500\u6bc1\u5e73\u53f0\u672c\u4f53\u548c\u4e2d\u5fc3\u6807\u8bb0 </summary>\r\n\tpublic void DestroyAll()\r\n\t{\r\n\t\tif ( _marker.IsValid() ) _marker.Destroy();\r\n\t\tGameObject.Destroy();\r\n\t}\r\n}"
},
{
"Ident": "slfy.tiaotiao",
"Path": ".obj/__compiler_extra.cs",
"FileName": "__compiler_extra.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"tiaotiao\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"tiaotiao\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"slfy\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"slfy.tiaotiao\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"29\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-09-12T12:41:12.2306560Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.249.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.249.0\")]"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Assembly.cs",
"FileName": "Assembly.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "global using System;\r\nglobal using Sandbox;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\n"
},
{
"Ident": "slfy.tiaotiao",
"Path": "jump/jumphud.cs.scss",
"FileName": "jumphud.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "JumpHud\r\n{\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\tright: 0;\r\n\tbottom: 0;\r\n\tfont-family: Inter;\r\n\tcolor: #3c4249;\r\n\tpointer-events: none;\r\n\r\n\t.score\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 26px;\r\n\t\tleft: 0;\r\n\t\tright: 0;\r\n\t\tfont-size: 96px;\r\n\t\tfont-weight: 800;\r\n\t\tcolor: #343a41;\r\n\t\ttext-align: center;\r\n\t}\r\n\r\n\t.streak\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 150px;\r\n\t\tleft: 0;\r\n\t\tright: 0;\r\n\t\tfont-size: 34px;\r\n\t\tfont-weight: 700;\r\n\t\tcolor: #e8590c;\r\n\t\ttext-align: center;\r\n\t}\r\n\r\n\t.hint\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tbottom: 76px;\r\n\t\tleft: 0;\r\n\t\tright: 0;\r\n\t\tfont-size: 22px;\r\n\t\tcolor: #7a828c;\r\n\t\ttext-align: center;\r\n\t}\r\n\r\n\t.tutorial\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tbottom: 130px;\r\n\t\tleft: 0;\r\n\t\tright: 0;\r\n\t\tjustify-content: center;\r\n\r\n\t\t.tutorial-text\r\n\t\t{\r\n\t\t\tbackground-color: rgba(255, 255, 255, 0.92);\r\n\t\t\tborder-radius: 16px;\r\n\t\t\tpadding: 14px 30px;\r\n\t\t\tfont-size: 26px;\r\n\t\t\tfont-weight: 600;\r\n\t\t\tcolor: #343a41;\r\n\t\t}\r\n\t}\r\n\r\n\t.diff\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 16px;\r\n\t\tright: 26px;\r\n\t\tfont-size: 24px;\r\n\t\tcolor: #565d66;\r\n\t}\r\n\r\n\t.gameover\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 0;\r\n\t\tleft: 0;\r\n\t\tright: 0;\r\n\t\tbottom: 0;\r\n\t\tbackground-color: rgba(255, 255, 255, 0.74);\r\n\t\talign-items: center;\r\n\t\tjustify-content: center;\r\n\t\tflex-direction: column;\r\n\t\tcursor: default;\r\n\r\n\t\t.title\r\n\t\t{\r\n\t\t\tfont-size: 54px;\r\n\t\t\tfont-weight: 800;\r\n\t\t\tletter-spacing: 10px;\r\n\t\t\tcolor: #565d66;\r\n\t\t}\r\n\r\n\t\t.finalscore\r\n\t\t{\r\n\t\t\tfont-size: 190px;\r\n\t\t\tfont-weight: 800;\r\n\t\t\tline-height: 1;\r\n\t\t\tmargin-top: 14px;\r\n\t\t\tcolor: #e8590c;\r\n\t\t}\r\n\r\n\t\t.board-wrap\r\n\t\t{\r\n\t\t\tflex-direction: row;\r\n\t\t\tjustify-content: center;\r\n\t\t\tmargin-top: 22px;\r\n\r\n\t\t\t.board-col\r\n\t\t\t{\r\n\t\t\t\tflex-direction: column;\r\n\t\t\t\tmin-width: 300px;\r\n\t\t\t\tmargin: 0 12px;\r\n\t\t\t\tpadding: 16px 22px;\r\n\t\t\t\tbackground-color: rgba(255, 255, 255, 0.82);\r\n\t\t\t\tborder-radius: 14px;\r\n\r\n\t\t\t\t.board-title\r\n\t\t\t\t{\r\n\t\t\t\t\tfont-size: 21px;\r\n\t\t\t\t\tfont-weight: 700;\r\n\t\t\t\t\tcolor: #e8590c;\r\n\t\t\t\t\tmargin-bottom: 14px;\r\n\t\t\t\t}\r\n\r\n\t\t\t\t.board-empty\r\n\t\t\t\t{\r\n\t\t\t\t\tfont-size: 19px;\r\n\t\t\t\t\tcolor: #7a828c;\r\n\t\t\t\t\tpadding: 8px 0;\r\n\t\t\t\t}\r\n\r\n\t\t\t\t.board-row\r\n\t\t\t\t{\r\n\t\t\t\t\tflex-direction: row;\r\n\t\t\t\t\talign-items: center;\r\n\t\t\t\t\tmargin-bottom: 10px;\r\n\r\n\t\t\t\t\t.cell-rank\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\twidth: 28px;\r\n\t\t\t\t\t\tcolor: #7a828c;\r\n\t\t\t\t\t\tfont-style: italic;\r\n\t\t\t\t\t\tfont-weight: 700;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t.avatar\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\twidth: 36px;\r\n\t\t\t\t\t\theight: 36px;\r\n\t\t\t\t\t\tborder-radius: 9px;\r\n\t\t\t\t\t\talign-items: center;\r\n\t\t\t\t\t\tjustify-content: center;\r\n\t\t\t\t\t\tmargin-right: 12px;\r\n\r\n\t\t\t\t\t\t.avatar-char\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tcolor: #ffffff;\r\n\t\t\t\t\t\t\tfont-size: 17px;\r\n\t\t\t\t\t\t\tfont-weight: 700;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t.cell-name\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\twidth: 150px;\r\n\t\t\t\t\t\tcolor: #343a41;\r\n\t\t\t\t\t\tfont-size: 19px;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t.cell-score\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\twidth: 70px;\r\n\t\t\t\t\t\ttext-align: right;\r\n\t\t\t\t\t\tcolor: #e8590c;\r\n\t\t\t\t\t\tfont-size: 24px;\r\n\t\t\t\t\t\tfont-weight: 800;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t.sub\r\n\t\t{\r\n\t\t\tfont-size: 26px;\r\n\t\t\tmargin-top: 30px;\r\n\t\t\tcolor: #7a828c;\r\n\t\t}\r\n\t}\r\n\r\n\t.menu\r\n\t{\r\n\t\tposition: absolute;\r\n\t\ttop: 0;\r\n\t\tleft: 0;\r\n\t\tright: 0;\r\n\t\tbottom: 0;\r\n\t\tbackground-color: rgba(30, 36, 44, 0.35);\r\n\t\tflex-direction: column;\r\n\t\talign-items: center;\r\n\t\tjustify-content: center;\r\n\t\tpointer-events: all;\r\n\t\tcursor: default;\r\n\r\n\t\t.menu-title\r\n\t\t{\r\n\t\t\tfont-size: 120px;\r\n\t\t\tfont-weight: 800;\r\n\t\t\tletter-spacing: 12px;\r\n\t\t\tcolor: #2f343a;\r\n\t\t}\r\n\r\n\t\t.menu-sub\r\n\t\t{\r\n\t\t\tfont-size: 28px;\r\n\t\t\tcolor: #5c636b;\r\n\t\t\tmargin-top: 4px;\r\n\t\t\tmargin-bottom: 44px;\r\n\t\t}\r\n\r\n\t\t.menu-btn-row\r\n\t\t{\r\n\t\t\tflex-direction: row;\r\n\r\n\t\t\t.menu-btn\r\n\t\t\t{\r\n\t\t\t\tpadding: 18px 46px;\r\n\t\t\t\tmargin: 0 14px;\r\n\t\t\t\tborder-radius: 14px;\r\n\t\t\t\tbackground-color: rgba(255, 255, 255, 0.94);\r\n\t\t\t\tcolor: #343a41;\r\n\t\t\t\tfont-size: 30px;\r\n\t\t\t\tfont-weight: 700;\r\n\t\t\t\tcursor: pointer;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t.menu-best\r\n\t\t{\r\n\t\t\tfont-size: 26px;\r\n\t\t\tcolor: #c8ced6;\r\n\t\t\tmargin-top: 34px;\r\n\t\t}\r\n\t}\r\n}"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpHud.cs",
"FileName": "JumpHud.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "using System;\nusing Sandbox.UI;\n// board\n\n/// <summary>\n/// \u8df3\u4e00\u8df3 HUD\uff1a\u5f00\u59cb\u83dc\u5355\uff08AI \u80cc\u666f\u6f14\u793a\uff09\u3001\u8ba1\u5206\u3001\u8fde\u51fb\u63d0\u793a\u3001\u64cd\u4f5c\u63d0\u793a\u3001\u65b0\u624b\u6f14\u793a\u5b57\u5e55\u3001\u7ed3\u675f\u753b\u9762\u3001\u96be\u5ea6\u89d2\u6807\u3002\n/// \u7528\u7eaf C# Panel \u6784\u5efa\uff08\u7f16\u8f91\u5668\u70ed\u91cd\u8f7d\u4e0d\u4f1a\u4e3a\u65b0\u52a0\u7684 .razor \u91cd\u65b0\u7f16\u8bd1\uff0cC# \u9762\u677f\u53ef\u4ee5\u5373\u6539\u5373\u751f\u6548\uff09\uff0c\n/// \u6837\u5f0f\u5728\u540c\u540d\u6837\u5f0f\u8868 JumpHud.cs.scss \u91cc\uff08PanelComponent \u4f1a\u6309\"\u7c7b\u540d.cs.scss\"\u81ea\u52a8\u52a0\u8f7d\uff09\u3002\n/// </summary>\npublic sealed class JumpHud : PanelComponent\n{\n\tLabel _score;\n\tLabel _streak;\n\tLabel _hint;\n\tPanel _tutorialWrap;\n\tLabel _tutorialText;\n\tPanel _gameOver;\n\tLabel _gameOverScore;\n\tPanel _menuWrap;\n\tLabel _menuBest;\n\tLabel _difficultyLabel;\n\tPanel _boardWrap;\n\tPanel[] _localRows;\n\tLabel[] _localRank, _localAvatarChar, _localNameLabels, _localScore;\n\tPanel[] _localAvatar;\n\tPanel[] _cloudRows;\n\tLabel[] _cloudRank, _cloudAvatarChar, _cloudName, _cloudScore;\n\tPanel[] _cloudAvatar;\n\tLabel _localEmpty;\n\tLabel _cloudEmpty;\n\tconst int BoardRows = 5;\n\tstatic string _localName;\n\n\t/// <summary> \u672c\u5730\u73a9\u5bb6\u540d\u5b57\uff08\u62ff\u4e0d\u5230\u5c31\u663e\u793a\"\u4f60\"\uff09 </summary>\n\tstatic string LocalName()\n\t{\n\t\tif ( _localName is not null ) return _localName;\n\t\ttry { _localName = string.IsNullOrWhiteSpace( Connection.Local?.DisplayName ) ? \"You\" : Connection.Local.DisplayName; }\n\t\tcatch { _localName = \"You\"; }\n\t\treturn _localName;\n\t}\n\n\tstatic readonly Color[] AvatarColors =\n\t{\n\t\tnew Color( 0.86f, 0.47f, 0.28f ),\n\t\tnew Color( 0.36f, 0.62f, 0.86f ),\n\t\tnew Color( 0.44f, 0.72f, 0.50f ),\n\t\tnew Color( 0.72f, 0.50f, 0.86f ),\n\t\tnew Color( 0.90f, 0.72f, 0.30f ),\n\t};\n\tLabel _localTitle;\n\tLabel _cloudTitle;\n\tstring _menuBoardRequested;\n\tbool _built;\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\t\tBuildUi();\n\t}\n\n\tvoid BuildUi()\n\t{\n\t\tvar root = Panel;\n\t\tif ( root is null ) return;\n\n\t\t// \u5df2\u5efa\u597d\u4e14\u6240\u6709\u5f15\u7528\u6709\u6548 \u2192 \u4e0d\u91cd\u590d\u5efa\u3002\n\t\t// \u70ed\u91cd\u8f7d\u53ef\u80fd\u4fdd\u7559\u65e7\u5b9e\u4f8b\uff08\u8001\u5b57\u6bb5\u6709\u6548\u4f46\u65b0\u589e\u5b57\u6bb5\u4e3a null\uff09\uff0c\u6216\u6784\u5efa\u4e2d\u9014\u88ab\u6253\u65ad\n\t\t// \uff08\u6570\u7ec4\u534a\u6ee1\uff09\uff0c\u4efb\u4f55\u4e00\u9879\u5931\u6548\u90fd\u6574\u4f53\u6e05\u7a7a\u91cd\u5efa\n\t\tvar healthy = _built && _score.IsValid() && _menuBest.IsValid() && _menuWrap.IsValid()\n\t\t\t&& _tutorialWrap.IsValid() && _localTitle.IsValid() && _cloudTitle.IsValid()\n\t\t\t&& _localEmpty.IsValid() && _cloudEmpty.IsValid()\n\t\t\t&& ( _localAvatarChar?.All( x => x is not null ) ?? false )\n\t\t\t&& ( _localNameLabels?.All( x => x is not null ) ?? false )\n\t\t\t&& ( _cloudName?.All( x => x is not null ) ?? false )\n\t\t\t&& root.ChildrenCount > 0;\n\t\tif ( healthy ) return;\n\n\t\troot.DeleteChildren( true );\n\n\t\troot.Style.Position = PositionMode.Absolute;\n\t\troot.Style.Left = 0f;\n\t\troot.Style.Top = 0f;\n\t\troot.Style.Right = 0f;\n\t\troot.Style.Bottom = 0f;\n\t\troot.Style.PointerEvents = PointerEvents.None;\n\n\t\t_score = new Label() { Classes = \"score\" };\n\n\t\t_streak = new Label() { Classes = \"streak\" };\n\t\t_streak.Style.Display = DisplayMode.None;\n\n\t\t_hint = new Label() { Classes = \"hint\" };\n\t\t_hint.Text = \"Hold Left Mouse to charge \u00b7 release to jump \u00b7 hold S to jump back \u00b7 Q \u2014 Menu\";\n\t\t_hint.Style.Display = DisplayMode.None;\n\n\t\t_tutorialWrap = new Panel() { Classes = \"tutorial\" };\n\t\t_tutorialText = new Label() { Classes = \"tutorial-text\" };\n\t\t_tutorialWrap.AddChild( _tutorialText );\n\t\t_tutorialWrap.Style.Display = DisplayMode.None;\n\n\t\t_gameOver = new Panel() { Classes = \"gameover\" };\n\t\t_gameOver.Style.Display = DisplayMode.None;\n\n\t\tvar title = new Label() { Classes = \"title\" };\n\t\ttitle.Text = \"GAME OVER\";\n\t\t_gameOver.AddChild( title );\n\n\t\t_gameOverScore = new Label() { Classes = \"finalscore\" };\n\t\t_gameOver.AddChild( _gameOverScore );\n\n\t\t_boardWrap = new Panel() { Classes = \"board-wrap\" };\n\n\t\tvar localCol = new Panel() { Classes = \"board-col\" };\n\t\t_localTitle = new Label() { Classes = \"board-title\" };\n\t\tlocalCol.AddChild( _localTitle );\n\t\t_localRows = new Panel[BoardRows];\n\t\t_localRank = new Label[BoardRows];\n\t\t_localAvatar = new Panel[BoardRows];\n\t\t_localAvatarChar = new Label[BoardRows];\n\t\t_localNameLabels = new Label[BoardRows];\n\t\t_localScore = new Label[BoardRows];\n\t\tfor ( int i = 0; i < BoardRows; i++ )\n\t\t{\n\t\t\t( _localRows[i], _localRank[i], _localAvatar[i], _localAvatarChar[i], _localNameLabels[i], _localScore[i] ) = MakeBoardRow( localCol );\n\t\t}\n\t\t_localEmpty = new Label() { Classes = \"board-empty\" };\n\t\t_localEmpty.Style.Display = DisplayMode.None;\n\t\tlocalCol.AddChild( _localEmpty );\n\t\t_boardWrap.AddChild( localCol );\n\n\t\tvar cloudCol = new Panel() { Classes = \"board-col\" };\n\t\t_cloudTitle = new Label() { Classes = \"board-title\" };\n\t\tcloudCol.AddChild( _cloudTitle );\n\t\t_cloudRows = new Panel[BoardRows];\n\t\t_cloudRank = new Label[BoardRows];\n\t\t_cloudAvatar = new Panel[BoardRows];\n\t\t_cloudAvatarChar = new Label[BoardRows];\n\t\t_cloudName = new Label[BoardRows];\n\t\t_cloudScore = new Label[BoardRows];\n\t\tfor ( int i = 0; i < BoardRows; i++ )\n\t\t{\n\t\t\t( _cloudRows[i], _cloudRank[i], _cloudAvatar[i], _cloudAvatarChar[i], _cloudName[i], _cloudScore[i] ) = MakeBoardRow( cloudCol );\n\t\t}\n\t\t_cloudEmpty = new Label() { Classes = \"board-empty\" };\n\t\t_cloudEmpty.Style.Display = DisplayMode.None;\n\t\tcloudCol.AddChild( _cloudEmpty );\n\t\t_boardWrap.AddChild( cloudCol );\n\n\t\t_gameOver.AddChild( _boardWrap );\n\n\t\tvar goHint = new Label() { Classes = \"sub\" };\n\t\tgoHint.Text = \"Click to restart \u00b7 Q \u2014 Menu\";\n\t\t_gameOver.AddChild( goHint );\n\n\t\t_menuWrap = new Panel() { Classes = \"menu\" };\n\n\t\tvar menuTitle = new Label() { Classes = \"menu-title\" };\n\t\tmenuTitle.Text = \"Jump Jump\";\n\t\t_menuWrap.AddChild( menuTitle );\n\n\t\tvar menuSub = new Label() { Classes = \"menu-sub\" };\n\t\tmenuSub.Text = \"Select Difficulty \u00b7 press 1 / 2 / 3\";\n\t\t_menuWrap.AddChild( menuSub );\n\n\t\tvar menuBtnRow = new Panel() { Classes = \"menu-btn-row\" };\n\t\tAddMenuButton( menuBtnRow, \"Easy\", JumpDifficulty.Easy );\n\t\tAddMenuButton( menuBtnRow, \"Normal\", JumpDifficulty.Normal );\n\t\tAddMenuButton( menuBtnRow, \"Hard\", JumpDifficulty.Hard );\n\t\t_menuWrap.AddChild( menuBtnRow );\n\n\t\t_menuBest = new Label() { Classes = \"menu-best\" };\n\t\t_menuWrap.AddChild( _menuBest );\n\n\t\t_difficultyLabel = new Label() { Classes = \"diff\" };\n\n\t\troot.AddChild( _score );\n\t\troot.AddChild( _streak );\n\t\troot.AddChild( _hint );\n\t\troot.AddChild( _tutorialWrap );\n\t\troot.AddChild( _gameOver );\n\t\troot.AddChild( _difficultyLabel );\n\t\troot.AddChild( _menuWrap );\n\n\t\t_built = true;\n\t}\n\n\tvoid AddMenuButton( Panel parent, string text, JumpDifficulty difficulty )\n\t{\n\t\tvar btn = new Label() { Classes = \"menu-btn\" };\n\t\tbtn.Text = text;\n\t\tbtn.AddEventListener( \"onclick\", () =>\n\t\t{\n\t\t\tSound.Play( \"sounds/kenney/ui/ui.button.press.sound\" );\n\t\t\tJumpGame.Current?.StartGame( difficulty );\n\t\t} );\n\t\tparent.AddChild( btn );\n\t}\n\n\tstatic (Panel row, Label rank, Panel avatar, Label avatarChar, Label name, Label score) MakeBoardRow( Panel parent )\n\t{\n\t\tvar row = new Panel() { Classes = \"board-row\" };\n\n\t\tvar rank = new Label() { Classes = \"cell-rank\" };\n\t\trow.AddChild( rank );\n\n\t\tvar avatar = new Panel() { Classes = \"avatar\" };\n\t\tvar avatarChar = new Label() { Classes = \"avatar-char\" };\n\t\tavatar.AddChild( avatarChar );\n\t\trow.AddChild( avatar );\n\n\t\tvar name = new Label() { Classes = \"cell-name\" };\n\t\trow.AddChild( name );\n\n\t\tvar score = new Label() { Classes = \"cell-score\" };\n\t\trow.AddChild( score );\n\n\t\tparent.AddChild( row );\n\t\treturn ( row, rank, avatar, avatarChar, name, score );\n\t}\n\n\tstatic void SetVisible( Panel panel, bool visible )\n\t{\n\t\tpanel.Style.Display = visible ? DisplayMode.Flex : DisplayMode.None;\n\t}\n\n\tstatic string DifficultyName( JumpDifficulty difficulty ) => difficulty switch\n\t{\n\t\tJumpDifficulty.Easy => \"Easy\",\n\t\tJumpDifficulty.Normal => \"Normal\",\n\t\t_ => \"Hard\"\n\t};\n\n\tprotected override void OnUpdate()\n\t{\n\t\tBuildUi();\n\n\t\tvar game = JumpGame.Current;\n\t\tif ( game is null || !_built || _score is null || _tutorialText is null || _localAvatarChar is null ) return;\n\n\t\t_score.Text = game.Score.ToString();\n\t\t_streak.Text = $\"PERFECT \u00d7{game.Streak}\";\n\t\t_gameOverScore.Text = game.Score.ToString();\n\t\t_tutorialText.Text = game.TutorialText ?? \"\";\n\n\t\tvar inMenu = game.InMenu;\n\n\t\t_difficultyLabel.Text = DifficultyName( game.Difficulty ) + \" \u00b7 Q \u2014 Menu\";\n\n\t\t// \u56de\u5230\u83dc\u5355\u65f6\u62c9\u4e00\u6b21\u4e91\u7aef\u699c\uff08\u5f02\u6b65\u3001\u5c3d\u529b\u800c\u4e3a\uff09\uff1b\u952e\u4e0a\u5e26\u96be\u5ea6\uff0c\u6362\u96be\u5ea6\u5c31\u91cd\u62c9\n\t\tvar boardKey = $\"{inMenu}:{game.Difficulty}\";\n\t\tif ( inMenu && _menuBoardRequested != boardKey )\n\t\t{\n\t\t\t_menuBoardRequested = boardKey;\n\t\t\t_ = JumpScores.RefreshCloud( game.Difficulty );\n\t\t}\n\t\telse if ( !inMenu )\n\t\t{\n\t\t\t_menuBoardRequested = null;\n\t\t}\n\n\t\t// \u83dc\u5355\u91cc\u7684\"\u6700\u4f73\"\u8ddf\u5f53\u524d\u96be\u5ea6\uff08\u96be\u5ea6\u6309\u94ae/\u6570\u5b57\u952e\u7acb\u5373\u6539\u5199 game.Difficulty\uff09\u3002\n\t\t// \u539f\u6765\u7528\u7684\u662f\u4e0d\u5206\u96be\u5ea6\u7684 _scores[0]\uff0c\u663e\u793a\u7684\u662f\u522b\u7684\u96be\u5ea6\u7684\u6700\u9ad8\u5206\n\t\tvar best = JumpScores.Best( game.Difficulty );\n\t\t_menuBest.Text = best > 0 ? $\"{DifficultyName( game.Difficulty )} Best {best}\" : \"Set your first record!\";\n\n\t\t// \u5f00\u59cb\u83dc\u5355\u76d6\u4f4f\u6e38\u620f HUD\uff0c\u53ea\u4fdd\u7559\u80cc\u666f\u6f14\u793a\n\t\tSetVisible( _menuWrap, inMenu );\n\t\tSetVisible( _score, !game.IsGameOver && !inMenu );\n\t\tSetVisible( _streak, !game.IsGameOver && game.Streak >= 2 && !inMenu );\n\t\tSetVisible( _hint, !game.HasJumped && !game.IsGameOver && !game.IsTutorial && !inMenu );\n\t\tSetVisible( _tutorialWrap, !inMenu && !string.IsNullOrEmpty( game.TutorialText ) );\n\t\tSetVisible( _gameOver, game.IsGameOver && !inMenu );\n\t\tSetVisible( _difficultyLabel, !inMenu );\n\n\t\t// \u7ed3\u7b97\u753b\u9762\u663e\u793a\u6392\u884c\u699c\uff1a\u5de6\u672c\u5730 / \u53f3\u5e73\u53f0\uff0c\u4e24\u8fb9\u90fd\u6309\u5f53\u524d\u96be\u5ea6\u53d6\n\t\tvar diffName = DifficultyName( game.Difficulty );\n\t\t_localTitle.Text = $\"{diffName} \u00b7 Local Best\";\n\t\t_cloudTitle.Text = $\"{diffName} \u00b7 Global Best\";\n\n\t\t// \u53ea\u663e\u793a\u5f53\u524d\u96be\u5ea6\u7684\u672c\u5730\u6392\u884c\uff08\u5934\u50cf\u5757 = \u672c\u5730\u73a9\u5bb6\uff09\n\t\ttry\n\t\t{\n\t\t\tvar top = JumpScores.Top( BoardRows, game.Difficulty );\n\t\t\tfor ( int i = 0; i < BoardRows; i++ )\n\t\t\t{\n\t\t\t\tvar row = _localRows[i];\n\t\t\t\tif ( i < top.Count )\n\t\t\t\t{\n\t\t\t\t\tvar e = top[i];\n\t\t\t\t\tvar name = LocalName();\n\t\t\t\t\trow.Style.Display = DisplayMode.Flex;\n\t\t\t\t\t_localRank[i].Text = $\"{i + 1}.\";\n\t\t\t\t\t_localAvatarChar[i].Text = name.Substring( 0, 1 );\n\t\t\t\t\t_localAvatar[i].Style.BackgroundColor = AvatarColors[Math.Abs( name.GetHashCode() ) % AvatarColors.Length];\n\t\t\t\t\t_localNameLabels[i].Text = name;\n\t\t\t\t\t_localScore[i].Text = e.Score.ToString();\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\trow.Style.Display = DisplayMode.None;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_localEmpty.Text = \"No records yet\";\n\t\t\t_localEmpty.Style.Display = top.Count == 0 ? DisplayMode.Flex : DisplayMode.None;\n\t\t}\n\t\tcatch ( Exception )\n\t\t{\n\t\t\t_built = false; // \u7ed3\u6784\u5f02\u5e38 \u2192 \u4e0b\u5e27\u6574\u4f53\u91cd\u5efa\n\t\t}\n\n\t\t// \u5e73\u53f0\u6392\u884c\uff1a\u672c\u5730\u5305\u6ca1\u6709\u4e91\u7aef\u8eab\u4efd\u3001\u6216\u8be5\u96be\u5ea6\u8fd8\u6ca1\u62c9\u5230\u65f6\u663e\u793a\u5360\u4f4d\u3002\n\t\t// \u53d6\u7684\u662f\u6309\u96be\u5ea6\u7f13\u5b58\u7684\u5217\u8868\uff0c\u4e0d\u4f1a\u4e32\u5230\u522b\u7684\u96be\u5ea6\n\t\tvar cloud = JumpScores.Cloud( game.Difficulty );\n\t\tif ( cloud.Count == 0 )\n\t\t{\n\t\t\tfor ( int i = 0; i < BoardRows; i++ ) _cloudRows[i].Style.Display = DisplayMode.None;\n\t\t\t_cloudEmpty.Text = \"No records yet\";\n\t\t\t_cloudEmpty.Style.Display = DisplayMode.Flex;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_cloudEmpty.Style.Display = DisplayMode.None;\n\t\t\tfor ( int i = 0; i < BoardRows; i++ )\n\t\t\t{\n\t\t\t\tvar row = _cloudRows[i];\n\t\t\t\tif ( i < cloud.Count )\n\t\t\t\t{\n\t\t\t\t\tvar (rank, name, score) = cloud[i];\n\t\t\t\t\trow.Style.Display = DisplayMode.Flex;\n\t\t\t\t\t_cloudRank[i].Text = $\"{rank}.\";\n\t\t\t\t\t_cloudAvatarChar[i].Text = string.IsNullOrEmpty( name ) ? \"?\" : name.Substring( 0, 1 );\n\t\t\t\t\t_cloudAvatar[i].Style.BackgroundColor = AvatarColors[Math.Abs( name.GetHashCode() ) % AvatarColors.Length];\n\t\t\t\t\t_cloudName[i].Text = name;\n\t\t\t\t\t_cloudScore[i].Text = score.ToString();\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\trow.Style.Display = DisplayMode.None;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}"
},
{
"Ident": "slfy.tiaotiao",
"Path": "Jump/JumpGame.cs",
"FileName": "JumpGame.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 380088,
"IsPrivate": false,
"Code": "using System;\r\n\r\n/// <summary>\r\n/// \u8df3\u4e00\u8df3\u4e3b\u903b\u8f91\uff1a\u5e73\u53f0\u751f\u6210\u3001\u84c4\u529b\u8df3\u8dc3\u3001\u843d\u70b9\u5224\u5b9a\u4e0e\u8ba1\u5206\u3002\r\n/// GameObjectSystem \u4f1a\u5728\u573a\u666f\u52a0\u8f7d\u65f6\u81ea\u52a8\u5b9e\u4f8b\u5316\uff08\u65e0\u9700\u6302\u8f7d\u5230\u573a\u666f\uff09\uff0c\r\n/// \u4f46\u7f16\u8f91\u5668\u7684\u7f16\u8f91\u6a21\u5f0f\u4e0b\u4e5f\u4f1a tick\uff0c\u6240\u4ee5\u6240\u6709\u903b\u8f91\u90fd\u7528 Game.IsPlaying \u62e6\u622a\u3002\r\n/// \u6574\u4e2a\u4e16\u754c\u5728\u8fd0\u884c\u65f6\u751f\u6210\uff0c\u4e0d\u4f9d\u8d56\u573a\u666f\u91cc\u5df2\u6709\u7684\u5185\u5bb9\uff08\u6a21\u677f\u5bf9\u8c61\u4f1a\u88ab\u5c4f\u853d\uff09\u3002\r\n/// </summary>\r\npublic sealed class JumpGame : GameObjectSystem<JumpGame>\r\n{\r\n\t// ---- \u624b\u611f\u53c2\u6570\uff08\u53ef\u5728 \u9879\u76ee\u8bbe\u7f6e \u2192 Systems \u91cc\u8986\u76d6\uff09----\r\n\r\n\t/// <summary> \u84c4\u529b\u4ece 0 \u5230 1 \u9700\u8981\u7684\u79d2\u6570 </summary>\r\n\t[Property] public float ChargeTime { get; set; } = 2.0f;\r\n\r\n\t/// <summary> \u8f7b\u70b9\u65f6\u7684\u8df3\u8dc3\u8ddd\u79bb </summary>\r\n\t[Property] public float MinJumpDistance { get; set; } = 130f;\r\n\r\n\t/// <summary>\r\n\t/// \u84c4\u6ee1\u65f6\u7684\u8df3\u8dc3\u8ddd\u79bb\u3002\u5fc5\u987b\u8d34\u7740 CenterDistance.y\uff08\u5e73\u53f0\u4e2d\u5fc3\u8ddd\u4e0a\u9650\uff09\u53d6\u503c\uff1a\r\n\t/// \u5e73\u53f0\u6700\u8fdc\u53ea\u751f\u6210\u5230 470\uff0c\u82e5\u84c4\u6ee1\u80fd\u98de 780\uff0c\u5219\u84c4\u529b\u6761\u6709\u8fd1\u4e00\u534a\u7684\u91cf\u7a0b\u6c38\u8fdc\u4e0d\u8be5\u88ab\u7528\u5230\uff0c\r\n\t/// \u800c\u591a\u84c4\u4e00\u70b9\u70b9\u5c31\u76f4\u63a5\u98de\u8fc7\u5934\u3002\u53d6 470 + \u5e73\u53f0\u534a\u5bbd + \u5224\u5b9a\u4f59\u91cf \u2248 560\uff0c\r\n\t/// \u84c4\u6ee1\u6b63\u597d\u662f\"\u6700\u8fdc\u7684\u5e73\u53f0\u64e6\u8fb9\u591f\u5230\"\uff0c\u6574\u6761\u84c4\u529b\u6761\u90fd\u6709\u7528\u3002\r\n\t/// </summary>\r\n\t[Property] public float MaxJumpDistance { get; set; } = 560f;\r\n\r\n\t/// <summary> \u843d\u70b9\u8ddd\u5e73\u53f0\u4e2d\u5fc3\u5c0f\u4e8e\u8be5\u503c\u7b97\"\u5b8c\u7f8e\"\uff0c\u89e6\u53d1\u8fde\u51fb\u52a0\u5206 </summary>\r\n\t[Property] public float PerfectRadius { get; set; } = 20f;\r\n\r\n\t/// <summary> \u5e73\u53f0\u9876\u9762\u534a\u5bbd\u7684\u968f\u673a\u8303\u56f4 </summary>\r\n\t[Property] public Vector2 PlatformHalfSize { get; set; } = new Vector2( 48, 90 );\r\n\r\n\t/// <summary> \u76f8\u90bb\u5e73\u53f0\u4e2d\u5fc3\u8ddd\u79bb\u7684\u968f\u673a\u8303\u56f4 </summary>\r\n\t[Property] public Vector2 CenterDistance { get; set; } = new Vector2( 250, 470 );\r\n\r\n\t/// <summary> \u6bcf\u4e00\u6b65\u65b9\u5411\u7684\u968f\u673a\u504f\u8f6c\u89d2\uff08\u5ea6\uff0c\u5de6\u53f3\u4ea4\u66ff\u65bd\u52a0\uff09 </summary>\r\n\t[Property] public Vector2 TurnAngle { get; set; } = new Vector2( 14, 34 );\r\n\r\n\t// ---- \u5e38\u91cf ----\r\n\r\n\tconst float KillDepth = 420f; // \u4f4e\u4e8e\u9644\u8fd1\u5e73\u53f0\u9876\u9762\u591a\u5c11\u7b97\u5760\u843d\u51fa\u5c40\r\n\tconst float Gravity = 2400f; // \u81ea\u7531\u843d\u4f53\u9636\u6bb5\u7684\u91cd\u529b\uff08\u4ec5\u7528\u4e8e\u5760\u843d\u8868\u73b0\uff09\r\n\r\n\tstatic readonly Color[] PlatformColors =\r\n\t{\r\n\t\tnew Color( 0.98f, 0.74f, 0.62f ),\r\n\t\tnew Color( 0.64f, 0.86f, 0.74f ),\r\n\t\tnew Color( 0.99f, 0.88f, 0.58f ),\r\n\t\tnew Color( 0.66f, 0.80f, 0.95f ),\r\n\t\tnew Color( 0.93f, 0.77f, 0.90f ),\r\n\t\tnew Color( 0.96f, 0.96f, 0.94f ),\r\n\t};\r\n\r\n\t// ---- \u5bf9\u5916\u72b6\u6001\uff08HUD / \u76f8\u673a\u8bfb\u53d6\uff09----\r\n\r\n\tpublic int Score { get; private set; }\r\n\tpublic int Streak { get; private set; }\r\n\tpublic bool HasJumped { get; private set; }\r\n\tpublic bool IsGameOver { get; private set; }\r\n\tpublic bool IsFalling => _falling;\r\n\tpublic bool IsTutorial => _tutorialActive;\r\n\r\n\t/// <summary> \u5f53\u524d\u96be\u5ea6\uff08\u5f00\u59cb\u83dc\u5355\u9009\u62e9\uff09 </summary>\r\n\tpublic JumpDifficulty Difficulty { get; private set; } = JumpDifficulty.Normal;\r\n\r\n\t/// <summary> \u662f\u5426\u5728\u5f00\u59cb\u83dc\u5355\uff08\u80cc\u666f\u7531 AI \u81ea\u52a8\u6f14\u793a\u8df3\u8dc3\uff09 </summary>\r\n\tpublic bool InMenu => _menuMode;\r\n\r\n\t/// <summary> \u65b0\u624b\u6f14\u793a\u5b57\u5e55\uff08\u975e\u7a7a\u65f6 HUD \u663e\u793a\uff09 </summary>\r\n\tpublic string TutorialText { get; private set; }\r\n\r\n\tpublic Vector3 PiecePosition { get; private set; }\r\n\r\n\t// ---- \u5185\u90e8\u72b6\u6001 ----\r\n\r\n\tGameObject _root;\r\n\tJumpPiece _piece;\r\n\tJumpCamera _camera;\r\n\treadonly List<JumpPlatform> _platforms = new();\r\n\treadonly List<(GameObject Go, TimeSince Born, Vector3 Scale)> _dots = new();\r\n\r\n\tstatic GameObject _sharedRoot; // \u70ed\u91cd\u8f7d/\u91cd\u5efa\u65f6\u590d\u7528\u540c\u4e00\u4e2a\u4e16\u754c\u6839\uff0c\u907f\u514d\u5806\u53e0\u591a\u5957\u573a\u666f\r\n\r\n\tJumpPlatform _current;\r\n\tJumpPlatform _target;\r\n\tJumpPlatform _previous; // \u8eab\u540e\u90a3\u4e00\u5757\uff08\u5012\u8df3\u76ee\u6807\uff09\r\n\r\n\tbool _worldReady;\r\n\tbool _wasDown;\r\n\tbool _charging;\r\n\tfloat _power;\r\n\tfloat _dirAngle;\r\n\tfloat _turnSide = 1f;\r\n\tbool _menuMode = true; // \u5f00\u5c40\u8fdb\u5165\u5f00\u59cb\u83dc\u5355\uff0c\u80cc\u666f AI \u81ea\u52a8\u8df3\r\n\r\n\t// \u7b80\u5355\u6a21\u5f0f\u8f85\u52a9\u7ebf\uff08\u84c4\u529b\u65f6\u663e\u793a\u9884\u6d4b\u5f39\u9053\u4e0e\u843d\u70b9\u73af\uff09\r\n\tconst int AssistDotCount = 14;\r\n\tstatic readonly Color AssistHitColor = new Color( 0.30f, 0.78f, 0.42f );\r\n\tstatic readonly Color AssistMissColor = new Color( 0.92f, 0.32f, 0.28f );\r\n\treadonly List<GameObject> _assistDots = new();\r\n\tGameObject _assistRing;\r\n\tModelRenderer _assistRingRenderer;\r\n\tbool _assistVisible;\r\n\tstatic readonly Model BoxModel = Model.Load( \"models/dev/box.vmdl\" );\r\n\r\n\t// \u65b0\u624b\u6f14\u793a\r\n\tbool _tutorialActive;\r\n\tbool _tutorialDone; // \u6bcf\u4e2a play \u4f1a\u8bdd\u53ea\u6f14\u793a\u4e00\u6b21\uff0c\u91cd\u5f00\u4e0d\u518d\u64ad\r\n\tbool _waitingForInput; // \u5f00\u5c40\u7b49\u5f85\u671f\uff1a\u73a9\u5bb6 3 \u79d2\u5185\u6309\u4e0b\u5219\u8df3\u8fc7\u6f14\u793a\uff0c\u95f2\u7f6e\u5219\u64ad\u653e\r\n\tTimeSince _sinceSpawn;\r\n\tfloat _demoPower;\r\n\tTimeSince _tutorialT;\r\n\tTimeSince _sinceTutorialEnd;\r\n\tTimeSince _dotTimer;\r\n\tstatic readonly Model SphereModel = Model.Load( \"models/dev/sphere.vmdl\" );\r\n\r\n\t// \u98de\u884c\u72b6\u6001\r\n\tbool _flying;\r\n\tbool _falling;\r\n\tVector3 _jumpFrom;\r\n\tVector3 _jumpTo;\r\n\tfloat _jumpTime;\r\n\tfloat _jumpDuration;\r\n\tfloat _jumpHeight;\r\n\tfloat _prevBaseZ;\r\n\tfloat _killZ;\r\n\tVector3 _fallVelocity;\r\n\r\n\tpublic JumpGame( Scene scene ) : base( scene )\r\n\t{\r\n\t\tListen( Stage.UpdateBones, 0, Tick, \"JumpGame.Tick\" );\r\n\t}\r\n\r\n\tvoid Tick()\r\n\t{\r\n\t\tif ( !Game.IsPlaying ) return;\r\n\r\n\t\tEnsureWorld();\r\n\r\n\t\tvar down = Input.Down( \"Attack1\" ) || Input.Down( \"Jump\" );\r\n\r\n\t\t// Q\uff1a\u4ece\u6e38\u620f / \u7ed3\u7b97\u753b\u9762\u8fd4\u56de\u5f00\u59cb\u83dc\u5355\r\n\t\tif ( !InMenu && Input.Pressed( \"Menu\" ) )\r\n\t\t{\r\n\t\t\tEnterMenu();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( IsGameOver )\r\n\t\t{\r\n\t\t\tif ( down && !_wasDown ) ResetGame();\r\n\t\t\t_wasDown = down;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _piece.IsValid() ) PiecePosition = _piece.BasePosition;\r\n\r\n\t\tTickTrailDots();\r\n\t\tUpdateAssistLine();\r\n\r\n\t\tif ( _flying )\r\n\t\t{\r\n\t\t\tTickFlight( Time.Delta );\r\n\t\t\t_wasDown = down;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// \u5f00\u59cb\u83dc\u5355\u80cc\u666f / \u65b0\u624b\u6f14\u793a\uff1aAI \u81ea\u52a8\u8868\u6f14\"\u84c4\u529b\u2192\u8d77\u8df3\u2192\u843d\u70b9\"\r\n\t\tif ( _menuMode || _tutorialActive )\r\n\t\t{\r\n\t\t\t// \u83dc\u5355\u91cc\u652f\u6301\u952e\u76d8\u9009\u96be\u5ea6\uff08\u6570\u5b57\u952e 1/2/3\uff09\r\n\t\t\tif ( _menuMode )\r\n\t\t\t{\r\n\t\t\t\tif ( Input.Pressed( \"Slot1\" ) ) { StartGame( JumpDifficulty.Easy ); return; }\r\n\t\t\t\tif ( Input.Pressed( \"Slot2\" ) ) { StartGame( JumpDifficulty.Normal ); return; }\r\n\t\t\t\tif ( Input.Pressed( \"Slot3\" ) ) { StartGame( JumpDifficulty.Hard ); return; }\r\n\t\t\t}\r\n\r\n\t\t\tTickAutoJump( Time.Delta );\r\n\t\t\t_wasDown = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( TutorialText != null && _sinceTutorialEnd > 2.4f ) TutorialText = null;\r\n\r\n\t\t// \u5f00\u5c40 3 \u79d2\u89c2\u5bdf\u7a97\uff1a\u73a9\u5bb6\u81ea\u5df1\u6309\u4e0b \u2192 \u8df3\u8fc7\u6f14\u793a\uff1b\u4e00\u76f4\u95f2\u7f6e \u2192 \u64ad\u653e\u6f14\u793a\r\n\t\tif ( _waitingForInput )\r\n\t\t{\r\n\t\t\tif ( down && !_wasDown )\r\n\t\t\t{\r\n\t\t\t\t_waitingForInput = false;\r\n\t\t\t\t_tutorialDone = true;\r\n\t\t\t}\r\n\t\t\telse if ( _sinceSpawn > 3f )\r\n\t\t\t{\r\n\t\t\t\t_waitingForInput = false;\r\n\t\t\t\tStartTutorial();\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// \u5730\u9762\u72b6\u6001\uff1a\u6309\u4e0b\u84c4\u529b\uff0c\u677e\u5f00\u8d77\u8df3\r\n\t\tif ( down && !_wasDown )\r\n\t\t{\r\n\t\t\t_charging = true;\r\n\t\t\t_power = 0f;\r\n\t\t}\r\n\r\n\t\tif ( _charging )\r\n\t\t{\r\n\t\t\tif ( down )\r\n\t\t\t{\r\n\t\t\t\t_power = Math.Min( _power + Time.Delta / ChargeTime, 1f );\r\n\t\t\t\t_piece?.SetCharge( _power );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_charging = false;\r\n\t\t\t\tLaunch();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_wasDown = down;\r\n\t}\r\n\r\n\t// ---- \u4e16\u754c\u642d\u5efa ----\r\n\r\n\tvoid EnsureWorld()\r\n\t{\r\n\t\tif ( _worldReady ) return;\r\n\t\t_worldReady = true;\r\n\r\n\t\t// \u590d\u7528\u65e7\u7684\u4e16\u754c\u6839\uff08\u70ed\u91cd\u8f7d\u7b49\u573a\u666f\u4e0b\u907f\u514d\u5806\u53e0\u591a\u5957\u5e73\u53f0/\u76f8\u673a\uff09\uff0c\u5e76\u6e05\u6389\u5176\u52a8\u6001\u5b50\u7269\u4f53\u3002\r\n\t\t// \u6ce8\u610f\uff1aPlay \u505c\u6b62\u540e static \u5f15\u7528\u53ef\u80fd\u6307\u5411\u5df2\u5378\u8f7d\u7684\u65e7\u573a\u666f\u5bf9\u8c61\uff08IsValid \u4ecd\u4e3a\u771f\uff09\uff0c\r\n\t\t// \u5fc5\u987b\u6821\u9a8c\u573a\u666f\u5f52\u5c5e\uff0c\u5426\u5219\u5411\u65e7\u573a\u666f\u6839\u6302\u5b50\u7269\u4f53\u76f4\u63a5\u65ad\u8a00\u5931\u8d25\r\n\t\tif ( _sharedRoot.IsValid() && _sharedRoot.Scene == Scene )\r\n\t\t{\r\n\t\t\t_root = _sharedRoot;\r\n\t\t\tforeach ( var child in _root.Children.ToArray() )\r\n\t\t\t{\r\n\t\t\t\tif ( child.IsValid() ) child.Destroy();\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_sharedRoot = null;\r\n\t\t\t_root = new GameObject( true, \"JumpGame\" );\r\n\t\t\t_sharedRoot = _root;\r\n\r\n\t\t\t// \u5c4f\u853d\u573a\u666f\u91cc\u6a21\u677f\u7559\u4e0b\u7684\u6240\u6709\u5bf9\u8c61\uff08\u793a\u4f8b\u76f8\u673a\u3001\u65b9\u5757\u7b49\uff09\uff0c\u53ea\u8dd1\u6211\u4eec\u751f\u6210\u7684\u4e16\u754c\u3002\r\n\t\t\t// Scene \u672c\u8eab\u5c31\u662f\u6839 GameObject\uff0c\u9876\u5c42\u5bf9\u8c61\u90fd\u5728\u5b83\u7684 Children \u91cc\r\n\t\t\tforeach ( var go in Scene.Children )\r\n\t\t\t{\r\n\t\t\t\tif ( go.IsValid() && go != _root ) go.Enabled = false;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tCreateLight();\r\n\t\tCreateCamera();\r\n\t\tCreateHud();\r\n\t\tBuildInitialWorld();\r\n\t}\r\n\r\n\tvoid CreateLight()\r\n\t{\r\n\t\tvar sun = new GameObject( true, \"Sun\" );\r\n\t\tsun.Parent = _root;\r\n\t\tsun.WorldRotation = Rotation.LookAt( new Vector3( -0.5f, -0.6f, -1.1f ).Normal );\r\n\r\n\t\tvar light = sun.AddComponent<DirectionalLight>();\r\n\t\tlight.LightColor = new Color( 1f, 0.96f, 0.90f );\r\n\t\tlight.Shadows = true;\r\n\t\tlight.ShadowCascadeCount = 2;\r\n\r\n\t\tvar ambientGo = new GameObject( true, \"Ambient\" );\r\n\t\tambientGo.Parent = _root;\r\n\t\tambientGo.AddComponent<AmbientLight>().Color = new Color( 0.48f, 0.52f, 0.60f );\r\n\t}\r\n\r\n\tvoid CreateCamera()\r\n\t{\r\n\t\tvar go = new GameObject( true, \"JumpCamera\" );\r\n\t\tgo.Parent = _root;\r\n\r\n\t\tvar cam = go.AddComponent<CameraComponent>();\r\n\t\tcam.IsMainCamera = true;\r\n\t\tcam.Priority = -100;\r\n\t\tcam.FieldOfView = 55f;\r\n\t\tcam.BackgroundColor = new Color( 0.80f, 0.81f, 0.83f );\r\n\r\n\t\t_camera = go.AddComponent<JumpCamera>();\r\n\t}\r\n\r\n\tvoid CreateHud()\r\n\t{\r\n\t\tvar go = new GameObject( true, \"Hud\" );\r\n\t\tgo.Parent = _root;\r\n\t\tgo.AddComponent<ScreenPanel>();\r\n\t\tgo.AddComponent<JumpHud>();\r\n\t}\r\n\r\n\tvoid BuildInitialWorld()\r\n\t{\r\n\t\tScore = 0;\r\n\t\tStreak = 0;\r\n\t\tHasJumped = false;\r\n\t\tIsGameOver = false;\r\n\t\t_charging = false;\r\n\t\t_power = 0f;\r\n\t\t_flying = false;\r\n\t\t_falling = false;\r\n\t\t_dirAngle = 0f;\r\n\r\n\t\tforeach ( var p in _platforms ) p.DestroyAll();\r\n\t\t_platforms.Clear();\r\n\t\t_target = null;\r\n\t\t_previous = null;\r\n\r\n\t\tif ( _piece.IsValid() ) _piece.GameObject.Destroy();\r\n\r\n\t\tvar first = CreatePlatform( Vector2.Zero, 80f, 80f, 100f, PlatformColors[0] );\r\n\t\t_current = first;\r\n\r\n\t\tvar pieceGo = new GameObject( true, \"Piece\" );\r\n\t\tpieceGo.Parent = _root;\r\n\t\t_piece = pieceGo.AddComponent<JumpPiece>();\r\n\t\t_piece.Init( first );\r\n\r\n\t\t// \u5f00\u5c40\uff1a\u5f53\u524d\u5e73\u53f0\u5c31\u662f\u7b2c\u4e00\u5757\uff0c\u8eab\u540e\u6ca1\u6709\u5e73\u53f0\uff0c\u6240\u4ee5\u6ca1\u6709\u5012\u8df3\u76ee\u6807\r\n\t\t_previous = null;\r\n\t\tSpawnNextPlatform();\r\n\r\n\t\t_camera?.SnapTo( PiecePosition = _piece.BasePosition );\r\n\r\n\t\t// \u83dc\u5355\u80cc\u666f\u6f14\u793a / \u5f00\u5c40\u5f15\u5bfc\uff08\u672c\u4f1a\u8bdd\u7b2c\u4e00\u6b21\u8fdb\u5165\u65f6\u5148\u7b49 3 \u79d2\uff1a\r\n\t\t// \u73a9\u5bb6 3 \u79d2\u5185\u81ea\u5df1\u6309\u4e0b \u2192 \u8df3\u8fc7\u6f14\u793a\u76f4\u63a5\u73a9\uff1b\u4e00\u76f4\u95f2\u7f6e \u2192 \u81ea\u52a8\u64ad\u653e\u6f14\u793a\uff09\r\n\t\tif ( _menuMode )\r\n\t\t{\r\n\t\t\t_waitingForInput = false;\r\n\t\t\tTutorialText = null;\r\n\t\t\t_tutorialT = 0;\r\n\t\t\t_demoPower = ComputeDemoPower();\r\n\t\t}\r\n\t\telse if ( _tutorialDone )\r\n\t\t{\r\n\t\t\tTutorialText = null;\r\n\t\t\t_waitingForInput = false;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_waitingForInput = true;\r\n\t\t\t_sinceSpawn = 0;\r\n\t\t\tTutorialText = null;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary> \u8ba1\u7b97\u8ba9\u68cb\u5b50\u6b63\u597d\u843d\u5728\u76ee\u6807\u5e73\u53f0\u4e2d\u5fc3\u7684\u84c4\u529b\u503c </summary>\r\n\tfloat ComputeDemoPower()\r\n\t{\r\n\t\tif ( !_target.IsValid() || !_piece.IsValid() ) return 0.35f;\r\n\r\n\t\tvar dx = _target.CenterX - _piece.BasePosition.x;\r\n\t\tvar dy = _target.CenterY - _piece.BasePosition.y;\r\n\t\tvar dist = MathF.Sqrt( dx * dx + dy * dy );\r\n\r\n\t\treturn Math.Clamp( ( dist - MinJumpDistance ) / ( MaxJumpDistance - MinJumpDistance ), 0.05f, 1f );\r\n\t}\r\n\r\n\t/// <summary> \u8fdb\u5165\u5f00\u59cb\u83dc\u5355\uff08\u80cc\u666f AI \u6f14\u793a\uff09 </summary>\r\n\tpublic void EnterMenu()\r\n\t{\r\n\t\t_menuMode = true;\r\n\t\t_tutorialActive = false;\r\n\t\tResetGame();\r\n\r\n\t\t// \u83dc\u5355\u906e\u7f69\u4f1a\u5728\u4e0b\u4e00\u5e27 HUD \u5237\u65b0\u65f6\u624d\u663e\u793a\uff0c\"\u8fd4\u56de\u83dc\u5355\"\u8fd9\u4e00\u4e0b\u70b9\u51fb\u4e0d\u8be5\u987a\u624b\u628a\u84c4\u529b\u4e5f\u5f00\u59cb\u4e86\r\n\t\tif ( Input.Down( \"Attack1\" ) ) _wasDown = true;\r\n\t}\r\n\r\n\t/// <summary> \u4ece\u5f00\u59cb\u83dc\u5355\u9009\u62e9\u96be\u5ea6\u8fdb\u5165\u6b63\u5f0f\u6e38\u620f </summary>\r\n\tpublic void StartGame( JumpDifficulty difficulty )\r\n\t{\r\n\t\tDifficulty = difficulty;\r\n\t\t_menuMode = false;\r\n\t\t_tutorialActive = false;\r\n\t\tResetGame();\r\n\r\n\t\t// \u6309\u94ae\u662f\u5728\u9f20\u6807\u6309\u4e0b/\u677e\u5f00\u90a3\u4e00\u523b\u88ab\u70b9\u4e2d\u7684\uff0c\u90a3\u4e00\u6b21\u70b9\u51fb\u7528\u6765\u9009\u96be\u5ea6\uff0c\r\n\t\t// \u4e0d\u7f6e\u4f4d\u7684\u8bdd\u677e\u624b\u4f1a\u88ab\u5f53\u6210\"\u84c4\u529b\u7ed3\u675f\"\u7acb\u523b\u8d77\u8df3\u4e00\u6b21\r\n\t\tif ( Input.Down( \"Attack1\" ) ) _wasDown = true;\r\n\r\n\t\tSound.Play( \"sounds/kenney/ui/ui.navigate.forward.sound\", PiecePosition, 0f );\r\n\t}\r\n\r\n\t/// <summary> \u5f00\u5c40\u6f14\u793a\uff1aAI \u81ea\u52a8\u8868\u6f14\u4e00\u6b21\u84c4\u529b\u8df3\u8dc3\uff0c\u843d\u70b9\u5bf9\u51c6\u76ee\u6807\u4e2d\u5fc3 </summary>\r\n\tvoid StartTutorial()\r\n\t{\r\n\t\t_tutorialDone = true;\r\n\t\t_tutorialActive = true;\r\n\t\t_tutorialT = 0;\r\n\t\tTutorialText = \"Watch! Hold to charge, release to jump\";\r\n\t\t_demoPower = ComputeDemoPower();\r\n\t}\r\n\r\n\tvoid TickAutoJump( float dt )\r\n\t{\r\n\t\tvar t = (float)_tutorialT;\r\n\r\n\t\tif ( t < 0.9f )\r\n\t\t{\r\n\t\t\t// \u5148\u505c\u987f\u4e00\u62cd\uff0c\u8ba9\u73a9\u5bb6\u770b\u6e05\u5f00\u5c40\r\n\t\t}\r\n\t\telse if ( t < 1.7f )\r\n\t\t{\r\n\t\t\t// \u6a21\u62df\u84c4\u529b\uff1a\u529b\u5ea6\u722c\u5347\u5230\"\u521a\u597d\u80fd\u843d\u5728\u4e2d\u5fc3\"\u7684\u503c\r\n\t\t\t_power = Math.Min( ( t - 0.9f ) / 0.8f, 1f ) * _demoPower;\r\n\t\t\t_piece?.SetCharge( _power );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tLaunch();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary> \u6f14\u793a\u8df3\u8dc3\u7684\u8f68\u8ff9\u70b9\uff1a\u68cb\u5b50\u98de\u8fc7\u7684\u5730\u65b9\u7559\u4e0b\u5c0f\u5706\u70b9\u5e76\u6e10\u6e10\u6d88\u6563 </summary>\r\n\tvoid SpawnTrailDot( Vector3 basePos )\r\n\t{\r\n\t\tvar go = new GameObject( true, \"TrailDot\" );\r\n\t\tgo.Parent = _root;\r\n\r\n\t\tvar r = go.AddComponent<ModelRenderer>();\r\n\t\tr.Model = SphereModel;\r\n\t\tr.Tint = new Color( 0.25f, 0.28f, 0.34f );\r\n\r\n\t\tvar size = SphereModel.Bounds.Size.x;\r\n\t\tvar scale = 14f / size;\r\n\t\tvar dotScale = new Vector3( scale, scale, scale );\r\n\t\tgo.LocalScale = dotScale;\r\n\t\tgo.WorldPosition = basePos + Vector3.Up * ( SphereModel.Bounds.Size.z * scale * 0.5f + 2f );\r\n\r\n\t\t_dots.Add( ( go, 0, dotScale ) );\r\n\t}\r\n\r\n\tvoid TickTrailDots()\r\n\t{\r\n\t\tfor ( int i = _dots.Count - 1; i >= 0; i-- )\r\n\t\t{\r\n\t\t\tvar dot = _dots[i];\r\n\t\t\tvar age = (float)dot.Born;\r\n\r\n\t\t\tif ( age >= 0.9f || !dot.Go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tif ( dot.Go.IsValid() ) dot.Go.Destroy();\r\n\t\t\t\t_dots.RemoveAt( i );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tdot.Go.LocalScale = dot.Scale * ( 1f - age / 0.9f );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid ResetGame()\r\n\t{\r\n\t\tBuildInitialWorld();\r\n\t}\r\n\r\n\tJumpPlatform CreatePlatform( Vector2 center, float halfX, float halfY, float topZ, Color color )\r\n\t{\r\n\t\tvar go = new GameObject( true, \"Platform\" );\r\n\t\tgo.Parent = _root;\r\n\r\n\t\tvar platform = go.AddComponent<JumpPlatform>();\r\n\t\tplatform.Init( center, halfX, halfY, topZ, color );\r\n\t\t_platforms.Add( platform );\r\n\r\n\t\treturn platform;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// \u5728\u5f53\u524d\u5e73\u53f0\u524d\u65b9\u751f\u6210\u4e0b\u4e00\u5757\u3002\u5de6\u53f3\u4e25\u683c\u4ea4\u66ff\u3001\u76f8\u5bf9\u4e3b\u8f74(+X)\u5b9a\u89d2\uff08\u4e0d\u7d2f\u52a0\uff09\uff0c\r\n\t/// \u4fdd\u8bc1\u8def\u7ebf\u50cf\u539f\u7248\u4e00\u6837\u86c7\u5f62\u524d\u8fdb\u3001\u4e0b\u4e00\u5757\u5e73\u53f0\u59cb\u7ec8\u7559\u5728\u753b\u9762\u5185\u3002\r\n\t/// </summary>\r\n\tvoid SpawnNextPlatform()\r\n\t{\r\n\t\tvar prev = _current;\r\n\r\n\t\t_turnSide = -_turnSide;\r\n\t\t_dirAngle = _turnSide * Game.Random.Float( TurnAngle.x, TurnAngle.y ) * MathF.PI / 180f;\r\n\r\n\t\tvar dist = Game.Random.Float( CenterDistance.x, CenterDistance.y );\r\n\t\tvar px = prev.CenterX + MathF.Cos( _dirAngle ) * dist;\r\n\t\tvar py = prev.CenterY + MathF.Sin( _dirAngle ) * dist;\r\n\r\n\t\tvar halfX = Game.Random.Float( PlatformHalfSize.x, PlatformHalfSize.y );\r\n\t\tvar halfY = Game.Random.Float( PlatformHalfSize.x, PlatformHalfSize.y );\r\n\t\tvar topZ = Math.Clamp( prev.TopZ + Game.Random.Float( -50f, 60f ), 40f, 210f );\r\n\r\n\t\tif ( _target.IsValid() ) _target.IsTarget = false;\r\n\t\t_target = CreatePlatform( new Vector2( px, py ), halfX, halfY, topZ,\r\n\t\t\tGame.Random.FromArray( PlatformColors ) );\r\n\t\t_target.IsTarget = true;\r\n\t}\r\n\r\n\tvoid PruneOldPlatforms()\r\n\t{\r\n\t\t// \u4ece\u6700\u65e7\u7684\u4e00\u5757\u5f00\u59cb\u626b\uff0c\u5220\u6389\u7b2c\u4e00\u5757\"\u5df2\u7ecf\u6ca1\u7528\u4e86\"\u7684\u5e73\u53f0\u3002\r\n\t\t// \u4ecd\u5728\u7528\u7684\uff08\u5f53\u524d\u4f4d\u7f6e / \u76ee\u6807 / \u8eab\u540e\u90a3\u4e00\u5757\uff09\u53ea\u662f\u8df3\u8fc7\uff0c\u4e0d\u80fd\u632a\u5230\u961f\u5c3e\u2014\u2014\r\n\t\t// \u4e00\u65e6\u6253\u4e71\"\u6700\u65e7\u5728\u524d\"\u7684\u987a\u5e8f\uff0c\u5783\u573e\u5c31\u6c38\u8fdc\u6392\u4e0d\u5230\u961f\u5934\uff0c\u6c60\u5b50\u4f1a\u65e0\u4e0a\u9650\u5730\u957f\u3002\r\n\t\tfor ( int i = 0; i < _platforms.Count && _platforms.Count > 3; i++ )\r\n\t\t{\r\n\t\t\tvar p = _platforms[i];\r\n\r\n\t\t\tif ( p == _current || p == _target || p == _previous ) continue;\r\n\r\n\t\t\t_platforms.RemoveAt( i );\r\n\t\t\tp.DestroyAll();\r\n\t\t\ti--;\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- \u8df3\u8dc3\u4e0e\u5224\u5b9a ----\r\n\r\n\t/// <summary>\r\n\t/// \u73a9\u5bb6\u662f\u5426\u60f3\u8981\u5012\u7740\u8df3\uff1a\u53ea\u6309\u540e\u9000\u952e\uff08\u53ea\u6309\u524d\u8fdb\u3001\u4e24\u4e2a\u90fd\u6309\u3001\u90fd\u4e0d\u6309\uff0c\u4e00\u5f8b\u89c6\u4e3a\u524d\u8fdb\uff09\u3002\r\n\t/// \u83dc\u5355\u80cc\u666f\u6f14\u793a\u548c\u65b0\u624b\u6f14\u793a\u7531 AI \u9a71\u52a8\uff0c\u5fc5\u987b\u59cb\u7ec8\u5f80\u524d\u8df3\uff0c\u6240\u4ee5\u8fd9\u91cc\u4e00\u5e76\u6392\u9664\u3002\r\n\t/// </summary>\r\n\tbool WantsBackJump => !_menuMode && !_tutorialActive\r\n\t\t&& Input.Down( \"Backward\" ) && !Input.Down( \"Forward\" );\r\n\r\n\t/// <summary> \u4f9d\u636e\u84c4\u529b\u503c\u8ba1\u7b97\u4e00\u6b21\u8df3\u8dc3\u7684\u843d\u70b9/\u65f6\u957f/\u9ad8\u5ea6\uff08\u8d77\u8df3\u4e0e\u8f85\u52a9\u7ebf\u5171\u7528\uff09 </summary>\r\n\t(Vector3 to, float duration, float height) PlanJump( float power )\r\n\t{\r\n\t\tvar from = _piece.BasePosition;\r\n\r\n\t\t// \u53ea\u6309\u540e\u9000 = \u5728\"\u6765\u8def\u65b9\u5411\"\u4e0a\u6309\u5f53\u524d\u529b\u5ea6\u51fa\u624b\uff08\u529b\u5ea6\u5408\u9002\u5c31\u5f39\u56de\u4e0a\u4e00\u5757\u5e73\u53f0\uff09\r\n\t\tvar origin = WantsBackJump && _previous.IsValid() ? _previous : _target;\r\n\t\tvar dir = new Vector2( origin.CenterX - from.x, origin.CenterY - from.y ).Normal;\r\n\r\n\t\tvar dist = Math.Min( MinJumpDistance + power * ( MaxJumpDistance - MinJumpDistance ), MaxJumpDistance );\r\n\r\n\t\treturn ( new Vector3( from.x + dir.x * dist, from.y + dir.y * dist, from.z ),\r\n\t\t\t0.42f + 0.22f * power,\r\n\t\t\t150f + 60f * power );\r\n\t}\r\n\r\n\tvoid Launch()\r\n\t{\r\n\t\tif ( _target is null || !_piece.IsValid() ) return;\r\n\r\n\t\tHasJumped = true;\r\n\r\n\t\tvar (to, duration, height) = PlanJump( _power );\r\n\r\n\t\t_jumpFrom = _piece.BasePosition;\r\n\t\t_jumpTo = to;\r\n\t\t_jumpDuration = duration;\r\n\t\t_jumpHeight = height;\r\n\t\t_jumpTime = 0f;\r\n\t\t_prevBaseZ = _jumpFrom.z;\r\n\t\t_killZ = Math.Min( _current.TopZ, _target.TopZ ) - KillDepth;\r\n\t\t_flying = true;\r\n\t\t_falling = false;\r\n\r\n\t\t_piece.SetCharge( 0f );\r\n\t\t_piece.Impulse( 3.4f );\r\n\t\tSound.Play( \"sounds/footsteps/footstep-concrete-jump.sound\", _jumpFrom, 0f );\r\n\r\n\t\tif ( !_menuMode && !_tutorialActive ) JumpAchievements.OnJump();\r\n\t}\r\n\r\n\tvoid TickFlight( float dt )\r\n\t{\r\n\t\tif ( !_piece.IsValid() ) return;\r\n\r\n\t\tif ( !_falling )\r\n\t\t{\r\n\t\t\t_jumpTime += dt;\r\n\t\t\tvar u = Math.Min( _jumpTime / _jumpDuration, 1f );\r\n\r\n\t\t\tvar x = MathX.Lerp( _jumpFrom.x, _jumpTo.x, u );\r\n\t\t\tvar y = MathX.Lerp( _jumpFrom.y, _jumpTo.y, u );\r\n\t\t\t// \u629b\u7269\u7ebf\uff1a\u8d77\u70b9 0\u3001\u7ec8\u70b9 0\u3001\u9876\u70b9 jumpHeight\r\n\t\t\tvar z = _jumpFrom.z + 4f * _jumpHeight * u * ( 1f - u );\r\n\r\n\t\t\t_piece.SetBasePosition( new Vector3( x, y, z ) );\r\n\r\n\t\t\t// \u6f14\u793a\u8df3\u8dc3\u65f6\u7559\u4e0b\u8f68\u8ff9\u70b9\uff0c\u6807\u51fa\u629b\u7269\u7ebf\r\n\t\t\tif ( _tutorialActive && _dotTimer > 0.055f )\r\n\t\t\t{\r\n\t\t\t\t_dotTimer = 0;\r\n\t\t\t\tSpawnTrailDot( new Vector3( x, y, z ) );\r\n\t\t\t}\r\n\r\n\t\t\t// \u4e0b\u964d\u9636\u6bb5\u505a\u843d\u70b9\u5224\u5b9a\uff08\u5e27\u95f4\u8de8\u8fc7\u5e73\u53f0\u9876\u9762\u624d\u7b97\u843d\u4e0a\uff0c\u4efb\u4f55\u5e27\u7387\u4e0b\u90fd\u51c6\u786e\uff09\r\n\t\t\tif ( z <= _prevBaseZ )\r\n\t\t\t{\r\n\t\t\t\tResolveLanding( x, y, z, _prevBaseZ );\r\n\t\t\t}\r\n\t\t\t_prevBaseZ = z;\r\n\r\n\t\t\t// \u629b\u7269\u7ebf\u8d70\u5b8c\u4ecd\u6ca1\u843d\u5230\u4efb\u4f55\u5e73\u53f0 \u2192 \u8f6c\u81ea\u7531\u843d\u4f53\r\n\t\t\tif ( u >= 1f && !_falling && _flying )\r\n\t\t\t{\r\n\t\t\t\t_falling = true;\r\n\t\t\t\t_fallVelocity = new Vector3(\r\n\t\t\t\t\t( _jumpTo.x - _jumpFrom.x ) / _jumpDuration,\r\n\t\t\t\t\t( _jumpTo.y - _jumpFrom.y ) / _jumpDuration,\r\n\t\t\t\t\t-4f * _jumpHeight / _jumpDuration );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( _falling )\r\n\t\t{\r\n\t\t\t_fallVelocity -= Vector3.Up * Gravity * dt;\r\n\t\t\tvar pos = _piece.BasePosition + _fallVelocity * dt;\r\n\t\t\t_piece.SetBasePosition( pos );\r\n\r\n\t\t\tResolveLanding( pos.x, pos.y, pos.z, pos.z - _fallVelocity.z * dt );\r\n\r\n\t\t\tif ( pos.z < _killZ )\r\n\t\t\t{\r\n\t\t\t\tif ( _menuMode )\r\n\t\t\t\t{\r\n\t\t\t\t\tResetGame();\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\t\t\t\tOnGameOver();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tvoid ResolveLanding( float x, float y, float baseZ, float prevBaseZ )\r\n\t{\r\n\t\t// \u5148\u5224\"\u6b63\u597d\u8de8\u8fc7\u67d0\u4e2a\u5e73\u53f0\u9876\u9762\"\u2192 \u6210\u529f\u843d\u4e0a\r\n\t\tforeach ( var p in LandablePlatforms() )\r\n\t\t{\r\n\t\t\tif ( !InsidePlatform( p, x, y ) ) continue;\r\n\t\t\tif ( prevBaseZ >= p.TopZ && baseZ <= p.TopZ )\r\n\t\t\t{\r\n\t\t\t\tLandOn( p, x, y );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// \u5df2\u7ecf\u4f4e\u4e8e\u5e73\u53f0\u9876\u9762\u8fd8\u5904\u5728\u5e73\u53f0\u6295\u5f71\u5185 \u2192 \u649e\u4fa7\u9762\uff0c\u8f6c\u5760\u843d\uff08\u6e38\u620f\u5931\u8d25\uff09\r\n\t\tif ( _flying && !_falling )\r\n\t\t{\r\n\t\t\tforeach ( var p in LandablePlatforms() )\r\n\t\t\t{\r\n\t\t\t\tif ( !InsidePlatform( p, x, y ) ) continue;\r\n\t\t\t\tif ( baseZ < p.TopZ )\r\n\t\t\t\t{\r\n\t\t\t\t\t_falling = true;\r\n\t\t\t\t\tif ( !_menuMode )\r\n\t\t\t\t\t\tSound.Play( \"sounds/player_use_fail.sound\", new Vector3( x, y, baseZ ), 0f );\r\n\t\t\t\t\tvar horizontal = _jumpDuration > 0f\r\n\t\t\t\t\t\t? new Vector3( ( _jumpTo.x - _jumpFrom.x ) / _jumpDuration, ( _jumpTo.y - _jumpFrom.y ) / _jumpDuration, 0 )\r\n\t\t\t\t\t\t: Vector3.Zero;\r\n\t\t\t\t\t_fallVelocity = horizontal + Vector3.Up * ( -4f * _jumpHeight * ( 2f * Math.Min( _jumpTime / _jumpDuration, 1f ) - 1f ) / _jumpDuration );\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tIEnumerable<JumpPlatform> LandablePlatforms()\r\n\t{\r\n\t\tif ( _target.IsValid() ) yield return _target;\r\n\t\tif ( _current.IsValid() && _current != _target ) yield return _current;\r\n\t\tif ( _previous.IsValid() && _previous != _target && _previous != _current ) yield return _previous;\r\n\t}\r\n\r\n\tbool InsidePlatform( JumpPlatform p, float x, float y )\r\n\t{\r\n\t\treturn MathF.Abs( x - p.CenterX ) <= p.HalfX + LandingMargin()\r\n\t\t\t&& MathF.Abs( y - p.CenterY ) <= p.HalfY + LandingMargin();\r\n\t}\r\n\r\n\tfloat LandingMargin() => _piece.IsValid() ? _piece.Radius * 0.55f : 14f;\r\n\r\n\tvoid LandOn( JumpPlatform p, float x, float y )\r\n\t{\r\n\t\t_flying = false;\r\n\t\t_falling = false;\r\n\r\n\t\t// \u56f0\u96be\u6a21\u5f0f\uff1a\u843d\u70b9\u4e0d\u5438\u9644\u5e73\u53f0\u4e2d\u5fc3\uff0c\u68cb\u5b50\u505c\u5728\u771f\u5b9e\u843d\u70b9\r\n\t\tif ( Difficulty == JumpDifficulty.Hard )\r\n\t\t\t_piece.SetBasePosition( new Vector3( x, y, p.TopZ ) );\r\n\t\telse\r\n\t\t\t_piece.PlaceOn( p );\r\n\t\t_piece.Impulse( -3.4f );\r\n\r\n\t\tif ( p == _target )\r\n\t\t{\r\n\t\t\tif ( _menuMode )\r\n\t\t\t{\r\n\t\t\t\t// \u80cc\u666f\u6f14\u793a\u4e0d\u8ba1\u5206\uff1b\u529b\u5ea6\u5728\u65b0\u76ee\u6807\u751f\u6210\u540e\u91cd\u7b97\uff08\u89c1\u4e0b\u65b9 SpawnNextPlatform \u4e4b\u540e\uff09\r\n\t\t\t\t_tutorialT = 0;\r\n\t\t\t}\r\n\t\t\telse if ( _tutorialActive )\r\n\t\t\t{\r\n\t\t\t\t// \u6f14\u793a\u843d\u70b9\u4e0d\u8ba1\u5206\uff0c\u628a\u63a7\u5236\u6743\u4ea4\u8fd8\u73a9\u5bb6\r\n\t\t\t\t_tutorialActive = false;\r\n\t\t\t\t_sinceTutorialEnd = 0;\r\n\t\t\t\tTutorialText = \"Your turn! Hold & release\";\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tvar centerDist = MathF.Sqrt( ( x - p.CenterX ) * ( x - p.CenterX ) + ( y - p.CenterY ) * ( y - p.CenterY ) );\r\n\r\n\t\t\t\tif ( centerDist <= PerfectRadius )\r\n\t\t\t\t{\r\n\t\t\t\t\tStreak++;\r\n\t\t\t\t\tScore += Math.Min( Streak * 2, 16 );\r\n\t\t\t\t\tvar handle = Sound.Play( \"sounds/kenney/ui/ui.upvote.sound\", p.Center, 0f );\r\n\t\t\t\t\tif ( handle.IsValid() ) handle.Pitch = 1f + 0.1f * Math.Min( Streak - 1, 8 );\r\n\r\n\t\t\t\t\tJumpAchievements.OnPerfect( Streak );\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tStreak = 0;\r\n\t\t\t\t\tScore += 1;\r\n\t\t\t\t\tSound.Play( \"sounds/kenney/ui/ui.downvote.sound\", p.Center, 0f );\r\n\r\n\t\t\t\t\tJumpAchievements.OnLanding();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t// \u5f80\u524d\u8df3\uff1a\u521a\u79bb\u5f00\u7684\u5e73\u53f0\u53d8\u6210\"\u8eab\u540e\u90a3\u4e00\u5757\"\uff08\u5012\u8df3\u76ee\u6807\uff09\uff0c\u66f4\u65e9\u7684\u90a3\u5757\u5df2\u65e0\u7528\u3002\r\n\t\t\t// \u987a\u5e8f\u8981\u7d27\uff1aSpawnNextPlatform \u662f\u4ece _current \u5f80\u524d\u751f\u6210\uff0c\u6240\u4ee5\u5148\u8bb0\u4e0b\u65e7\u503c\u518d\u6539 _current\r\n\t\t\tvar origin = _current;\r\n\t\t\t_previous = origin;\r\n\t\t\t_current = p;\r\n\r\n\t\t\tp.IsTarget = false;\r\n\t\t\t_target = null;\r\n\t\t\tSpawnNextPlatform();\r\n\t\t\tPruneOldPlatforms();\r\n\r\n\t\t\t// \u65b0\u76ee\u6807\u751f\u6210\u540e\u518d\u8ba1\u7b97\u6f14\u793a\u529b\u5ea6\uff0c\u4fdd\u8bc1\u80cc\u666f AI \u6bcf\u4e00\u8df3\u90fd\u6210\u529f\u843d\u5728\u4e2d\u5fc3\r\n\t\t\tif ( _menuMode )\r\n\t\t\t{\r\n\t\t\t\t_tutorialT = 0;\r\n\t\t\t\t_demoPower = ComputeDemoPower();\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( p == _previous )\r\n\t\t{\r\n\t\t\t// \u5012\u7740\u5f39\u56de\u4e0a\u4e00\u5757\u5e73\u53f0\uff1a\u8ba1\u5206\u89c4\u5219\u4e00\u81f4\uff0c\u7136\u540e\u628a\"\u6765\u8def\"\u63a5\u56de\u6765\u5f53\u65b0\u7684\u524d\u8fdb\u65b9\u5411\r\n\t\t\tSound.Play( \"sounds/kenney/ui/ui.navigate.back.sound\", p.Center, 0f );\r\n\r\n\t\t\tif ( !_menuMode && !_tutorialActive )\r\n\t\t\t{\r\n\t\t\t\tvar centerDist = MathF.Sqrt( ( x - p.CenterX ) * ( x - p.CenterX ) + ( y - p.CenterY ) * ( y - p.CenterY ) );\r\n\r\n\t\t\t\tif ( centerDist <= PerfectRadius )\r\n\t\t\t\t{\r\n\t\t\t\t\tStreak++;\r\n\t\t\t\t\tScore += Math.Min( Streak * 2, 16 );\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tStreak = 0;\r\n\t\t\t\t\tScore += 1;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tJumpAchievements.OnBackJump();\r\n\t\t\t}\r\n\r\n\t\t\t// \u6b63\u5728 P2 \u7ad9\u7740\u3001\u5f80\u524d\u76ee\u6807 P3\uff0c\u5012\u8df3\u843d\u56de P1\uff1a\r\n\t\t\t// \u65b0\u7684\u524d\u8fdb\u65b9\u5411\u662f\u521a\u79bb\u5f00\u7684 P2\uff08\u6b63\u597d\u8fd8\u80fd\u518d\u8df3\u4e00\u6b21\uff09\uff0cP1 \u8eab\u540e\u6ca1\u6709\u8bb0\u5f55\uff0c\u5012\u8df3\u5230\u6b64\u4e3a\u6b62\u3002\r\n\t\t\t// \u4e0d\u5fc5\u91cd\u65b0\u751f\u6210\u5e73\u53f0\u2014\u2014\u53ea\u662f\u628a\u5f53\u524d\u6240\u5904\u7684\u5e73\u53f0\u632a\u4e00\u4e0b\r\n\t\t\tvar forward = _current;\r\n\t\t\tif ( _target.IsValid() ) _target.IsTarget = false;\r\n\t\t\t_previous = null;\r\n\t\t\t_current = p;\r\n\t\t\t_target = forward;\r\n\t\t\tp.IsTarget = true;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t// \u529b\u5ea6\u4e0d\u8db3\u843d\u56de\u5f53\u524d\u6240\u7ad9\u7684\u90a3\u4e00\u5757\uff1a\u72b6\u6001\u5b8c\u5168\u4e0d\u53d8\uff0c\u53ea\u7528\u56de\u9000\u97f3\u6548\u533a\u5206\r\n\t\t\tSound.Play( \"sounds/kenney/ui/ui.navigate.back.sound\", p.Center, 0f );\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- \u7b80\u5355\u6a21\u5f0f\u8f85\u52a9\u7ebf ----\r\n\r\n\tvoid UpdateAssistLine()\r\n\t{\r\n\t\t// \u73a9\u5bb6\u84c4\u529b\uff08\u7b80\u5355\u6a21\u5f0f\uff09\u6216\u65b0\u624b\u6559\u5b66\u6f14\u793a\u84c4\u529b\u65f6\u663e\u793a\uff1b\u5f00\u59cb\u83dc\u5355\u80cc\u666f\u4e0d\u663e\u793a\r\n\t\tvar autoCharging = _tutorialActive && _power > 0.01f;\r\n\t\tvar want = ( _charging || autoCharging ) && !IsGameOver\r\n\t\t\t&& ( Difficulty == JumpDifficulty.Easy || _tutorialActive )\r\n\t\t\t&& _target.IsValid() && _piece.IsValid();\r\n\r\n\t\tif ( !want )\r\n\t\t{\r\n\t\t\tif ( _assistVisible ) HideAssist();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEnsureAssistObjects();\r\n\r\n\t\tvar (to, _, height) = PlanJump( _power );\r\n\t\tvar from = _piece.BasePosition;\r\n\r\n\t\tfor ( int i = 0; i < AssistDotCount; i++ )\r\n\t\t{\r\n\t\t\tvar u = ( i + 1 ) / (float)AssistDotCount;\r\n\t\t\tvar x = MathX.Lerp( from.x, to.x, u );\r\n\t\t\tvar y = MathX.Lerp( from.y, to.y, u );\r\n\t\t\tvar z = from.z + 4f * height * u * ( 1f - u );\r\n\t\t\t_assistDots[i].WorldPosition = new Vector3( x, y, z + SphereModel.Bounds.Size.z * 0.06f );\r\n\t\t\t_assistDots[i].Enabled = true;\r\n\t\t}\r\n\r\n\t\t// \u843d\u70b9\u73af\uff1a\u4f1a\u843d\u5728\u5e73\u53f0\u4e0a\u5c31\u8d34\u5176\u9876\u9762\u5e76\u53d8\u7eff\uff0c\u5426\u5219\u60ac\u7a7a\u53d8\u7ea2\r\n\t\tvar surface = from.z;\r\n\t\tvar hit = false;\r\n\t\tforeach ( var p in LandablePlatforms() )\r\n\t\t{\r\n\t\t\tif ( InsidePlatform( p, to.x, to.y ) )\r\n\t\t\t{\r\n\t\t\t\tsurface = p.TopZ;\r\n\t\t\t\thit = true;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_assistRing.WorldPosition = new Vector3( to.x, to.y, surface + 2f );\r\n\t\t_assistRingRenderer.Tint = hit ? AssistHitColor : AssistMissColor;\r\n\t\t_assistRing.Enabled = true;\r\n\t\t_assistVisible = true;\r\n\t}\r\n\r\n\tvoid EnsureAssistObjects()\r\n\t{\r\n\t\tif ( _assistDots.Count == AssistDotCount && _assistDots.All( d => d.IsValid() ) && _assistRing.IsValid() ) return;\r\n\r\n\t\tHideAssist();\r\n\t\t_assistDots.Clear();\r\n\r\n\t\tvar dotScale = 9f / SphereModel.Bounds.Size.x;\r\n\t\tfor ( int i = 0; i < AssistDotCount; i++ )\r\n\t\t{\r\n\t\t\tvar go = new GameObject( false, \"AssistDot\" );\r\n\t\t\tgo.Parent = _root;\r\n\t\t\tgo.LocalScale = new Vector3( dotScale, dotScale, dotScale );\r\n\r\n\t\t\tvar r = go.AddComponent<ModelRenderer>();\r\n\t\t\tr.Model = SphereModel;\r\n\t\t\tr.Tint = new Color( 0.30f, 0.34f, 0.42f );\r\n\r\n\t\t\t_assistDots.Add( go );\r\n\t\t}\r\n\r\n\t\t_assistRing = new GameObject( false, \"AssistRing\" );\r\n\t\t_assistRing.Parent = _root;\r\n\t\tvar bs = BoxModel.Bounds.Size;\r\n\t\t_assistRing.LocalScale = new Vector3( 30f / bs.x, 30f / bs.y, 2.2f / bs.z );\r\n\t\t_assistRingRenderer = _assistRing.AddComponent<ModelRenderer>();\r\n\t\t_assistRingRenderer.Model = BoxModel;\r\n\t}\r\n\r\n\tvoid HideAssist()\r\n\t{\r\n\t\tforeach ( var dot in _assistDots )\r\n\t\t{\r\n\t\t\tif ( dot.IsValid() ) dot.Enabled = false;\r\n\t\t}\r\n\r\n\t\tif ( _assistRing.IsValid() ) _assistRing.Enabled = false;\r\n\t\t_assistVisible = false;\r\n\t}\r\n\r\n\r\n\tvoid OnGameOver()\r\n\t{\r\n\t\tif ( IsGameOver ) return;\r\n\r\n\t\tIsGameOver = true;\r\n\t\tif ( !_menuMode )\r\n\t\t{\r\n\t\t\tSound.Play( \"sounds/kenney/ui/ui.button.deny.sound\", PiecePosition, 0f );\r\n\t\t\tJumpScores.Record( Score, Difficulty );\r\n\t\t\tJumpAchievements.OnGameEnd( Score, Difficulty );\r\n\t\t}\r\n\t}\r\n}"
}
]
}