Flash/Sbox/FlashEditor.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Flash;
using Sandbox;
using Sandbox.UI;
using FMatrix = Flash.Matrix;
using FJson = Flash.Json;
/// <summary>
/// One layout tweak, editable in the s&box inspector. Put these on child GameObjects of the Flash Stage.
/// Path is what F2 mode shows for the selected thing (e.g. "beforefight_panel" or "#1502@26").
/// </summary>
[Title( "Flash Layout Item" ), Category( "Shoes and Daggers" ), Icon( "open_with" )]
public sealed class FlashLayoutItem : Component
{
/// <summary>Which object to move (copy it from F2 mode).</summary>
[Property] public string Path { get; set; } = "";
/// <summary>Offset in pixels of the original 640x420 game.</summary>
[Property] public float X { get; set; }
[Property] public float Y { get; set; }
[Property] public float ScaleX { get; set; } = 1;
[Property] public float ScaleY { get; set; } = 1;
[Property] public float Rotation { get; set; }
[Property] public bool Hidden { get; set; }
public LayoutAdjust ToAdjust() => new() { X = X, Y = Y, ScaleX = ScaleX, ScaleY = ScaleY, Rotation = Rotation, Hidden = Hidden };
protected override void OnValidate() => FlashStage.Instance?.Editor?.ReloadFromScene();
}
/// <summary>
/// F2 in game: click something to select it, drag to move it, arrows nudge (Shift ×10), [ ] scale, , . rotate,
/// H hide, R reset, Backspace selects the parent, P pauses the game, Ctrl+S saves (FileSystem.Data/ss2/layout.json).
/// </summary>
public sealed class FlashEditor
{
readonly FlashStage stage;
readonly Panel root;
Panel overlay, box;
Label info;
public bool Active { get; private set; }
public bool Paused { get; private set; }
DisplayObject selected;
bool dragging;
Vector2 dragStart;
double startX, startY;
string status = "";
public FlashEditor( FlashStage stage, Panel root )
{
this.stage = stage;
this.root = root;
}
Player Player => stage.Player;
// ------------------------------------------------------------------ persistence
const string FilePath = "ss2/layout.json";
public void LoadLayout()
{
Player.Layout.Clear();
try
{
if ( FileSystem.Data.FileExists( FilePath ) )
{
var j = FJson.Parse( FileSystem.Data.ReadAllText( FilePath ) ) as Dictionary<string, object>;
if ( j != null )
foreach ( var kv in j )
{
var a = FJson.Doubles( kv.Value );
if ( a == null || a.Length < 6 ) continue;
Player.Layout[kv.Key] = new LayoutAdjust { X = a[0], Y = a[1], ScaleX = a[2], ScaleY = a[3], Rotation = a[4], Hidden = a[5] != 0 };
}
}
}
catch ( Exception e ) { Log.Warning( $"FlashEditor: layout.json unreadable: {e.Message}" ); }
// items in the scene win over the saved file
foreach ( var item in stage.Components.GetAll<FlashLayoutItem>( FindMode.EverythingInSelfAndDescendants ) )
if ( !string.IsNullOrWhiteSpace( item.Path ) ) Player.Layout[item.Path.Trim()] = item.ToAdjust();
Player.ReapplyLayout();
}
public void ReloadFromScene()
{
if ( Player == null ) return;
LoadLayout();
}
public void Save()
{
var sb = new StringBuilder( "{\n" );
bool first = true;
foreach ( var kv in Player.Layout )
{
if ( kv.Value.IsIdentity ) continue;
var a = kv.Value;
if ( !first ) sb.Append( ",\n" );
first = false;
sb.Append( " \"" ).Append( kv.Key.Replace( "\"", "\\\"" ) ).Append( "\": [" )
.Append( string.Join( ", ", new[] { a.X, a.Y, a.ScaleX, a.ScaleY, a.Rotation, a.Hidden ? 1 : 0 }.Select( v => v.ToString( "0.###", CultureInfo.InvariantCulture ) ) ) ).Append( ']' );
}
sb.Append( "\n}\n" );
FileSystem.Data.CreateDirectory( "ss2" );
FileSystem.Data.WriteAllText( FilePath, sb.ToString() );
status = $"saved {Player.Layout.Count} tweaks to data/{FilePath}";
}
// ------------------------------------------------------------------ input
static bool Key( string name ) => Input.Keyboard.Pressed( name ) || Input.Keyboard.Pressed( name.ToUpperInvariant() );
static bool Held( string name ) => Input.Keyboard.Down( name ) || Input.Keyboard.Down( name.ToUpperInvariant() );
public void Update()
{
if ( Key( "f2" ) ) Toggle();
if ( !Active ) return;
if ( Key( "p" ) ) Paused = !Paused;
if ( Held( "ctrl" ) && Key( "s" ) ) Save();
if ( selected == null || selected.Removed ) { selected = null; return; }
double step = Held( "shift" ) ? 10 : 1;
if ( Key( "left" ) ) Nudge( -step, 0 );
if ( Key( "right" ) ) Nudge( step, 0 );
if ( Key( "up" ) ) Nudge( 0, -step );
if ( Key( "down" ) ) Nudge( 0, step );
if ( Key( "[" ) || Key( "leftbracket" ) ) Scale( 1 / 1.05 );
if ( Key( "]" ) || Key( "rightbracket" ) ) Scale( 1.05 );
if ( Key( "," ) || Key( "comma" ) ) Rotate( -(Held( "shift" ) ? 15 : 1) );
if ( Key( "." ) || Key( "period" ) ) Rotate( Held( "shift" ) ? 15 : 1 );
if ( Key( "h" ) ) { var a = Edit(); a.Hidden = !a.Hidden; }
if ( Key( "r" ) ) { Player.Layout.Remove( selected.StablePath ); selected.Adjust = null; Player.ReapplyLayout(); }
if ( Key( "backspace" ) && selected.RenderParent != null && selected.RenderParent != Player.Root ) selected = selected.RenderParent;
}
void Toggle()
{
Active = !Active;
if ( !Active ) { Paused = false; overlay?.Delete( true ); overlay = null; selected = null; return; }
overlay = new Panel { Parent = root };
overlay.Style.Position = PositionMode.Absolute;
overlay.Style.Left = 0; overlay.Style.Top = 0;
overlay.Style.Width = Length.Percent( 100 ); overlay.Style.Height = Length.Percent( 100 );
overlay.Style.PointerEvents = PointerEvents.None;
overlay.Style.ZIndex = 100000;
info = new Label { Parent = overlay };
info.Style.Position = PositionMode.Absolute;
info.Style.Left = 4; info.Style.Top = 2;
info.Style.FontSize = 8;
info.Style.FontColor = Color.White;
info.Style.BackgroundColor = new Color( 0, 0, 0, 0.75f );
info.Style.WhiteSpace = WhiteSpace.PreLine;
box = new Panel { Parent = overlay };
box.Style.Position = PositionMode.Absolute;
box.Style.BorderWidth = 1;
box.Style.BorderColor = new Color( 1, 0.6f, 0, 1 );
}
LayoutAdjust Edit()
{
var key = selected.StablePath;
if ( !Player.Layout.TryGetValue( key, out var a ) ) { a = new LayoutAdjust(); Player.Layout[key] = a; }
selected.Adjust = a;
return a;
}
void Nudge( double dx, double dy ) { var a = Edit(); a.X += dx; a.Y += dy; }
void Scale( double f ) { var a = Edit(); a.ScaleX *= f; a.ScaleY *= f; }
void Rotate( double deg ) { var a = Edit(); a.Rotation += deg; }
public void MouseDown( Vector2 s )
{
var hit = Pick( s.x, s.y );
if ( hit != null && hit != selected ) selected = hit;
if ( selected == null ) return;
dragging = true;
dragStart = s;
var a = Edit();
startX = a.X; startY = a.Y;
}
public void MouseMove( Vector2 s )
{
if ( !dragging || selected == null ) return;
// stage delta → parent space
var pm = selected.RenderParent?.WorldMatrix ?? FMatrix.Identity;
var inv = pm.Inverse();
double dx = s.x - dragStart.x, dy = s.y - dragStart.y;
double lx = inv.A * dx + inv.C * dy, ly = inv.B * dx + inv.D * dy;
var a = Edit();
a.X = Math.Round( startX + lx, 1 );
a.Y = Math.Round( startY + ly, 1 );
}
public void MouseUp() { dragging = false; }
/// <summary>The thing under the mouse: the leaf's container (button / clip), never the whole screen.</summary>
DisplayObject Pick( double x, double y )
{
var list = new List<RenderItem>();
Player.CollectRender( list );
for ( int i = list.Count - 1; i >= 0; i-- )
{
var o = list[i].Obj;
if ( !o.HitTestPoint( x, y, true ) ) continue;
var parent = o.RenderParent;
if ( parent == null || parent == Player.Root ) return o;
return parent;
}
return null;
}
// ------------------------------------------------------------------ drawing
public void Draw( List<RenderItem> items )
{
if ( !Active || info == null ) return;
var sb = new StringBuilder();
sb.Append( "EDIT MODE (F2) click: select drag: move arrows: nudge (shift x10) [ ]: scale , .: rotate H: hide R: reset Backspace: parent P: pause Ctrl+S: save\n" );
if ( selected != null && !selected.Removed )
{
var a = selected.Adjust;
sb.Append( $"Path: {selected.StablePath} " );
if ( a != null ) sb.Append( $"X {a.X:0.#} Y {a.Y:0.#} scale {a.ScaleX:0.##},{a.ScaleY:0.##} rot {a.Rotation:0.#}{(a.Hidden ? " HIDDEN" : "")}" );
var b = selected.BoundsIn( selected.WorldMatrix );
if ( b != null )
{
var p0 = stage.StageToScreen( new Vector2( (float)b.Value.x0, (float)b.Value.y0 ) ) / stage.StageScale;
var p1 = stage.StageToScreen( new Vector2( (float)b.Value.x1, (float)b.Value.y1 ) ) / stage.StageScale;
box.Style.Left = p0.x; box.Style.Top = p0.y;
box.Style.Width = p1.x - p0.x; box.Style.Height = p1.y - p0.y;
box.Style.Display = DisplayMode.Flex;
}
}
else
{
sb.Append( "Nothing selected" );
box.Style.Display = DisplayMode.None;
}
if ( Paused ) sb.Append( " [PAUSED]" );
if ( status.Length > 0 ) sb.Append( "\n" ).Append( status );
info.Text = sb.ToString();
}
}