Files
zeling_v2/Assets/_Game/Scripts/Dialogue/InteractionPromptController.cs
Joywayer 6eaa83dc71 feat: Round 48 narrative systems improvements
- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop
- QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip
- IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info)
- IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it
- IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event
- QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions
- DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix)
- DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks
- DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match
- WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API
- DialogueModule: List badge shows warning indicator for unconditional-shadowing variants
- DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 00:05:15 +08:00

164 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace BaseGames.Dialogue
{
/// <summary>
/// 世界空间交互提示控制器(架构 14_NarrativeModule §2 升级版)。
/// 挂在每个 InteractableNPC 子节点Prefab 实例),默认隐藏。
///
/// 功能:
/// • 自动订阅父级 InteractableNPC 的进/出范围事件,免手动调用 Show/Hide
/// • TMP_Text 实时显示 InteractPrompt如"接受任务"/"提交任务"),随任务状态动态刷新
/// • 根据当前活跃输入设备自动切换按键图标(键盘/手柄)
/// • 支持淡入/淡出动画
/// </summary>
public class InteractionPromptController : MonoBehaviour
{
[Header("UI 引用")]
[Tooltip("整个提示根节点(包含图标和文字),控制显示/隐藏。")]
[SerializeField] private GameObject _promptRoot;
[Tooltip("按键图标 Image 组件(可选)。有输入设备时显示对应图标。")]
[SerializeField] private Image _icon;
[Tooltip("提示文字 TMP_Text 组件(可选)。自动显示 InteractableNPC.InteractPrompt 的当前值。")]
[SerializeField] private TMP_Text _label;
[Header("按键图标")]
[Tooltip("键盘/鼠标设备激活时使用的按键图标 Sprite。")]
[SerializeField] private Sprite _keyboardIcon;
[Tooltip("手柄设备激活时使用的按键图标 Sprite。")]
[SerializeField] private Sprite _gamepadIcon;
[Header("位置与动画")]
[Tooltip("相对于本组件 transform 的世界空间偏移。调整此值可控制气泡与 NPC 的相对位置。")]
[SerializeField] private Vector3 _offset = new Vector3(0f, 1.8f, 0f);
[Tooltip("是否随相机方向 Billboard 朝向(世界空间 Canvas 推荐开启)。")]
[SerializeField] private bool _billboard = true;
[Tooltip("淡入持续时间。0 = 立即显示,无动画。")]
[SerializeField] [Min(0f)] private float _fadeInDuration = 0.12f;
[Tooltip("淡出持续时间。0 = 立即隐藏,无动画。")]
[SerializeField] [Min(0f)] private float _fadeOutDuration = 0.08f;
// ── 运行时状态 ────────────────────────────────────────────────────────
private InteractableNPC _npc;
private bool _visible;
private float _alpha;
private Camera _cam;
// ── Lifecycle ─────────────────────────────────────────────────────────
private void Awake()
{
// 自动连接父级 InteractableNPC 事件(无需手动调用 Show/Hide
_npc = GetComponentInParent<InteractableNPC>();
if (_npc != null)
{
_npc.PlayerEnteredRange += OnPlayerEntered;
_npc.PlayerExitedRange += OnPlayerExited;
}
SetVisible(false, immediate: true);
}
private void OnDestroy()
{
if (_npc != null)
{
_npc.PlayerEnteredRange -= OnPlayerEntered;
_npc.PlayerExitedRange -= OnPlayerExited;
}
}
private void Update()
{
// 位置偏移(世界空间气泡)
if (_offset != Vector3.zero)
transform.position = (_npc != null ? _npc.transform.position : transform.parent.position) + _offset;
// Billboard
if (_billboard && _visible)
{
if (_cam == null) _cam = Camera.main;
if (_cam != null)
transform.forward = _cam.transform.forward;
}
// 淡入/淡出
if (_promptRoot == null) return;
if (_visible && _alpha < 1f)
{
float speed = _fadeInDuration > 0f ? Time.deltaTime / _fadeInDuration : 1f;
_alpha = Mathf.MoveTowards(_alpha, 1f, speed);
ApplyAlpha(_alpha);
}
else if (!_visible && _alpha > 0f)
{
float speed = _fadeOutDuration > 0f ? Time.deltaTime / _fadeOutDuration : 1f;
_alpha = Mathf.MoveTowards(_alpha, 0f, speed);
ApplyAlpha(_alpha);
if (_alpha <= 0f) _promptRoot.SetActive(false);
}
}
// ── 公开 API兼容旧调用 / 脚本手动控制)────────────────────────────
/// <summary>手动显示提示。通常由 InteractableNPC 自动调用,无需手动触发。</summary>
public void Show()
{
if (_npc != null) _label.text = _npc.InteractPrompt;
SetVisible(true, immediate: false);
UpdateIcon();
}
/// <summary>手动隐藏提示。通常由 InteractableNPC 自动调用,无需手动触发。</summary>
public void Hide() => SetVisible(false, immediate: false);
// ── 私有辅助 ─────────────────────────────────────────────────────────
private void OnPlayerEntered(Transform player)
{
// 刷新文字(每次进入都读取最新 InteractPrompt确保任务状态变化后文字正确
if (_label != null && _npc != null)
_label.text = _npc.InteractPrompt;
SetVisible(true, immediate: false);
UpdateIcon();
}
private void OnPlayerExited() => SetVisible(false, immediate: false);
private void SetVisible(bool show, bool immediate)
{
_visible = show;
if (immediate)
{
_alpha = show ? 1f : 0f;
if (_promptRoot != null)
{
_promptRoot.SetActive(show);
ApplyAlpha(_alpha);
}
}
else if (show && _promptRoot != null)
{
_promptRoot.SetActive(true); // 淡出由 Update 结束时 SetActive(false)
}
}
private void UpdateIcon()
{
if (_icon == null) return;
bool isGamepad = Gamepad.current != null && Gamepad.current.enabled;
_icon.sprite = isGamepad ? _gamepadIcon : _keyboardIcon;
}
private void ApplyAlpha(float a)
{
if (_icon != null) { var c = _icon.color; c.a = a; _icon.color = c; }
if (_label != null) { var c = _label.color; c.a = a; _label.color = c; }
}
}
}