using Cthangover.Core.Mods; using Cthangover.Core.Mods.Resolvers; using Cthangover.Core.Scenes; using Cthangover.Core.Utils; using Cthangover.Core.UI; using Cthangover.Core.UI.Theming; using Godot; namespace Cthangover.Mods.Interface.View { /// /// Core viewport container: manages background/foreground TextureRects with /// a shared shader material (timed_sprite from mods) for time-of-day lighting. /// Supports smooth background transitions via a black ColorRect overlay that /// fades in, swaps the texture mid-transition, then fades out — a classic /// crossfade approach that doesn't require a second TextureRect. The zoom /// system scales the content Control between minZoom and maxZoom based on /// a 0–1 input value. Alignment positions content within the viewport using /// ViewBoxNavigator's 9-point grid. The #if TOOLS block forces content size /// to defaultResolution for editor preview. /// /// Interactive layers are Control containers placed between Background and /// Foreground (and above Foreground for UI) that host interactive scene objects. /// public partial class ViewBox : Control, IBackgroundView { [Export] private Vector2I defaultResolution = new(1920, 1024); [Export] private Control content; [Export] private float minZoom; [Export] private float maxZoom; /// /// The content node that holds all scene visuals. Scaling and alignment are applied /// directly to this node. /// public Control Content => content; /// Logical resolution of the content area (default 1920x1024). Used for normalised coordinate calculations. public Vector2I LogicalSize => defaultResolution; /// /// The background rendered behind all interactive layers. /// Receives a timed_sprite shader material for time-of-day lighting when /// no material is already assigned. /// public TextureRect Background { get; private set; } /// /// The foreground rendered above interactive layers. /// Receives the same timed_sprite shader as the background. /// public TextureRect Foreground { get; private set; } /// Container for interactive objects rendered behind the foreground. public Control InteractiveLayerBackground { get; private set; } /// Container for interactive objects rendered above the foreground. public Control InteractiveLayerForeground { get; private set; } /// Container for interactive objects rendered above all scene visuals (UI layer). public Control InteractiveLayerUI { get; private set; } private ColorRect transitionOverlay; private Tween transitionTween; /// Resolves the background and foreground nodes and assigns the time-of-day shader when no material is set. public override void _Ready() { Background ??= GetNodeOrNull("Background"); Foreground ??= GetNodeOrNull("Foreground"); var timedShader = Shaders.Resolve("timed_sprite"); if (timedShader != null) { if (Background != null && Background.Material == null) { Background.Material = new ShaderMaterial { Shader = timedShader }; GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Background", LogLevel.Debug); } if (Foreground != null && Foreground.Material == null) { Foreground.Material = new ShaderMaterial { Shader = timedShader }; GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Foreground", LogLevel.Debug); } } SetZoom(0f); SetAlign(AlignType.CenterCenter); } /// /// Returns the interactive layer container for the given logical layer name. /// Creates the layer lazily as a direct child of ViewBox if it doesn't exist yet. /// /// "background", "foreground" or "ui". /// The matching Control container. public Control GetInteractiveLayer(string layerName) { var result = layerName?.ToLowerInvariant() switch { "background" => InteractiveLayerBackground ??= CreateLayerDirect("InteractiveLayer_Background", 1), "foreground" => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3), "ui" => InteractiveLayerUI ??= CreateLayerDirect("InteractiveLayer_UI", 5), _ => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3) }; return result; } private Control CreateLayerDirect(string name, int zIndex) { var layer = new Control { Name = name, MouseFilter = MouseFilterEnum.Ignore, ZIndex = zIndex, Size = new Vector2(defaultResolution.X, defaultResolution.Y) }; AddChild(layer); return layer; } /// /// Assigns a texture to the background . This is the /// primary mechanism for scenes to set the backdrop visual. Logs the new background /// ID from for debugging. /// public void SetBackgroundTexture(Texture2D texture) { if (Background != null) { Background.Texture = texture; GameLogger.Log("VIEW", $"set background '" + SceneContextNode.LastBackgroundID + "'"); } } /// /// Assigns a texture to the foreground . Typically used /// for scene overlays rendered above interactive objects but below UI. /// public void SetForegroundTexture(Texture2D texture) { if (Foreground != null) { Foreground.Texture = texture; GameLogger.Log("VIEW", $"set foreground"); } } /// /// Returns the currently assigned background texture, or null if none is set /// or the node is missing. /// public Texture2D GetBackgroundTexture() { return Background?.Texture; } /// /// Smoothly transitions the background to a new texture using a black overlay crossfade: /// fades a to full opacity over the first half of , /// swaps the background texture mid-transition, then fades back out over the second half. /// If the target texture is already set or the background node is missing, no-op. /// cancels any in-progress transition first. /// /// The replacement background texture. /// Total crossfade duration in seconds (default 0.5). public void TransitionBackground(Texture2D newTexture) { TransitionBackground(newTexture, 0.5f); } public void TransitionBackground(Texture2D newTexture, float duration = 0.5f) { if (Background == null) { if (newTexture != null) SetBackgroundTexture(newTexture); return; } if (Background.Texture == newTexture) return; KillTransitionTween(); var overlay = GetOrCreateTransitionOverlay(); overlay.Visible = true; overlay.Modulate = UiTokens.Alpha(Colors.Black, 0f); overlay.MouseFilter = MouseFilterEnum.Ignore; var halfDuration = duration / 2f; transitionTween = CreateTween(); transitionTween.TweenProperty(overlay, "modulate:a", 1f, halfDuration); transitionTween.TweenCallback(Callable.From(() => { SetBackgroundTexture(newTexture); })); transitionTween.TweenProperty(overlay, "modulate:a", 0f, halfDuration); transitionTween.TweenCallback(Callable.From(() => { overlay.Visible = false; transitionTween = null; })); } /// /// Aborts any in-progress background transition by killing the tween and resetting /// the overlay to invisible. Safe to call when no transition is active. /// public void KillTransitionTween() { if (transitionTween != null) { transitionTween.Kill(); transitionTween = null; } if (transitionOverlay != null) { transitionOverlay.Visible = false; transitionOverlay.Modulate = UiTokens.Alpha(Colors.Black, 0f); } } private ColorRect GetOrCreateTransitionOverlay() { if (transitionOverlay == null) { transitionOverlay = new ColorRect { Name = "TransitionOverlay", Color = UiTokens.Alpha(Colors.Black, 1f), MouseFilter = MouseFilterEnum.Ignore, Visible = false }; transitionOverlay.AnchorRight = 1f; transitionOverlay.AnchorBottom = 1f; transitionOverlay.GrowHorizontal = GrowDirection.Both; transitionOverlay.GrowVertical = GrowDirection.Both; AddChild(transitionOverlay); } return transitionOverlay; } /// /// Applies a zoom level to the content node. The is a 0–1 lerp /// factor between minZoom and maxZoom. Scale is uniform (X and Y are equal). /// /// Normalized zoom factor: 0 = minZoom, 1 = maxZoom. public void SetZoom(float value) { var zoom = minZoom + (maxZoom - minZoom) * value; if (content != null) content.Scale = Vector2.One * zoom; } /// /// Positions the content node within the viewport using the 9-point alignment grid /// from . /// /// Which corner/edge/center to align to. public void SetAlign(AlignType align) { if (content != null) content.Position = ViewBoxNavigator.GetPositionByAlign(align, content.Size); } #if TOOLS && DEBUG /// Editor-only: forces the content back to the default resolution rect while inspecting properties. public override void _ValidateProperty(Godot.Collections.Dictionary property) { base._ValidateProperty(property); if (content != null) content.Size = defaultResolution; } #endif } }