Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.6 031262d374 Wire Tutorial Panel reference and battle layout fields in scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 18:01:51 -07:00
adminandClaude Opus 4.6 92a81c0ce6 Move dialogue panel up in battle mode via Bottom offset
Sets Tutorial Panel's Bottom offset to 650 in Shardok battles (vs 400
default) so the panel doesn't cover the hex grid. Both values are
Inspector fields — no runtime save/restore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 18:00:22 -07:00
adminandClaude Opus 4.6 78fdefea9e Add VerticalLayoutGroup with flexible spacer to anchor dialogue content at bottom
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 16:38:49 -07:00
adminandClaude Opus 4.6 920d0f72bb Remove old modal tutorial system and fix dialogue panel positioning
Delete TutorialModalPanel and TutorialCanvasBuilder (old runtime-built
modal system). Remove SetPanelPosition from DialoguePanelController —
it was overriding the editor layout by setting anchoredPosition.y = 0,
causing the panel to shift and buttons to jump between steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 16:35:37 -07:00
adminandClaude Opus 4.6 43b6526f58 Use fixed bottom offset so short first steps leave room to grow
The battle tutorial's first step is short, so -height/2 placed the
bottom edge high on screen. Subsequent longer steps then grew off
the top. Use a fixed -200px bottom offset from center instead, so
the bottom position is consistent regardless of first step height.

