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