FilmCut.cs
namespace NiuLai;
/// <summary>
/// Janky in-engine cutscenes. Theater mud + crowd laugh. No film stills.
/// E skips a shot. Camera slams. Models slide.
/// </summary>
public enum CutId
{
None,
OpenEyes,
LarkBack,
EatFirst,
LeopardFace,
MilkDad,
TooClose,
Headbutt,
NoRoad,
Ng
}
public static class FilmCut
{
public static bool Playing { get; private set; }
public static CutId Id { get; private set; }
public static int Shot { get; private set; }
public static int Version { get; private set; }
public static string CardCn { get; private set; } = "";
public static string CardEn { get; private set; } = "";
static float _t;
static float _hold;
static int _shots;
static Action _done;
static Vector3 _camPos;
static Rotation _camRot;
static GameObject _focus;
static bool _tipped;
public static void Play( CutId id, Action done = null )
{
if ( id == CutId.None )
return;
Id = id;
Playing = true;
Shot = 0;
_t = 0;
_done = done;
_tipped = false;
CardCn = "";
CardEn = "";
_shots = id switch
{
CutId.OpenEyes => 3,
CutId.LarkBack => 2,
CutId.EatFirst => 1,
CutId.LeopardFace => 2,
CutId.MilkDad => 2,
CutId.TooClose => 2,
CutId.Headbutt => 2,
CutId.NoRoad => 2,
CutId.Ng => 2,
_ => 1
};
BeginShot();
Version++;
}
public static void Skip()
{
if ( !Playing )
return;
AdvanceShot();
}
public static void Tick( float dt )
{
if ( !Playing )
return;
_t += dt;
Animate();
AimCamera();
if ( _t >= _hold )
AdvanceShot();
}
static void AdvanceShot()
{
Shot++;
if ( Shot >= _shots )
{
End();
return;
}
_t = 0;
BeginShot();
Version++;
}
static void BeginShot()
{
_hold = 3.4f;
_focus = null;
CardCn = "";
CardEn = "";
switch ( Id )
{
case CutId.OpenEyes:
if ( Shot == 0 )
{
CardCn = "第一场 · 睁开眼睛";
CardEn = "Scene 1 · Open your eyes";
_hold = 2.2f;
GameAudio.PlayClip( GameAudio.NameNiu, 0.85f );
}
else if ( Shot == 1 )
{
TipPlayer( true );
_focus = CalfPlayer.Instance?.GameObject;
_hold = 4.2f;
GameState.Talk( "牛妈妈 / Mama", Lines.MomSleep );
GameAudio.PlayClip( GameAudio.MamaWake );
GameAudio.PlayCrowd( GameAudio.CrowdWake );
}
else
{
TipPlayer( false );
_focus = CalfPlayer.Instance?.GameObject;
_hold = 1.6f;
GameAudio.PlayClip( GameAudio.Stand, 0.9f );
}
break;
case CutId.LarkBack:
if ( Shot == 0 )
{
CardCn = "愿意到我背上来吗";
CardEn = "Willing come up on my back?";
_hold = 2.0f;
}
LieNamed( "lark", true );
_focus = Named( "lark" );
if ( Shot == 1 )
{
_hold = 4.6f;
GameState.Talk( "云玎 / Yun Ding", Lines.Dream );
GameAudio.PlayClip( GameAudio.LarkRide );
GameAudio.PlayCrowd( GameAudio.CrowdRide );
}
break;
case CutId.EatFirst:
CardCn = "吃饱了再去不好吗";
CardEn = "Eat full then go, not better?";
_focus = Named( "mom" ) ?? CalfPlayer.Instance?.GameObject;
_hold = 3.8f;
GameState.Talk( "牛妈妈 / Mama", Lines.EatFirst );
GameAudio.PlayClip( GameAudio.SlowDown );
break;
case CutId.LeopardFace:
if ( Shot == 0 )
{
CardCn = "豹拉特写 · 院线在笑";
CardEn = "Bao La close-up · theater laughing";
_hold = 1.8f;
}
_focus = Named( "leopard" );
if ( Shot == 1 )
{
_hold = 5.0f;
GameState.Talk( "豹拉 / Bao La", Lines.Protect );
GameAudio.PlayClip( GameAudio.Leopard );
GameAudio.PlayCrowd( GameAudio.CrowdBig );
}
break;
case CutId.MilkDad:
if ( Shot == 0 )
{
CardCn = "少了点妈妈的味道";
CardEn = "Missing a little Mama flavor";
_hold = 1.8f;
GameAudio.PlayClip( GameAudio.Milk );
}
_focus = Named( "leopard" );
if ( Shot == 1 )
{
_hold = 5.2f;
GameState.Talk( "豹拉 / Bao La", Lines.MilkScene );
GameAudio.PlayCrowd( GameAudio.CrowdMilk );
}
break;
case CutId.TooClose:
if ( Shot == 0 )
{
CardCn = "第一,他不能离牛群太近";
CardEn = "First, he cannot leave herd too near";
_hold = 2.0f;
}
_focus = Named( "niuer" ) ?? Named( "dad" );
if ( Shot == 1 )
{
_hold = 4.4f;
GameState.Talk( "牛二 / Niu Er", Lines.TooClose );
GameAudio.PlayClip( GameAudio.SlowDown );
GameAudio.PlayCrowd( GameAudio.CrowdMilk );
}
break;
case CutId.Headbutt:
CardCn = "我喜欢顶脑门儿";
CardEn = "I like bump forehead";
_focus = Named( "leopard" );
_hold = Shot == 0 ? 1.6f : 4.0f;
if ( Shot == 1 )
{
GameState.Talk( "豹拉 / Bao La", Lines.Headbutt );
GameAudio.PlayClip( GameAudio.LikeHere );
GameAudio.PlayCrowd( GameAudio.CrowdBig );
}
break;
case CutId.NoRoad:
if ( Shot == 0 )
{
CardCn = "没路了";
CardEn = "No road anymore";
_hold = 2.2f;
GameAudio.PlayClip( GameAudio.CartGrow );
}
_focus = Named( "cart" );
if ( Shot == 1 )
{
_hold = 4.6f;
GameState.Talk( "旁白 / Narrator", Lines.NoRoad );
GameAudio.PlayCrowd( GameAudio.CrowdCart );
}
break;
case CutId.Ng:
if ( Shot == 0 )
{
CardCn = "嗯";
CardEn = "Ng.";
_hold = 1.6f;
}
LieNamed( "lark", false );
_focus = Named( "lark" );
if ( Shot == 1 )
{
_hold = 4.2f;
GameState.Talk( "云玎 / Yun Ding", Lines.Farewell );
GameAudio.PlayClip( GameAudio.LarkNg );
GameAudio.PlayCrowd( GameAudio.CrowdNg );
}
break;
}
}
static void Animate()
{
if ( Id == CutId.Headbutt && _focus.IsValid() && CalfPlayer.Instance.IsValid() )
{
var a = CalfPlayer.Instance.WorldPosition;
var b = _focus.WorldPosition;
var mid = (a + b) * 0.5f;
var bump = MathF.Sin( _t * 10f ) * 8f;
CalfPlayer.Instance.WorldPosition = mid + Vector3.Left * (18f + bump);
_focus.WorldPosition = mid + Vector3.Right * (18f - bump);
}
if ( Id == CutId.Ng && _focus.IsValid() )
{
_focus.WorldPosition = GameState.DreamPos + new Vector3( 0, 0, 40f + MathF.Sin( _t * 2f ) * 8f );
_focus.WorldRotation = Rotation.FromYaw( _t * 40f ) * Rotation.FromRoll( 18f );
}
}
static void AimCamera()
{
var cam = CalfPlayer.Instance?.Scene?.Camera;
if ( cam is null || !cam.IsValid() )
return;
var target = _focus.IsValid() ? _focus.WorldPosition : (CalfPlayer.Instance?.WorldPosition ?? Vector3.Zero);
target += Vector3.Up * (Id == CutId.LeopardFace ? 22f : 28f);
var dist = Id switch
{
CutId.LeopardFace => 38f,
CutId.OpenEyes => 42f,
CutId.LarkBack => 55f,
CutId.Ng => 90f,
CutId.NoRoad => 220f,
_ => 70f
};
var side = Id == CutId.LarkBack ? 10f : 16f;
var height = Id == CutId.LarkBack ? 12f : 22f;
if ( Id == CutId.Ng )
height = 8f;
_camPos = target - Vector3.Forward * dist + Vector3.Right * side + Vector3.Up * height;
if ( Id == CutId.NoRoad )
_camPos = target + new Vector3( -180, -140, 90 );
if ( Id == CutId.Ng )
_camPos = target + new Vector3( 0, -70, 10 );
var look = (target - _camPos);
if ( look.Length > 1f )
_camRot = Rotation.LookAt( look, Vector3.Up );
cam.WorldPosition = _camPos;
cam.WorldRotation = _camRot;
if ( GameState.InVoid || Id == CutId.Ng )
cam.BackgroundColor = Color.Black;
}
static void End()
{
Playing = false;
var done = _done;
var id = Id;
Id = CutId.None;
_done = null;
CardCn = "";
CardEn = "";
LieNamed( "lark", false );
TipPlayer( false );
Version++;
done?.Invoke();
if ( id == CutId.LarkBack )
GameState.EnterDreamFromCut();
else if ( id == CutId.Ng )
GameState.FinishFarewellCut();
}
static void TipPlayer( bool down )
{
var p = CalfPlayer.Instance;
if ( !p.IsValid() )
return;
_tipped = down;
p.WorldRotation = down
? Rotation.FromPitch( 80f )
: Rotation.FromYaw( p.WorldRotation.Yaw() );
}
static void LieNamed( string name, bool lie )
{
var go = Named( name );
if ( !go.IsValid() )
return;
go.WorldRotation = lie
? Rotation.FromRoll( 90f ) * Rotation.FromPitch( 20f )
: Rotation.Identity;
}
static GameObject Named( string name )
{
if ( WorldBuilder.Stage.TryGetValue( name, out var go ) && go.IsValid() )
return go;
return null;
}
}