Also restore Top anchor to pivot Y=1 (grows downward from top edge),
since growing upward from the top of the screen doesn't make sense.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 15:41:48 -07:00
adminandClaude Opus 4.6 3bbaf2b839 Only reposition when anchor/target changes, not every step
Tutorial sequences change anchors between steps (e.g., Center for
welcome, Right for province info, Top for commands). Skip
repositioning only when the same anchor and target are reused
between consecutive steps, so the panel properly moves for battle
tutorials while keeping the bottom edge fixed within same-position
sequences.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 15:41:48 -07:00
adminandClaude Opus 4.6 23a9c11d9d Simplify: only position panel on first step, let pivot handle the rest
Instead of recomputing position every step (which fights the pivot),
just position once on step 0. ContentSizeFitter + pivot Y=0 (set in
the inspector) grows the panel upward, keeping the bottom edge fixed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 15:41:48 -07:00
adminandClaude Opus 4.6 bca005b264 Save and restore bottom edge position across tutorial steps
The pivot change alone wasn't enough — PositionPanel recomputes
anchoredPosition.y from height on every step, so the bottom still
shifted. Now we save anchoredPosition.y on step 0 and restore it
on subsequent steps, keeping the bottom edge (and buttons) fixed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 15:41:47 -07:00
adminandClaude Opus 4.6 774f3e80cb Fix tutorial panel buttons jumping when stepping through tutorial
Bottom-anchor the panel pivot (Y=0) so the bottom edge stays fixed
when ContentSizeFitter resizes the panel between tutorial steps.
Buttons no longer shift position on Continue/Skip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 15:41:47 -07:00
9 changed files with 238 additions and 994 deletions
@@ -341,7 +341,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 52abae862492497cb7a3f8c72f837b37, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Eagle0.Tutorial.TutorialUIManager
ModalPanel: {fileID: 0}
OverlayController: {fileID: 0}
HintIndicatorPrefab: {fileID: 0}
TutorialCanvas: {fileID: 0}
@@ -38288,14 +38287,14 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 896085699}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -0.000012874603}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1295407723}
- {fileID: 267176577}
m_Father: {fileID: 1117263354}
m_Father: {fileID: 1128356212}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
@@ -52817,10 +52816,8 @@ GameObject:
m_Component:
- component: {fileID: 1117263354}
- component: {fileID: 1117263357}
- component: {fileID: 1117263356}
- component: {fileID: 1117263358}
- component: {fileID: 1117263359}
- component: {fileID: 1117263360}
m_Layer: 0
m_Name: Tutorial Panel
m_TagString: Untagged
@@ -52840,46 +52837,15 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 896085700}
- {fileID: 2070655606}
- {fileID: 1726547713}
- {fileID: 1762433297}
- {fileID: 1128356212}
m_Father: {fileID: 300036923}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 800, y: 328.52}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: 0, y: 200}
m_SizeDelta: {x: 800, y: -400}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1117263356
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1117263353}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 0.7058824, g: 0.7058824, b: 0.7058824, a: 0.9647059}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1117263357
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -52909,6 +52875,9 @@ MonoBehaviour:
type: 2}
continueButton: {fileID: 1936854701}
skipButton: {fileID: 606632695}
tutorialPanel: {fileID: 1117263354}
defaultBottom: 400
battleBottom: 650
--- !u!114 &1117263359
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -52924,10 +52893,10 @@ MonoBehaviour:
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 20
m_Bottom: 10
m_ChildAlignment: 4
m_Spacing: 10
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 7
m_Spacing: 0
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -52935,20 +52904,6 @@ MonoBehaviour:
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &1117263360
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1117263353}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
m_HorizontalFit: 0
m_VerticalFit: 2
--- !u!1 &1118217569
GameObject:
m_ObjectHideFlags: 0
@@ -53166,6 +53121,147 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1125275705}
m_CullTransparentMesh: 0
--- !u!1 &1128356211
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1128356212}
- component: {fileID: 1128356216}
- component: {fileID: 1128356215}
- component: {fileID: 1128356214}
- component: {fileID: 1128356213}
- component: {fileID: 1128356217}
m_Layer: 0
m_Name: Content
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1128356212
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1128356211}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 896085700}
- {fileID: 2070655606}
- {fileID: 1726547713}
m_Father: {fileID: 1117263354}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 400, y: -515.74}
m_SizeDelta: {x: 800, y: 328.52}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1128356213
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1128356211}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 20
m_Bottom: 10
m_ChildAlignment: 7
m_Spacing: 10
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
m_ChildControlHeight: 1
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &1128356214
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1128356211}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
m_HorizontalFit: 0
m_VerticalFit: 2
--- !u!114 &1128356215
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1128356211}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 0.7058824, g: 0.7058824, b: 0.7058824, a: 0.9647059}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1128356216
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1128356211}
m_CullTransparentMesh: 1
--- !u!114 &1128356217
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1128356211}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &1131569341
GameObject:
m_ObjectHideFlags: 0
@@ -82976,14 +83072,14 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1726547712}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 606632693}
- {fileID: 1936854700}
m_Father: {fileID: 1117263354}
m_Father: {fileID: 1128356212}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
@@ -85505,6 +85601,62 @@ MonoBehaviour:
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!1 &1762433296
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1762433297}
- component: {fileID: 1762433298}
m_Layer: 0
m_Name: Spacer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1762433297
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1762433296}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0.000012874603}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1117263354}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 400, y: -175.73999}
m_SizeDelta: {x: 800, y: 351.48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1762433298
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1762433296}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &1765511185
GameObject:
m_ObjectHideFlags: 0
@@ -94143,13 +94295,13 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2070655605}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -0.000012874603}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 647524251}
m_Father: {fileID: 1117263354}
m_Father: {fileID: 1128356212}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
@@ -153,10 +153,7 @@ namespace Eagle0.Tutorial {
_activeScript = script;
_currentStepIndex = 0;
// In combat, always anchor to top so the panel doesn't cover the hex grid.
// On the strategic map, always use the default position.
var position = _shardokController?.Model != null ? "top" : null;
panelController.SetPanelPosition(position);
panelController.SetBattleMode(_shardokController?.Model != null);
ShowCurrentStep();
}
@@ -287,7 +284,7 @@ namespace Eagle0.Tutorial {
_activeScript = null;
_currentStepIndex = 0;
panelController.SetPanelPosition(null);
panelController.SetBattleMode(false);
panelController.Hide();
panelController.continueButton.onClick.RemoveAllListeners();
panelController.skipButton.onClick.RemoveAllListeners();
@@ -24,36 +24,24 @@ namespace Eagle0.Tutorial {
public Button continueButton;
public Button skipButton;
private Vector2 _defaultAnchorMin;
private Vector2 _defaultAnchorMax;
private Vector2 _defaultPivot;
private bool _defaultsSaved;
[Header("Battle Layout")]
[Tooltip("The Tutorial Panel RectTransform (vertically-stretched container)")]
public RectTransform tutorialPanel;
[Tooltip("Bottom offset of the Tutorial Panel in default (Eagle) mode")]
public float defaultBottom = 400f;
[Tooltip("Bottom offset of the Tutorial Panel in battle mode")]
public float battleBottom = 650f;
/// <summary>
/// Repositions the panel vertically. Call before Show().
/// "top" anchors to the top of the screen; null/empty restores default.
/// Raises the panel's bottom edge for battle view, or restores default.
/// </summary>
public void SetPanelPosition(string position) {
var rt = GetComponent<RectTransform>();
if (!_defaultsSaved) {
_defaultAnchorMin = rt.anchorMin;
_defaultAnchorMax = rt.anchorMax;
_defaultPivot = rt.pivot;
_defaultsSaved = true;
}
if (position == "top") {
rt.anchorMin = new Vector2(rt.anchorMin.x, 1f);
rt.anchorMax = new Vector2(rt.anchorMax.x, 1f);
rt.pivot = new Vector2(rt.pivot.x, 1f);
rt.anchoredPosition = new Vector2(rt.anchoredPosition.x, -10f);
} else {
rt.anchorMin = _defaultAnchorMin;
rt.anchorMax = _defaultAnchorMax;
rt.pivot = _defaultPivot;
rt.anchoredPosition = new Vector2(rt.anchoredPosition.x, 0f);
}
public void SetBattleMode(bool battle) {
if (tutorialPanel == null) return;
var offset = tutorialPanel.offsetMin;
offset.y = battle ? battleBottom : defaultBottom;
tutorialPanel.offsetMin = offset;
}
/// <summary>
@@ -1,472 +0,0 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Eagle0.Tutorial {
/// <summary>
/// Builds the tutorial Canvas UI programmatically at runtime.
/// Creates a complete TutorialModalPanel with all required UI elements.
/// </summary>
public static class TutorialCanvasBuilder {
// Colors matching the game's fantasy RPG style
private static readonly Color PanelBackgroundColor = new Color(0.15f, 0.12f, 0.2f, 0.95f);
private static readonly Color BorderColor = new Color(0.6f, 0.5f, 0.3f, 1f);
private static readonly Color TitleColor = new Color(1f, 0.9f, 0.7f, 1f);
private static readonly Color TextColor = new Color(0.9f, 0.85f, 0.8f, 1f);
private static readonly Color ButtonColor = new Color(0.4f, 0.3f, 0.2f, 1f);
private static readonly Color ButtonHoverColor = new Color(0.5f, 0.4f, 0.25f, 1f);
private static readonly Color ButtonTextColor = new Color(1f, 0.95f, 0.85f, 1f);
private static readonly Color ModalBlockerColor = new Color(0f, 0f, 0f, 0.75f);
private static readonly Color ProgressBarColor = new Color(0.6f, 0.5f, 0.3f, 1f);
private static readonly Color ProgressBackgroundColor = new Color(0.2f, 0.15f, 0.1f, 1f);
/// <summary>
/// Creates a complete tutorial Canvas with modal panel.
/// Returns the Canvas GameObject and sets up the TutorialModalPanel component.
/// </summary>
/// <param name="font">TextMeshPro font to use (assign Stoke-Regular-SDF in
/// inspector)</param>
public static Canvas BuildTutorialCanvas(TMP_FontAsset font) {
// Create Canvas
GameObject canvasObj = new GameObject("TutorialCanvas");
Canvas canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 1000;
CanvasScaler scaler = canvasObj.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.matchWidthOrHeight = 0.5f;
canvasObj.AddComponent<GraphicRaycaster>();
// Create Modal Blocker
GameObject blockerObj = CreateModalBlocker(canvasObj.transform);
// Create Panel Container
GameObject panelContainer = CreatePanelContainer(canvasObj.transform, font);
// Add TutorialModalPanel component and wire it up
TutorialModalPanel modalPanel = canvasObj.AddComponent<TutorialModalPanel>();
WireUpModalPanel(modalPanel, blockerObj, panelContainer, font);
// Set up button listeners (Awake runs before fields are wired, so do it here)
SetupButtonListeners(modalPanel);
// Start with everything hidden
blockerObj.SetActive(false);
panelContainer.SetActive(false);
return canvas;
}
private static GameObject CreateModalBlocker(Transform parent) {
GameObject blocker = new GameObject("ModalBlocker");
blocker.transform.SetParent(parent, false);
RectTransform rect = blocker.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
Image image = blocker.AddComponent<Image>();
image.color = ModalBlockerColor;
image.raycastTarget = true;
return blocker;
}
private static GameObject CreatePanelContainer(Transform parent, TMP_FontAsset font) {
// Panel Container (centered)
GameObject panel = new GameObject("PanelContainer");
panel.transform.SetParent(parent, false);
RectTransform panelRect = panel.AddComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.sizeDelta = new Vector2(540, 500);
panelRect.anchoredPosition = Vector2.zero;
// Panel Background
Image panelBg = panel.AddComponent<Image>();
panelBg.color = PanelBackgroundColor;
panelBg.raycastTarget = true;
// Add outline/border effect
Outline outline = panel.AddComponent<Outline>();
outline.effectColor = BorderColor;
outline.effectDistance = new Vector2(3, 3);
// Add vertical layout group
VerticalLayoutGroup layout = panel.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(20, 20, 15, 15);
layout.spacing = 10;
layout.childAlignment = TextAnchor.UpperCenter;
layout.childControlHeight = true; // Let layout control child heights
layout.childControlWidth = true;
layout.childForceExpandHeight = false;
layout.childForceExpandWidth = true;
// Add ContentSizeFitter to auto-size panel to content
ContentSizeFitter fitter = panel.AddComponent<ContentSizeFitter>();
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
// Title
CreateTitle(panel.transform, font);
// Icon (placeholder - starts hidden)
CreateIcon(panel.transform);
// Description
CreateDescription(panel.transform, font);
// Progress section (VerticalLayoutGroup spacing handles gaps)
CreateProgressSection(panel.transform, font);
// Button row
CreateButtonRow(panel.transform, font);
return panel;
}
private static void CreateTitle(Transform parent, TMP_FontAsset font) {
GameObject titleObj = new GameObject("TitleText");
titleObj.transform.SetParent(parent, false);
RectTransform rect = titleObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(460, 36);
TextMeshProUGUI text = titleObj.AddComponent<TextMeshProUGUI>();
text.text = "Tutorial";
text.fontSize = 28;
text.fontStyle = FontStyles.Bold;
text.color = TitleColor;
text.alignment = TextAlignmentOptions.Center;
if (font != null) text.font = font;
LayoutElement layoutElement = titleObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 36;
}
private static void CreateIcon(Transform parent) {
GameObject iconObj = new GameObject("IconImage");
iconObj.transform.SetParent(parent, false);
RectTransform rect = iconObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(100, 100);
Image image = iconObj.AddComponent<Image>();
image.color = Color.white;
image.preserveAspect = true;
LayoutElement layoutElement = iconObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 100;
layoutElement.preferredWidth = 100;
// Start hidden
iconObj.SetActive(false);
}
private static void CreateDescription(Transform parent, TMP_FontAsset font) {
GameObject descObj = new GameObject("DescriptionText");
descObj.transform.SetParent(parent, false);
RectTransform rect = descObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(460, 180);
TextMeshProUGUI text = descObj.AddComponent<TextMeshProUGUI>();
text.text = "";
text.fontSize = 18;
text.color = TextColor;
text.alignment = TextAlignmentOptions.TopLeft;
text.enableWordWrapping = true;
text.overflowMode = TextOverflowModes.Overflow; // Don't truncate
if (font != null) text.font = font;
// Auto-size height to fit text content
ContentSizeFitter fitter = descObj.AddComponent<ContentSizeFitter>();
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
LayoutElement layoutElement = descObj.AddComponent<LayoutElement>();
layoutElement.minHeight = 40; // Minimum height for short text
layoutElement.preferredWidth = 460;
}
private static void CreateSpacer(Transform parent, float height) {
GameObject spacer = new GameObject("Spacer");
spacer.transform.SetParent(parent, false);
spacer.AddComponent<RectTransform>();
LayoutElement layoutElement = spacer.AddComponent<LayoutElement>();
layoutElement.preferredHeight = height;
}
private static void CreateFlexibleSpacer(Transform parent) {
GameObject spacer = new GameObject("FlexibleSpacer");
spacer.transform.SetParent(parent, false);
spacer.AddComponent<RectTransform>();
LayoutElement layoutElement = spacer.AddComponent<LayoutElement>();
layoutElement.flexibleHeight = 1;
}
private static void CreateProgressSection(Transform parent, TMP_FontAsset font) {
GameObject progressSection = new GameObject("ProgressSection");
progressSection.transform.SetParent(parent, false);
RectTransform rect = progressSection.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(720, 50);
VerticalLayoutGroup layout = progressSection.AddComponent<VerticalLayoutGroup>();
layout.spacing = 8;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlHeight = false;
layout.childControlWidth = true;
LayoutElement layoutElement = progressSection.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 50;
// Progress Text
GameObject progressTextObj = new GameObject("ProgressText");
progressTextObj.transform.SetParent(progressSection.transform, false);
RectTransform textRect = progressTextObj.AddComponent<RectTransform>();
textRect.sizeDelta = new Vector2(720, 24);
TextMeshProUGUI progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
progressText.text = "Step 1 of 1";
progressText.fontSize = 14;
progressText.color = TextColor;
progressText.alignment = TextAlignmentOptions.Center;
if (font != null) progressText.font = font;
LayoutElement textLayout = progressTextObj.AddComponent<LayoutElement>();
textLayout.preferredHeight = 24;
// Progress Slider
CreateProgressSlider(progressSection.transform);
}
private static void CreateProgressSlider(Transform parent) {
GameObject sliderObj = new GameObject("ProgressSlider");
sliderObj.transform.SetParent(parent, false);
RectTransform sliderRect = sliderObj.AddComponent<RectTransform>();
sliderRect.sizeDelta = new Vector2(400, 12);
Slider slider = sliderObj.AddComponent<Slider>();
slider.minValue = 0;
slider.maxValue = 1;
slider.value = 0;
slider.interactable = false;
LayoutElement sliderLayout = sliderObj.AddComponent<LayoutElement>();
sliderLayout.preferredHeight = 12;
sliderLayout.preferredWidth = 400;
// Background
GameObject bgObj = new GameObject("Background");
bgObj.transform.SetParent(sliderObj.transform, false);
RectTransform bgRect = bgObj.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.offsetMin = Vector2.zero;
bgRect.offsetMax = Vector2.zero;
Image bgImage = bgObj.AddComponent<Image>();
bgImage.color = ProgressBackgroundColor;
// Fill Area
GameObject fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(sliderObj.transform, false);
RectTransform fillAreaRect = fillArea.AddComponent<RectTransform>();
fillAreaRect.anchorMin = Vector2.zero;
fillAreaRect.anchorMax = Vector2.one;
fillAreaRect.offsetMin = Vector2.zero;
fillAreaRect.offsetMax = Vector2.zero;
// Fill
GameObject fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
RectTransform fillRect = fill.AddComponent<RectTransform>();
fillRect.anchorMin = Vector2.zero;
fillRect.anchorMax = new Vector2(0, 1);
fillRect.offsetMin = Vector2.zero;
fillRect.offsetMax = Vector2.zero;
Image fillImage = fill.AddComponent<Image>();
fillImage.color = ProgressBarColor;
// Wire up slider
slider.fillRect = fillRect;
}
private static void SetupButtonListeners(TutorialModalPanel panel) {
// Button listeners need to be set up after wiring since Awake runs before fields exist
if (panel.ContinueButton != null) {
panel.ContinueButton.onClick.RemoveAllListeners();
panel.ContinueButton.onClick.AddListener(panel.OnContinueClicked);
}
if (panel.SkipAllButton != null) {
panel.SkipAllButton.onClick.RemoveAllListeners();
panel.SkipAllButton.onClick.AddListener(panel.OnSkipAllClicked);
}
}
private static void CreateButtonRow(Transform parent, TMP_FontAsset font) {
GameObject buttonRow = new GameObject("ButtonRow");
buttonRow.transform.SetParent(parent, false);
RectTransform rect = buttonRow.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(720, 60);
HorizontalLayoutGroup layout = buttonRow.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 20;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlHeight = false;
layout.childControlWidth = false;
layout.childForceExpandHeight = false;
layout.childForceExpandWidth = false;
LayoutElement layoutElement = buttonRow.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 44;
// Skip Tutorial Button (left) - only shown during onboarding
CreateButton(buttonRow.transform, "SkipAllButton", "Skip Tutorial", 140, 36, font);
// Spacer
GameObject spacer = new GameObject("ButtonSpacer");
spacer.transform.SetParent(buttonRow.transform, false);
spacer.AddComponent<RectTransform>();
LayoutElement spacerLayout = spacer.AddComponent<LayoutElement>();
spacerLayout.flexibleWidth = 1;
// Continue Button (right)
CreateButton(buttonRow.transform, "ContinueButton", "Continue", 120, 36, font);
}
private static GameObject CreateButton(
Transform parent,
string name,
string text,
float width,
float height,
TMP_FontAsset font) {
GameObject buttonObj = new GameObject(name);
buttonObj.transform.SetParent(parent, false);
RectTransform rect = buttonObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(width, height);
Image image = buttonObj.AddComponent<Image>();
image.color = ButtonColor;
Button button = buttonObj.AddComponent<Button>();
ColorBlock colors = button.colors;
colors.normalColor = ButtonColor;
colors.highlightedColor = ButtonHoverColor;
colors.pressedColor = ButtonHoverColor;
colors.selectedColor = ButtonHoverColor;
button.colors = colors;
// Add outline
Outline outline = buttonObj.AddComponent<Outline>();
outline.effectColor = BorderColor;
outline.effectDistance = new Vector2(2, 2);
LayoutElement layoutElement = buttonObj.AddComponent<LayoutElement>();
layoutElement.preferredWidth = width;
layoutElement.preferredHeight = height;
// Button text
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(buttonObj.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
TextMeshProUGUI buttonText = textObj.AddComponent<TextMeshProUGUI>();
buttonText.text = text;
buttonText.fontSize = 16;
buttonText.color = ButtonTextColor;
buttonText.alignment = TextAlignmentOptions.Center;
if (font != null) buttonText.font = font;
return buttonObj;
}
private static void WireUpModalPanel(
TutorialModalPanel panel,
GameObject blocker,
GameObject panelContainer,
TMP_FontAsset font) {
panel.ModalBlocker = blocker;
panel.PanelContainer = panelContainer;
// Find and wire up components
Transform containerTransform = panelContainer.transform;
// Title
Transform titleTransform = containerTransform.Find("TitleText");
if (titleTransform != null) {
panel.TitleText = titleTransform.GetComponent<TextMeshProUGUI>();
}
// Icon
Transform iconTransform = containerTransform.Find("IconImage");
if (iconTransform != null) { panel.IconImage = iconTransform.GetComponent<Image>(); }
// Description
Transform descTransform = containerTransform.Find("DescriptionText");
if (descTransform != null) {
panel.DescriptionText = descTransform.GetComponent<TextMeshProUGUI>();
}
// Progress Section
Transform progressSection = containerTransform.Find("ProgressSection");
if (progressSection != null) {
panel.ProgressSection = progressSection.gameObject;
Transform progressTextTransform = progressSection.Find("ProgressText");
if (progressTextTransform != null) {
panel.ProgressText = progressTextTransform.GetComponent<TextMeshProUGUI>();
}
Transform sliderTransform = progressSection.Find("ProgressSlider");
if (sliderTransform != null) {
panel.ProgressSlider = sliderTransform.GetComponent<Slider>();
}
}
// Button Row
Transform buttonRow = containerTransform.Find("ButtonRow");
if (buttonRow != null) {
Transform skipAllTransform = buttonRow.Find("SkipAllButton");
if (skipAllTransform != null) {
panel.SkipAllButton = skipAllTransform.GetComponent<Button>();
}
Transform continueTransform = buttonRow.Find("ContinueButton");
if (continueTransform != null) {
panel.ContinueButton = continueTransform.GetComponent<Button>();
Transform continueTextTransform = continueTransform.Find("Text");
if (continueTextTransform != null) {
panel.ContinueButtonText =
continueTextTransform.GetComponent<TextMeshProUGUI>();
}
}
}
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 98467fdbaf584fbb928a373340b542d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,360 +0,0 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Eagle0.Tutorial {
/// <summary>
/// Modal dialog panel for displaying tutorial content.
/// Shows title, description, icon, and action buttons.
/// </summary>
public class TutorialModalPanel : MonoBehaviour {
[Header("Content")]
[Tooltip("Title text")]
public TextMeshProUGUI TitleText;
[Tooltip("Description text")]
public TextMeshProUGUI DescriptionText;
[Tooltip("Icon image")]
public Image IconImage;
[Header("Buttons")]
[Tooltip("Continue/Got it button")]
public Button ContinueButton;
[Tooltip("Text on continue button")]
public TextMeshProUGUI ContinueButtonText;
[Tooltip("Skip tutorial button (onboarding only)")]
public Button SkipAllButton;
[Header("Progress")]
[Tooltip("Progress section container (hidden for single-step tutorials)")]
public GameObject ProgressSection;
[Tooltip("Progress text (e.g., 'Step 3 of 7')")]
public TextMeshProUGUI ProgressText;
[Tooltip("Progress slider/bar")]
public Slider ProgressSlider;
[Header("Background")]
[Tooltip("Modal blocker/dimmer")]
public GameObject ModalBlocker;
[Tooltip("Panel container")]
public GameObject PanelContainer;
[Header("Animation")]
[Tooltip("Animator for panel animations")]
public Animator PanelAnimator;
// Callbacks
private Action _onContinue;
private void Awake() {
// Set up button listeners
if (ContinueButton != null) { ContinueButton.onClick.AddListener(OnContinueClicked); }
if (SkipAllButton != null) { SkipAllButton.onClick.AddListener(OnSkipAllClicked); }
// Start hidden
Hide();
}
/// <summary>
/// Shows the modal with tutorial step content.
/// </summary>
public void Show(
TutorialStep step,
int currentIndex,
int totalSteps,
Action onContinue,
bool isOnboarding) {
_onContinue = onContinue;
// Set content
if (TitleText != null) { TitleText.text = step.Title ?? ""; }
if (DescriptionText != null) {
DescriptionText.text = step.Description ?? "";
// Force layout rebuild so ContentSizeFitter recalculates
LayoutRebuilder.ForceRebuildLayoutImmediate(
DescriptionText.GetComponent<RectTransform>());
}
if (IconImage != null) {
if (step.Icon != null) {
IconImage.sprite = step.Icon;
IconImage.gameObject.SetActive(true);
} else {
IconImage.gameObject.SetActive(false);
}
}
// Set button text
if (ContinueButtonText != null) {
ContinueButtonText.text = currentIndex >= totalSteps - 1 ? "Got it!" : "Continue";
}
// Show Skip Tutorial button only during onboarding (and when skipping is allowed)
if (SkipAllButton != null) {
SkipAllButton.gameObject.SetActive(isOnboarding && step.AllowSkip);
}
// Set progress - hide entire section for single-step tutorials
bool showProgress = totalSteps > 1;
if (ProgressSection != null) { ProgressSection.SetActive(showProgress); }
if (showProgress) {
if (ProgressText != null) {
ProgressText.text = $"Step {currentIndex + 1} of {totalSteps}";
}
if (ProgressSlider != null) {
ProgressSlider.value = (float)(currentIndex + 1) / totalSteps;
}
}
// Force layout rebuild so panel resizes to fit content
if (PanelContainer != null) {
LayoutRebuilder.ForceRebuildLayoutImmediate(
PanelContainer.GetComponent<RectTransform>());
}
// Position panel - either adjacent to target or based on anchor setting
if (!string.IsNullOrEmpty(step.AdjacentTargetPath)) {
PositionAdjacentTo(step.AdjacentTargetPath, step.PanelAnchor);
} else {
PositionPanel(step.PanelAnchor);
}
// Show panel - ensure all parent containers are active first
ActivateParents();
// Only show modal blocker if step blocks interaction
if (ModalBlocker != null) { ModalBlocker.SetActive(step.BlocksInteraction); }
if (PanelContainer != null) { PanelContainer.SetActive(true); }
gameObject.SetActive(true);
// Trigger animation
if (PanelAnimator != null) { PanelAnimator.SetTrigger("Show"); }
}
/// <summary>
/// Positions the panel based on the anchor setting.
/// Size is determined by ContentSizeFitter, so we only set position.
/// </summary>
private void PositionPanel(TutorialPanelAnchor anchor) {
if (PanelContainer == null) return;
RectTransform rect = PanelContainer.GetComponent<RectTransform>();
if (rect == null) return;
switch (anchor) {
case TutorialPanelAnchor.Center:
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = Vector2.zero;
break;
case TutorialPanelAnchor.Left:
rect.anchorMin = new Vector2(0f, 0.5f);
rect.anchorMax = new Vector2(0f, 0.5f);
rect.pivot = new Vector2(0f, 0.5f);
rect.anchoredPosition = new Vector2(20f, 0f); // 20px padding from edge
break;
case TutorialPanelAnchor.Right:
rect.anchorMin = new Vector2(1f, 0.5f);
rect.anchorMax = new Vector2(1f, 0.5f);
rect.pivot = new Vector2(1f, 0.5f);
rect.anchoredPosition = new Vector2(-20f, 0f); // 20px padding from edge
break;
case TutorialPanelAnchor.Top:
rect.anchorMin = new Vector2(0.5f, 1f);
rect.anchorMax = new Vector2(0.5f, 1f);
rect.pivot = new Vector2(0.5f, 1f);
rect.anchoredPosition = new Vector2(0f, -20f); // 20px padding from edge
break;
case TutorialPanelAnchor.Bottom:
rect.anchorMin = new Vector2(0.5f, 0f);
rect.anchorMax = new Vector2(0.5f, 0f);
rect.pivot = new Vector2(0.5f, 0f);
rect.anchoredPosition = new Vector2(0f, 20f); // 20px padding from edge
break;
}
}
/// <summary>
/// Positions the panel adjacent to a target GameObject.
/// Size is determined by ContentSizeFitter, so we only set position.
/// </summary>
private void PositionAdjacentTo(string targetId, TutorialPanelAnchor side) {
if (PanelContainer == null) return;
// Use registry to find target
Transform target = GetTarget(targetId);
if (target == null) {
Debug.LogWarning($"TutorialModalPanel: Could not find target '{targetId}'");
PositionPanel(side);
return;
}
RectTransform targetRect = target.GetComponent<RectTransform>();
if (targetRect == null) {
PositionPanel(side);
return;
}
RectTransform panelRect = PanelContainer.GetComponent<RectTransform>();
if (panelRect == null) return;
// Get canvas for coordinate conversion
Canvas canvas = GetComponentInParent<Canvas>();
RectTransform canvasRect = canvas?.GetComponent<RectTransform>();
if (canvasRect == null) {
PositionPanel(side);
return;
}
// Get target bounds in screen space
Vector3[] targetCorners = new Vector3[4];
targetRect.GetWorldCorners(targetCorners);
// Convert target corners to canvas space
for (int i = 0; i < 4; i++) {
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRect,
RectTransformUtility.WorldToScreenPoint(
canvas.worldCamera,
targetCorners[i]),
canvas.worldCamera,
out Vector2 localPoint);
targetCorners[i] = localPoint;
}
// Calculate target bounds
float targetLeft = Mathf.Min(targetCorners[0].x, targetCorners[1].x);
float targetRight = Mathf.Max(targetCorners[2].x, targetCorners[3].x);
float targetBottom = Mathf.Min(targetCorners[0].y, targetCorners[3].y);
float targetTop = Mathf.Max(targetCorners[1].y, targetCorners[2].y);
float targetCenterX = (targetLeft + targetRight) / 2f;
float targetWidth = targetRight - targetLeft;
float padding = 10f;
Vector2 position;
// Use center anchor for the panel
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
// Position based on which side
switch (side) {
case TutorialPanelAnchor.Top:
// Position panel just above target, with bottom edge touching target top
// Use bottom-center pivot so panel extends upward from the target
panelRect.pivot = new Vector2(0.5f, 0f);
position = new Vector2(targetCenterX, targetTop + padding);
// Ensure minimum width so text isn't too tall
if (panelRect.sizeDelta.x < 720f) {
panelRect.sizeDelta = new Vector2(720f, panelRect.sizeDelta.y);
}
break;
case TutorialPanelAnchor.Bottom:
// Position panel below target with top edge aligned
panelRect.pivot = new Vector2(0.5f, 1f);
position = new Vector2(targetCenterX, targetBottom - padding);
break;
case TutorialPanelAnchor.Right:
// Position panel to the right of target
panelRect.pivot = new Vector2(0f, 0.5f);
position = new Vector2(targetRight + padding, (targetTop + targetBottom) / 2f);
break;
case TutorialPanelAnchor.Left:
// Position panel to the left of target
panelRect.pivot = new Vector2(1f, 0.5f);
position = new Vector2(targetLeft - padding, (targetTop + targetBottom) / 2f);
break;
default: PositionPanel(side); return;
}
// Clamp X to stay within canvas bounds
float canvasHalfWidth = canvasRect.rect.width / 2f;
float screenPadding = 20f;
position.x = Mathf.Clamp(
position.x,
-canvasHalfWidth + screenPadding,
canvasHalfWidth - screenPadding);
panelRect.anchoredPosition = position;
}
/// <summary>
/// Activates all parent GameObjects to ensure this object can be active in hierarchy.
/// </summary>
private void ActivateParents() {
Transform parent = transform.parent;
while (parent != null) {
if (!parent.gameObject.activeSelf) { parent.gameObject.SetActive(true); }
parent = parent.parent;
}
}
/// <summary>
/// Hides the modal panel.
/// </summary>
public void Hide() {
if (PanelAnimator != null) { PanelAnimator.SetTrigger("Hide"); }
if (ModalBlocker != null) { ModalBlocker.SetActive(false); }
if (PanelContainer != null) { PanelContainer.SetActive(false); }
gameObject.SetActive(false);
_onContinue = null;
}
/// <summary>
/// Called when Continue button is clicked.
/// Public to allow external setup of button listeners.
/// </summary>
public void OnContinueClicked() {
var callback = _onContinue;
Hide();
callback?.Invoke();
}
/// <summary>
/// Called when Skip Tutorial button is clicked.
/// Skips all remaining tutorial steps. User can restart from Settings.
/// </summary>
public void OnSkipAllClicked() {
Hide();
TutorialManager.Instance?.SkipAllOnboarding();
}
/// <summary>
/// Gets a target Transform by ID, using the registry if available.
/// Falls back to GameObject.Find for unregistered targets.
/// </summary>
private Transform GetTarget(string targetId) {
if (string.IsNullOrEmpty(targetId)) return null;
// Try registry first
var registry = TutorialManager.Instance?.TargetRegistry;
if (registry != null) { return registry.GetTargetOrFind(targetId); }
// Fallback to GameObject.Find
var targetObj = GameObject.Find(targetId);
return targetObj?.transform;
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 461d5eac2fdd4d509a15185a42677886
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -8,7 +8,7 @@ namespace Eagle0.Tutorial {
/// Creates highlight frame, tooltip, and dimmer components for TutorialOverlayController.
/// </summary>
public static class TutorialOverlayBuilder {
// Colors matching the game's fantasy RPG style (same as TutorialCanvasBuilder)
// Colors matching the game's fantasy RPG style
private static readonly Color DimmerColor = new Color(0f, 0f, 0f, 0.7f);
private static readonly Color HighlightBorderColor =
new Color(1f, 0.85f, 0.4f, 1f); // Gold
@@ -10,9 +10,6 @@ namespace Eagle0.Tutorial {
/// </summary>
public class TutorialUIManager : MonoBehaviour {
[Header("UI Components")]
[Tooltip("Modal dialog panel for important tutorials")]
public TutorialModalPanel ModalPanel;
[Tooltip("Overlay controller for highlighting UI elements")]
public TutorialOverlayController OverlayController;
@@ -51,9 +48,6 @@ namespace Eagle0.Tutorial {
private bool _fallbackIsOnboarding;
private void Awake() {
// Auto-build Canvas UI if needed
if (ModalPanel == null && AutoBuildCanvasUI) { BuildCanvasUI(); }
// Auto-build overlay UI if needed
if (OverlayController == null && AutoBuildCanvasUI && TutorialCanvas != null) {
BuildOverlayUI();
@@ -65,25 +59,6 @@ namespace Eagle0.Tutorial {
}
}
/// <summary>
/// Builds the Canvas-based tutorial UI at runtime.
/// Called automatically if AutoBuildCanvasUI is true and ModalPanel is not assigned.
/// </summary>
private void BuildCanvasUI() {
TutorialCanvas = TutorialCanvasBuilder.BuildTutorialCanvas(CanvasFont);
if (TutorialCanvas != null) {
// Make canvas a child of this object so it persists with TutorialManager
TutorialCanvas.transform.SetParent(transform, false);
// Get the modal panel that was created
ModalPanel = TutorialCanvas.GetComponent<TutorialModalPanel>();
Debug.Log("TutorialUIManager: Built Canvas UI at runtime");
} else {
Debug.LogWarning("TutorialUIManager: Failed to build Canvas UI");
}
}
/// <summary>
/// Builds the overlay UI components at runtime.
/// Called automatically if AutoBuildCanvasUI is true and OverlayController is not assigned.
@@ -284,20 +259,7 @@ namespace Eagle0.Tutorial {
}
}
if (ModalPanel != null) {
// Ensure canvas is active
if (TutorialCanvas != null && !TutorialCanvas.gameObject.activeSelf) {
TutorialCanvas.gameObject.SetActive(true);
}
// Wrap callback to clear highlight when modal is dismissed
Action wrappedCallback = () => {
OverlayController?.ClearHighlight();
onComplete?.Invoke();
};
ModalPanel.Show(step, currentIndex, totalSteps, wrappedCallback, isOnboarding);
} else if (UseFallbackUI) {
if (UseFallbackUI) {
ShowFallbackModal(step, currentIndex, totalSteps, onComplete, isOnboarding);
} else {
Debug.LogWarning("TutorialUIManager: No ModalPanel assigned and fallback disabled");
@@ -389,7 +351,6 @@ namespace Eagle0.Tutorial {
/// Hides all tutorial UI.
/// </summary>
public void HideAll() {
ModalPanel?.Hide();
OverlayController?.HideOverlay();
HideFallbackModal();
// Note: Don't hide hints automatically - they persist until dismissed