CalfPlayer.cs
namespace NiuLai;
public sealed class CalfPlayer : Component
{
public static CalfPlayer Instance { get; set; }
[Property] public float WalkSpeed { get; set; } = 220f;
[Property] public float RunSpeed { get; set; } = 340f;
[Property] public float JumpSpeed { get; set; } = 340f;
float _yaw = 180f;
float _pitch = 18f;
Vector3 _vel;
bool _grounded;
float _bob;
float _jumpDelay = -1f;
float _jankTimer;
float _idle;
float _nagCool;
GameObject _mesh;
protected override void OnAwake()
{
Instance = this;
}
protected override void OnDestroy()
{
if ( Instance == this )
Instance = null;
}
protected override void OnStart()
{
_mesh = CowMesh.Build( GameObject, CowMesh.Calf, 1f );
}
protected override void OnUpdate()
{
if ( Input.EscapePressed )
{
Input.EscapePressed = false;
if ( GameState.Talking )
GameState.ClearTalk();
else
GameFlow.TogglePause();
}
if ( GameFlow.BlocksWorld )
return;
var look = Input.AnalogLook;
_yaw += look.yaw;
_pitch = MathX.Clamp( _pitch + look.pitch, -8f, 42f );
if ( FilmCut.Playing )
{
if ( PressedUse() )
{
if ( GameState.Talking )
GameState.AdvanceTalk();
FilmCut.Skip();
}
return;
}
if ( GameState.Talking )
{
if ( PressedUse() )
GameState.AdvanceTalk();
return;
}
if ( PressedUse() )
TryUse();
if ( Input.Pressed( "Moo" ) || Input.Pressed( "moo" ) )
Moo();
}
protected override void OnFixedUpdate()
{
if ( GameFlow.BlocksWorld || GameState.Talking )
{
_vel = _vel.WithX( 0 ).WithY( 0 );
ApplyGravity( Time.Delta );
Slide();
return;
}
var dt = Time.Delta;
var analog = Input.AnalogMove.WithZ( 0 );
var cam = Rotation.FromYaw( _yaw );
var wish = cam * analog;
if ( wish.Length > 1f )
wish = wish.Normal;
var speed = Input.Down( "Run" ) ? RunSpeed : WalkSpeed;
speed *= GameState.WalkMul;
if ( GameState.InVoid )
speed *= 0.55f;
_vel.x = wish.x * speed;
_vel.y = wish.y * speed;
if ( wish.Length > 0.1f )
{
var face = Rotation.LookAt( wish.WithZ( 0 ), Vector3.Up );
WorldRotation = Rotation.Slerp( WorldRotation, face, 1f - MathF.Pow( 0.001f, dt ) );
_bob += dt * 9f;
_idle = 0;
if ( Input.Down( "Run" ) && _nagCool <= 0f && GameState.Beat == Beat.Migrate )
{
_nagCool = 10f;
Chatter.Say( "牛妈妈 / Mama", Chatter.Pick( Lines.MomSlow ) );
GameAudio.PlayClip( GameAudio.SlowDown, 0.7f );
}
}
else
{
_bob = 0;
_idle += dt;
if ( _idle > 7.5f && _nagCool <= 0f )
{
_idle = 0;
_nagCool = 12f;
Chatter.Say( "牛妈妈 / Mama", Chatter.Pick( Lines.MomSleep ) );
GameAudio.PlayClip( GameAudio.MamaWake, 0.75f );
}
}
_nagCool -= dt;
if ( _jumpDelay >= 0f )
{
_jumpDelay -= dt;
if ( _jumpDelay <= 0f && _grounded )
{
_vel.z = JumpSpeed;
_grounded = false;
_jumpDelay = -1f;
}
}
else if ( _grounded && Input.Pressed( "Jump" ) )
{
// Movie jank: the calf thinks about jumping, then hops too hard.
_jumpDelay = 0.16f;
Trophies.Unlock( "think_jump" );
}
ApplyGravity( dt );
Slide();
UpdateMesh();
UpdateCamera( dt );
WatchBeats();
}
void WatchBeats()
{
if ( GameState.Beat == Beat.Migrate && GameState.GrassEaten < 1 && WorldPosition.x > 240f )
{
WorldPosition = WorldPosition.WithX( 230f );
if ( !GameState.DidEatNag )
{
GameState.MarkEatNag();
FilmCut.Play( CutId.EatFirst );
}
else
{
Chatter.Say( "牛妈妈 / Mama", Chatter.Pick( Lines.EatFirst ) );
}
}
if ( GameState.Beat == Beat.Creek && !GameState.CreekJumped && WorldPosition.x > 500f )
GameState.JumpCreek();
if ( GameState.Beat == Beat.Wake && WorldPosition.y > 280f )
Trophies.Unlock( "fly_line" );
if ( !GameState.SawTail && GameState.Beat >= Beat.Night && GameState.Beat < Beat.Farewell )
{
if ( WorldBuilder.Stage.TryGetValue( "leopard", out var leo ) && leo.IsValid() )
{
if ( (WorldPosition - leo.WorldPosition).Length < 85f )
{
GameState.MarkTail();
Chatter.Say( "牛来 / Niu Lai", Chatter.Pick( Lines.Tail ), 3.3f );
GameAudio.PlayClip( GameAudio.Snake, 0.85f );
}
}
}
if ( _nagCool <= 0f && !GameState.Talking )
{
if ( GameState.Beat == Beat.Snake && WorldPosition.x > 220f && WorldPosition.x < 300f )
{
_nagCool = 16f;
Chatter.Say( "旁白 / Narrator", Lines.Tease[0] );
}
else if ( GameState.Beat == Beat.Night && WorldPosition.x > 640f )
{
_nagCool = 16f;
Chatter.Say( "旁白 / Narrator", Lines.Tease[1] );
}
else if ( GameState.Beat == Beat.Wake && WorldPosition.y > 120f )
{
_nagCool = 16f;
Chatter.Say( "云玎 / Yun Ding", Lines.Tease[2] );
}
else if ( GameState.Beat == Beat.Cart && WorldPosition.x > 700f )
{
_nagCool = 16f;
Chatter.Say( "旁白 / Narrator", Lines.Tease[3] );
}
}
}
void ApplyGravity( float dt )
{
if ( !_grounded )
_vel.z -= 900f * dt;
}
void Slide()
{
var dt = Time.Delta;
if ( GameState.WantHop )
{
GameState.WantHop = false;
_vel.z = 420f;
_grounded = false;
}
var next = WorldPosition + _vel * dt;
if ( !GameState.CreekJumped && next.x > 400f && next.x < 480f && GameState.Beat is Beat.Creek or Beat.Snake )
{
next.x = 400f;
_vel.x = 0;
if ( _nagCool <= 0f )
{
_nagCool = 5f;
Chatter.Say( "牛来 / Niu Lai", Chatter.Pick( Lines.CreekFear ) );
}
}
var wall = Scene.Trace.Ray( WorldPosition + Vector3.Up * 20f, next + Vector3.Up * 20f )
.IgnoreGameObjectHierarchy( GameObject )
.Radius( 14f )
.Run();
if ( wall.Hit )
next = WorldPosition + (next - WorldPosition) * MathF.Max( wall.Fraction - 0.05f, 0f );
var downFrom = next + Vector3.Up * 48f;
var ground = Scene.Trace.Ray( downFrom, next + Vector3.Down * 80f )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
if ( ground.Hit )
{
next = ground.EndPosition;
_grounded = _vel.z <= 20f;
if ( _vel.z < 0f )
_vel.z = 0f;
}
else
{
_grounded = false;
}
WorldPosition = next;
}
void UpdateMesh()
{
if ( !_mesh.IsValid() )
return;
var g = GameState.Growth;
_mesh.LocalScale = g;
var stiff = MathF.Sin( _bob ) * 8f;
_mesh.LocalRotation = new Angles( stiff, 0, MathF.Sin( _bob * 0.5f ) * 3f );
_mesh.LocalPosition = Vector3.Up * MathF.Abs( MathF.Sin( _bob ) ) * 3f;
}
void UpdateCamera( float dt )
{
if ( !Scene.Camera.IsValid() )
return;
_jankTimer -= dt;
var hitch = _jankTimer > 0f ? 8f : 0f;
if ( Random.Shared.Float() < 0.004f )
_jankTimer = 0.08f;
var rot = Rotation.From( _pitch, _yaw, hitch );
var head = WorldPosition + Vector3.Up * (48f * GameState.Growth + 16f);
var desired = head - rot.Forward * (170f + GameState.Growth * 20f) + rot.Right * 12f;
var camHit = Scene.Trace.Ray( head, desired )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
if ( camHit.Hit )
desired = camHit.EndPosition + camHit.Normal * 6f;
Scene.Camera.WorldPosition = desired;
Scene.Camera.WorldRotation = rot;
}
void TryUse()
{
Hotspot best = null;
var bestD = float.MaxValue;
foreach ( var hs in Scene.GetAllComponents<Hotspot>() )
{
if ( !hs.IsValid() || !hs.InRange( WorldPosition ) )
continue;
var d = (hs.WorldPosition - WorldPosition).Length;
if ( d < bestD )
{
bestD = d;
best = hs;
}
}
best?.Use();
}
void Moo()
{
Trophies.Unlock( "moo" );
Hotspot near = null;
var best = float.MaxValue;
foreach ( var hs in Scene.GetAllComponents<Hotspot>() )
{
if ( !hs.IsValid() )
continue;
var d = (hs.WorldPosition - WorldPosition).Length;
if ( d < best && d < 110f )
{
best = d;
near = hs;
}
}
GameState.MooWorld( near?.Id );
}
public void RefreshPrompt()
{
if ( GameFlow.BlocksWorld || GameState.Talking )
return;
Hotspot best = null;
var bestD = float.MaxValue;
foreach ( var hs in Scene.GetAllComponents<Hotspot>() )
{
if ( !hs.IsValid() || !hs.InRange( WorldPosition ) )
continue;
var d = (hs.WorldPosition - WorldPosition).Length;
if ( d < bestD )
{
bestD = d;
best = hs;
}
}
if ( best is null )
{
GameState.SetPrompt( "", "" );
return;
}
GameState.SetPrompt( $"按 E {best.LabelCn}", $"E {best.LabelEn}" );
}
static bool PressedUse() =>
Input.Pressed( "Use" ) || Input.Pressed( "use" ) || Input.Pressed( "Submit" ) || Input.Pressed( "submit" );
}