World/Stage.cs
using System;
// The building blocks of every place in the game. They're plain GameObjects in scenes/arena.scene under
// "World", so you can move, add or delete them in the s&box editor and see the result straight away.
/// <summary>
/// A flat S&S2 picture standing in the world, lit by the scene's lights. Height follows the picture.
/// </summary>
[Title( "Cutout" ), Category( "Shoes and Daggers" ), Icon( "image" )]
public sealed class Cutout : Component, Component.ExecuteInEditor
{
/// <summary>Picture under Assets/Resources/sad/, e.g. ss/p/2102.png</summary>
[Property] public string Image { get; set; } = "ss/p/2102.png";
/// <summary>Width in world units (a gladiator is about 70 tall).</summary>
[Property] public float Width { get; set; } = 200f;
[Property] public Color Tint { get; set; } = Color.White;
/// <summary>Lit by the scene's lights (off = always full brightness, e.g. the sky).</summary>
[Property] public bool Lit { get; set; } = true;
[Property] public bool CastShadows { get; set; }
/// <summary>Glows: added on top, unlit (fire, sun, stained glass light).</summary>
[Property] public bool Glow { get; set; }
/// <summary>The object's position is the bottom edge (stands on the floor) instead of the middle.</summary>
[Property] public bool StandOnBottom { get; set; } = true;
[Property] public bool FlipX { get; set; }
/// <summary>Turn the picture on the spot (degrees, clockwise), e.g. for chains.</summary>
[Property] public float Spin { get; set; }
GameObject spriteGo;
SpriteRenderer sr;
int built;
int Key => HashCode.Combine( HashCode.Combine( Image, Width, Tint, Lit, CastShadows, Glow, StandOnBottom ), FlipX, Spin, World3D.TurnSprites );
protected override void OnEnabled() => Rebuild();
protected override void OnDisabled()
{
if ( spriteGo.IsValid() ) spriteGo.Destroy();
spriteGo = null;
sr = null;
}
protected override void OnUpdate()
{
if ( Key != built || !spriteGo.IsValid() ) Rebuild();
}
void Rebuild()
{
built = Key;
if ( !spriteGo.IsValid() )
{
spriteGo = new GameObject( GameObject, true, "picture" );
spriteGo.Flags |= GameObjectFlags.NotSaved;
sr = spriteGo.AddComponent<SpriteRenderer>();
}
var tex = string.IsNullOrEmpty( Image ) ? null : SpritePuppet.Tex( Image );
if ( tex is null || tex.Width <= 0 )
{
sr.Enabled = false;
return;
}
float h = Width * tex.Height / (float)tex.Width;
sr.Enabled = true;
sr.Sprite = SpritePuppet.SpriteFor( Image );
sr.Size = new Vector2( Width, h );
sr.Billboard = SpriteRenderer.BillboardMode.None;
sr.Lighting = Lit && !Glow;
sr.Additive = Glow;
sr.Shadows = CastShadows && !Glow;
sr.Opaque = !Glow;
sr.AlphaCutoff = 0.5f;
sr.Color = Tint;
sr.FlipHorizontal = FlipX;
spriteGo.LocalPosition = StandOnBottom ? Vector3.Up * (h * 0.5f) : Vector3.Zero;
var face = World3D.TurnSprites ? Rotation.FromYaw( 180 ) : Rotation.Identity;
spriteGo.LocalRotation = face * Rotation.FromAxis( Vector3.Forward, World3D.TurnSprites ? Spin : -Spin );
}
}
/// <summary>A plain lit box: floors, ledges, steps. The object's position is the middle of the TOP face.</summary>
[Title( "Box" ), Category( "Shoes and Daggers" ), Icon( "check_box_outline_blank" )]
public sealed class StageBox : Component, Component.ExecuteInEditor
{
[Property] public Vector3 BoxSize { get; set; } = new( 400, 1200, 20 );
[Property] public Color Tint { get; set; } = new( 0.85f, 0.7f, 0.46f );
GameObject boxGo;
ModelRenderer mr;
int built;
int Key => HashCode.Combine( BoxSize, Tint );
protected override void OnEnabled() => Rebuild();
protected override void OnDisabled()
{
if ( boxGo.IsValid() ) boxGo.Destroy();
boxGo = null;
}
protected override void OnUpdate()
{
if ( Key != built || !boxGo.IsValid() ) Rebuild();
}
void Rebuild()
{
built = Key;
if ( !boxGo.IsValid() )
{
boxGo = new GameObject( GameObject, true, "box" );
boxGo.Flags |= GameObjectFlags.NotSaved;
mr = boxGo.AddComponent<ModelRenderer>();
mr.Model = Model.Cube;
}
var b = Model.Cube.Bounds.Size;
boxGo.LocalPosition = new Vector3( 0, 0, -BoxSize.z * 0.5f );
boxGo.LocalRotation = Rotation.Identity;
boxGo.LocalScale = new Vector3( BoxSize.x / MathF.Max( b.x, 0.01f ), BoxSize.y / MathF.Max( b.y, 0.01f ), BoxSize.z / MathF.Max( b.z, 0.01f ) );
mr.Tint = Tint;
}
}
public enum StageLightKind { Sun, Point, Spot, Ambient }
/// <summary>A light for a place. Sun and Spot shine along the object's forward (blue arrow).</summary>
[Title( "Stage Light" ), Category( "Shoes and Daggers" ), Icon( "light_mode" )]
public sealed class StageLight : Component, Component.ExecuteInEditor
{
[Property] public StageLightKind Kind { get; set; } = StageLightKind.Point;
[Property] public Color Colour { get; set; } = Color.White;
[Property] public float Brightness { get; set; } = 1f;
/// <summary>How far a point/spot light reaches.</summary>
[Property] public float Radius { get; set; } = 400f;
[Property] public bool Shadows { get; set; } = true;
/// <summary>Torch/candle flicker.</summary>
[Property] public bool Flicker { get; set; }
/// <summary>Sun only: the colour the sky adds to shadows.</summary>
[Property] public Color SkyColour { get; set; } = new( 0.35f, 0.45f, 0.6f );
GameObject lightGo;
int built;
int Key => HashCode.Combine( Kind, Colour, Brightness, Radius, Shadows, Flicker, SkyColour );
protected override void OnEnabled() => Rebuild();
protected override void OnDisabled()
{
if ( lightGo.IsValid() ) lightGo.Destroy();
lightGo = null;
}
protected override void OnUpdate()
{
if ( Key != built || !lightGo.IsValid() ) Rebuild();
}
void Rebuild()
{
built = Key;
if ( lightGo.IsValid() ) lightGo.Destroy();
lightGo = new GameObject( GameObject, true, "light" );
lightGo.Flags |= GameObjectFlags.NotSaved;
switch ( Kind )
{
case StageLightKind.Sun:
{
var l = lightGo.AddComponent<DirectionalLight>();
l.LightColor = Colour;
l.Intensity = Brightness;
l.SkyColor = SkyColour;
l.Shadows = Shadows;
break;
}
case StageLightKind.Point:
{
var l = lightGo.AddComponent<PointLight>();
l.LightColor = Colour;
l.Intensity = Brightness;
l.Radius = Radius;
l.Shadows = Shadows;
if ( Flicker )
{
l.AnimationEnabled = true;
l.AnimationPattern = LightAnimationPattern.Flicker;
}
break;
}
case StageLightKind.Spot:
{
var l = lightGo.AddComponent<SpotLight>();
l.LightColor = Colour;
l.Intensity = Brightness;
l.Radius = Radius;
l.Shadows = Shadows;
if ( Flicker )
{
l.AnimationEnabled = true;
l.AnimationPattern = LightAnimationPattern.Candle;
}
break;
}
default:
{
var a = lightGo.AddComponent<AmbientLight>();
a.Color = new Color( Colour.r * Brightness, Colour.g * Brightness, Colour.b * Brightness );
break;
}
}
}
}
/// <summary>
/// One place in the game (the town, a shop, the dungeon, the arena...). Only the place for the current
/// screen is switched on. Put StageSpots inside it for the camera and where people stand.
/// </summary>
[Title( "Stage Set" ), Category( "Shoes and Daggers" ), Icon( "theaters" )]
public sealed class StageSet : Component
{
/// <summary>Which screens show this place, comma separated: Title, Create, Intro, Town, Weaponsmith,
/// Armoury, Magic, Church, Stats, Fitting, Arena (the arena fights), Duel (the dungeon fights).</summary>
[Property] public string Screens { get; set; } = "Town";
/// <summary>Colour behind everything (where no picture covers the view).</summary>
[Property] public Color Background { get; set; } = Color.Black;
public bool Shows( string screen ) =>
(Screens ?? "").Split( ',' ).Any( x => x.Trim().Equals( screen, StringComparison.OrdinalIgnoreCase ) );
public StageSpot Spot( string role ) =>
GameObject.GetComponentsInChildren<StageSpot>( true, true ).FirstOrDefault( x => string.Equals( x.Role, role, StringComparison.OrdinalIgnoreCase ) );
public IEnumerable<StageHotspot> Hotspots => GameObject.GetComponentsInChildren<StageHotspot>( true, true );
/// <summary>Switches everything in the place on or off (the set itself stays on so it can be found).</summary>
public void Show( bool on )
{
foreach ( var child in GameObject.Children )
if ( child.Enabled != on ) child.Enabled = on;
}
}
/// <summary>
/// A named spot in a place. Roles: "Camera" (or "Camera:Title", "Camera:Versus"... for one screen),
/// "Player", "Hero", "Chained", "Fight" (middle of the fighting ground).
/// Camera spots look along their forward (blue arrow).
/// </summary>
[Title( "Stage Spot" ), Category( "Shoes and Daggers" ), Icon( "place" )]
public sealed class StageSpot : Component
{
[Property] public string Role { get; set; } = "Camera";
/// <summary>Camera spots: field of view (degrees, across the screen).</summary>
[Property] public float FieldOfView { get; set; } = 60f;
/// <summary>People spots: how big the gladiator is drawn (1 = normal).</summary>
[Property] public float Size { get; set; } = 1f;
}
/// <summary>A clickable label in the town that takes you somewhere (sits where this object is).</summary>
[Title( "Stage Hotspot" ), Category( "Shoes and Daggers" ), Icon( "ads_click" )]
public sealed class StageHotspot : Component
{
[Property] public GameScreen Target { get; set; } = GameScreen.Weaponsmith;
[Property] public string Label { get; set; } = "Weaponsmith";
}