Files
2026-07-17 05:42:52 +08:00

1365 lines
46 KiB
C#
Raw Permalink 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 System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ExamFlowManager : MonoBehaviour
{
private const int StepCount = 7;
private const string ManMubanName = "manMuban";
private const string ManInjuryName = "manInjury";
private readonly int[] _stepScores = { 6, 22, 11, 11, 11, 22, 17 };
private readonly bool[] _answered = new bool[StepCount];
private readonly bool[] _correct = new bool[StepCount];
private readonly int[] _earned = new int[StepCount];
private readonly string[] _feedback = new string[StepCount];
private readonly HashSet<string> _placedPings = new HashSet<string>();
private readonly HashSet<string> _wrappedPings = new HashSet<string>();
private readonly HashSet<SplintObjectClickHandler> _clickedSplintPlanks = new HashSet<SplintObjectClickHandler>();
private Transform _dialogueBubble;
private GameObject _gradeResult;
private GameObject _endPanel;
private GameObject _manMuban;
private GameObject _manInjury;
private Button _dialogueYesButton;
private Button _dialogueBackButton;
private Text _gradeContent;
private Text _gradeScoreText;
private Font _cachedChineseFont;
private PingClickHandler[] _splintPings;
private SplintObjectClickHandler _splintBodyPad;
private SplintObjectClickHandler[] _splintPlanks;
private int _currentStep;
private bool _examActive;
private bool _waitingForPoint2;
private bool _waitingForShose;
private bool _waitingForWrapClicks;
private bool _splintBodyClicked;
private bool IsSplintExam
{
get
{
return GameManager.Instance != null &&
GameManager.Instance.CurrentFixationMethod == FixationMethod.SplintFixation;
}
}
private void Awake()
{
AutoBind();
}
private void Start()
{
AutoBind();
BindButtons();
EnsureToggleGroups();
HideAllPanels();
}
private void Update()
{
bool shouldRun = GameManager.Instance != null &&
GameManager.Instance.CurrentMode == ExperimentMode.Exam &&
IsExperimentPageVisible();
if (!shouldRun && _examActive)
{
ResetExamState(false);
}
}
public void BeginExam()
{
if (GameManager.Instance == null ||
GameManager.Instance.CurrentMode != ExperimentMode.Exam ||
!IsExperimentPageVisible())
{
ResetExamState(false);
return;
}
AutoBind();
BindButtons();
EnsureToggleGroups();
ResetExamState(true);
_examActive = true;
_waitingForPoint2 = true;
if (GameManager.Instance != null)
{
GameManager.Instance.InitExamRecord(StepCount);
}
ForceHideExamOnlyOutlines();
MoveWomanToExamPoint();
ConfigureExamModelState();
ShowStep(1);
Debug.Log("[ExamFlow] 考核模式流程启动: " + GameManager.Instance.CurrentFixationMethod);
}
public void StopExam()
{
ResetExamState(false);
}
public void ShowStep(int stepNumber)
{
AutoBind();
_currentStep = Mathf.Clamp(stepNumber, 1, StepCount);
if (_dialogueBubble == null) return;
_dialogueBubble.gameObject.SetActive(true);
EnsureStepPanelsDoNotBlockButtons();
for (int i = 0; i < _dialogueBubble.childCount; i++)
{
Transform child = _dialogueBubble.GetChild(i);
if (child.name.StartsWith("step"))
{
child.gameObject.SetActive(child.name == "step" + _currentStep);
}
}
ApplyStepText(_currentStep);
if (_currentStep == 3 || _currentStep == 7)
{
ResetStepToggles(_currentStep);
}
ShowTip(GetExamTipText(_currentStep));
Debug.Log("[ExamFlow] 显示 step" + _currentStep);
}
public void OnObjectClicked(Component clicked)
{
if (!_examActive || clicked == null) return;
if (clicked is Point2ClickHandler)
{
if (_waitingForPoint2 && clicked.name == "point2")
{
_waitingForPoint2 = false;
RecordStep(1, true, "已按要求完成前置操作起始步骤。");
ShowTip("伤情评估已完成。下一步:阅读弹窗提示,准备完成前置处理。");
StartCoroutine(ShowStepDelayed(2, 1f));
}
return;
}
if (clicked is ShoseClickHandler)
{
if (_waitingForShose || _currentStep <= 2)
{
_waitingForShose = false;
RecordStep(2, true, IsSplintExam ? "已完成夹板固定前置处理。" : "已完成下肢固定前置处理。");
ShowTip(IsSplintExam ? "夹板固定前置处理完成。下一步:选择固定带宽度。" : "下肢固定前置处理完成。下一步:选择绑带宽度。");
StartCoroutine(ShowStepDelayed(3, 1f));
}
return;
}
var ping = clicked as PingClickHandler;
if (ping != null)
{
if (IsSplintExam)
{
HandleSplintPingClicked(ping);
return;
}
bool isWrapClick = ping.chanraoObject != null && ping.chanraoObject.activeSelf;
if (isWrapClick)
{
_wrappedPings.Add(ping.name);
Debug.Log("[ExamFlow] 系带点击 " + _wrappedPings.Count + "/4: " + ping.name);
if (_waitingForWrapClicks && _wrappedPings.Count >= 4)
{
_waitingForWrapClicks = false;
RecordStep(5, true, "已按由远端到近端的顺序完成系带。");
ShowTip("系带顺序已完成。下一步:阅读打结位置判断题。");
ShowStep(6);
}
}
else
{
_placedPings.Add(ping.name);
Debug.Log("[ExamFlow] 放带点击 " + _placedPings.Count + "/4: " + ping.name);
}
return;
}
var splintObject = clicked as SplintObjectClickHandler;
if (splintObject != null)
{
if (IsSplintExam)
{
HandleSplintObjectClicked(splintObject);
}
return;
}
if (clicked is ChanraoTuibuHandler)
{
if (!_answered[3])
{
RecordStep(4, _placedPings.Count >= 4, _placedPings.Count >= 4
? "已按要求完成四条绑带摆放。"
: "绑带尚未全部摆放完成。");
}
_waitingForWrapClicks = true;
ShowTip("环绕固定带已显示。下一步:依次点击 4 个蓝色绑带点完成系带固定。");
ShowStep(5);
}
}
private void HandleSplintPingClicked(PingClickHandler ping)
{
if (!IsSplintPing(ping)) return;
bool isFinalFixClick = ping.chanraoObject != null && ping.chanraoObject.activeSelf;
if (isFinalFixClick)
{
_wrappedPings.Add(ping.name);
Debug.Log("[ExamFlow] 夹板固定点击 " + _wrappedPings.Count + "/4: " + ping.name);
if (_waitingForWrapClicks && _wrappedPings.Count >= 4)
{
_waitingForWrapClicks = false;
RecordStep(6, true, "已按由远端到近端的顺序完成夹板固定。");
ShowTip("夹板系带固定已完成。下一步:选择固定后末梢观察位置。");
ShowStep(7);
}
return;
}
_placedPings.Add(ping.name);
Debug.Log("[ExamFlow] 夹板绑带摆放 " + _placedPings.Count + "/4: " + ping.name);
if (_placedPings.Count >= 4 && !_answered[3])
{
RecordStep(4, true, "已按由远端到近端的顺序预置四条绑带。");
ShowTip("四条固定带已预置。下一步:阅读弹窗,准备放置衬垫和夹板。");
ShowStep(5);
}
}
private void HandleSplintObjectClicked(SplintObjectClickHandler splintObject)
{
if (splintObject == null) return;
if (splintObject == _splintBodyPad)
{
_splintBodyClicked = true;
ShowSplintPlankTargets();
Debug.Log("[ExamFlow] 已放置夹板衬垫 Body.001");
ShowTip("衬垫已放置。下一步:依次点击两块木板蓝色边框,放置伤肢两侧夹板。");
return;
}
if (!_splintBodyClicked) return;
_clickedSplintPlanks.Add(splintObject);
Debug.Log("[ExamFlow] 已放置夹板 " + _clickedSplintPlanks.Count + "/2: " + splintObject.name);
if (_clickedSplintPlanks.Count >= 2 && !_answered[4])
{
RecordStep(5, true, "已先放置衬垫,再完成伤肢两侧夹板放置。");
ShowTip("两侧夹板已放置。下一步:阅读弹窗,准备按顺序系带固定。");
ShowStep(6);
}
}
private IEnumerator ShowStepDelayed(int stepNumber, float delay)
{
yield return new WaitForSeconds(delay);
ShowStep(stepNumber);
}
private void OnDialogueYesClicked()
{
if (!_examActive)
{
HideDialogue();
return;
}
switch (_currentStep)
{
case 1:
HideDialogue();
_waitingForPoint2 = true;
ShowTip("请点击伤员大腿附近,完成伤情评估。");
break;
case 2:
HideDialogue();
_waitingForShose = true;
ShowTip("请点击伤侧鞋子,完成脱鞋和前置处理。");
break;
case 3:
RecordStep(3, GetSelectedToggleIndex(3) == 1, "正确答案:10cm。");
ShowStep(4);
if (IsSplintExam) ActivateFirstSplintPing();
else ActivateFirstPing();
ShowTip(IsSplintExam
? "请由脚踝向大腿依次点击 4 个蓝色绑带点,预置固定带。"
: "请由脚踝向大腿依次点击 4 个蓝色绑带点,摆放绑带。");
break;
case 4:
HideDialogue();
ShowTip(IsSplintExam
? "请继续由远端向近端点击蓝色绑带点,完成 4 条固定带预置。"
: "请继续由远端向近端点击蓝色绑带点,完成 4 条绑带摆放。");
break;
case 5:
HideDialogue();
if (IsSplintExam)
{
ShowSplintBodyTarget();
ShowTip("请点击 Body.001 蓝色边框,先放置夹板衬垫。");
}
else
{
_waitingForWrapClicks = true;
ShowTip("请依次点击蓝色绑带点,按由远端到近端顺序完成系带。");
}
break;
case 6:
if (IsSplintExam)
{
HideDialogue();
_waitingForWrapClicks = true;
EnableSplintFinalPingStage();
ShowTip("请由脚踝向大腿依次点击 4 个蓝色固定点,完成夹板系带固定。");
}
else
{
RecordStep(6, false, "错误操作:在受伤腿一侧打结。正确做法是在未受伤腿位置打结。");
ShowStep(7);
}
break;
case 7:
RecordStep(7, GetSelectedToggleIndex(7) == 1, "正确答案:足背。");
ShowTip("考核答题完成,正在生成成绩汇总。");
ShowGradeResult();
break;
default:
HideDialogue();
break;
}
}
private void ShowGradeResult()
{
AutoBind();
BindResultButtons();
BindEndPanelButtons();
HideDialogue();
int totalScore;
string gradeContent = BuildGradeContent(out totalScore);
if (_gradeResult != null) _gradeResult.SetActive(true);
if (_gradeContent != null)
{
ApplyGradeTextStyle(_gradeContent);
_gradeContent.text = gradeContent;
}
if (_gradeScoreText != null) _gradeScoreText.text = totalScore.ToString();
RefreshGradeContentLayout();
}
private string BuildGradeContent(out int total)
{
total = 0;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.AppendLine("考核结果汇总");
sb.AppendLine();
for (int i = 0; i < StepCount; i++)
{
total += _earned[i];
bool answered = _answered[i];
bool correct = answered && _correct[i];
sb.AppendFormat("考核点{0}{1}分):{2}", i + 1, _stepScores[i], correct ? "正确" : "错误");
if (!answered) sb.Append("(未完成)");
sb.AppendLine();
sb.AppendLine("题目:" + GetQuestionText(i + 1));
sb.AppendLine("答案:" + GetAnswerText(i + 1));
sb.AppendFormat("得分:{0}/{1}", _earned[i], _stepScores[i]);
sb.AppendLine();
if (!string.IsNullOrEmpty(_feedback[i]) && (!correct || i == 5))
{
sb.AppendLine("说明:" + _feedback[i]);
}
sb.AppendLine();
}
sb.AppendFormat("最终得分:{0}/100", total);
return sb.ToString();
}
private void RecordStep(int oneBasedStep, bool isCorrect, string feedback)
{
int index = oneBasedStep - 1;
if (index < 0 || index >= StepCount || _answered[index]) return;
_answered[index] = true;
_correct[index] = isCorrect;
_earned[index] = isCorrect ? _stepScores[index] : 0;
_feedback[index] = feedback;
if (GameManager.Instance != null)
{
GameManager.Instance.RecordExamResult(index, _stepScores[index], isCorrect ? 1f : 0f, feedback);
}
}
private void ActivateFirstPing()
{
PingClickHandler firstHandler = null;
foreach (var ping in Resources.FindObjectsOfTypeAll<PingClickHandler>())
{
if (ping == null || !ping.gameObject.scene.IsValid()) continue;
if (ping.name == "ping.004" || ping.pingIndex == 0)
{
firstHandler = ping;
break;
}
}
if (firstHandler != null)
{
firstHandler.gameObject.SetActive(true);
firstHandler.ResetState(true);
}
}
private void ActivateFirstSplintPing()
{
ConfigureSplintObjects();
PingClickHandler firstHandler = GetSplintPingByIndex(0);
if (firstHandler == null) return;
firstHandler.gameObject.SetActive(true);
firstHandler.ResetState(true);
}
private void ConfigureExamModelState()
{
if (IsSplintExam)
{
ConfigureSplintObjects();
if (_manInjury != null) _manInjury.SetActive(false);
if (_manMuban != null) _manMuban.SetActive(true);
HideSplintObjects();
}
else
{
if (_manMuban == null) _manMuban = FindSceneObject(ManMubanName);
if (_manMuban != null) _manMuban.SetActive(false);
if (_manInjury == null) _manInjury = FindSceneObject(ManInjuryName);
if (_manInjury != null) _manInjury.SetActive(true);
}
}
private void ConfigureSplintObjects()
{
_manMuban = FindSceneObject(ManMubanName);
_manInjury = FindSceneObject(ManInjuryName);
if (_manMuban == null) return;
ConfigureSplintPings();
ConfigureSplintBodyPad();
ConfigureSplintPlanks();
}
private void ConfigureSplintPings()
{
if (_manMuban == null) return;
_splintPings = new PingClickHandler[4];
string[] guideNames = { "ping.001", "ping.002", "ping.003", "ping.004" };
string[] targetNames = { "Armature/Bottoms.001", "ping.005", "ping.006", "ping.007" };
for (int i = 0; i < guideNames.Length; i++)
{
Transform guide = _manMuban.transform.Find(guideNames[i]);
Transform target = _manMuban.transform.Find(targetNames[i]);
if (guide == null || target == null) continue;
PingClickHandler handler = guide.GetComponent<PingClickHandler>();
if (handler == null) handler = guide.gameObject.AddComponent<PingClickHandler>();
handler.pingIndex = i;
handler.chanraoObject = target.gameObject;
handler.nextPing = null;
_splintPings[i] = handler;
target.gameObject.SetActive(false);
guide.gameObject.SetActive(false);
}
for (int i = 0; i < _splintPings.Length - 1; i++)
{
if (_splintPings[i] != null && _splintPings[i + 1] != null)
_splintPings[i].nextPing = _splintPings[i + 1].gameObject;
}
}
private void ConfigureSplintBodyPad()
{
if (_manMuban == null) return;
Transform body = _manMuban.transform.Find("Body.001");
if (body == null) return;
_splintBodyPad = body.GetComponent<SplintObjectClickHandler>();
if (_splintBodyPad == null) _splintBodyPad = body.gameObject.AddComponent<SplintObjectClickHandler>();
body.gameObject.SetActive(false);
}
private void ConfigureSplintPlanks()
{
if (_manMuban == null) return;
string[] plankNames = { "Wood plank - warm oak board", "Wood plank - warm oak board.001" };
List<SplintObjectClickHandler> planks = new List<SplintObjectClickHandler>();
for (int i = 0; i < plankNames.Length; i++)
{
Transform plank = _manMuban.transform.Find(plankNames[i]);
if (plank == null) continue;
SplintObjectClickHandler handler = plank.GetComponent<SplintObjectClickHandler>();
if (handler == null) handler = plank.gameObject.AddComponent<SplintObjectClickHandler>();
planks.Add(handler);
plank.gameObject.SetActive(false);
}
_splintPlanks = planks.ToArray();
}
private void HideSplintObjects()
{
if (_manMuban == null) return;
string[] names =
{
"ping.001", "ping.002", "ping.003", "ping.004",
"ping.005", "ping.006", "ping.007",
"Body.001",
"Wood plank - warm oak board", "Wood plank - warm oak board.001"
};
for (int i = 0; i < names.Length; i++)
{
Transform target = _manMuban.transform.Find(names[i]);
if (target != null) target.gameObject.SetActive(false);
}
Transform bottoms = _manMuban.transform.Find("Armature/Bottoms.001");
if (bottoms != null) bottoms.gameObject.SetActive(false);
}
private void ShowSplintBodyTarget()
{
if (_splintBodyPad == null) ConfigureSplintBodyPad();
if (_splintBodyPad == null) return;
_splintBodyPad.gameObject.SetActive(true);
_splintBodyPad.ResetState(true);
}
private void ShowSplintPlankTargets()
{
if (_splintPlanks == null || _splintPlanks.Length == 0) ConfigureSplintPlanks();
for (int i = 0; i < _splintPlanks.Length; i++)
{
if (_splintPlanks[i] == null) continue;
_splintPlanks[i].gameObject.SetActive(true);
_splintPlanks[i].ResetState(true);
}
}
private void EnableSplintFinalPingStage()
{
if (_splintPings == null || _splintPings.Length == 0) ConfigureSplintPings();
for (int i = 0; i < _splintPings.Length; i++)
{
if (_splintPings[i] == null) continue;
_splintPings[i].gameObject.SetActive(true);
_splintPings[i].ReEnableBorder();
}
}
private PingClickHandler GetSplintPingByIndex(int index)
{
if (_splintPings == null || _splintPings.Length == 0) ConfigureSplintPings();
if (_splintPings == null || index < 0 || index >= _splintPings.Length) return null;
return _splintPings[index];
}
private bool IsSplintPing(PingClickHandler ping)
{
if (ping == null || _splintPings == null) return false;
for (int i = 0; i < _splintPings.Length; i++)
{
if (_splintPings[i] == ping) return true;
}
return false;
}
private void AutoBind()
{
Transform canvas = FindSceneObject("Canvas")?.transform;
if (canvas == null) return;
if (_dialogueBubble == null) _dialogueBubble = canvas.Find("DialogueBubble");
if (_gradeResult == null)
{
Transform t = canvas.Find("GradeResult");
if (t != null) _gradeResult = t.gameObject;
}
if (_endPanel == null)
{
Transform t = canvas.Find("EndPanel");
if (t != null) _endPanel = t.gameObject;
}
if (_dialogueBubble != null)
{
_dialogueYesButton = _dialogueBubble.Find("BtnYes")?.GetComponent<Button>();
_dialogueBackButton = _dialogueBubble.Find("BtnBack")?.GetComponent<Button>();
}
if (_gradeResult != null)
{
Transform grade = _gradeResult.transform.Find("Grade");
if (grade != null) _gradeScoreText = grade.GetComponent<Text>();
_gradeContent = FindGradeDetailText(_gradeResult.transform);
}
}
private Text FindGradeDetailText(Transform root)
{
if (root == null) return null;
Transform scrollView = root.Find("Scroll View");
if (scrollView != null)
{
Transform viewportContent = scrollView.Find("Viewport/Content");
Text text = EnsureTextOnContent(viewportContent);
if (text != null) return text;
text = FindTextByName(scrollView, "Content");
if (text != null) return text;
text = FindTextByName(scrollView, "content");
if (text != null) return text;
}
Transform legacyContent = root.Find("content");
return legacyContent != null ? legacyContent.GetComponent<Text>() : null;
}
private Text EnsureTextOnContent(Transform content)
{
if (content == null) return null;
HideLegacyContentTextChildren(content);
Text text = content.GetComponent<Text>();
if (text == null)
{
text = content.gameObject.AddComponent<Text>();
}
ApplyGradeTextStyle(text);
RectTransform rect = content.GetComponent<RectTransform>();
if (rect != null)
{
rect.anchorMin = new Vector2(0f, 1f);
rect.anchorMax = new Vector2(1f, 1f);
rect.pivot = new Vector2(0f, 1f);
rect.anchoredPosition = Vector2.zero;
}
ContentSizeFitter fitter = content.GetComponent<ContentSizeFitter>();
if (fitter == null) fitter = content.gameObject.AddComponent<ContentSizeFitter>();
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
return text;
}
private void HideLegacyContentTextChildren(Transform content)
{
if (content == null) return;
for (int i = 0; i < content.childCount; i++)
{
Transform child = content.GetChild(i);
if (child == null) continue;
if (child.GetComponent<Text>() != null || child.GetComponentInChildren<Text>(true) != null)
{
child.gameObject.SetActive(false);
}
}
}
private void ApplyGradeTextStyle(Text text)
{
if (text == null) return;
Font chineseFont = ResolveChineseUiFont(text);
if (chineseFont != null) text.font = chineseFont;
text.fontSize = 22;
text.color = new Color(0.12f, 0.12f, 0.12f, 1f);
text.alignment = TextAnchor.UpperLeft;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Overflow;
text.supportRichText = false;
text.raycastTarget = false;
RectTransform rect = text.GetComponent<RectTransform>();
if (rect != null)
{
rect.anchorMin = new Vector2(0f, 1f);
rect.anchorMax = new Vector2(1f, 1f);
rect.pivot = new Vector2(0f, 1f);
rect.anchoredPosition = Vector2.zero;
rect.offsetMin = new Vector2(0f, rect.offsetMin.y);
rect.offsetMax = new Vector2(0f, rect.offsetMax.y);
}
}
private Font ResolveChineseUiFont(Text exclude)
{
if (_cachedChineseFont != null) return _cachedChineseFont;
Text[] texts = Resources.FindObjectsOfTypeAll<Text>();
for (int i = 0; i < texts.Length; i++)
{
Text candidate = texts[i];
if (candidate == null || candidate == exclude || candidate.font == null) continue;
if (!candidate.gameObject.scene.IsValid()) continue;
if (!ContainsChinese(candidate.text)) continue;
_cachedChineseFont = candidate.font;
return _cachedChineseFont;
}
return exclude != null ? exclude.font : null;
}
private bool ContainsChinese(string value)
{
if (string.IsNullOrEmpty(value)) return false;
for (int i = 0; i < value.Length; i++)
{
char ch = value[i];
if (ch >= '\u4e00' && ch <= '\u9fff') return true;
}
return false;
}
private Text FindTextByName(Transform root, string objectName)
{
if (root == null) return null;
Text direct = root.name == objectName ? root.GetComponent<Text>() : null;
if (direct != null) return direct;
for (int i = 0; i < root.childCount; i++)
{
Text childText = FindTextByName(root.GetChild(i), objectName);
if (childText != null) return childText;
}
return null;
}
private void BindButtons()
{
if (_dialogueYesButton != null)
{
_dialogueYesButton.onClick.RemoveAllListeners();
_dialogueYesButton.interactable = true;
_dialogueYesButton.enabled = true;
_dialogueYesButton.onClick.AddListener(OnDialogueYesClicked);
}
if (_dialogueBackButton != null)
{
_dialogueBackButton.onClick.RemoveAllListeners();
_dialogueBackButton.interactable = true;
_dialogueBackButton.enabled = true;
_dialogueBackButton.onClick.AddListener(HideDialogue);
}
BindResultButtons();
BindEndPanelButtons();
}
private void BindResultButtons()
{
if (_gradeResult == null) return;
Button yes = _gradeResult.transform.Find("BtnYes")?.GetComponent<Button>();
if (yes != null)
{
yes.onClick.RemoveAllListeners();
yes.interactable = true;
yes.enabled = true;
yes.onClick.AddListener(ShowEndPanelFromGrade);
}
Button back = _gradeResult.transform.Find("BtnBack")?.GetComponent<Button>();
if (back != null)
{
back.onClick.RemoveAllListeners();
back.interactable = true;
back.enabled = true;
back.onClick.AddListener(() => _gradeResult.SetActive(false));
}
}
private void BindEndPanelButtons()
{
if (_endPanel == null) return;
Button yes = _endPanel.transform.Find("BtnYes")?.GetComponent<Button>();
if (yes != null)
{
yes.onClick.RemoveAllListeners();
yes.interactable = true;
yes.enabled = true;
yes.onClick.AddListener(ReturnToHome);
}
}
private void ShowEndPanelFromGrade()
{
AutoBind();
if (_gradeResult != null) _gradeResult.SetActive(false);
if (_endPanel != null)
{
_endPanel.SetActive(true);
BindEndPanelButtons();
}
}
private void ReturnToHome()
{
PageController page = FindObjectOfType<PageController>();
if (page != null) page.ReturnToHome();
}
private void ApplyStepText(int stepNumber)
{
Transform step = _dialogueBubble?.Find("step" + stepNumber);
if (step == null) return;
Text title = FindStepText(step, "Title");
Text content = FindStepText(step, "content");
if (title != null)
{
ApplyQuestionTextStyle(title, true);
title.text = "考核点" + ToChineseNumber(stepNumber);
}
if (content != null)
{
ApplyQuestionTextStyle(content, false);
content.text = GetQuestionText(stepNumber);
}
if (stepNumber == 3)
{
ConfigureStepToggles(stepNumber);
SetToggleLabel(step, "Toggle1", "5cm");
SetToggleLabel(step, "Toggle2", "10cm");
SetToggleLabel(step, "Toggle3", "20cm");
}
else if (stepNumber == 7)
{
ConfigureStepToggles(stepNumber);
SetToggleLabel(step, "Toggle1", "足底");
SetToggleLabel(step, "Toggle2", "足背");
}
}
private string GetQuestionText(int stepNumber)
{
if (IsSplintExam)
{
switch (stepNumber)
{
case 1: return "操作题:点击确定后,请先完成现场安全确认、伤员沟通与伤情初步评估。";
case 2: return "操作题:点击确定后,请继续完成夹板固定前置处理,包括脱除伤侧鞋袜、暴露并观察伤肢。";
case 3: return "单选题:请选择夹板固定时较合适的固定带宽度。";
case 4: return "操作题:点击确定后,请按照由远心端到近心端的顺序依次预置四条固定带。";
case 5: return "操作题:点击确定后,请先放置衬垫保护骨突,再将两块夹板分别放在伤肢两侧。";
case 6: return "操作题:点击确定后,请按正确顺序收紧并固定四条绑带,注意从远端向近端依次固定。";
case 7: return "单选题:固定完成后,应优先观察哪个位置判断足部末梢血运与感觉?";
default: return "";
}
}
switch (stepNumber)
{
case 1: return "操作题:点击确定后,请先评估现场环境和伤员情况,确认可以开展救护。";
case 2: return "操作题:点击确定后,请按顺序完成下肢固定前置处理,包括取出用品、脱除伤侧鞋子并保护踝关节。";
case 3: return "单选题:请选择下肢固定时较合适的带子宽度。";
case 4: return "操作题:点击确定后,请按由远心端到近心端的顺序依次摆放绑带。";
case 5: return "操作题:点击确定后,请按正确顺序进行系带固定。";
case 6: return "判断题:本次演示中带子打结在受伤腿一侧,请判断该操作是否正确。";
case 7: return "单选题:踝关节处打结应选择哪个位置?";
default: return "";
}
}
private string GetExamTipText(int stepNumber)
{
if (IsSplintExam)
{
switch (stepNumber)
{
case 1: return "阅读考核点一,点击确定后开始伤情评估。";
case 2: return "阅读考核点二,点击确定后完成脱鞋和伤肢暴露观察。";
case 3: return "请选择固定带宽度,选择后点击确定。";
case 4: return "点击确定后,按由脚踝到大腿的顺序预置 4 条固定带。";
case 5: return "点击确定后,先放置衬垫,再放置两侧夹板。";
case 6: return "点击确定后,由远端到近端依次完成 4 个固定点。";
case 7: return "请选择固定完成后用于观察末梢血运的位置。";
default: return "";
}
}
switch (stepNumber)
{
case 1: return "阅读考核点一,点击确定后开始评估伤情。";
case 2: return "阅读考核点二,点击确定后完成脱鞋和下肢固定前置处理。";
case 3: return "请选择绑带宽度,选择后点击确定。";
case 4: return "点击确定后,按由脚踝到大腿的顺序摆放 4 条绑带。";
case 5: return "点击确定后,继续由远端到近端完成系带固定。";
case 6: return "阅读打结位置判断题,点击确定后进入下一题。";
case 7: return "请选择踝关节打结位置,选择后点击确定查看成绩。";
default: return "";
}
}
private string GetAnswerText(int stepNumber)
{
if (IsSplintExam)
{
switch (stepNumber)
{
case 1: return "确认现场安全,表明救护身份,询问并评估伤员,判断疑似骨折后准备夹板固定用品。";
case 2: return "轻柔暴露伤肢,脱除伤侧鞋袜,观察肿胀畸形和末梢情况,操作中避免牵拉骨折端。";
case 3: return "10cm。宽度过窄易产生局部压迫,过宽则不利于稳定贴合。";
case 4: return "从脚踝等远心端开始,向膝部和大腿近心端依次预置四条固定带。";
case 5: return "先在骨突和空隙处放置衬垫,再将两块夹板置于伤肢两侧,夹板长度应跨过骨折上下相邻关节。";
case 6: return "先固定远端脚踝,再由远心端到近心端逐条固定,松紧以能固定且不影响血运为准。";
case 7: return "足背。足背动脉、皮肤颜色、温度和感觉可用于判断固定后末梢循环情况。";
default: return "";
}
}
switch (stepNumber)
{
case 1: return "先评估现场安全和伤员意识、疼痛、畸形等情况,取得配合后再开始固定。";
case 2: return "取出急救用品,脱除伤侧鞋子,摆正伤肢并保护踝关节,避免加重损伤。";
case 3: return "10cm。带子过窄容易压迫皮肤,过宽会影响固定贴合。";
case 4: return "由远心端(脚踝)向近心端(大腿根部)依次摆放绑带。";
case 5: return "先固定远端脚踝,再依次向小腿、膝部、大腿近端收紧捆绑。";
case 6: return "错误。带结应打在未受伤腿一侧,避免压迫伤肢,也便于观察和后续拆除。";
case 7: return "足背。踝关节处应避免把结打在足底受压位置。";
default: return "";
}
}
private void ShowTip(string text)
{
UIManager uiManager = FindObjectOfType<UIManager>();
if (uiManager != null) uiManager.ShowTip(text);
}
private string ToChineseNumber(int number)
{
string[] values = { "", "一", "二", "三", "四", "五", "六", "七" };
return number >= 0 && number < values.Length ? values[number] : number.ToString();
}
private void SetToggleLabel(Transform root, string toggleName, string label)
{
Transform toggle = FindToggleTransform(root, toggleName);
if (toggle == null) return;
Text text = FindStepText(toggle, "Label");
if (text == null)
{
Text[] labels = toggle.GetComponentsInChildren<Text>(true);
if (labels.Length > 0) text = labels[0];
}
if (text != null)
{
ApplyOptionTextStyle(text);
text.text = label;
}
}
private Text FindStepText(Transform root, string objectName)
{
if (root == null) return null;
Text direct = root.name.Equals(objectName, System.StringComparison.OrdinalIgnoreCase)
? root.GetComponent<Text>()
: null;
if (direct != null) return direct;
for (int i = 0; i < root.childCount; i++)
{
Text child = FindStepText(root.GetChild(i), objectName);
if (child != null) return child;
}
return null;
}
private Transform FindToggleTransform(Transform root, string toggleName)
{
if (root == null) return null;
Transform direct = root.Find(toggleName);
if (direct != null) return direct;
Toggle[] toggles = GetOrderedToggles(root);
for (int i = 0; i < toggles.Length; i++)
{
if (toggles[i] != null && toggles[i].name == toggleName)
return toggles[i].transform;
}
int index = GetTrailingNumber(toggleName) - 1;
if (index >= 0 && index < toggles.Length && toggles[index] != null)
return toggles[index].transform;
return null;
}
private int GetTrailingNumber(string value)
{
if (string.IsNullOrEmpty(value)) return 0;
int multiplier = 1;
int result = 0;
bool found = false;
for (int i = value.Length - 1; i >= 0; i--)
{
char ch = value[i];
if (ch < '0' || ch > '9') break;
found = true;
result += (ch - '0') * multiplier;
multiplier *= 10;
}
return found ? result : 0;
}
private void ApplyQuestionTextStyle(Text text, bool isTitle)
{
if (text == null) return;
Font chineseFont = ResolveChineseUiFont(text);
if (chineseFont != null) text.font = chineseFont;
text.fontSize = isTitle ? Mathf.Max(text.fontSize, 30) : Mathf.Max(text.fontSize, 26);
text.color = isTitle ? Color.white : Color.black;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Overflow;
}
private void ApplyOptionTextStyle(Text text)
{
if (text == null) return;
Font chineseFont = ResolveChineseUiFont(text);
if (chineseFont != null) text.font = chineseFont;
text.fontSize = Mathf.Max(text.fontSize, 24);
text.color = new Color(0.18f, 0.18f, 0.18f, 1f);
text.horizontalOverflow = HorizontalWrapMode.Overflow;
text.verticalOverflow = VerticalWrapMode.Overflow;
}
private int GetSelectedToggleIndex(int stepNumber)
{
Transform step = _dialogueBubble?.Find("step" + stepNumber);
if (step == null) return -1;
Toggle[] toggles = GetOrderedToggles(step);
for (int i = 0; i < toggles.Length; i++)
{
if (toggles[i].isOn) return i;
}
return -1;
}
private void ResetStepToggles(int stepNumber)
{
Transform step = _dialogueBubble?.Find("step" + stepNumber);
if (step == null) return;
Toggle[] toggles = GetOrderedToggles(step);
for (int i = 0; i < toggles.Length; i++)
{
toggles[i].SetIsOnWithoutNotify(false);
}
}
private void EnsureToggleGroups()
{
EnsureToggleGroup(3);
EnsureToggleGroup(7);
}
private void EnsureStepPanelsDoNotBlockButtons()
{
if (_dialogueBubble == null) return;
for (int i = 0; i < _dialogueBubble.childCount; i++)
{
Transform child = _dialogueBubble.GetChild(i);
if (!child.name.StartsWith("step")) continue;
Graphic[] graphics = child.GetComponentsInChildren<Graphic>(true);
for (int j = 0; j < graphics.Length; j++)
{
Graphic graphic = graphics[j];
if (graphic == null) continue;
if (graphic.GetComponentInParent<Toggle>() != null) continue;
graphic.raycastTarget = false;
}
}
}
private void RefreshGradeContentLayout()
{
if (_gradeContent == null) return;
RectTransform textRect = _gradeContent.GetComponent<RectTransform>();
if (textRect != null)
{
float preferredHeight = _gradeContent.preferredHeight + 32f;
textRect.sizeDelta = new Vector2(textRect.sizeDelta.x, Mathf.Max(preferredHeight, 400f));
}
ScrollRect scrollRect = _gradeResult != null ? _gradeResult.GetComponentInChildren<ScrollRect>(true) : null;
if (scrollRect != null)
{
Canvas.ForceUpdateCanvases();
scrollRect.verticalNormalizedPosition = 1f;
LayoutRebuilder.ForceRebuildLayoutImmediate(scrollRect.content);
}
}
private void EnsureToggleGroup(int stepNumber)
{
Transform step = _dialogueBubble?.Find("step" + stepNumber);
if (step == null) return;
ConfigureStepToggles(stepNumber);
}
private void ConfigureStepToggles(int stepNumber)
{
Transform step = _dialogueBubble?.Find("step" + stepNumber);
if (step == null) return;
ToggleGroup group = step.GetComponent<ToggleGroup>();
if (group == null) group = step.gameObject.AddComponent<ToggleGroup>();
group.allowSwitchOff = true;
Toggle[] toggles = GetOrderedToggles(step);
for (int i = 0; i < toggles.Length; i++)
{
int selectedIndex = i;
Toggle toggle = toggles[i];
if (toggle == null) continue;
toggle.enabled = true;
toggle.interactable = true;
toggle.group = group;
EnsureToggleRaycastArea(toggle);
toggle.onValueChanged.RemoveAllListeners();
toggle.onValueChanged.AddListener((isOn) =>
{
if (!isOn) return;
Toggle[] currentToggles = GetOrderedToggles(step);
for (int j = 0; j < currentToggles.Length; j++)
{
if (j == selectedIndex || currentToggles[j] == null) continue;
currentToggles[j].SetIsOnWithoutNotify(false);
}
});
}
}
private Toggle[] GetOrderedToggles(Transform step)
{
Toggle[] toggles = step != null ? step.GetComponentsInChildren<Toggle>(true) : new Toggle[0];
System.Array.Sort(toggles, (a, b) => string.Compare(a.name, b.name, System.StringComparison.Ordinal));
return toggles;
}
private void EnsureToggleRaycastArea(Toggle toggle)
{
if (toggle == null) return;
Graphic rootGraphic = toggle.GetComponent<Graphic>();
if (rootGraphic == null)
{
Image image = toggle.gameObject.AddComponent<Image>();
image.color = new Color(1f, 1f, 1f, 0f);
rootGraphic = image;
}
rootGraphic.raycastTarget = true;
toggle.targetGraphic = rootGraphic;
Graphic[] graphics = toggle.GetComponentsInChildren<Graphic>(true);
for (int i = 0; i < graphics.Length; i++)
{
if (graphics[i] == null) continue;
graphics[i].raycastTarget = true;
}
RectTransform rect = toggle.GetComponent<RectTransform>();
if (rect != null)
{
Vector2 size = rect.sizeDelta;
if (Mathf.Abs(size.x) < 120f) size.x = 220f;
if (Mathf.Abs(size.y) < 32f) size.y = 48f;
rect.sizeDelta = size;
}
}
private void HideDialogue()
{
if (_dialogueBubble != null) _dialogueBubble.gameObject.SetActive(false);
}
private void HideAllPanels()
{
HideDialogue();
if (_gradeResult != null) _gradeResult.SetActive(false);
if (_endPanel != null) _endPanel.SetActive(false);
}
private void ResetExamState(bool keepActive)
{
StopAllCoroutines();
_examActive = keepActive;
_currentStep = 0;
_waitingForPoint2 = false;
_waitingForShose = false;
_waitingForWrapClicks = false;
_splintBodyClicked = false;
_placedPings.Clear();
_wrappedPings.Clear();
_clickedSplintPlanks.Clear();
for (int i = 0; i < StepCount; i++)
{
_answered[i] = false;
_correct[i] = false;
_earned[i] = 0;
_feedback[i] = "";
}
HideAllPanels();
if (!keepActive)
{
HideSplintObjects();
if (_manMuban != null) _manMuban.SetActive(false);
}
}
private void ForceHideExamOnlyOutlines()
{
ShoseClickHandler shose = FindObjectOfType<ShoseClickHandler>(true);
if (shose != null) shose.ForceHideOutline();
ChanraoTuibuHandler chanraoTuibu = FindObjectOfType<ChanraoTuibuHandler>(true);
if (chanraoTuibu != null) chanraoTuibu.ForceHideOutline();
}
private void MoveWomanToExamPoint()
{
GameObject woman = FindSceneObject("woman");
GameObject pointArray = FindSceneObject("womanPointArray");
Transform target = pointArray != null ? pointArray.transform.Find("point1") : null;
if (woman == null || target == null) return;
woman.transform.position = target.position;
woman.transform.rotation = target.rotation;
Animator anim = woman.GetComponent<Animator>();
if (anim != null)
{
anim.SetBool("IsCheck", false);
anim.SetBool("isCrouch", false);
}
}
private bool IsExperimentPageVisible()
{
GameObject panel = FindSceneObject("ExperimentPanel");
return panel == null || panel.activeInHierarchy;
}
private GameObject FindSceneObject(string objectName)
{
GameObject found = GameObject.Find(objectName);
if (found != null) return found;
Transform[] all = Resources.FindObjectsOfTypeAll<Transform>();
for (int i = 0; i < all.Length; i++)
{
Transform t = all[i];
if (t != null && t.gameObject.scene.IsValid() && t.name == objectName)
{
return t.gameObject;
}
}
return null;
}
}