Line endings: LF everywhere
.gitattributes forces LF (CRLF only for .bat/.cmd), core.autocrlf is off; files that differed only by CRLF are normalized.
This commit is contained in:
1 parent
c77c8ec960
commit
ba237ec7dd
7 files changed
+504
-474
No files matched your search
@@ -0,0 +1,30 @@
|
|||||||
|
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Binaries: never converted, never diffed as text.
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.webp binary
|
||||||
|
*.svg text
|
||||||
|
*.ogg binary
|
||||||
|
*.wav binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.dll binary
|
||||||
|
*.so binary
|
||||||
|
*.dylib binary
|
||||||
|
*.pdb binary
|
||||||
|
*.exe binary
|
||||||
|
*.zip binary
|
||||||
|
*.pck binary
|
||||||
|
*.moc3 binary
|
||||||
|
*.res binary
|
||||||
|
*.scn binary
|
||||||
+37
-37
@@ -1,37 +1,37 @@
|
|||||||
# Godot 4.x
|
# Godot 4.x
|
||||||
.godot/
|
.godot/
|
||||||
.idea/
|
.idea/
|
||||||
sdk/
|
sdk/
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
|
||||||
#ai
|
#ai
|
||||||
.omo/
|
.omo/
|
||||||
.opencode/
|
.opencode/
|
||||||
|
|
||||||
gen/
|
gen/
|
||||||
obj/
|
obj/
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
*.tres~
|
*.tres~
|
||||||
*.tscn~
|
*.tscn~
|
||||||
|
|
||||||
# GDScript LSP
|
# GDScript LSP
|
||||||
.godot/
|
.godot/
|
||||||
|
|
||||||
# Export
|
# Export
|
||||||
*.exe
|
*.exe
|
||||||
*.dmg
|
*.dmg
|
||||||
*.apk
|
*.apk
|
||||||
*.aab
|
*.aab
|
||||||
*.pck
|
*.pck
|
||||||
*.zip
|
*.zip
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
*.import
|
*.import
|
||||||
*.godot.uid
|
*.godot.uid
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Cthangover.Core.Factories;
|
using Cthangover.Core.Factories;
|
||||||
using Cthangover.Core.UI.Theming;
|
using Cthangover.Core.UI.Theming;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.Core.Interactive
|
namespace Cthangover.Core.Interactive
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JSON-driven definition of an interactive scene object loaded from mods.
|
/// JSON-driven definition of an interactive scene object loaded from mods.
|
||||||
/// The texture is always full-screen (alpha determines visibility outside the collider).
|
/// The texture is always full-screen (alpha determines visibility outside the collider).
|
||||||
/// The collider defines the clickable region in normalised 0..1 viewport coordinates.
|
/// The collider defines the clickable region in normalised 0..1 viewport coordinates.
|
||||||
/// Interactives keep their own resolver and <c>{"Items": [...]}</c> envelope;
|
/// Interactives keep their own resolver and <c>{"Items": [...]}</c> envelope;
|
||||||
/// unlike other entity kinds they are not part of the kind-routed pipeline.
|
/// unlike other entity kinds they are not part of the kind-routed pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class InteractiveDefinition : IIdentifiable
|
public class InteractiveDefinition : IIdentifiable
|
||||||
{
|
{
|
||||||
@@ -166,4 +166,4 @@ namespace Cthangover.Core.Interactive
|
|||||||
public string[] Commands { get; set; }
|
public string[] Commands { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+399
-399
@@ -1,399 +1,399 @@
|
|||||||
using System;
|
using System;
|
||||||
using Cthangover.Core.Mods;
|
using Cthangover.Core.Mods;
|
||||||
using Cthangover.Core.Mods.Resolvers;
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
using Cthangover.Core.Scenes;
|
using Cthangover.Core.Scenes;
|
||||||
using Cthangover.Core.UI.Executable;
|
using Cthangover.Core.UI.Executable;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Cthangover.Core.UI.Theming;
|
using Cthangover.Core.UI.Theming;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.Core.Interactive
|
namespace Cthangover.Core.Interactive
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runtime node for an interactive scene object. The Control fills its parent
|
/// Runtime node for an interactive scene object. The Control fills its parent
|
||||||
/// (the interactive layer container) and displays a full-screen texture with
|
/// (the interactive layer container) and displays a full-screen texture with
|
||||||
/// alpha. The collider (rect, circle, or polygon) defines the clickable region
|
/// alpha. The collider (rect, circle, or polygon) defines the clickable region
|
||||||
/// in normalised viewport coordinates, converted to pixels at runtime.
|
/// in normalised viewport coordinates, converted to pixels at runtime.
|
||||||
///
|
///
|
||||||
/// Hit detection uses <c>_HasPoint</c> overrides verified against global
|
/// Hit detection uses <c>_HasPoint</c> overrides verified against global
|
||||||
/// mouse position and the pixel-space collider geometry.
|
/// mouse position and the pixel-space collider geometry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class InteractiveObject : Control, IEventObject
|
public partial class InteractiveObject : Control, IEventObject
|
||||||
{
|
{
|
||||||
private static readonly Color _transparent = new(0f, 0f, 0f, 0f);
|
private static readonly Color _transparent = new(0f, 0f, 0f, 0f);
|
||||||
|
|
||||||
/// <summary>Unique instance identifier, used for registry and <c>IEventObject</c> tracking.</summary>
|
/// <summary>Unique instance identifier, used for registry and <c>IEventObject</c> tracking.</summary>
|
||||||
public string ID { get; set; }
|
public string ID { get; set; }
|
||||||
|
|
||||||
/// <summary>Reference back to the <c>InteractiveDefinition.ID</c> that spawned this object.</summary>
|
/// <summary>Reference back to the <c>InteractiveDefinition.ID</c> that spawned this object.</summary>
|
||||||
public string DefinitionId { get; private set; }
|
public string DefinitionId { get; private set; }
|
||||||
|
|
||||||
/// <summary>Whether the object responds to mouse input.</summary>
|
/// <summary>Whether the object responds to mouse input.</summary>
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>True while the pointer is within the collider.</summary>
|
/// <summary>True while the pointer is within the collider.</summary>
|
||||||
public bool IsHovered { get; private set; }
|
public bool IsHovered { get; private set; }
|
||||||
|
|
||||||
/// <summary>Invoked on hover enter with the definition's hover-enter DSL (wired by <see cref="InteractiveManager"/>).</summary>
|
/// <summary>Invoked on hover enter with the definition's hover-enter DSL (wired by <see cref="InteractiveManager"/>).</summary>
|
||||||
internal Action<string> OnHoverEnterCallback { get; set; }
|
internal Action<string> OnHoverEnterCallback { get; set; }
|
||||||
/// <summary>Invoked on hover leave with the definition's hover-leave DSL.</summary>
|
/// <summary>Invoked on hover leave with the definition's hover-leave DSL.</summary>
|
||||||
internal Action<string> OnHoverLeaveCallback { get; set; }
|
internal Action<string> OnHoverLeaveCallback { get; set; }
|
||||||
/// <summary>Invoked on click with the definition id (wired by <see cref="InteractiveManager"/>).</summary>
|
/// <summary>Invoked on click with the definition id (wired by <see cref="InteractiveManager"/>).</summary>
|
||||||
internal Action<string> OnClickCallback { get; set; }
|
internal Action<string> OnClickCallback { get; set; }
|
||||||
|
|
||||||
private TextureRect _visual;
|
private TextureRect _visual;
|
||||||
private ShaderMaterial _highlightMaterial;
|
private ShaderMaterial _highlightMaterial;
|
||||||
private Tween _highlightTween;
|
private Tween _highlightTween;
|
||||||
|
|
||||||
private HitAreaType _hitType = HitAreaType.Rect;
|
private HitAreaType _hitType = HitAreaType.Rect;
|
||||||
private float _hitRadiusPx;
|
private float _hitRadiusPx;
|
||||||
private Vector2[] _hitPolygonPx;
|
private Vector2[] _hitPolygonPx;
|
||||||
private Rect2 _hitRectPx;
|
private Rect2 _hitRectPx;
|
||||||
|
|
||||||
private Vector2 _contentSize;
|
private Vector2 _contentSize;
|
||||||
|
|
||||||
private Color _highlightColor = _transparent;
|
private Color _highlightColor = _transparent;
|
||||||
private float _highlightScale = 1f;
|
private float _highlightScale = 1f;
|
||||||
private float _highlightDuration = 0.15f;
|
private float _highlightDuration = 0.15f;
|
||||||
|
|
||||||
private string _onHoverEnterDsl;
|
private string _onHoverEnterDsl;
|
||||||
private string _onHoverLeaveDsl;
|
private string _onHoverLeaveDsl;
|
||||||
|
|
||||||
private ColorRect _debugRect;
|
private ColorRect _debugRect;
|
||||||
private Label _debugLabel;
|
private Label _debugLabel;
|
||||||
private bool _visualReady;
|
private bool _visualReady;
|
||||||
|
|
||||||
/// <summary>Prepares the full-screen visual, highlight material and mouse handlers.</summary>
|
/// <summary>Prepares the full-screen visual, highlight material and mouse handlers.</summary>
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
MouseFilter = MouseFilterEnum.Stop;
|
MouseFilter = MouseFilterEnum.Stop;
|
||||||
ClipContents = true;
|
ClipContents = true;
|
||||||
|
|
||||||
EnsureVisualReady();
|
EnsureVisualReady();
|
||||||
|
|
||||||
MouseEntered += OnMouseEntered;
|
MouseEntered += OnMouseEntered;
|
||||||
MouseExited += OnMouseExited;
|
MouseExited += OnMouseExited;
|
||||||
|
|
||||||
GameLogger.Log("INTERACTIVE", $"_Ready: id='{ID}' size=({Size.X:F0}x{Size.Y:F0}) visible={Visible} enabled={IsEnabled}");
|
GameLogger.Log("INTERACTIVE", $"_Ready: id='{ID}' size=({Size.X:F0}x{Size.Y:F0}) visible={Visible} enabled={IsEnabled}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies a definition to this instance. The Control is made full-screen
|
/// Applies a definition to this instance. The Control is made full-screen
|
||||||
/// within its parent, the texture fills the entire area, and the collider
|
/// within its parent, the texture fills the entire area, and the collider
|
||||||
/// geometry is computed from normalised coordinates multiplied by the
|
/// geometry is computed from normalised coordinates multiplied by the
|
||||||
/// logical viewport size.
|
/// logical viewport size.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Configure(InteractiveDefinition def, Vector2 contentSize)
|
public void Configure(InteractiveDefinition def, Vector2 contentSize)
|
||||||
{
|
{
|
||||||
if (def == null)
|
if (def == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
DefinitionId = def.ID;
|
DefinitionId = def.ID;
|
||||||
_contentSize = contentSize;
|
_contentSize = contentSize;
|
||||||
|
|
||||||
EnsureVisualReady();
|
EnsureVisualReady();
|
||||||
|
|
||||||
AnchorLeft = 0f;
|
AnchorLeft = 0f;
|
||||||
AnchorTop = 0f;
|
AnchorTop = 0f;
|
||||||
AnchorRight = 1f;
|
AnchorRight = 1f;
|
||||||
AnchorBottom = 1f;
|
AnchorBottom = 1f;
|
||||||
|
|
||||||
ZIndex = def.ZIndex;
|
ZIndex = def.ZIndex;
|
||||||
Visible = def.Visible;
|
Visible = def.Visible;
|
||||||
IsEnabled = def.Enabled;
|
IsEnabled = def.Enabled;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(def.Texture))
|
if (!string.IsNullOrEmpty(def.Texture))
|
||||||
{
|
{
|
||||||
var tex = Textures.Resolve(def.Texture);
|
var tex = Textures.Resolve(def.Texture);
|
||||||
if (tex != null)
|
if (tex != null)
|
||||||
_visual.Texture = tex;
|
_visual.Texture = tex;
|
||||||
else
|
else
|
||||||
GameLogger.Log("INTERACTIVE", $"Configure: texture '{def.Texture}' for '{def.ID}' not found in any mod", LogLevel.Warning);
|
GameLogger.Log("INTERACTIVE", $"Configure: texture '{def.Texture}' for '{def.ID}' not found in any mod", LogLevel.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyHitArea(def.HitArea, contentSize);
|
ApplyHitArea(def.HitArea, contentSize);
|
||||||
|
|
||||||
if (def.Highlight != null)
|
if (def.Highlight != null)
|
||||||
{
|
{
|
||||||
_highlightColor = def.Highlight.Color;
|
_highlightColor = def.Highlight.Color;
|
||||||
_highlightScale = def.Highlight.Scale;
|
_highlightScale = def.Highlight.Scale;
|
||||||
_highlightDuration = def.Highlight.Duration;
|
_highlightDuration = def.Highlight.Duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onHoverEnterDsl = def.Actions?.OnHoverEnter;
|
_onHoverEnterDsl = def.Actions?.OnHoverEnter;
|
||||||
_onHoverLeaveDsl = def.Actions?.OnHoverLeave;
|
_onHoverLeaveDsl = def.Actions?.OnHoverLeave;
|
||||||
|
|
||||||
SetCursor(def.Cursor);
|
SetCursor(def.Cursor);
|
||||||
|
|
||||||
GameLogger.Log("INTERACTIVE", $"Configure: id='{def.ID}' fullScreen contentSize=({contentSize.X:F0},{contentSize.Y:F0}) hitType={_hitType}");
|
GameLogger.Log("INTERACTIVE", $"Configure: id='{def.ID}' fullScreen contentSize=({contentSize.X:F0},{contentSize.Y:F0}) hitType={_hitType}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether the global mouse position falls within the collider
|
/// Checks whether the global mouse position falls within the collider
|
||||||
/// geometry in pixel space. Bypasses Godot's local coordinate issues.
|
/// geometry in pixel space. Bypasses Godot's local coordinate issues.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override bool _HasPoint(Vector2 point)
|
public override bool _HasPoint(Vector2 point)
|
||||||
{
|
{
|
||||||
if (!IsEnabled || !Visible)
|
if (!IsEnabled || !Visible)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var globalMouse = GetGlobalMousePosition();
|
var globalMouse = GetGlobalMousePosition();
|
||||||
var globalPos = GlobalPosition;
|
var globalPos = GlobalPosition;
|
||||||
|
|
||||||
switch (_hitType)
|
switch (_hitType)
|
||||||
{
|
{
|
||||||
case HitAreaType.Rect:
|
case HitAreaType.Rect:
|
||||||
{
|
{
|
||||||
var rect = new Rect2(globalPos + _hitRectPx.Position, _hitRectPx.Size);
|
var rect = new Rect2(globalPos + _hitRectPx.Position, _hitRectPx.Size);
|
||||||
return rect.HasPoint(globalMouse);
|
return rect.HasPoint(globalMouse);
|
||||||
}
|
}
|
||||||
case HitAreaType.Circle:
|
case HitAreaType.Circle:
|
||||||
{
|
{
|
||||||
var center = globalPos + new Vector2(_hitRectPx.Position.X + _hitRectPx.Size.X / 2f, _hitRectPx.Position.Y + _hitRectPx.Size.Y / 2f);
|
var center = globalPos + new Vector2(_hitRectPx.Position.X + _hitRectPx.Size.X / 2f, _hitRectPx.Position.Y + _hitRectPx.Size.Y / 2f);
|
||||||
return (globalMouse - center).Length() <= _hitRadiusPx;
|
return (globalMouse - center).Length() <= _hitRadiusPx;
|
||||||
}
|
}
|
||||||
case HitAreaType.Polygon:
|
case HitAreaType.Polygon:
|
||||||
{
|
{
|
||||||
if (_hitPolygonPx == null || _hitPolygonPx.Length < 3)
|
if (_hitPolygonPx == null || _hitPolygonPx.Length < 3)
|
||||||
return false;
|
return false;
|
||||||
var local = globalMouse - globalPos;
|
var local = globalMouse - globalPos;
|
||||||
return Geometry2D.IsPointInPolygon(local, _hitPolygonPx);
|
return Geometry2D.IsPointInPolygon(local, _hitPolygonPx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unregisters from the <c>SceneContextNode</c> event-object tracker and
|
/// Unregisters from the <c>SceneContextNode</c> event-object tracker and
|
||||||
/// frees the Godot node. Called by <c>InteractiveManager.ClearAll</c> on
|
/// frees the Godot node. Called by <c>InteractiveManager.ClearAll</c> on
|
||||||
/// scene transition and by <c>InteractiveManager.Remove</c> for targeted
|
/// scene transition and by <c>InteractiveManager.Remove</c> for targeted
|
||||||
/// cleanup, ensuring no dangling references survive scene teardown.
|
/// cleanup, ensuring no dangling references survive scene teardown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Destruct()
|
public void Destruct()
|
||||||
{
|
{
|
||||||
SceneContextNode.Instance?.RemoveEventObject(ID);
|
SceneContextNode.Instance?.RemoveEventObject(ID);
|
||||||
QueueFree();
|
QueueFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates or hides a debug overlay showing the hit-area bounds rectangle
|
/// Creates or hides a debug overlay showing the hit-area bounds rectangle
|
||||||
/// and the object's <c>ID</c> label. The rectangle colour changes to
|
/// and the object's <c>ID</c> label. The rectangle colour changes to
|
||||||
/// yellow when the pointer is inside the collider. Used by
|
/// yellow when the pointer is inside the collider. Used by
|
||||||
/// <c>InteractiveManager.ShowDebugBounds</c> to toggle all objects at once.
|
/// <c>InteractiveManager.ShowDebugBounds</c> to toggle all objects at once.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="show">Whether to create/show or hide the debug visuals.</param>
|
/// <param name="show">Whether to create/show or hide the debug visuals.</param>
|
||||||
public void UpdateDebugVisual(bool show)
|
public void UpdateDebugVisual(bool show)
|
||||||
{
|
{
|
||||||
if (show)
|
if (show)
|
||||||
{
|
{
|
||||||
if (_debugRect == null)
|
if (_debugRect == null)
|
||||||
{
|
{
|
||||||
_debugRect = new ColorRect
|
_debugRect = new ColorRect
|
||||||
{
|
{
|
||||||
Name = "DebugRect",
|
Name = "DebugRect",
|
||||||
MouseFilter = MouseFilterEnum.Ignore,
|
MouseFilter = MouseFilterEnum.Ignore,
|
||||||
ZIndex = 100
|
ZIndex = 100
|
||||||
};
|
};
|
||||||
AddChild(_debugRect);
|
AddChild(_debugRect);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_debugLabel == null)
|
if (_debugLabel == null)
|
||||||
{
|
{
|
||||||
_debugLabel = new Label
|
_debugLabel = new Label
|
||||||
{
|
{
|
||||||
Name = "DebugLabel",
|
Name = "DebugLabel",
|
||||||
MouseFilter = MouseFilterEnum.Ignore,
|
MouseFilter = MouseFilterEnum.Ignore,
|
||||||
ZIndex = 101,
|
ZIndex = 101,
|
||||||
Position = new Vector2(2f, 2f)
|
Position = new Vector2(2f, 2f)
|
||||||
};
|
};
|
||||||
AddChild(_debugLabel);
|
AddChild(_debugLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
_debugRect.Position = _hitRectPx.Position;
|
_debugRect.Position = _hitRectPx.Position;
|
||||||
_debugRect.Size = _hitRectPx.Size;
|
_debugRect.Size = _hitRectPx.Size;
|
||||||
_debugRect.Visible = true;
|
_debugRect.Visible = true;
|
||||||
_debugRect.Color = IsHovered
|
_debugRect.Color = IsHovered
|
||||||
? UiTokens.Alpha(UiTokens.Accent, 0.35f)
|
? UiTokens.Alpha(UiTokens.Accent, 0.35f)
|
||||||
: UiTokens.Alpha(UiTokens.Success, 0.2f);
|
: UiTokens.Alpha(UiTokens.Success, 0.2f);
|
||||||
|
|
||||||
_debugLabel.Visible = true;
|
_debugLabel.Visible = true;
|
||||||
_debugLabel.Text = DefinitionId ?? ID;
|
_debugLabel.Text = DefinitionId ?? ID;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_debugRect != null)
|
if (_debugRect != null)
|
||||||
_debugRect.Visible = false;
|
_debugRect.Visible = false;
|
||||||
if (_debugLabel != null)
|
if (_debugLabel != null)
|
||||||
_debugLabel.Visible = false;
|
_debugLabel.Visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupHighlightMaterial()
|
private void SetupHighlightMaterial()
|
||||||
{
|
{
|
||||||
var shader = Shaders.Resolve("interactive_highlight");
|
var shader = Shaders.Resolve("interactive_highlight");
|
||||||
if (shader == null)
|
if (shader == null)
|
||||||
{
|
{
|
||||||
GameLogger.Log("INTERACTIVE", "highlight shader 'interactive_highlight' not found", LogLevel.Warning);
|
GameLogger.Log("INTERACTIVE", "highlight shader 'interactive_highlight' not found", LogLevel.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_highlightMaterial = new ShaderMaterial { Shader = shader };
|
_highlightMaterial = new ShaderMaterial { Shader = shader };
|
||||||
_highlightMaterial.SetShaderParameter("modulate_color", _transparent);
|
_highlightMaterial.SetShaderParameter("modulate_color", _transparent);
|
||||||
_visual.Material = _highlightMaterial;
|
_visual.Material = _highlightMaterial;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureVisualReady()
|
private void EnsureVisualReady()
|
||||||
{
|
{
|
||||||
if (_visualReady)
|
if (_visualReady)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_visualReady = true;
|
_visualReady = true;
|
||||||
|
|
||||||
_visual = new TextureRect
|
_visual = new TextureRect
|
||||||
{
|
{
|
||||||
Name = "Visual",
|
Name = "Visual",
|
||||||
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
|
||||||
StretchMode = TextureRect.StretchModeEnum.Scale,
|
StretchMode = TextureRect.StretchModeEnum.Scale,
|
||||||
MouseFilter = MouseFilterEnum.Ignore,
|
MouseFilter = MouseFilterEnum.Ignore,
|
||||||
AnchorRight = 1f,
|
AnchorRight = 1f,
|
||||||
AnchorBottom = 1f
|
AnchorBottom = 1f
|
||||||
};
|
};
|
||||||
AddChild(_visual);
|
AddChild(_visual);
|
||||||
|
|
||||||
SetupHighlightMaterial();
|
SetupHighlightMaterial();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyHitArea(HitAreaDefinition hitDef, Vector2 contentSize)
|
private void ApplyHitArea(HitAreaDefinition hitDef, Vector2 contentSize)
|
||||||
{
|
{
|
||||||
_hitType = HitAreaType.Rect;
|
_hitType = HitAreaType.Rect;
|
||||||
var vpW = contentSize.X;
|
var vpW = contentSize.X;
|
||||||
var vpH = contentSize.Y;
|
var vpH = contentSize.Y;
|
||||||
|
|
||||||
if (hitDef == null)
|
if (hitDef == null)
|
||||||
{
|
{
|
||||||
_hitRectPx = new Rect2(0, 0, vpW, vpH);
|
_hitRectPx = new Rect2(0, 0, vpW, vpH);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (hitDef.Type?.ToLowerInvariant())
|
switch (hitDef.Type?.ToLowerInvariant())
|
||||||
{
|
{
|
||||||
case "circle":
|
case "circle":
|
||||||
_hitType = HitAreaType.Circle;
|
_hitType = HitAreaType.Circle;
|
||||||
_hitRadiusPx = hitDef.Radius * Mathf.Min(vpW, vpH);
|
_hitRadiusPx = hitDef.Radius * Mathf.Min(vpW, vpH);
|
||||||
var cx = hitDef.X * vpW;
|
var cx = hitDef.X * vpW;
|
||||||
var cy = hitDef.Y * vpH;
|
var cy = hitDef.Y * vpH;
|
||||||
_hitRectPx = new Rect2(cx - _hitRadiusPx, cy - _hitRadiusPx, _hitRadiusPx * 2f, _hitRadiusPx * 2f);
|
_hitRectPx = new Rect2(cx - _hitRadiusPx, cy - _hitRadiusPx, _hitRadiusPx * 2f, _hitRadiusPx * 2f);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "polygon":
|
case "polygon":
|
||||||
_hitType = HitAreaType.Polygon;
|
_hitType = HitAreaType.Polygon;
|
||||||
if (hitDef.Vertices != null && hitDef.Vertices.Length >= 3)
|
if (hitDef.Vertices != null && hitDef.Vertices.Length >= 3)
|
||||||
{
|
{
|
||||||
_hitPolygonPx = new Vector2[hitDef.Vertices.Length];
|
_hitPolygonPx = new Vector2[hitDef.Vertices.Length];
|
||||||
float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
|
float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
|
||||||
for (var i = 0; i < hitDef.Vertices.Length; i++)
|
for (var i = 0; i < hitDef.Vertices.Length; i++)
|
||||||
{
|
{
|
||||||
var v = new Vector2(hitDef.Vertices[i].X * vpW, hitDef.Vertices[i].Y * vpH);
|
var v = new Vector2(hitDef.Vertices[i].X * vpW, hitDef.Vertices[i].Y * vpH);
|
||||||
_hitPolygonPx[i] = v;
|
_hitPolygonPx[i] = v;
|
||||||
minX = Mathf.Min(minX, v.X);
|
minX = Mathf.Min(minX, v.X);
|
||||||
minY = Mathf.Min(minY, v.Y);
|
minY = Mathf.Min(minY, v.Y);
|
||||||
maxX = Mathf.Max(maxX, v.X);
|
maxX = Mathf.Max(maxX, v.X);
|
||||||
maxY = Mathf.Max(maxY, v.Y);
|
maxY = Mathf.Max(maxY, v.Y);
|
||||||
}
|
}
|
||||||
_hitRectPx = new Rect2(minX, minY, maxX - minX, maxY - minY);
|
_hitRectPx = new Rect2(minX, minY, maxX - minX, maxY - minY);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
_hitType = HitAreaType.Rect;
|
_hitType = HitAreaType.Rect;
|
||||||
var rx = hitDef.X * vpW;
|
var rx = hitDef.X * vpW;
|
||||||
var ry = hitDef.Y * vpH;
|
var ry = hitDef.Y * vpH;
|
||||||
var rw = hitDef.Width * vpW;
|
var rw = hitDef.Width * vpW;
|
||||||
var rh = hitDef.Height * vpH;
|
var rh = hitDef.Height * vpH;
|
||||||
_hitRectPx = new Rect2(rx, ry, rw, rh);
|
_hitRectPx = new Rect2(rx, ry, rw, rh);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetCursor(string cursorName)
|
private void SetCursor(string cursorName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(cursorName))
|
if (string.IsNullOrEmpty(cursorName))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Enum.TryParse<CursorShape>(cursorName, true, out var shape))
|
if (Enum.TryParse<CursorShape>(cursorName, true, out var shape))
|
||||||
MouseDefaultCursorShape = shape;
|
MouseDefaultCursorShape = shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMouseEntered()
|
private void OnMouseEntered()
|
||||||
{
|
{
|
||||||
GameLogger.Log("INTERACTIVE", $"OnMouseEntered: id='{ID}' enabled={IsEnabled} hitType={_hitType}");
|
GameLogger.Log("INTERACTIVE", $"OnMouseEntered: id='{ID}' enabled={IsEnabled} hitType={_hitType}");
|
||||||
|
|
||||||
if (!IsEnabled)
|
if (!IsEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IsHovered = true;
|
IsHovered = true;
|
||||||
ApplyHighlight(true);
|
ApplyHighlight(true);
|
||||||
UpdateDebugVisual(InteractiveManager.Instance?.ShowDebugBounds ?? false);
|
UpdateDebugVisual(InteractiveManager.Instance?.ShowDebugBounds ?? false);
|
||||||
OnHoverEnterCallback?.Invoke(_onHoverEnterDsl);
|
OnHoverEnterCallback?.Invoke(_onHoverEnterDsl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMouseExited()
|
private void OnMouseExited()
|
||||||
{
|
{
|
||||||
GameLogger.Log("INTERACTIVE", $"OnMouseExited: id='{ID}'");
|
GameLogger.Log("INTERACTIVE", $"OnMouseExited: id='{ID}'");
|
||||||
|
|
||||||
if (!IsEnabled)
|
if (!IsEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IsHovered = false;
|
IsHovered = false;
|
||||||
ApplyHighlight(false);
|
ApplyHighlight(false);
|
||||||
UpdateDebugVisual(InteractiveManager.Instance?.ShowDebugBounds ?? false);
|
UpdateDebugVisual(InteractiveManager.Instance?.ShowDebugBounds ?? false);
|
||||||
OnHoverLeaveCallback?.Invoke(_onHoverLeaveDsl);
|
OnHoverLeaveCallback?.Invoke(_onHoverLeaveDsl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyHighlight(bool active)
|
private void ApplyHighlight(bool active)
|
||||||
{
|
{
|
||||||
if (_highlightMaterial == null)
|
if (_highlightMaterial == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_highlightTween?.Kill();
|
_highlightTween?.Kill();
|
||||||
|
|
||||||
var targetScale = active ? _highlightScale : 1f;
|
var targetScale = active ? _highlightScale : 1f;
|
||||||
|
|
||||||
_highlightTween = CreateTween();
|
_highlightTween = CreateTween();
|
||||||
_highlightTween.SetParallel(true);
|
_highlightTween.SetParallel(true);
|
||||||
|
|
||||||
_highlightTween.TweenProperty(this, "scale", Vector2.One * targetScale, _highlightDuration);
|
_highlightTween.TweenProperty(this, "scale", Vector2.One * targetScale, _highlightDuration);
|
||||||
|
|
||||||
if (_highlightColor.A > 0f)
|
if (_highlightColor.A > 0f)
|
||||||
{
|
{
|
||||||
_highlightTween.TweenMethod(
|
_highlightTween.TweenMethod(
|
||||||
Callable.From<float>(f =>
|
Callable.From<float>(f =>
|
||||||
{
|
{
|
||||||
var c = new Color(_highlightColor.R, _highlightColor.G, _highlightColor.B, _highlightColor.A * f);
|
var c = new Color(_highlightColor.R, _highlightColor.G, _highlightColor.B, _highlightColor.A * f);
|
||||||
_highlightMaterial.SetShaderParameter("modulate_color", c);
|
_highlightMaterial.SetShaderParameter("modulate_color", c);
|
||||||
}),
|
}),
|
||||||
active ? 0f : 1f,
|
active ? 0f : 1f,
|
||||||
active ? 1f : 0f,
|
active ? 1f : 0f,
|
||||||
_highlightDuration
|
_highlightDuration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Handles left-click press: fires the click callback and consumes the event.</summary>
|
/// <summary>Handles left-click press: fires the click callback and consumes the event.</summary>
|
||||||
public override void _GuiInput(InputEvent @event)
|
public override void _GuiInput(InputEvent @event)
|
||||||
{
|
{
|
||||||
if (!IsEnabled)
|
if (!IsEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (@event is InputEventMouseButton mb && mb.ButtonIndex == MouseButton.Left && mb.Pressed)
|
if (@event is InputEventMouseButton mb && mb.ButtonIndex == MouseButton.Left && mb.Pressed)
|
||||||
{
|
{
|
||||||
GameLogger.Log("INTERACTIVE", $"clicked: '{DefinitionId}'");
|
GameLogger.Log("INTERACTIVE", $"clicked: '{DefinitionId}'");
|
||||||
OnClickCallback?.Invoke(DefinitionId);
|
OnClickCallback?.Invoke(DefinitionId);
|
||||||
AcceptEvent();
|
AcceptEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"Items": [{
|
"Items": [{
|
||||||
"id": "kitchen_lamp",
|
"id": "kitchen_lamp",
|
||||||
"texture": "lamp",
|
"texture": "lamp",
|
||||||
"layer": "foreground",
|
"layer": "foreground",
|
||||||
"zIndex": 10,
|
"zIndex": 10,
|
||||||
"hitArea": {
|
"hitArea": {
|
||||||
"type": "rect",
|
"type": "rect",
|
||||||
"x": 0.68,
|
"x": 0.68,
|
||||||
"y": 0.40,
|
"y": 0.40,
|
||||||
"width": 0.08,
|
"width": 0.08,
|
||||||
"height": 0.08
|
"height": 0.08
|
||||||
},
|
},
|
||||||
"highlight": {
|
"highlight": {
|
||||||
"color": "#FFFF0033",
|
"color": "#FFFF0033",
|
||||||
"scale": 1.1,
|
"scale": 1.1,
|
||||||
"duration": 0.12
|
"duration": 0.12
|
||||||
},
|
},
|
||||||
"cursor": "PointingHand",
|
"cursor": "PointingHand",
|
||||||
"actions": {
|
"actions": {
|
||||||
"onClick": {
|
"onClick": {
|
||||||
"scenario": "scenarios/kitchen_clicked.scenario"
|
"scenario": "scenarios/kitchen_clicked.scenario"
|
||||||
},
|
},
|
||||||
"onHoverEnter": "set cursor_hint Кликни_лампу",
|
"onHoverEnter": "set cursor_hint Кликни_лампу",
|
||||||
"onHoverLeave": "set cursor_hint \"\""
|
"onHoverLeave": "set cursor_hint \"\""
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
test_interactive/click = You clicked the lamp! The interactive object works.
|
test_interactive/click = You clicked the lamp! The interactive object works.
|
||||||
@@ -1 +1 @@
|
|||||||
test_interactive/click = Вы кликнули по лампе! Интерактивный объект работает.
|
test_interactive/click = Вы кликнули по лампе! Интерактивный объект работает.
|
||||||
Reference in new issue
Block a user