World/World3D.cs
using System;
/// <summary>
/// Shows the 3D place for the current screen (see <see cref="StageSet"/> in scenes/arena.scene), puts the
/// camera on that place's camera spot, and stands the gladiators in it as lit paper puppets.
/// </summary>
public sealed class World3D : Component
{
public static World3D Instance { get; private set; }
/// <summary>World units per rig pixel: a gladiator is about 70 units tall.</summary>
public const float U = 0.16f;
/// <summary>Sprites are seen from the front only when turned round; flip this if all the art shows up mirrored.</summary>
public static bool TurnSprites = true;
[Property, Title( "Turn sprites around (untick if the art looks mirrored)" )]
public bool TurnSpritesAround { get => TurnSprites; set => TurnSprites = value; }
/// <summary>Fights: how close the camera gets to the two fighters (world units).</summary>
[Property] public float FightZoomNear { get; set; } = 210f;
[Property] public float FightZoomFar { get; set; } = 430f;
// what a screen can override on the gladiator it shows (character creation, fitting room, intro)
public Look? HeroLook { get; set; }
public string HeroAnim { get; set; }
public Mood HeroMood { get; set; } = Mood.Neutral;
public StageSet CurrentSet { get; private set; }
public SpritePuppet Hero { get; private set; }
public SpritePuppet LeftFighter { get; private set; }
public SpritePuppet RightFighter { get; private set; }
string activeKey;
GameScreen lastScreen = (GameScreen)(-1);
GameObject puppets;
readonly List<Cutout> chains = new();
float follow, zoom;
protected override void OnAwake() => Instance = this;
static GameManager Gm => GameManager.Instance;
/// <summary>Which place a screen uses (fights: "Duel" in the dungeon, "Arena" in the arena).</summary>
public string PlaceKey
{
get
{
if ( !Gm.IsValid() ) return "Title";
if ( Gm.Screen == GameScreen.Arena )
{
var f = Scene.Get<ArenaFight>();
return f.IsValid() && f.Mode == FightMode.Duel ? "Duel" : "Arena";
}
return Gm.Screen.ToString();
}
}
protected override void OnStart()
{
puppets = new GameObject( true, "Puppets" );
puppets.Flags |= GameObjectFlags.NotSaved;
Hero = NewPuppet( "Hero" );
LeftFighter = NewPuppet( "Left fighter" );
RightFighter = NewPuppet( "Right fighter" );
RightFighter.Flip = true;
Hero.LookSource = () => HeroLook ?? Profile.Current?.Look ?? DefaultHero;
}
SpritePuppet NewPuppet( string name ) => new GameObject( puppets, true, name ).AddComponent<SpritePuppet>();
/// <summary>The gladiator on the title screen.</summary>
public static Look DefaultHero
{
get
{
var l = Look.Default;
l.Hair = 2; l.Beard = 1; l.HairColour = 2;
l.Chest = 8; l.Shoulders = 6; l.Gauntlets = 6; l.Greaves = 6; l.Boots = 6; l.Shield = 6;
l.Weapon = 22;
return l;
}
}
protected override void OnUpdate()
{
if ( !Gm.IsValid() ) return;
// a screen's own look/animation for the gladiator only lasts while that screen is up
if ( Gm.Screen != lastScreen )
{
lastScreen = Gm.Screen;
HeroLook = null;
HeroAnim = null;
HeroMood = Mood.Neutral;
}
string key = PlaceKey;
if ( key != activeKey )
{
activeKey = key;
var sets = Scene.GetAllComponents<StageSet>().ToList();
var set = sets.FirstOrDefault( s => s.Shows( key ) );
foreach ( var s in sets ) s.Show( s == set );
CurrentSet = set;
zoom = 0;
if ( !set.IsValid() ) Log.Warning( $"No StageSet in the scene shows \"{key}\"" );
}
var cam = Scene.Camera;
if ( !CurrentSet.IsValid() || !cam.IsValid() )
{
HidePuppets();
return;
}
// ---- camera on the place's spot for this screen ----
var screen = Gm.Screen;
var fight = Scene.Get<ArenaFight>();
bool versus = screen == GameScreen.Arena && fight.IsValid() && fight.State == FightState.Intro;
var camSpot = (versus ? CurrentSet.Spot( "Camera:Versus" ) : null) ?? CurrentSet.Spot( $"Camera:{screen}" ) ?? CurrentSet.Spot( "Camera" );
if ( !camSpot.IsValid() ) return;
var rot = camSpot.WorldRotation;
var right = rot.Right.WithZ( 0 ).Normal;
var toCam = (-rot.Forward).WithZ( 0 ).Normal;
var camPos = camSpot.WorldPosition;
cam.FieldOfView = camSpot.FieldOfView;
cam.BackgroundColor = CurrentSet.Background;
// ---- the gladiators ----
bool fighting = screen == GameScreen.Arena && fight.IsValid() && fight.Left.IsValid() && fight.Right.IsValid();
if ( fighting )
{
var spot = CurrentSet.Spot( "Fight" );
var centre = spot.IsValid() ? spot.WorldPosition : Vector3.Zero;
Place( LeftFighter, fight.Left, centre, right, toCam );
Place( RightFighter, fight.Right, centre, right, toCam );
Hero.Enabled = false;
// follow the fight: slide sideways to the middle, move in as they close up
float a = Lateral( fight.Left.DisplayX ), b = Lateral( fight.Right.DisplayX );
if ( !float.IsNaN( a ) && !float.IsNaN( b ) )
{
float baseDist = Vector3.Dot( centre - camPos, rot.Forward );
float want = versus ? FightZoomNear * 0.75f : Math.Clamp( 150f + MathF.Abs( b - a ) * 0.7f, FightZoomNear, FightZoomFar );
float k = Math.Clamp( Time.Delta * 3f, 0f, 1f );
if ( zoom <= 0 ) { zoom = want; follow = (a + b) * 0.5f; }
zoom += (want - zoom) * k;
follow += ((a + b) * 0.5f - follow) * k;
// the place's camera spot is the "far" view; only move closer, never further back.
// The camera stays level; it comes down as it moves in so the feet stay at the same height on screen.
float forward = MathF.Max( 0, baseDist - zoom );
float height = camPos.z - centre.z;
camPos += right * follow + rot.Forward * forward;
if ( baseDist > 1f ) camPos = camPos.WithZ( centre.z + height * (baseDist - forward) / baseDist );
}
}
else
{
LeftFighter.Enabled = RightFighter.Enabled = false;
LeftFighter.Fighter = RightFighter.Fighter = null;
string role = screen switch
{
GameScreen.Title => "Hero",
GameScreen.Intro => "Chained",
GameScreen.Fitting => "Fitting",
_ => "Player"
};
var spot = CurrentSet.Spot( role ) ?? (role == "Fitting" ? CurrentSet.Spot( "Player" ) : null);
if ( spot.IsValid() && screen is not (GameScreen.Weaponsmith or GameScreen.Armoury or GameScreen.Magic or GameScreen.Church) )
{
Hero.Enabled = true;
Hero.Feet = spot.WorldPosition;
Hero.ScreenRight = right;
Hero.ToCamera = toCam;
Hero.Size = spot.Size;
Hero.Anim = HeroAnim ?? (screen == GameScreen.Title ? "win" : screen == GameScreen.Intro ? "chained" : "idle");
Hero.FixedMood = HeroMood;
}
else Hero.Enabled = false;
}
cam.WorldPosition = camPos;
cam.WorldRotation = rot;
UpdateChains( screen == GameScreen.Intro && Hero.Enabled, toCam );
}
void Place( SpritePuppet p, Gladiator g, Vector3 centre, Vector3 right, Vector3 toCam )
{
p.Enabled = true;
p.Fighter = g;
p.Size = 1f;
p.ScreenRight = right;
p.ToCamera = toCam;
if ( !float.IsNaN( g.DisplayX ) )
p.Feet = centre + right * Lateral( g.DisplayX ) + Vector3.Up * Lateral( g.Lift );
}
void HidePuppets()
{
if ( Hero.IsValid() ) Hero.Enabled = false;
if ( LeftFighter.IsValid() ) LeftFighter.Enabled = false;
if ( RightFighter.IsValid() ) RightFighter.Enabled = false;
UpdateChains( false, Vector3.Forward );
}
/// <summary>Fight positions (stage pixels) to world units.</summary>
public static float Lateral( float stagePx ) => stagePx / ArenaView.BaseScale * U;
/// <summary>A fighter position (stage pixels, as the fight uses them) in the world.</summary>
public Vector3 FighterWorld( float displayX, float lift )
{
var spot = CurrentSet.IsValid() ? CurrentSet.Spot( "Fight" ) : null;
var centre = spot.IsValid() ? spot.WorldPosition : Vector3.Zero;
var cam = Scene.Camera;
var right = cam.IsValid() ? cam.WorldRotation.Right.WithZ( 0 ).Normal : Vector3.Right;
return centre + right * Lateral( displayX ) + Vector3.Up * Lateral( lift );
}
/// <summary>A world point on the 1920x1080 UI stage.</summary>
public Vector2 ToStage( Vector3 world )
{
var cam = Scene.Camera;
if ( !cam.IsValid() || Screen.Width <= 0 ) return new Vector2( 960, 540 );
var px = cam.PointToScreenPixels( world );
float s = MathF.Min( Screen.Width / 1920f, Screen.Height / 1080f );
var offset = new Vector2( (Screen.Width - 1920f * s) / 2f, (Screen.Height - 1080f * s) / 2f );
return (px - offset) / s;
}
/// <summary>The intro: chains from the ceiling down to the shackles on the gladiator's wrists.</summary>
void UpdateChains( bool on, Vector3 toCam )
{
if ( !on )
{
foreach ( var c in chains ) c.GameObject.Enabled = false;
return;
}
int n = 0;
foreach ( var hand in new[] { "farm_b", "farm_f" } )
{
// the wrist: down the forearm from the elbow joint
var wrist = Hero.BoneWorld( hand == "farm_b" ? "shield" : "weapon" );
var shackle = Chain( n++, "ss/p/shackle.png", 9f );
shackle.GameObject.WorldPosition = wrist - Vector3.Up * 3f + toCam * 4f;
for ( float z = wrist.z + 4f; z < wrist.z + 220f; z += 9f )
{
var link = Chain( n++, "ss/p/chain.png", 6f );
link.GameObject.WorldPosition = new Vector3( wrist.x, wrist.y, z ) + toCam * 3.5f;
}
}
for ( int i = n; i < chains.Count; i++ ) chains[i].GameObject.Enabled = false;
}
Cutout Chain( int index, string image, float width )
{
while ( chains.Count <= index )
{
var go = new GameObject( puppets, true, "chain" );
go.Flags |= GameObjectFlags.NotSaved;
var c = go.AddComponent<Cutout>();
c.CastShadows = true;
chains.Add( c );
}
var cut = chains[index];
cut.GameObject.Enabled = true;
cut.Image = image;
cut.Width = width;
cut.StandOnBottom = false;
// the cutout turns its own picture round if needed; the object just faces the camera
var cam = Scene.Camera;
var toCam = cam.IsValid() ? (-cam.WorldRotation.Forward).WithZ( 0 ).Normal : Vector3.Forward;
cut.GameObject.WorldRotation = Rotation.LookAt( toCam, Vector3.Up );
return cut;
}
}