upd: sync local changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a30f4b8c5
commit
6884cd5374
36 files changed
+3363
-6
No files matched your search
@@ -0,0 +1,32 @@
|
||||
namespace Cthangover.Mods.Interface.View
|
||||
{
|
||||
/// <summary>
|
||||
/// 9-point alignment grid for viewport content positioning. The naming
|
||||
/// uses horizontal-first, vertical-second convention (LeftTop, CenterCenter,
|
||||
/// RightBottom) — matching the intuitive order for CSS-like layout.
|
||||
/// </summary>
|
||||
public enum AlignType
|
||||
{
|
||||
/// <summary>Content aligned to top-left corner of the viewport.</summary>
|
||||
LeftTop,
|
||||
/// <summary>Content horizontally left, vertically centered in the viewport.</summary>
|
||||
LeftCenter,
|
||||
/// <summary>Content horizontally left, vertically at the bottom of the viewport.</summary>
|
||||
LeftBottom,
|
||||
|
||||
/// <summary>Content horizontally centered, vertically at the top of the viewport.</summary>
|
||||
CenterTop,
|
||||
/// <summary>Content centered both horizontally and vertically in the viewport.</summary>
|
||||
CenterCenter,
|
||||
/// <summary>Content horizontally centered, vertically at the bottom of the viewport.</summary>
|
||||
CenterBottom,
|
||||
|
||||
/// <summary>Content horizontally right, vertically at the top of the viewport.</summary>
|
||||
RightTop,
|
||||
/// <summary>Content horizontally right, vertically centered in the viewport.</summary>
|
||||
RightCenter,
|
||||
/// <summary>Content aligned to the bottom-right corner of the viewport.</summary>
|
||||
RightBottom,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The content node that holds all scene visuals. Scaling and alignment are applied
|
||||
/// directly to this node.
|
||||
/// </summary>
|
||||
public Control Content => content;
|
||||
|
||||
/// <summary>Logical resolution of the content area (default 1920x1024). Used for normalised coordinate calculations.</summary>
|
||||
public Vector2I LogicalSize => defaultResolution;
|
||||
|
||||
/// <summary>
|
||||
/// The background <see cref="TextureRect"/> rendered behind all interactive layers.
|
||||
/// Receives a <c>timed_sprite</c> shader material for time-of-day lighting when
|
||||
/// no material is already assigned.
|
||||
/// </summary>
|
||||
public TextureRect Background { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The foreground <see cref="TextureRect"/> rendered above interactive layers.
|
||||
/// Receives the same <c>timed_sprite</c> shader as the background.
|
||||
/// </summary>
|
||||
public TextureRect Foreground { get; private set; }
|
||||
|
||||
/// <summary>Container for interactive objects rendered behind the foreground.</summary>
|
||||
public Control InteractiveLayerBackground { get; private set; }
|
||||
|
||||
/// <summary>Container for interactive objects rendered above the foreground.</summary>
|
||||
public Control InteractiveLayerForeground { get; private set; }
|
||||
|
||||
/// <summary>Container for interactive objects rendered above all scene visuals (UI layer).</summary>
|
||||
public Control InteractiveLayerUI { get; private set; }
|
||||
|
||||
private ColorRect transitionOverlay;
|
||||
private Tween transitionTween;
|
||||
|
||||
/// <summary>Resolves the background and foreground nodes and assigns the time-of-day shader when no material is set.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
Background ??= GetNodeOrNull<TextureRect>("Background");
|
||||
Foreground ??= GetNodeOrNull<TextureRect>("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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="layerName">"background", "foreground" or "ui".</param>
|
||||
/// <returns>The matching Control container.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a texture to the background <see cref="TextureRect"/>. This is the
|
||||
/// primary mechanism for scenes to set the backdrop visual. Logs the new background
|
||||
/// ID from <see cref="SceneContextNode.LastBackgroundID"/> for debugging.
|
||||
/// </summary>
|
||||
public void SetBackgroundTexture(Texture2D texture)
|
||||
{
|
||||
if (Background != null)
|
||||
{
|
||||
Background.Texture = texture;
|
||||
GameLogger.Log("VIEW", $"set background '" + SceneContextNode.LastBackgroundID + "'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a texture to the foreground <see cref="TextureRect"/>. Typically used
|
||||
/// for scene overlays rendered above interactive objects but below UI.
|
||||
/// </summary>
|
||||
public void SetForegroundTexture(Texture2D texture)
|
||||
{
|
||||
if (Foreground != null)
|
||||
{
|
||||
Foreground.Texture = texture;
|
||||
GameLogger.Log("VIEW", $"set foreground");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the currently assigned background texture, or <c>null</c> if none is set
|
||||
/// or the <see cref="Background"/> node is missing.
|
||||
/// </summary>
|
||||
public Texture2D GetBackgroundTexture()
|
||||
{
|
||||
return Background?.Texture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smoothly transitions the background to a new texture using a black overlay crossfade:
|
||||
/// fades a <see cref="ColorRect"/> to full opacity over the first half of <paramref name="duration"/>,
|
||||
/// 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.
|
||||
/// <see cref="KillTransitionTween"/> cancels any in-progress transition first.
|
||||
/// </summary>
|
||||
/// <param name="newTexture">The replacement background texture.</param>
|
||||
/// <param name="duration">Total crossfade duration in seconds (default 0.5).</param>
|
||||
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;
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aborts any in-progress background transition by killing the tween and resetting
|
||||
/// the overlay to invisible. Safe to call when no transition is active.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a zoom level to the content node. The <paramref name="value"/> is a 0–1 lerp
|
||||
/// factor between <c>minZoom</c> and <c>maxZoom</c>. Scale is uniform (X and Y are equal).
|
||||
/// </summary>
|
||||
/// <param name="value">Normalized zoom factor: 0 = minZoom, 1 = maxZoom.</param>
|
||||
public void SetZoom(float value)
|
||||
{
|
||||
var zoom = minZoom + (maxZoom - minZoom) * value;
|
||||
if (content != null)
|
||||
content.Scale = Vector2.One * zoom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the content node within the viewport using the 9-point alignment grid
|
||||
/// from <see cref="ViewBoxNavigator.GetPositionByAlign"/>.
|
||||
/// </summary>
|
||||
/// <param name="align">Which corner/edge/center to align to.</param>
|
||||
public void SetAlign(AlignType align)
|
||||
{
|
||||
if (content != null)
|
||||
content.Position = ViewBoxNavigator.GetPositionByAlign(align, content.Size);
|
||||
}
|
||||
|
||||
#if TOOLS && DEBUG
|
||||
/// <summary>Editor-only: forces the content back to the default resolution rect while inspecting properties.</summary>
|
||||
public override void _ValidateProperty(Godot.Collections.Dictionary property)
|
||||
{
|
||||
base._ValidateProperty(property);
|
||||
if (content != null)
|
||||
content.Size = defaultResolution;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.View
|
||||
{
|
||||
/// <summary>
|
||||
/// Static utility that calculates content positioning offsets for the 9-point
|
||||
/// AlignType grid. Uses the viewport's visible rect as the camera/view area
|
||||
/// and computes offsets so the content aligns correctly within it. The
|
||||
/// CenterCenter case negates X because the content position is treated as
|
||||
/// a negative offset from top-left in Godot's coordinate space.
|
||||
/// </summary>
|
||||
public static class ViewBoxNavigator
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the content node position offset for the given 9-point <see cref="AlignType"/>.
|
||||
/// Uses the visible viewport rect size as the camera area and computes offsets relative
|
||||
/// to the top-left corner. Negative X coordinates are intentional: Godot positions content
|
||||
/// relative to the container origin, so aligning center or right requires negative offsets
|
||||
/// when content is larger than the viewport.
|
||||
/// </summary>
|
||||
/// <param name="align">The target alignment from the 9-point grid.</param>
|
||||
/// <param name="contentSize">The size of the content node being positioned.</param>
|
||||
/// <returns>A position offset that places the content correctly for the chosen alignment.</returns>
|
||||
public static Vector2 GetPositionByAlign(AlignType align, Vector2 contentSize)
|
||||
{
|
||||
var viewport = Godot.Engine.GetMainLoop() as SceneTree;
|
||||
var screenSize = viewport?.Root?.GetVisibleRect().Size ?? new Vector2(1920, 1024);
|
||||
var cameraSize = screenSize;
|
||||
var alignPos = Vector2.Zero;
|
||||
|
||||
switch (align)
|
||||
{
|
||||
case AlignType.LeftTop:
|
||||
break;
|
||||
case AlignType.LeftCenter:
|
||||
alignPos.Y = (contentSize.Y - cameraSize.Y) * 0.5f;
|
||||
break;
|
||||
case AlignType.LeftBottom:
|
||||
alignPos.Y = contentSize.Y - cameraSize.Y;
|
||||
break;
|
||||
|
||||
case AlignType.CenterTop:
|
||||
alignPos.X = -(contentSize.X - cameraSize.X) * 0.5f;
|
||||
break;
|
||||
case AlignType.CenterCenter:
|
||||
alignPos = (contentSize - cameraSize) * 0.5f;
|
||||
alignPos.X = -alignPos.X;
|
||||
break;
|
||||
case AlignType.CenterBottom:
|
||||
alignPos.X = -(contentSize.X - cameraSize.X) * 0.5f;
|
||||
alignPos.Y = contentSize.Y - cameraSize.Y;
|
||||
break;
|
||||
|
||||
case AlignType.RightTop:
|
||||
alignPos.X = -(contentSize.X - cameraSize.X);
|
||||
break;
|
||||
case AlignType.RightCenter:
|
||||
alignPos.Y = (contentSize.Y - cameraSize.Y) * 0.5f;
|
||||
alignPos.X = -(contentSize.X - cameraSize.X);
|
||||
break;
|
||||
case AlignType.RightBottom:
|
||||
alignPos.X = -(contentSize.X - cameraSize.X);
|
||||
alignPos.Y = contentSize.Y - cameraSize.Y;
|
||||
break;
|
||||
}
|
||||
|
||||
return alignPos;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Interface.View
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin adapter that exposes ViewBox.SetZoom to external callers (e.g. UI
|
||||
/// sliders or scroll handlers) through a single method. Exists as a Node
|
||||
/// so it can be placed in the scene tree and receive signals.
|
||||
/// </summary>
|
||||
public partial class ZoomViewBoxController : Node
|
||||
{
|
||||
|
||||
[Export] private ViewBox viewBox;
|
||||
|
||||
/// <summary>
|
||||
/// Delegates to <see cref="ViewBox.SetZoom"/> on the linked <see cref="ViewBox"/>.
|
||||
/// Intended to be connected to a slider or scroll input signal. No-op if
|
||||
/// <c>viewBox</c> is not assigned.
|
||||
/// </summary>
|
||||
/// <param name="value">Normalized zoom factor (0–1).</param>
|
||||
public void OnChangeZoom(float value)
|
||||
{
|
||||
if (viewBox == null)
|
||||
return;
|
||||
|
||||
viewBox.SetZoom(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user