From a634d92b9c6a51f58d19cd3be24310c656051c71 Mon Sep 17 00:00:00 2001
From: bottlefish <781230111@qq.com>
Date: Mon, 3 Nov 2025 18:24:38 +0800
Subject: [PATCH 1/6] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=B0=9D=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Scripts/MiniGame/HuoShan/NewExpress.meta | 8 +
.../HuoShan/NewExpress/BackgroundTextLayer.cs | 195 +++++++++
.../NewExpress/BackgroundTextLayer.cs.meta | 11 +
.../HuoShan/NewExpress/CircularViewport.cs | 155 ++++++++
.../NewExpress/CircularViewport.cs.meta | 11 +
.../NewExpress/LanguageImaginationSystem.cs | 325 +++++++++++++++
.../LanguageImaginationSystem.cs.meta | 11 +
.../LanguageImaginationYarnCommand.cs | 55 +++
.../LanguageImaginationYarnCommand.cs.meta | 11 +
.../MiniGame/HuoShan/NewExpress/README.md | 96 +++++
.../HuoShan/NewExpress/README.md.meta | 7 +
.../HuoShan/NewExpress/SentenceBuilder.cs | 375 ++++++++++++++++++
.../NewExpress/SentenceBuilder.cs.meta | 11 +
.../MiniGame/HuoShan/NewExpress/TextNode.cs | 75 ++++
.../HuoShan/NewExpress/TextNode.cs.meta | 11 +
15 files changed, 1357 insertions(+)
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress.meta
new file mode 100644
index 000000000..06de43429
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a33a05a3339562c4a808f8f17e7a94b1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs
new file mode 100644
index 000000000..acb371ceb
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs
@@ -0,0 +1,195 @@
+using UnityEngine;
+using TMPro;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AibisDream.MiniGame.HuoShan
+{
+ ///
+ /// 背景文字层 - 生成和管理大量随机变化的文字,文字间寻找关联形成词语连线
+ ///
+ public class BackgroundTextLayer
+ {
+ private List _allNodes;
+ private List _chineseChars;
+ private float _fieldWidth;
+ private float _fieldHeight;
+ private int _nodeCount;
+ private Transform _parentTransform;
+ private GameObject _textPrefab;
+
+ // 文字变化参数
+ private float _changeInterval = 0.5f;
+ private float _lastChangeTime;
+
+ // 连线参数
+ private float _connectionRadius = 2f;
+ private float _connectionProbability = 0.3f;
+
+ public BackgroundTextLayer(float fieldWidth, float fieldHeight, int nodeCount, Transform parentTransform)
+ {
+ _fieldWidth = fieldWidth;
+ _fieldHeight = fieldHeight;
+ _nodeCount = nodeCount;
+ _parentTransform = parentTransform;
+ _allNodes = new List();
+
+ // 初始化中文字符集
+ InitializeChineseChars();
+
+ // 创建文字预制体
+ CreateTextPrefab();
+
+ // 生成初始节点
+ GenerateNodes();
+ }
+
+ private void InitializeChineseChars()
+ {
+ _chineseChars = new List();
+ // 常用中文字符(可以扩展)
+ for (int i = 0x4e00; i <= 0x9fff; i++)
+ {
+ _chineseChars.Add(char.ConvertFromUtf32(i));
+ }
+ // 添加一些常用字
+ _chineseChars.AddRange(new[] { "的", "是", "在", "了", "和", "有", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这" });
+ }
+
+ private void CreateTextPrefab()
+ {
+ _textPrefab = new GameObject("TextNodePrefab");
+ _textPrefab.hideFlags = HideFlags.HideAndDontSave;
+
+ var textMesh = _textPrefab.AddComponent();
+ textMesh.fontSize = 24;
+ textMesh.alignment = TextAlignmentOptions.Center;
+ textMesh.color = Color.white;
+ textMesh.enableWordWrapping = false;
+ textMesh.overflowMode = TextOverflowModes.Overflow;
+
+ _textPrefab.SetActive(false);
+ }
+
+ private void GenerateNodes()
+ {
+ for (int i = 0; i < _nodeCount; i++)
+ {
+ Vector2 randomPos = new Vector2(
+ Random.Range(-_fieldWidth * 0.5f, _fieldWidth * 0.5f),
+ Random.Range(-_fieldHeight * 0.5f, _fieldHeight * 0.5f)
+ );
+
+ string randomChar = _chineseChars[Random.Range(0, _chineseChars.Count)];
+ TextNode node = new TextNode(randomPos, randomChar);
+
+ // 创建GameObject
+ GameObject nodeObj = Object.Instantiate(_textPrefab, _parentTransform);
+ nodeObj.SetActive(true);
+ nodeObj.name = $"TextNode_{i}";
+ node.GameObject = nodeObj;
+ node.TextComponent = nodeObj.GetComponent();
+
+ node.UpdateVisual();
+ _allNodes.Add(node);
+ }
+
+ // 建立初始连接关系
+ UpdateConnections();
+ }
+
+ public void Update(float deltaTime)
+ {
+ _lastChangeTime += deltaTime;
+
+ if (_lastChangeTime >= _changeInterval)
+ {
+ _lastChangeTime = 0f;
+
+ // 随机改变一些文字(排除已选定的和候选的)
+ foreach (var node in _allNodes)
+ {
+ if (node.State == TextNodeState.Random)
+ {
+ // 随机改变文字
+ if (Random.value < 0.1f) // 10%概率改变
+ {
+ node.Text = _chineseChars[Random.Range(0, _chineseChars.Count)];
+ node.UpdateVisual();
+ }
+
+ // 随机改变亮度
+ node.Brightness = Random.Range(0.3f, 1f);
+ node.UpdateVisual();
+ }
+ }
+
+ // 更新连接关系
+ UpdateConnections();
+ }
+ }
+
+ private void UpdateConnections()
+ {
+ // 清除所有连接
+ foreach (var node in _allNodes)
+ {
+ node.ConnectedNodes.Clear();
+ }
+
+ // 重新建立连接(基于距离和概率)
+ for (int i = 0; i < _allNodes.Count; i++)
+ {
+ for (int j = i + 1; j < _allNodes.Count; j++)
+ {
+ var nodeA = _allNodes[i];
+ var nodeB = _allNodes[j];
+
+ float distance = Vector2.Distance(nodeA.Position, nodeB.Position);
+
+ if (distance < _connectionRadius && Random.value < _connectionProbability)
+ {
+ nodeA.AddConnection(nodeB);
+ nodeB.AddConnection(nodeA);
+ }
+ }
+ }
+ }
+
+ public List GetAllNodes()
+ {
+ return _allNodes;
+ }
+
+ public List GetNodesInRange(Vector2 center, float radius)
+ {
+ return _allNodes.Where(node =>
+ Vector2.Distance(node.Position, center) <= radius
+ ).ToList();
+ }
+
+ public void SetNodeState(TextNode node, TextNodeState state)
+ {
+ node.State = state;
+ node.UpdateVisual();
+ }
+
+ public void Cleanup()
+ {
+ foreach (var node in _allNodes)
+ {
+ if (node.GameObject != null)
+ {
+ Object.Destroy(node.GameObject);
+ }
+ }
+ _allNodes.Clear();
+
+ if (_textPrefab != null)
+ {
+ Object.Destroy(_textPrefab);
+ }
+ }
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta
new file mode 100644
index 000000000..b2b973262
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c5da4de45a1d4c741b06d9c9c0519ff4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs
new file mode 100644
index 000000000..831330086
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs
@@ -0,0 +1,155 @@
+using UnityEngine;
+using Shapes;
+
+namespace AibisDream.MiniGame.HuoShan
+{
+ ///
+ /// 圆形可视区域 - 使用Shapes绘制圆形遮罩,支持鼠标拖拽移动
+ ///
+ public class CircularViewport : ImmediateModeShapeDrawer
+ {
+ [Header("圆形区域设置")]
+ public float viewportRadius = 5f;
+ public Color viewportBorderColor = Color.white;
+ public float borderWidth = 0.1f;
+
+ [Header("拖拽设置")]
+ public bool enableDrag = true;
+ public float dragSpeed = 1f;
+
+ [Header("Gizmo调试设置")]
+ public bool showGizmos = true;
+ public bool showViewportCircle = true;
+ public bool showCenterPoint = true;
+
+ private Vector2 _viewportCenter;
+ private bool _isDragging = false;
+ private Vector2 _lastMousePosition;
+ private Camera _mainCamera;
+
+ public Vector2 ViewportCenter => _viewportCenter;
+ public float ViewportRadius => viewportRadius;
+
+ private void Start()
+ {
+ _viewportCenter = Vector2.zero;
+ _mainCamera = Camera.main;
+ if (_mainCamera == null)
+ {
+ _mainCamera = FindObjectOfType();
+ }
+ }
+
+ private void Update()
+ {
+ if (!enableDrag) return;
+
+ HandleMouseInput();
+ }
+
+ private void HandleMouseInput()
+ {
+ if (Input.GetMouseButtonDown(0))
+ {
+ Vector2 mouseWorldPos = GetMouseWorldPosition();
+ float distance = Vector2.Distance(mouseWorldPos, _viewportCenter);
+
+ // 如果点击在圆形区域内,开始拖拽
+ if (distance <= viewportRadius)
+ {
+ _isDragging = true;
+ _lastMousePosition = mouseWorldPos;
+ }
+ }
+
+ if (Input.GetMouseButton(0) && _isDragging)
+ {
+ Vector2 currentMousePos = GetMouseWorldPosition();
+ Vector2 delta = currentMousePos - _lastMousePosition;
+ _viewportCenter += delta * dragSpeed;
+ _lastMousePosition = currentMousePos;
+ }
+
+ if (Input.GetMouseButtonUp(0))
+ {
+ _isDragging = false;
+ }
+ }
+
+ private Vector2 GetMouseWorldPosition()
+ {
+ if (_mainCamera == null) return Vector2.zero;
+
+ Vector3 mousePos = Input.mousePosition;
+ mousePos.z = _mainCamera.nearClipPlane + 1f;
+ Vector3 worldPos = _mainCamera.ScreenToWorldPoint(mousePos);
+ return new Vector2(worldPos.x, worldPos.y);
+ }
+
+ public override void DrawShapes(Camera cam)
+ {
+ using (Draw.Command(cam))
+ {
+ Draw.ResetAllDrawStates();
+ Draw.LineGeometry = LineGeometry.Volumetric3D;
+
+ // 绘制圆形边框
+ Draw.Ring(new Vector3(_viewportCenter.x, _viewportCenter.y, 0),
+ viewportRadius, borderWidth, viewportBorderColor);
+ }
+ }
+
+ public bool IsPositionInViewport(Vector2 position)
+ {
+ return Vector2.Distance(position, _viewportCenter) <= viewportRadius;
+ }
+
+ public void SetViewportCenter(Vector2 center)
+ {
+ _viewportCenter = center;
+ }
+
+ private void OnDrawGizmos()
+ {
+ if (!showGizmos) return;
+
+ Vector3 center = new Vector3(_viewportCenter.x, _viewportCenter.y, 0) + transform.position;
+
+ // 绘制圆形视口
+ if (showViewportCircle)
+ {
+ Gizmos.color = viewportBorderColor;
+ // 使用多个线段绘制圆形
+ int segments = 32;
+ float angleStep = 360f / segments;
+ Vector3 prevPoint = center + new Vector3(viewportRadius, 0, 0);
+
+ for (int i = 1; i <= segments; i++)
+ {
+ float angle = i * angleStep * Mathf.Deg2Rad;
+ Vector3 currentPoint = center + new Vector3(
+ Mathf.Cos(angle) * viewportRadius,
+ Mathf.Sin(angle) * viewportRadius,
+ 0
+ );
+ Gizmos.DrawLine(prevPoint, currentPoint);
+ prevPoint = currentPoint;
+ }
+ }
+
+ // 绘制中心点
+ if (showCenterPoint)
+ {
+ Gizmos.color = Color.green;
+ Gizmos.DrawSphere(center, 0.1f);
+
+ // 绘制坐标轴
+ Gizmos.color = Color.red;
+ Gizmos.DrawLine(center, center + Vector3.right * 0.5f);
+ Gizmos.color = Color.green;
+ Gizmos.DrawLine(center, center + Vector3.up * 0.5f);
+ }
+ }
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta
new file mode 100644
index 000000000..70d934783
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 68f10137fe541b44485fa63022e29524
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs
new file mode 100644
index 000000000..38ab64240
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs
@@ -0,0 +1,325 @@
+using UnityEngine;
+using System.Collections.Generic;
+using Shapes;
+
+namespace AibisDream.MiniGame.HuoShan
+{
+ ///
+ /// 语言想象系统主管理器 - 协调各模块工作
+ ///
+ [RequireComponent(typeof(CircularViewport))]
+ public class LanguageImaginationSystem : ImmediateModeShapeDrawer
+ {
+ [Header("场设置")]
+ public float fieldWidth = 50f;
+ public float fieldHeight = 50f;
+ public int backgroundNodeCount = 200;
+
+ [Header("圆形视口设置")]
+ public float viewportRadius = 5f;
+
+ [Header("连线设置")]
+ public float connectionLineWidth = 0.05f;
+ public Color backgroundConnectionColor = new Color(1f, 1f, 1f, 0.3f);
+ public Color sentenceConnectionColor = Color.yellow;
+ public Color candidateConnectionColor = Color.cyan;
+
+ [Header("Gizmo调试设置")]
+ public bool showGizmos = true;
+ public bool showFieldBounds = true;
+ public bool showNodes = true;
+ public bool showConnections = true;
+ public bool showSentenceNodes = true;
+ public float nodeGizmoSize = 0.2f;
+
+ private BackgroundTextLayer _backgroundLayer;
+ private CircularViewport _viewport;
+ private SentenceBuilder _sentenceBuilder;
+
+ private void Start()
+ {
+ Initialize();
+ }
+
+ private void Initialize()
+ {
+ // 初始化圆形视口
+ _viewport = GetComponent();
+ if (_viewport == null)
+ {
+ _viewport = gameObject.AddComponent();
+ }
+ _viewport.viewportRadius = viewportRadius;
+
+ // 初始化背景文字层
+ _backgroundLayer = new BackgroundTextLayer(
+ fieldWidth,
+ fieldHeight,
+ backgroundNodeCount,
+ transform
+ );
+
+ // 初始化造句系统
+ _sentenceBuilder = new SentenceBuilder(_backgroundLayer, _viewport, this);
+ }
+
+ private void Update()
+ {
+ if (_backgroundLayer != null)
+ {
+ _backgroundLayer.Update(Time.deltaTime);
+ }
+ }
+
+ public override void DrawShapes(Camera cam)
+ {
+ using (Draw.Command(cam))
+ {
+ Draw.ResetAllDrawStates();
+ Draw.LineGeometry = LineGeometry.Volumetric3D;
+ Draw.Thickness = connectionLineWidth;
+
+ // 绘制背景文字之间的连线
+ if (_backgroundLayer != null)
+ {
+ DrawBackgroundConnections();
+ }
+
+ // 绘制句子和候选文字的连线
+ if (_sentenceBuilder != null)
+ {
+ DrawSentenceConnections();
+ }
+ }
+ }
+
+ private void DrawBackgroundConnections()
+ {
+ var allNodes = _backgroundLayer.GetAllNodes();
+ foreach (var node in allNodes)
+ {
+ // 只绘制在视口内的连线
+ if (!_viewport.IsPositionInViewport(node.Position))
+ continue;
+
+ foreach (var connectedNode in node.ConnectedNodes)
+ {
+ // 只绘制连接的另一端也在视口内的连线
+ if (!_viewport.IsPositionInViewport(connectedNode.Position))
+ continue;
+
+ // 跳过已经选定的节点之间的连线(由句子连线处理)
+ if (node.State == TextNodeState.Selected || connectedNode.State == TextNodeState.Selected)
+ continue;
+ if (node.State == TextNodeState.Candidate || connectedNode.State == TextNodeState.Candidate)
+ continue;
+
+ Vector3 posA = new Vector3(node.Position.x, node.Position.y, 0);
+ Vector3 posB = new Vector3(connectedNode.Position.x, connectedNode.Position.y, 0);
+
+ // 根据节点亮度调整连线颜色
+ float avgBrightness = (node.Brightness + connectedNode.Brightness) * 0.5f;
+ Color lineColor = backgroundConnectionColor * avgBrightness;
+
+ Draw.Line(posA, posB, connectionLineWidth, lineColor);
+ }
+ }
+ }
+
+ private void DrawSentenceConnections()
+ {
+ var sentenceNodes = _sentenceBuilder.GetSentenceNodes();
+
+ // 绘制句子节点之间的连线(稳定清晰)
+ for (int i = 0; i < sentenceNodes.Count - 1; i++)
+ {
+ var nodeA = sentenceNodes[i];
+ var nodeB = sentenceNodes[i + 1];
+
+ Vector3 posA = new Vector3(nodeA.Position.x, nodeA.Position.y, 0);
+ Vector3 posB = new Vector3(nodeB.Position.x, nodeB.Position.y, 0);
+
+ Draw.Line(posA, posB, connectionLineWidth * 2f, sentenceConnectionColor);
+ }
+
+ // 绘制候选文字与选定文字的连线
+ var allNodes = _backgroundLayer.GetAllNodes();
+ foreach (var node in allNodes)
+ {
+ if (node.State == TextNodeState.Candidate)
+ {
+ foreach (var connectedNode in node.ConnectedNodes)
+ {
+ if (connectedNode.State == TextNodeState.Selected)
+ {
+ Vector3 posA = new Vector3(node.Position.x, node.Position.y, 0);
+ Vector3 posB = new Vector3(connectedNode.Position.x, connectedNode.Position.y, 0);
+
+ // 根据概率调整连线颜色和宽度
+ float alpha = node.Probability;
+ Color lineColor = candidateConnectionColor;
+ lineColor.a = alpha;
+
+ Draw.Line(posA, posB, connectionLineWidth * (0.5f + alpha * 0.5f), lineColor);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// 开始构建句子(由Yarn指令调用)
+ ///
+ public void StartSentence(string sentence, int stuckIndex = -1)
+ {
+ if (_sentenceBuilder != null)
+ {
+ _sentenceBuilder.StartBuildingSentence(sentence, stuckIndex);
+ }
+ }
+
+ ///
+ /// 停止构建句子
+ ///
+ public void StopSentence()
+ {
+ if (_sentenceBuilder != null)
+ {
+ _sentenceBuilder.StopBuilding();
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if (_backgroundLayer != null)
+ {
+ _backgroundLayer.Cleanup();
+ }
+ }
+
+ private void OnDrawGizmos()
+ {
+ if (!showGizmos) return;
+
+ // 绘制场边界
+ if (showFieldBounds)
+ {
+ Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.3f);
+ Vector3 center = transform.position;
+ Vector3 size = new Vector3(fieldWidth, fieldHeight, 0.1f);
+ Gizmos.DrawWireCube(center, size);
+ }
+
+ // 绘制节点和连线
+ if (_backgroundLayer != null && Application.isPlaying)
+ {
+ var allNodes = _backgroundLayer.GetAllNodes();
+
+ // 绘制节点
+ if (showNodes)
+ {
+ foreach (var node in allNodes)
+ {
+ Vector3 nodePos = new Vector3(node.Position.x, node.Position.y, 0) + transform.position;
+
+ // 根据状态设置颜色
+ Color gizmoColor = GetNodeGizmoColor(node.State);
+ Gizmos.color = gizmoColor * node.Brightness;
+
+ // 根据状态设置大小
+ float size = nodeGizmoSize;
+ if (node.State == TextNodeState.Selected)
+ size = nodeGizmoSize * 1.5f;
+ else if (node.State == TextNodeState.Candidate)
+ size = nodeGizmoSize * 1.2f;
+ else if (node.State == TextNodeState.Stuck)
+ size = nodeGizmoSize * 1.3f;
+
+ Gizmos.DrawSphere(nodePos, size);
+
+ // 绘制文字标签
+ #if UNITY_EDITOR
+ UnityEditor.Handles.Label(nodePos + Vector3.up * 0.5f, node.Text);
+ #endif
+ }
+ }
+
+ // 绘制连线
+ if (showConnections)
+ {
+ foreach (var node in allNodes)
+ {
+ Vector3 nodePos = new Vector3(node.Position.x, node.Position.y, 0) + transform.position;
+
+ foreach (var connectedNode in node.ConnectedNodes)
+ {
+ Vector3 connectedPos = new Vector3(connectedNode.Position.x, connectedNode.Position.y, 0) + transform.position;
+
+ // 根据节点状态设置连线颜色
+ Color lineColor = GetConnectionGizmoColor(node, connectedNode);
+ Gizmos.color = lineColor;
+
+ Gizmos.DrawLine(nodePos, connectedPos);
+ }
+ }
+ }
+
+ // 绘制句子节点
+ if (showSentenceNodes && _sentenceBuilder != null)
+ {
+ var sentenceNodes = _sentenceBuilder.GetSentenceNodes();
+ for (int i = 0; i < sentenceNodes.Count; i++)
+ {
+ var node = sentenceNodes[i];
+ Vector3 nodePos = new Vector3(node.Position.x, node.Position.y, 0) + transform.position;
+
+ // 绘制句子节点高亮
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawWireSphere(nodePos, nodeGizmoSize * 2f);
+
+ // 绘制序号
+ #if UNITY_EDITOR
+ UnityEditor.Handles.Label(nodePos + Vector3.up * 1f, $"[{i}]");
+ #endif
+
+ // 绘制句子连线
+ if (i < sentenceNodes.Count - 1)
+ {
+ var nextNode = sentenceNodes[i + 1];
+ Vector3 nextPos = new Vector3(nextNode.Position.x, nextNode.Position.y, 0) + transform.position;
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawLine(nodePos, nextPos);
+ }
+ }
+ }
+ }
+ }
+
+ private Color GetNodeGizmoColor(TextNodeState state)
+ {
+ return state switch
+ {
+ TextNodeState.Selected => Color.yellow,
+ TextNodeState.Candidate => Color.cyan,
+ TextNodeState.Stuck => Color.red,
+ _ => Color.white
+ };
+ }
+
+ private Color GetConnectionGizmoColor(TextNode nodeA, TextNode nodeB)
+ {
+ // 句子连线
+ if (nodeA.State == TextNodeState.Selected && nodeB.State == TextNodeState.Selected)
+ return Color.yellow;
+
+ // 候选连线
+ if ((nodeA.State == TextNodeState.Candidate && nodeB.State == TextNodeState.Selected) ||
+ (nodeA.State == TextNodeState.Selected && nodeB.State == TextNodeState.Candidate))
+ return Color.cyan;
+
+ // 背景连线
+ return new Color(1f, 1f, 1f, 0.3f);
+ }
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta
new file mode 100644
index 000000000..0959e71e6
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bc6c154e0dac2e6469211e12b1f05fce
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs
new file mode 100644
index 000000000..6fae24ca1
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs
@@ -0,0 +1,55 @@
+using System.Collections;
+using Yarn.Unity;
+using UnityEngine;
+
+namespace AibisDream.MiniGame.HuoShan
+{
+ ///
+ /// 语言想象系统Yarn指令
+ ///
+ public static class LanguageImaginationYarnCommand
+ {
+ private static LanguageImaginationSystem GetSystem()
+ {
+ // 尝试从场景中查找LanguageImaginationSystem
+ LanguageImaginationSystem system = Object.FindObjectOfType();
+ if (system == null)
+ {
+ Debug.LogError("[LanguageImaginationYarnCommand] 未找到LanguageImaginationSystem组件!请在场景中添加该组件。");
+ }
+ return system;
+ }
+
+ ///
+ /// 开始构建句子
+ ///
+ /// 要构建的句子
+ /// 卡壳位置(从0开始,-1表示不卡壳)
+ [YarnCommand("language_imagine")]
+ public static IEnumerator LanguageImagine(string sentence, int stuckIndex = -1)
+ {
+ var system = GetSystem();
+ if (system != null)
+ {
+ system.StartSentence(sentence, stuckIndex);
+
+ // 等待句子构建完成(这里可以添加更复杂的等待逻辑)
+ yield return new WaitForSeconds(0.1f);
+ }
+ }
+
+ ///
+ /// 停止构建句子
+ ///
+ [YarnCommand("language_imagine_stop")]
+ public static void LanguageImagineStop()
+ {
+ var system = GetSystem();
+ if (system != null)
+ {
+ system.StopSentence();
+ }
+ }
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta
new file mode 100644
index 000000000..8dbc7fa7f
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ce8085db413e67d4c9ddd8a3415ae3cf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md b/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md
new file mode 100644
index 000000000..fc883a68b
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md
@@ -0,0 +1,96 @@
+# 语言想象功能使用说明
+
+## 功能概述
+
+这是一个语言想象系统,实现了以下功能:
+
+1. **背景文字层**:大量文字不断变化字符和明暗,文字间寻找关联形成词语连线
+2. **圆形可视区域**:通过鼠标拖拽移动圆形区域查看不同部分
+3. **造句功能**:根据Yarn指令传入的句子,逐步构建句子
+4. **卡壳功能**:可在指定位置卡壳,显示错误文字,需要点击移除
+
+## 文件结构
+
+- `TextNode.cs` - 文字节点类,管理单个文字的显示、状态和连接关系
+- `BackgroundTextLayer.cs` - 背景文字层系统,生成和管理背景文字
+- `CircularViewport.cs` - 圆形可视区域,使用Shapes绘制圆形遮罩
+- `SentenceBuilder.cs` - 造句系统,处理句子生成流程
+- `LanguageImaginationSystem.cs` - 主管理器,协调各模块工作
+- `LanguageImaginationYarnCommand.cs` - Yarn指令接口
+
+## 使用方法
+
+### 1. 场景设置
+
+1. 在场景中创建一个GameObject
+2. 添加 `LanguageImaginationSystem` 组件
+3. 调整参数:
+ - **场设置**:fieldWidth, fieldHeight, backgroundNodeCount
+ - **圆形视口设置**:viewportRadius
+ - **连线设置**:connectionLineWidth, 各种连线颜色
+
+### 2. Yarn指令使用
+
+#### 开始构建句子
+```
+<>
+```
+- 第一个参数:要构建的句子
+- 第二个参数(可选):卡壳位置(从0开始,-1表示不卡壳)。例如3表示在第4个字(索引3)处卡壳
+
+#### 停止构建句子
+```
+<>
+```
+
+### 3. 功能说明
+
+#### 背景文字层
+- 自动生成大量随机文字
+- 文字会随机变化字符和亮度
+- 文字之间会根据距离和概率建立连接关系
+- 连接关系会动态更新
+
+#### 圆形可视区域
+- 点击并拖拽圆形区域内的任意位置可以移动视口
+- 只有视口内的文字和连线会被显示
+
+#### 造句流程
+1. 在当前可视范围内找到或创建第一个字(如"这")
+2. 将该字设置为选定状态,放大并高亮显示
+3. 移动到句子位置
+4. 在选定字周围生成候选文字
+5. 候选文字的概率动态变化,直到目标字概率最高
+6. 目标字成为新的选定字,其他候选恢复随机
+7. 重复直到句子完成
+
+#### 卡壳功能
+- 当到达指定的卡壳位置时,会显示错误文字("我是个傻逼")
+- 错误文字显示为红色,概率100%
+- 点击错误文字可以移除
+- 全部移除后继续构建流程
+
+### 4. 视觉表现
+
+- **背景文字连线**:半透明白色,根据文字亮度调整
+- **句子连线**:黄色,较粗,稳定清晰
+- **候选文字连线**:青色,根据概率调整透明度和宽度
+- **选定文字**:黄色,放大1.5倍
+- **候选文字**:青色,亮度随概率变化
+- **卡壳文字**:红色,放大1.2倍
+
+## 注意事项
+
+1. 确保场景中有主摄像机
+2. 需要TextMeshPro支持
+3. 需要DOTween支持(用于动画)
+4. 需要Shapes库支持(用于绘制)
+5. 建议使用正交摄像机以获得更好的2D效果
+
+## 扩展建议
+
+1. 可以调整候选文字的数量和范围
+2. 可以自定义卡壳时的错误文字
+3. 可以添加更多视觉效果(如粒子效果)
+4. 可以优化性能(如对象池、LOD等)
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md.meta
new file mode 100644
index 000000000..489023e4c
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1500caf0ff27d574cbbc46a5b6954555
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs
new file mode 100644
index 000000000..9b19bbc25
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs
@@ -0,0 +1,375 @@
+using UnityEngine;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using DG.Tweening;
+
+namespace AibisDream.MiniGame.HuoShan
+{
+ ///
+ /// 造句系统 - 处理句子生成流程:选定文字、候选文字、概率变化、连线
+ ///
+ public class SentenceBuilder
+ {
+ private BackgroundTextLayer _backgroundLayer;
+ private CircularViewport _viewport;
+ private List _sentenceNodes;
+ private string _targetSentence;
+ private int _currentIndex = 0;
+ private bool _isBuilding = false;
+ private int _stuckIndex = -1; // 卡壳位置,-1表示不卡壳
+
+ // 候选文字相关
+ private List _candidateNodes;
+ private float _probabilityChangeSpeed = 0.5f;
+ private Coroutine _buildingCoroutine;
+ private MonoBehaviour _coroutineRunner;
+
+ // 句子连线设置
+ private float _sentenceLineSpacing = 1.5f;
+ private Vector2 _sentenceDirection = Vector2.right;
+
+ public SentenceBuilder(BackgroundTextLayer backgroundLayer, CircularViewport viewport, MonoBehaviour coroutineRunner)
+ {
+ _backgroundLayer = backgroundLayer;
+ _viewport = viewport;
+ _coroutineRunner = coroutineRunner;
+ _sentenceNodes = new List();
+ _candidateNodes = new List();
+ }
+
+ ///
+ /// 开始构建句子
+ ///
+ public void StartBuildingSentence(string sentence, int stuckIndex = -1)
+ {
+ if (_isBuilding)
+ {
+ StopBuilding();
+ }
+
+ _targetSentence = sentence;
+ _currentIndex = 0;
+ _stuckIndex = stuckIndex;
+ _isBuilding = true;
+
+ // 清理之前的句子节点
+ ClearSentence();
+
+ // 开始构建协程
+ _buildingCoroutine = _coroutineRunner.StartCoroutine(BuildingCoroutine());
+ }
+
+ private IEnumerator BuildingCoroutine()
+ {
+ // 等待一帧确保背景层已初始化
+ yield return null;
+
+ while (_currentIndex < _targetSentence.Length && _isBuilding)
+ {
+ char targetChar = _targetSentence[_currentIndex];
+
+ // 检查是否到达卡壳位置
+ if (_currentIndex == _stuckIndex)
+ {
+ yield return _coroutineRunner.StartCoroutine(HandleStuck());
+ continue;
+ }
+
+ // 在当前可视范围内找到或创建目标文字
+ TextNode targetNode = FindOrCreateNodeInViewport(targetChar.ToString());
+
+ if (targetNode != null)
+ {
+ // 设置为选定状态
+ SetNodeAsSelected(targetNode, _currentIndex);
+ _sentenceNodes.Add(targetNode);
+
+ // 移动到句子位置
+ yield return MoveNodeToSentencePosition(targetNode, _currentIndex);
+
+ // 如果不是最后一个字,准备下一个字的候选
+ if (_currentIndex < _targetSentence.Length - 1)
+ {
+ yield return _coroutineRunner.StartCoroutine(PrepareNextCandidate());
+ }
+
+ _currentIndex++;
+ }
+
+ yield return new WaitForSeconds(0.5f);
+ }
+
+ _isBuilding = false;
+ }
+
+ private TextNode FindOrCreateNodeInViewport(string text)
+ {
+ // 首先尝试在可视范围内找到匹配的文字
+ var nodesInViewport = _backgroundLayer.GetNodesInRange(_viewport.ViewportCenter, _viewport.ViewportRadius);
+ var matchingNode = nodesInViewport.FirstOrDefault(n => n.Text == text && n.State == TextNodeState.Random);
+
+ if (matchingNode != null)
+ {
+ return matchingNode;
+ }
+
+ // 如果找不到,在可视范围内随机选择一个节点并改变其文字
+ if (nodesInViewport.Count > 0)
+ {
+ var randomNode = nodesInViewport[Random.Range(0, nodesInViewport.Count)];
+ randomNode.Text = text;
+ randomNode.UpdateVisual();
+ return randomNode;
+ }
+
+ return null;
+ }
+
+ private void SetNodeAsSelected(TextNode node, int index)
+ {
+ node.State = TextNodeState.Selected;
+ node.SentenceIndex = index;
+ node.Scale = 1.5f;
+ node.Brightness = 1f;
+ node.Color = Color.yellow;
+ node.UpdateVisual();
+
+ // 放大动画
+ node.GameObject.transform.DOScale(1.5f, 0.3f).SetEase(Ease.OutBack);
+ }
+
+ private IEnumerator MoveNodeToSentencePosition(TextNode node, int index)
+ {
+ Vector2 targetPos = GetSentencePosition(index);
+
+ // 使用DOTween移动
+ yield return node.GameObject.transform.DOMove(
+ new Vector3(targetPos.x, targetPos.y, 0),
+ 0.5f
+ ).SetEase(Ease.OutQuad).WaitForCompletion();
+
+ node.Position = targetPos;
+ }
+
+ private Vector2 GetSentencePosition(int index)
+ {
+ // 以视口中心为起点,沿方向排列
+ Vector2 startPos = _viewport.ViewportCenter;
+ Vector2 offset = _sentenceDirection * index * _sentenceLineSpacing;
+ return startPos + offset;
+ }
+
+ private IEnumerator PrepareNextCandidate()
+ {
+ if (_currentIndex >= _targetSentence.Length - 1) yield break;
+
+ char nextChar = _targetSentence[_currentIndex + 1];
+
+ // 清除之前的候选
+ ClearCandidates();
+
+ // 在当前选定节点周围找到几个节点作为候选
+ TextNode selectedNode = _sentenceNodes[_currentIndex];
+ var nearbyNodes = _backgroundLayer.GetNodesInRange(selectedNode.Position, 3f)
+ .Where(n => n.State == TextNodeState.Random)
+ .Take(5)
+ .ToList();
+
+ // 确保目标字符在候选列表中
+ bool hasTargetChar = false;
+ foreach (var node in nearbyNodes)
+ {
+ if (node.Text == nextChar.ToString())
+ {
+ hasTargetChar = true;
+ break;
+ }
+ }
+
+ if (!hasTargetChar && nearbyNodes.Count > 0)
+ {
+ nearbyNodes[0].Text = nextChar.ToString();
+ nearbyNodes[0].UpdateVisual();
+ }
+
+ // 设置候选状态
+ _candidateNodes.Clear();
+ foreach (var node in nearbyNodes)
+ {
+ node.State = TextNodeState.Candidate;
+ node.Color = Color.cyan;
+ node.Probability = Random.Range(0f, 0.5f);
+ node.AddConnection(selectedNode);
+ selectedNode.AddConnection(node);
+ node.UpdateVisual();
+ _candidateNodes.Add(node);
+ }
+
+ // 概率变化动画
+ yield return _coroutineRunner.StartCoroutine(AnimateCandidateProbabilities(nextChar.ToString()));
+ }
+
+ private IEnumerator AnimateCandidateProbabilities(string targetText)
+ {
+ float duration = 2f;
+ float elapsed = 0f;
+
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ float t = elapsed / duration;
+
+ foreach (var node in _candidateNodes)
+ {
+ if (node.Text == targetText)
+ {
+ // 目标字符概率逐渐增加到1
+ node.Probability = Mathf.Lerp(node.Probability, 1f, t * 0.1f);
+ }
+ else
+ {
+ // 其他字符概率逐渐降低
+ node.Probability = Mathf.Lerp(node.Probability, 0f, t * 0.1f);
+ }
+
+ // 根据概率更新亮度
+ node.Brightness = 0.5f + node.Probability * 0.5f;
+ node.UpdateVisual();
+ }
+
+ yield return null;
+ }
+
+ // 找到概率最高的节点(应该是目标字符)
+ var selectedCandidate = _candidateNodes.OrderByDescending(n => n.Probability).FirstOrDefault();
+ if (selectedCandidate != null && selectedCandidate.Text == targetText)
+ {
+ // 清除候选状态,恢复随机
+ ClearCandidates();
+ selectedCandidate.State = TextNodeState.Random;
+ }
+ }
+
+ private IEnumerator HandleStuck()
+ {
+ // 生成卡壳的错误文字
+ string stuckText = "我是个傻逼";
+
+ // 在当前位置创建卡壳文字
+ List stuckNodes = new List();
+ Vector2 currentPos = GetSentencePosition(_currentIndex);
+
+ for (int i = 0; i < stuckText.Length; i++)
+ {
+ TextNode stuckNode = FindOrCreateNodeInViewport(stuckText[i].ToString());
+ if (stuckNode != null)
+ {
+ stuckNode.State = TextNodeState.Stuck;
+ stuckNode.IsStuck = true;
+ stuckNode.SentenceIndex = _currentIndex + i;
+ stuckNode.Color = Color.red;
+ stuckNode.Probability = 1f;
+ stuckNode.Scale = 1.2f;
+ stuckNode.Position = currentPos + Vector2.right * i * _sentenceLineSpacing;
+ stuckNode.UpdateVisual();
+ stuckNodes.Add(stuckNode);
+ }
+ }
+
+ // 等待玩家点击移除所有卡壳文字
+ while (stuckNodes.Count > 0)
+ {
+ yield return null;
+
+ // 检查点击
+ if (Input.GetMouseButtonDown(0))
+ {
+ Vector2 mousePos = GetMouseWorldPosition();
+ for (int i = stuckNodes.Count - 1; i >= 0; i--)
+ {
+ var node = stuckNodes[i];
+ float distance = Vector2.Distance(mousePos, node.Position);
+ if (distance < 1f) // 点击范围
+ {
+ // 移除节点
+ node.State = TextNodeState.Random;
+ node.IsStuck = false;
+ node.Color = Color.white;
+ node.UpdateVisual();
+ stuckNodes.RemoveAt(i);
+
+ // 销毁GameObject
+ if (node.GameObject != null)
+ {
+ Object.Destroy(node.GameObject);
+ }
+ }
+ }
+ }
+ }
+
+ // 卡壳处理完成,继续构建
+ }
+
+ private Vector2 GetMouseWorldPosition()
+ {
+ Camera cam = Camera.main;
+ if (cam == null) return Vector2.zero;
+
+ Vector3 mousePos = Input.mousePosition;
+ mousePos.z = cam.nearClipPlane + 1f;
+ Vector3 worldPos = cam.ScreenToWorldPoint(mousePos);
+ return new Vector2(worldPos.x, worldPos.y);
+ }
+
+ private void ClearCandidates()
+ {
+ foreach (var node in _candidateNodes)
+ {
+ if (node.State == TextNodeState.Candidate)
+ {
+ node.State = TextNodeState.Random;
+ node.Color = Color.white;
+ node.Probability = 0f;
+ node.Brightness = 1f;
+ node.UpdateVisual();
+ }
+ }
+ _candidateNodes.Clear();
+ }
+
+ private void ClearSentence()
+ {
+ foreach (var node in _sentenceNodes)
+ {
+ if (node.State == TextNodeState.Selected)
+ {
+ node.State = TextNodeState.Random;
+ node.SentenceIndex = -1;
+ node.Scale = 1f;
+ node.Color = Color.white;
+ node.UpdateVisual();
+ }
+ }
+ _sentenceNodes.Clear();
+ }
+
+ public void StopBuilding()
+ {
+ _isBuilding = false;
+ if (_buildingCoroutine != null)
+ {
+ _coroutineRunner.StopCoroutine(_buildingCoroutine);
+ _buildingCoroutine = null;
+ }
+ ClearCandidates();
+ }
+
+ public List GetSentenceNodes()
+ {
+ return _sentenceNodes;
+ }
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta
new file mode 100644
index 000000000..cec804d5c
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fcab091b0b0e39142ae616cc9a548074
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs
new file mode 100644
index 000000000..c431deb0a
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs
@@ -0,0 +1,75 @@
+using UnityEngine;
+using TMPro;
+using System.Collections.Generic;
+
+namespace AibisDream.MiniGame.HuoShan
+{
+ ///
+ /// 文字节点类 - 管理单个文字的显示、状态和连接关系
+ ///
+ public class TextNode
+ {
+ public Vector2 Position { get; set; }
+ public string Text { get; set; }
+ public TextNodeState State { get; set; }
+ public float Brightness { get; set; } = 1f;
+ public float Scale { get; set; } = 1f;
+ public Color Color { get; set; } = Color.white;
+ public float Probability { get; set; } = 0f; // 候选文字的概率
+
+ // 连接关系
+ public List ConnectedNodes { get; private set; }
+
+ // UI组件引用
+ public TextMeshPro TextComponent { get; set; }
+ public GameObject GameObject { get; set; }
+
+ // 句子相关
+ public int SentenceIndex { get; set; } = -1; // 在句子中的索引,-1表示不在句子中
+ public bool IsStuck { get; set; } = false; // 是否处于卡壳状态
+
+ public TextNode(Vector2 position, string text)
+ {
+ Position = position;
+ Text = text;
+ State = TextNodeState.Random;
+ ConnectedNodes = new List();
+ }
+
+ public void AddConnection(TextNode node)
+ {
+ if (!ConnectedNodes.Contains(node))
+ {
+ ConnectedNodes.Add(node);
+ }
+ }
+
+ public void RemoveConnection(TextNode node)
+ {
+ ConnectedNodes.Remove(node);
+ }
+
+ public void UpdateVisual()
+ {
+ if (TextComponent != null)
+ {
+ TextComponent.text = Text;
+ TextComponent.color = Color * Brightness;
+ TextComponent.transform.localScale = Vector3.one * Scale;
+ TextComponent.transform.position = new Vector3(Position.x, Position.y, 0);
+ }
+ }
+ }
+
+ ///
+ /// 文字节点状态
+ ///
+ public enum TextNodeState
+ {
+ Random, // 随机变化状态
+ Selected, // 已选定的文字(句子中的字)
+ Candidate, // 候选文字
+ Stuck // 卡壳状态
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta
new file mode 100644
index 000000000..4f69f4ade
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 27e691dacc3b63545924959f964de0bb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
From 8cf629b1c6badded8a87ab04a71a59568cb577eb Mon Sep 17 00:00:00 2001
From: bottlefish <781230111@qq.com>
Date: Wed, 12 Nov 2025 19:46:16 +0800
Subject: [PATCH 2/6] =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=A8=A1=E5=9D=97?=
=?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=BF=AD=E4=BB=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../LiberationSans SDF.asset | 4 +-
Assets/Render/Renderer2Dtest.asset | 22 +-
Assets/Scenes/languageTest.unity | 496 +++++
Assets/Scenes/languageTest.unity.meta | 7 +
.../Scripts/Framework/OtherKit/CameraKit.cs | 4 +-
.../MiniGame/HuoShan/Emo/Untitled-2.txt | 1020 +++++++++
.../MiniGame/HuoShan/Emo/Untitled-2.txt.meta | 7 +
Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt | 1871 +++++++++++++++++
.../MiniGame/HuoShan/Emo/尝试2.txt.meta | 7 +
Assets/Scripts/MiniGame/HuoShan/Language.meta | 8 +
.../HuoShan/Language/CandidateParticle.cs | 203 ++
.../CandidateParticle.cs.meta} | 2 +-
.../HuoShan/Language/ConnectionRenderer.cs | 210 ++
.../ConnectionRenderer.cs.meta} | 2 +-
.../HuoShan/Language/EffectPropagation.cs | 154 ++
.../EffectPropagation.cs.meta} | 2 +-
.../HuoShan/Language/FloatingTextParticle.cs | 42 +
.../FloatingTextParticle.cs.meta} | 2 +-
.../Language/LanguageParticleManager.cs | 795 +++++++
.../Language/LanguageParticleManager.cs.meta | 11 +
.../LanguageParticleSystem_Summary.md | 132 ++
.../LanguageParticleSystem_Summary.md.meta | 7 +
.../MiniGame/HuoShan/Language/QuickStart.md | 150 ++
.../HuoShan/Language/QuickStart.md.meta | 7 +
.../MiniGame/HuoShan/Language/README.md | 212 ++
.../{NewExpress => Language}/README.md.meta | 2 +-
.../MiniGame/HuoShan/Language/TextParticle.cs | 238 +++
.../HuoShan/Language/TextParticle.cs.meta | 11 +
.../HuoShan/Language/WaveformPoint.cs | 210 ++
.../HuoShan/Language/WaveformPoint.cs.meta | 11 +
.../HuoShan/Language/WaveformRenderer.cs | 266 +++
.../HuoShan/Language/WaveformRenderer.cs.meta | 11 +
.../HuoShan/NewExpress/BackgroundTextLayer.cs | 195 --
.../HuoShan/NewExpress/CircularViewport.cs | 155 --
.../NewExpress/LanguageImaginationSystem.cs | 325 ---
.../LanguageImaginationYarnCommand.cs | 55 -
.../MiniGame/HuoShan/NewExpress/README.md | 96 -
.../HuoShan/NewExpress/SentenceBuilder.cs | 375 ----
.../NewExpress/SentenceBuilder.cs.meta | 11 -
.../MiniGame/HuoShan/NewExpress/TextNode.cs | 75 -
.../HuoShan/NewExpress/TextNode.cs.meta | 11 -
41 files changed, 6105 insertions(+), 1319 deletions(-)
create mode 100644 Assets/Scenes/languageTest.unity
create mode 100644 Assets/Scenes/languageTest.unity.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs
rename Assets/Scripts/MiniGame/HuoShan/{NewExpress/LanguageImaginationSystem.cs.meta => Language/CandidateParticle.cs.meta} (83%)
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs
rename Assets/Scripts/MiniGame/HuoShan/{NewExpress/BackgroundTextLayer.cs.meta => Language/ConnectionRenderer.cs.meta} (83%)
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs
rename Assets/Scripts/MiniGame/HuoShan/{NewExpress/LanguageImaginationYarnCommand.cs.meta => Language/EffectPropagation.cs.meta} (83%)
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs
rename Assets/Scripts/MiniGame/HuoShan/{NewExpress/CircularViewport.cs.meta => Language/FloatingTextParticle.cs.meta} (83%)
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/README.md
rename Assets/Scripts/MiniGame/HuoShan/{NewExpress => Language}/README.md.meta (75%)
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs.meta
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs
create mode 100644 Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs.meta
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs
delete mode 100644 Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta
diff --git a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset
index 40d3e1120..51f09fa9f 100644
--- a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset
+++ b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c3ff50e3133d989180b187748ffc67903f1fede099a5b85b4fcbd8cb6e8be958
-size 2256862
+oid sha256:8d6c87f520f1c076c6a23d015fbfacbef24af6f0a6312c00b1a7e2ff34f40c27
+size 2264562
diff --git a/Assets/Render/Renderer2Dtest.asset b/Assets/Render/Renderer2Dtest.asset
index 197d64d62..fdae0d5da 100644
--- a/Assets/Render/Renderer2Dtest.asset
+++ b/Assets/Render/Renderer2Dtest.asset
@@ -463,19 +463,15 @@ MonoBehaviour:
m_PostInfinity: 2
m_RotationOrder: 4
_filter:
- - 0.0048309183
- - 0.016401261
- - 0.04532712
- - 0.08293076
- - 0.12053438
- - 0.14946026
- - 0.1610306
- - 0.14946026
- - 0.12053438
- - 0.08293076
- - 0.04532712
- - 0.016401261
- - 0.0048309183
+ - 0.007228916
+ - 0.043749984
+ - 0.12409639
+ - 0.20444278
+ - 0.24096388
+ - 0.20444278
+ - 0.12409639
+ - 0.043749984
+ - 0.007228916
--- !u!114 &-6086986437317696584
MonoBehaviour:
m_ObjectHideFlags: 3
diff --git a/Assets/Scenes/languageTest.unity b/Assets/Scenes/languageTest.unity
new file mode 100644
index 000000000..b59dddfd8
--- /dev/null
+++ b/Assets/Scenes/languageTest.unity
@@ -0,0 +1,496 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!29 &1
+OcclusionCullingSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_OcclusionBakeSettings:
+ smallestOccluder: 5
+ smallestHole: 0.25
+ backfaceThreshold: 100
+ m_SceneGUID: 00000000000000000000000000000000
+ m_OcclusionCullingData: {fileID: 0}
+--- !u!104 &2
+RenderSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 9
+ m_Fog: 0
+ m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
+ m_FogMode: 3
+ m_FogDensity: 0.01
+ m_LinearFogStart: 0
+ m_LinearFogEnd: 300
+ m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
+ m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
+ m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
+ m_AmbientIntensity: 1
+ m_AmbientMode: 3
+ m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
+ m_SkyboxMaterial: {fileID: 0}
+ m_HaloStrength: 0.5
+ m_FlareStrength: 1
+ m_FlareFadeSpeed: 3
+ m_HaloTexture: {fileID: 0}
+ m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
+ m_DefaultReflectionMode: 0
+ m_DefaultReflectionResolution: 128
+ m_ReflectionBounces: 1
+ m_ReflectionIntensity: 1
+ m_CustomReflection: {fileID: 0}
+ m_Sun: {fileID: 0}
+ m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
+ m_UseRadianceAmbientProbe: 0
+--- !u!157 &3
+LightmapSettings:
+ m_ObjectHideFlags: 0
+ serializedVersion: 12
+ m_GIWorkflowMode: 1
+ m_GISettings:
+ serializedVersion: 2
+ m_BounceScale: 1
+ m_IndirectOutputScale: 1
+ m_AlbedoBoost: 1
+ m_EnvironmentLightingMode: 0
+ m_EnableBakedLightmaps: 0
+ m_EnableRealtimeLightmaps: 0
+ m_LightmapEditorSettings:
+ serializedVersion: 12
+ m_Resolution: 2
+ m_BakeResolution: 40
+ m_AtlasSize: 1024
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
+ m_Padding: 2
+ m_LightmapParameters: {fileID: 0}
+ m_LightmapsBakeMode: 1
+ m_TextureCompression: 1
+ m_FinalGather: 0
+ m_FinalGatherFiltering: 1
+ m_FinalGatherRayCount: 256
+ m_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 0}
+ m_LightingSettings: {fileID: 0}
+--- !u!196 &4
+NavMeshSettings:
+ serializedVersion: 2
+ m_ObjectHideFlags: 0
+ m_BuildSettings:
+ serializedVersion: 3
+ agentTypeID: 0
+ agentRadius: 0.5
+ agentHeight: 2
+ agentSlope: 45
+ agentClimb: 0.4
+ ledgeDropHeight: 0
+ maxJumpAcrossDistance: 0
+ minRegionArea: 2
+ manualCellSize: 0
+ cellSize: 0.16666667
+ manualTileSize: 0
+ tileSize: 256
+ buildHeightMesh: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
+ debug:
+ m_Flags: 0
+ m_NavMeshData: {fileID: 0}
+--- !u!1 &125585505
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 125585506}
+ - component: {fileID: 125585507}
+ m_Layer: 0
+ m_Name: LanguageParticleSystem
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &125585506
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 125585505}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -2.8477545, y: -5.088608, z: -8.612933}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &125585507
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 125585505}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 79af040068968934c9a7d092267034f6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ worldCanvas: {fileID: 1665911935}
+ canvasSize: 10
+ chineseFontAsset: {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2}
+ floatingParticlePrefab: {fileID: 0}
+ candidateParticlePrefab: {fileID: 0}
+ floatingCount: 100
+ candidateCount: 50
+ redParticleCount: 8
+ connectionDistance: 2
+ textMargin: 1
+ targetSentence: "\u522B\u8FC7\u6765\u6211\u611F\u89C9\u5BB3\u6015"
+ waveformAmplitude: 0.3
+ interactionRadius: 0.5
+--- !u!1 &786157861
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 786157866}
+ - component: {fileID: 786157865}
+ - component: {fileID: 786157864}
+ - component: {fileID: 786157862}
+ m_Layer: 0
+ m_Name: Main Camera
+ m_TagString: MainCamera
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &786157862
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 786157861}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_RenderShadows: 1
+ m_RequiresDepthTextureOption: 2
+ m_RequiresOpaqueTextureOption: 2
+ m_CameraType: 0
+ m_Cameras: []
+ m_RendererIndex: -1
+ m_VolumeLayerMask:
+ serializedVersion: 2
+ m_Bits: 1
+ m_VolumeTrigger: {fileID: 0}
+ m_VolumeFrameworkUpdateModeOption: 2
+ m_RenderPostProcessing: 0
+ m_Antialiasing: 0
+ m_AntialiasingQuality: 2
+ m_StopNaN: 0
+ m_Dithering: 0
+ m_ClearDepth: 1
+ m_AllowXRRendering: 1
+ m_AllowHDROutput: 1
+ m_UseScreenCoordOverride: 0
+ m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
+ m_RequiresDepthTexture: 0
+ m_RequiresColorTexture: 0
+ m_Version: 2
+ m_TaaSettings:
+ quality: 3
+ frameInfluence: 0.1
+ jitterScale: 1
+ mipBias: 0
+ varianceClampScale: 0.9
+ contrastAdaptiveSharpening: 0
+--- !u!81 &786157864
+AudioListener:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 786157861}
+ m_Enabled: 1
+--- !u!20 &786157865
+Camera:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 786157861}
+ m_Enabled: 1
+ serializedVersion: 2
+ m_ClearFlags: 1
+ m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
+ m_projectionMatrixMode: 1
+ m_GateFitMode: 2
+ m_FOVAxisMode: 0
+ m_Iso: 200
+ m_ShutterSpeed: 0.005
+ m_Aperture: 16
+ m_FocusDistance: 10
+ m_FocalLength: 50
+ m_BladeCount: 5
+ m_Curvature: {x: 2, y: 11}
+ m_BarrelClipping: 0.25
+ m_Anamorphism: 0
+ m_SensorSize: {x: 36, y: 24}
+ m_LensShift: {x: 0, y: 0}
+ m_NormalizedViewPortRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 1
+ height: 1
+ near clip plane: 0.3
+ far clip plane: 1000
+ field of view: 60
+ orthographic: 1
+ orthographic size: 5
+ m_Depth: -1
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingPath: -1
+ m_TargetTexture: {fileID: 0}
+ m_TargetDisplay: 0
+ m_TargetEye: 3
+ m_HDR: 1
+ m_AllowMSAA: 1
+ m_AllowDynamicResolution: 0
+ m_ForceIntoRT: 0
+ m_OcclusionCulling: 1
+ m_StereoConvergence: 10
+ m_StereoSeparation: 0.022
+--- !u!4 &786157866
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 786157861}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: -10.72}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1665911932
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1665911936}
+ - component: {fileID: 1665911935}
+ - component: {fileID: 1665911934}
+ - component: {fileID: 1665911933}
+ m_Layer: 5
+ m_Name: Canvas
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1665911933
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1665911932}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_IgnoreReversedGraphics: 1
+ m_BlockingObjects: 0
+ m_BlockingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+--- !u!114 &1665911934
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1665911932}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_UiScaleMode: 0
+ m_ReferencePixelsPerUnit: 100
+ m_ScaleFactor: 1
+ m_ReferenceResolution: {x: 800, y: 600}
+ m_ScreenMatchMode: 0
+ m_MatchWidthOrHeight: 0
+ m_PhysicalUnit: 3
+ m_FallbackScreenDPI: 96
+ m_DefaultSpriteDPI: 96
+ m_DynamicPixelsPerUnit: 1
+ m_PresetInfoIsWorld: 1
+--- !u!223 &1665911935
+Canvas:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1665911932}
+ m_Enabled: 1
+ serializedVersion: 3
+ m_RenderMode: 2
+ m_Camera: {fileID: 0}
+ m_PlaneDistance: 100
+ m_PixelPerfect: 0
+ m_ReceivesEvents: 1
+ m_OverrideSorting: 0
+ m_OverridePixelPerfect: 0
+ m_SortingBucketNormalizedSize: 0
+ m_VertexColorAlwaysGammaSpace: 0
+ m_AdditionalShaderChannelsFlag: 25
+ m_UpdateRectTransformForStandalone: 0
+ m_SortingLayerID: 0
+ m_SortingOrder: 0
+ m_TargetDisplay: 0
+--- !u!224 &1665911936
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1665911932}
+ 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: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 10, y: 10}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!1 &1764870496
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1764870499}
+ - component: {fileID: 1764870498}
+ - component: {fileID: 1764870497}
+ m_Layer: 0
+ m_Name: EventSystem
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1764870497
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1764870496}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
+ m_HorizontalAxis: Horizontal
+ m_VerticalAxis: Vertical
+ m_SubmitButton: Submit
+ m_CancelButton: Cancel
+ m_InputActionsPerSecond: 10
+ m_RepeatDelay: 0.5
+ m_ForceModuleActive: 0
+--- !u!114 &1764870498
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1764870496}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_FirstSelected: {fileID: 0}
+ m_sendNavigationEvents: 1
+ m_DragThreshold: 10
+--- !u!4 &1764870499
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1764870496}
+ serializedVersion: 2
+ 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: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1660057539 &9223372036854775807
+SceneRoots:
+ m_ObjectHideFlags: 0
+ m_Roots:
+ - {fileID: 1665911936}
+ - {fileID: 786157866}
+ - {fileID: 1764870499}
+ - {fileID: 125585506}
diff --git a/Assets/Scenes/languageTest.unity.meta b/Assets/Scenes/languageTest.unity.meta
new file mode 100644
index 000000000..e356c7ee0
--- /dev/null
+++ b/Assets/Scenes/languageTest.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a10c0994c14362743ab6f65c6382410d
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Framework/OtherKit/CameraKit.cs b/Assets/Scripts/Framework/OtherKit/CameraKit.cs
index b3f2635a1..c5b500772 100644
--- a/Assets/Scripts/Framework/OtherKit/CameraKit.cs
+++ b/Assets/Scripts/Framework/OtherKit/CameraKit.cs
@@ -419,6 +419,8 @@ namespace AibisDream.Framework
BreakCamera,
DreamCamera,
Subway,
- SubwayTV
+ SubwayTV,
+ Language,
+ LanguageDeep
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt
new file mode 100644
index 000000000..a9b058f6a
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt
@@ -0,0 +1,1020 @@
+// 粒子类
+class Particle {
+ constructor(x, y, targetX, targetY) {
+ this.x = x;
+ this.y = y;
+ this.targetX = targetX;
+ this.targetY = targetY;
+ this.life = 1.0;
+ this.speed = random(0.02, 0.05);
+ this.size = random(2, 4);
+ this.alpha = random(150, 255);
+ }
+
+ update() {
+ this.x = lerp(this.x, this.targetX, this.speed);
+ this.y = lerp(this.y, this.targetY, this.speed);
+
+ let dist = distance(this.x, this.y, this.targetX, this.targetY);
+ if (dist < 5) {
+ this.life -= 0.05;
+ }
+ }
+
+ display() {
+ push();
+ noStroke();
+ fill(100, 200, 255, this.alpha * this.life);
+ circle(this.x, this.y, this.size);
+ pop();
+ }
+
+ isDead() {
+ return this.life <= 0;
+ }
+}
+
+function distance(x1, y1, x2, y2) {
+ return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
+}
+
+// 文字对象类
+class FloatingText {
+ constructor(x, y, radius) {
+ this.centerX = x;
+ this.centerY = y;
+ this.maxRadius = radius;
+
+ let angle = random(TWO_PI);
+ let r = random(this.maxRadius * 0.9);
+ this.x = this.centerX + cos(angle) * r;
+ this.y = this.centerY + sin(angle) * r;
+
+ this.targetX = this.x;
+ this.targetY = this.y;
+
+ this.speedX = random(-0.3, 0.3);
+ this.speedY = random(-0.3, 0.3);
+
+ this.noiseOffsetX = random(1000);
+ this.noiseOffsetY = random(1000);
+
+ this.updateText();
+
+ this.baseSize = random(20, 32);
+ this.size = this.baseSize;
+ this.targetSize = this.baseSize;
+
+ this.baseAlpha = random(50, 150);
+ this.alpha = this.baseAlpha;
+ this.targetAlpha = this.baseAlpha;
+
+ this.changeTimer = random(1, 2);
+ this.changeCounter = 0;
+
+ // 造句相关
+ this.isCandidate = false;
+ this.isSelected = false;
+ this.isFinal = false;
+ this.isEliminated = false;
+
+ // 概率相关
+ this.probability = 0;
+ this.targetProbability = 0;
+
+ // 候选字变化
+ this.candidateChars = [];
+ this.candidateIndex = 0;
+ this.candidateChangeTimer = 0;
+
+ // 抖动效果
+ this.shakeAmount = 0;
+ }
+
+ updateText() {
+ const chars = [
+ '梦', '想', '希', '望', '光', '影', '星', '月', '云', '风',
+ '诗', '歌', '舞', '画', '音', '色', '情', '爱', '心', '灵',
+ '天', '地', '人', '和', '美', '真', '善', '雅', '韵', '意',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ ];
+
+ let numChars = random() > 0.6 ? 2 : 1;
+ this.text = '';
+ for (let i = 0; i < numChars; i++) {
+ this.text += random(chars);
+ }
+ }
+
+ setCandidate(candidateChars, initialProbability) {
+ this.isCandidate = true;
+ this.candidateChars = candidateChars;
+ this.candidateIndex = 0;
+ this.text = candidateChars[0];
+ this.candidateChangeTimer = 0;
+ this.probability = initialProbability;
+ this.targetProbability = initialProbability;
+
+ // 根据初始概率设置大小
+ this.updateSizeByProbability();
+ this.shakeAmount = 3; // 不确定时抖动
+ }
+
+ updateSizeByProbability() {
+ // 概率越高,字越大
+ let sizeMultiplier = map(this.targetProbability, 0, 100, 1.2, 2.0);
+ this.targetSize = this.baseSize * sizeMultiplier;
+ this.targetAlpha = map(this.targetProbability, 0, 100, 150, 255);
+ }
+
+ setProbability(prob) {
+ this.targetProbability = prob;
+ this.updateSizeByProbability();
+
+ // 概率高的时候抖动减少
+ this.shakeAmount = map(prob, 0, 100, 5, 0);
+ }
+
+ eliminate() {
+ this.isEliminated = true;
+ this.targetAlpha = 0;
+ this.targetSize = this.baseSize * 0.5;
+ }
+
+ setSelected(targetX, targetY, isFirst) {
+ this.isCandidate = false;
+ this.isSelected = true;
+ this.shakeAmount = 0;
+
+ let dx = targetX - this.x;
+ let dy = targetY - this.y;
+ let maxMove = 40;
+
+ if (abs(dx) > maxMove || abs(dy) > maxMove) {
+ let angle = atan2(dy, dx);
+ this.targetX = this.x + cos(angle) * maxMove;
+ this.targetY = this.y + sin(angle) * maxMove;
+ } else {
+ this.targetX = targetX;
+ this.targetY = targetY;
+ }
+
+ if (isFirst) {
+ this.targetSize = 55;
+ this.targetAlpha = 255;
+ } else {
+ this.targetSize = 42;
+ this.targetAlpha = 255;
+ }
+ }
+
+ cancelCandidate() {
+ this.isCandidate = false;
+ this.isEliminated = false;
+ this.probability = 0;
+ this.targetProbability = 0;
+ this.targetSize = this.baseSize;
+ this.targetAlpha = this.baseAlpha;
+ this.candidateChars = [];
+ this.shakeAmount = 0;
+ this.updateText();
+ }
+
+ finalize() {
+ this.isFinal = true;
+ this.shakeAmount = 0;
+ }
+
+ reset() {
+ this.isCandidate = false;
+ this.isSelected = false;
+ this.isFinal = false;
+ this.isEliminated = false;
+ this.probability = 0;
+ this.targetProbability = 0;
+ this.targetSize = this.baseSize;
+ this.targetAlpha = this.baseAlpha;
+ this.size = this.baseSize;
+ this.alpha = this.baseAlpha;
+ this.candidateChars = [];
+ this.shakeAmount = 0;
+ this.updateText();
+ }
+
+ update() {
+ // 候选字缓慢变化
+ if (this.isCandidate && this.candidateChars.length > 1 && !this.isEliminated) {
+ this.candidateChangeTimer++;
+ if (this.candidateChangeTimer >= 12) {
+ this.candidateIndex = (this.candidateIndex + 1) % this.candidateChars.length;
+ this.text = this.candidateChars[this.candidateIndex];
+ this.candidateChangeTimer = 0;
+ }
+ }
+
+ // 概率平滑过渡
+ this.probability = lerp(this.probability, this.targetProbability, 0.1);
+
+ if (this.isSelected || this.isCandidate) {
+ this.x = lerp(this.x, this.targetX, 0.08);
+ this.y = lerp(this.y, this.targetY, 0.08);
+ this.size = lerp(this.size, this.targetSize, 0.1);
+ this.alpha = lerp(this.alpha, this.targetAlpha, 0.1);
+ } else {
+ let noiseX = noise(this.noiseOffsetX) * 2 - 1;
+ let noiseY = noise(this.noiseOffsetY) * 2 - 1;
+
+ this.x += noiseX * 0.5 + this.speedX;
+ this.y += noiseY * 0.5 + this.speedY;
+
+ this.noiseOffsetX += 0.01;
+ this.noiseOffsetY += 0.01;
+
+ let dx = this.x - this.centerX;
+ let dy = this.y - this.centerY;
+ let dist = sqrt(dx * dx + dy * dy);
+
+ if (dist > this.maxRadius) {
+ let angle = atan2(dy, dx);
+ this.x = this.centerX + cos(angle) * this.maxRadius;
+ this.y = this.centerY + sin(angle) * this.maxRadius;
+ this.speedX *= -0.8;
+ this.speedY *= -0.8;
+ }
+
+ this.changeCounter++;
+ if (this.changeCounter >= this.changeTimer) {
+ this.updateText();
+ this.changeCounter = 0;
+ this.changeTimer = random(5, 10);
+ }
+ }
+ }
+
+ display() {
+ push();
+
+ // 计算抖动偏移
+ let shakeX = random(-this.shakeAmount, this.shakeAmount);
+ let shakeY = random(-this.shakeAmount, this.shakeAmount);
+
+ // 根据概率显示彩色光晕
+ if (this.isCandidate && !this.isEliminated) {
+ let hue = map(this.probability, 0, 100, 200, 0); // 蓝色到红色
+ let glowSize = map(this.probability, 0, 100, 20, 50);
+
+ noStroke();
+ fill(hue, 200, 255, 30);
+ circle(this.x + shakeX, this.y + shakeY, this.size + glowSize);
+
+ // 边框
+ stroke(hue, 200, 255, this.alpha * 0.5);
+ strokeWeight(2);
+ noFill();
+ circle(this.x + shakeX, this.y + shakeY, this.size + 15);
+ }
+
+ // 显示文字
+ fill(255, this.alpha);
+ noStroke();
+ textAlign(CENTER, CENTER);
+ textSize(this.size);
+ text(this.text, this.x + shakeX, this.y + shakeY);
+
+ // 显示概率
+ if (this.isCandidate && !this.isEliminated && this.probability > 0) {
+ textSize(12);
+ fill(255, 200);
+ text(int(this.probability) + '%', this.x + shakeX, this.y + shakeY + this.size * 0.6);
+ }
+
+ pop();
+ }
+}
+
+let floatingTexts = [];
+let numTexts = 60;
+let circleRadius = 280;
+
+// 造句相关
+let targetSentence = "我想要被看到";
+let sentenceProgress = 0;
+let selectedTexts = [];
+let candidateTexts = [];
+let sentenceStarted = false;
+let sentenceComplete = false;
+
+// 粒子系统
+let particles = [];
+
+// 预测状态机
+let predictionState = 'idle'; // idle, round1, round2, round3, confirming
+let stateTimer = 0;
+
+function setup() {
+ createCanvas(windowWidth, windowHeight);
+
+ // 自动创建词云
+ for (let i = 0; i < numTexts; i++) {
+ floatingTexts.push(new FloatingText(width / 2, height / 2, circleRadius));
+ }
+}
+
+function draw() {
+ background(0);
+
+ // 更新和显示所有文字
+ for (let text of floatingTexts) {
+ text.update();
+ text.display();
+ }
+
+ // 更新和显示粒子
+ for (let i = particles.length - 1; i >= 0; i--) {
+ particles[i].update();
+ particles[i].display();
+ if (particles[i].isDead()) {
+ particles.splice(i, 1);
+ }
+ }
+
+ // 绘制已确定字符的连线
+ if (selectedTexts.length > 1) {
+ stroke(255, 180);
+ strokeWeight(2);
+ for (let i = 0; i < selectedTexts.length - 1; i++) {
+ let current = selectedTexts[i];
+ let next = selectedTexts[i + 1];
+ if (next.isFinal) {
+ line(current.x, current.y, next.x, next.y);
+ }
+ }
+ }
+
+ // 发射粒子(在预测阶段)
+ if ((predictionState === 'round1' || predictionState === 'round2' || predictionState === 'round3')
+ && selectedTexts.length > 0 && frameCount % 3 === 0) {
+ let lastSelected = selectedTexts[selectedTexts.length - 1];
+
+ for (let candidate of candidateTexts) {
+ if (!candidate.text.isEliminated) {
+ // 粒子密度根据概率
+ let particleCount = map(candidate.text.probability, 0, 100, 0.5, 3);
+ if (random() < particleCount / 3) {
+ particles.push(new Particle(
+ lastSelected.x,
+ lastSelected.y,
+ candidate.text.x,
+ candidate.text.y
+ ));
+ }
+ }
+ }
+ }
+
+ // 显示状态提示
+ if (sentenceStarted && !sentenceComplete) {
+ displayStateHint();
+ }
+
+ // 预测过程
+ if (sentenceStarted && sentenceProgress < targetSentence.length) {
+ handlePrediction();
+ }
+}
+
+function displayStateHint() {
+ push();
+ textAlign(LEFT, TOP);
+ textSize(14);
+ fill(150, 200, 255, 200);
+ noStroke();
+
+ let hint = '';
+ switch(predictionState) {
+ case 'round1':
+ hint = '正在分析可能性...';
+ break;
+ case 'round2':
+ hint = '匹配语法结构...';
+ break;
+ case 'round3':
+ hint = '计算最终概率...';
+ break;
+ case 'confirming':
+ hint = '确认结果中...';
+ break;
+ }
+
+ if (hint) {
+ text(hint, 30, 30);
+ }
+ pop();
+}
+
+function handlePrediction() {
+ stateTimer++;
+
+ switch(predictionState) {
+ case 'idle':
+ startPrediction();
+ break;
+
+ case 'round1':
+ // 第一轮:6-7个候选,概率20-40%(持续0.8秒)
+ if (stateTimer >= 48) {
+ eliminateRound1();
+ predictionState = 'round2';
+ stateTimer = 0;
+ }
+ break;
+
+ case 'round2':
+ // 第二轮:3-4个候选,概率40-60%(持续0.7秒)
+ if (stateTimer >= 42) {
+ eliminateRound2();
+ predictionState = 'round3';
+ stateTimer = 0;
+ }
+ break;
+
+ case 'round3':
+ // 第三轮:2个候选竞争,概率60-80%(持续1秒)
+ if (stateTimer >= 60) {
+ predictionState = 'confirming';
+ stateTimer = 0;
+ confirmPrediction();
+ }
+ break;
+
+ case 'confirming':
+ // 确认阶段:正确答案概率飙升到95%+(持续0.5秒)
+ if (stateTimer >= 30) {
+ finalizePrediction();
+ predictionState = 'idle';
+ stateTimer = 0;
+ sentenceProgress++;
+
+ if (sentenceProgress >= targetSentence.length) {
+ sentenceComplete = true;
+ setTimeout(() => {
+ resetSentence();
+ }, 1500);
+ }
+ }
+ break;
+ }
+}
+
+function startPrediction() {
+ let correctChar = targetSentence[sentenceProgress];
+
+ // 如果是第一个字,直接显示
+ if (sentenceProgress === 0) {
+ let availableTexts = floatingTexts.filter(t => !t.isSelected);
+ let selectedText = random(availableTexts);
+
+ let startX = width / 2 - (targetSentence.length * 45) / 2;
+ selectedText.text = correctChar;
+ selectedText.setSelected(startX, height / 2, true);
+ selectedText.finalize();
+ selectedTexts.push(selectedText);
+
+ sentenceProgress++;
+ predictionState = 'idle';
+ stateTimer = 0;
+ return;
+ }
+
+ // 生成候选字(6-7个)
+ let candidates = [correctChar];
+ const possibleChars = ['要', '爱', '见', '到', '你', '他', '她', '们', '的', '了', '吗', '呢', '想', '看', '被', '着', '给', '和', '在'];
+
+ let numCandidates = int(random(6, 8));
+ while (candidates.length < numCandidates) {
+ let distractor = random(possibleChars);
+ if (!candidates.includes(distractor)) {
+ candidates.push(distractor);
+ }
+ }
+
+ // 找到可用的文字对象
+ candidateTexts = [];
+ let availableTexts = floatingTexts.filter(t => !t.isSelected && !t.isCandidate);
+
+ for (let i = 0; i < candidates.length && i < availableTexts.length; i++) {
+ let text = availableTexts[i];
+ let charVariations = [candidates[i]];
+
+ // 添加变化字符
+ for (let j = 0; j < 2; j++) {
+ let variation = random(possibleChars);
+ if (!charVariations.includes(variation)) {
+ charVariations.push(variation);
+ }
+ }
+ charVariations.push(candidates[i]);
+
+ // 初始概率随机分配
+ let initialProb = random(15, 35);
+ text.setCandidate(charVariations, initialProb);
+
+ candidateTexts.push({
+ text: text,
+ correctChar: candidates[i],
+ isCorrect: candidates[i] === correctChar
+ });
+ }
+
+ predictionState = 'round1';
+ stateTimer = 0;
+}
+
+function eliminateRound1() {
+ // 淘汰概率最低的3个
+ candidateTexts.sort((a, b) => {
+ if (a.isCorrect) return 1; // 确保正确答案不被淘汰
+ if (b.isCorrect) return -1;
+ return a.text.probability - b.text.probability;
+ });
+
+ let toEliminate = min(3, candidateTexts.length - 3);
+ for (let i = 0; i < toEliminate; i++) {
+ candidateTexts[i].text.eliminate();
+ }
+
+ // 剩余候选概率上升到40-60%
+ for (let i = toEliminate; i < candidateTexts.length; i++) {
+ let newProb = random(40, 60);
+ if (candidateTexts[i].isCorrect) {
+ newProb = random(50, 65); // 正确答案稍高
+ }
+ candidateTexts[i].text.setProbability(newProb);
+ }
+}
+
+function eliminateRound2() {
+ // 移除已淘汰的
+ candidateTexts = candidateTexts.filter(c => !c.text.isEliminated);
+
+ // 再淘汰1-2个
+ candidateTexts.sort((a, b) => {
+ if (a.isCorrect) return 1;
+ if (b.isCorrect) return -1;
+ return a.text.probability - b.text.probability;
+ });
+
+ let toEliminate = min(candidateTexts.length - 2, 2);
+ for (let i = 0; i < toEliminate; i++) {
+ candidateTexts[i].text.eliminate();
+ }
+
+ // 剩余2个候选概率上升到60-80%
+ for (let i = toEliminate; i < candidateTexts.length; i++) {
+ let newProb = random(60, 75);
+ if (candidateTexts[i].isCorrect) {
+ newProb = random(70, 82); // 正确答案更高
+ }
+ candidateTexts[i].text.setProbability(newProb);
+ }
+}
+
+function confirmPrediction() {
+ candidateTexts = candidateTexts.filter(c => !c.text.isEliminated);
+
+ // 正确答案概率飙升到95%+
+ for (let candidate of candidateTexts) {
+ if (candidate.isCorrect) {
+ candidate.text.setProbability(random(95, 99));
+ } else {
+ candidate.text.setProbability(random(5, 15));
+ candidate.text.eliminate();
+ }
+ }
+}
+
+function finalizePrediction() {
+ let correctText = null;
+
+ for (let candidate of candidateTexts) {
+ if (candidate.isCorrect) {
+ correctText = candidate.text;
+ } else {
+ candidate.text.cancelCandidate();
+ }
+ }
+
+ if (correctText) {
+ let startX = width / 2 - (targetSentence.length * 45) / 2;
+ let targetX = startX + sentenceProgress * 45;
+ let targetY = height / 2;
+
+ correctText.setSelected(targetX, targetY, false);
+
+ setTimeout(() => {
+ correctText.finalize();
+ }, 300);
+
+ selectedTexts.push(correctText);
+ }
+
+ candidateTexts = [];
+}
+
+function resetSentence() {
+ sentenceStarted = false;
+ sentenceComplete = false;
+ sentenceProgress = 0;
+ predictionState = 'idle';
+ stateTimer = 0;
+ particles = [];
+
+ for (let text of selectedTexts) {
+ text.reset();
+ }
+ selectedTexts = [];
+ candidateTexts = [];
+}
+
+function windowResized() {
+ resizeCanvas(windowWidth, windowHeight);
+ for (let text of floatingTexts) {
+ text.centerX = width / 2;
+ text.centerY = height / 2;
+ }
+}
+
+function mousePressed() {
+ if (!sentenceStarted && !sentenceComplete) {
+ sentenceStarted = true;
+ predictionState = 'idle';
+ stateTimer = 0;
+ }
+}
+能把这个的背景变成
+// 文字粒子类
+class TextParticle {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ this.vx = random(-0.5, 0.5);
+ this.vy = random(-0.5, 0.5);
+ this.char = this.randomChar();
+ this.alpha = random(40, 180);
+ this.baseAlpha = this.alpha;
+ this.size = random(25, 35);
+ this.changeTimer = random(10, 30);
+ this.connections = [];
+ this.connectionTimer = 0;
+ this.isConnected = false;
+ this.attractionTimer = 0;
+ }
+
+ randomChar() {
+ const chars = '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严';
+ return chars[floor(random(chars.length))];
+ }
+
+ update(offsetX, offsetY) {
+ this.x += this.vx;
+ this.y += this.vy;
+
+ // 轻微漂浮
+ this.vx += random(-0.1, 0.1);
+ this.vy += random(-0.1, 0.1);
+
+ // 限制速度
+ this.vx = constrain(this.vx, -1, 1);
+ this.vy = constrain(this.vy, -1, 1);
+
+ // 边界处理
+ const boundary = 800;
+ if (this.x < -boundary) this.x = -boundary;
+ if (this.x > boundary) this.x = boundary;
+ if (this.y < -boundary) this.y = -boundary;
+ if (this.y > boundary) this.y = boundary;
+
+ // 只有在没有连接时才变换文字
+ if (!this.isConnected) {
+ this.changeTimer--;
+ if (this.changeTimer <= 0) {
+ this.char = this.randomChar();
+ this.changeTimer = random(2, 4);
+ }
+ }
+
+ // 更新吸引计时器
+ if (this.attractionTimer > 0) {
+ this.attractionTimer--;
+ }
+ }
+
+ display(offsetX, offsetY) {
+ push();
+
+ // 如果有连接,显示更明显:放大
+ let displaySize = this.size;
+ if (this.isConnected) {
+ displaySize = this.size * 1.3;
+ }
+
+ // 绘制文字
+ fill(255, this.alpha);
+ noStroke();
+ textSize(displaySize);
+ textAlign(CENTER, CENTER);
+ text(this.char, this.x + offsetX, this.y + offsetY);
+ pop();
+ }
+
+ // 计算到其他粒子的距离
+ distTo(other) {
+ return dist(this.x, this.y, other.x, other.y);
+ }
+
+ // 检查粒子是否在遮罩范围内(考虑offset)
+ isInMask(offsetX, offsetY, centerX, centerY, radius) {
+ let screenX = this.x + offsetX;
+ let screenY = this.y + offsetY;
+ let d = dist(screenX, screenY, centerX, centerY);
+ return d < radius;
+ }
+}
+
+// ========== 文字密度调节区域 ==========
+const PARTICLE_COUNT = 250;
+// =====================================
+
+// ========== 连线粗细调节区域 ==========
+const LINE_THICKNESS = 2.5;
+// =====================================
+
+// 全局变量
+let particles = [];
+let offsetX = 0, offsetY = 0;
+let lastMouseX = 0, lastMouseY = 0;
+let isDragging = false;
+let maskRadius = 250;
+let phraseGroups = [];
+
+// 有意义的词组和短句(2-4字,像潜意识的想法)
+const meaningfulPhrases = [
+ '我想', '可以', '不行', '为什么',
+ '去哪', '做梦', '忘记', '记得',
+ '爱你', '讨厌', '喜欢', '害怕',
+ '自由', '困住', '离开', '回来',
+ '明天', '昨天', '现在', '以后',
+ '真的', '假的', '也许', '一定',
+ '孤独', '热闹', '安静', '吵闹',
+ '快乐', '悲伤', '愤怒', '平静',
+ '梦想', '现实', '理想', '幻想',
+ '逃避', '面对', '接受', '拒绝',
+ '开始', '结束', '继续', '放弃',
+ '找到', '失去', '得到', '放下',
+ '相信', '怀疑', '确定', '迷茫',
+ '成长', '退缩', '前进', '后退',
+ '温暖', '冷漠', '善良', '残忍',
+ '希望', '绝望', '勇气', '懦弱',
+ '坚持', '动摇', '改变', '守护',
+ '理解', '误解', '清楚', '混乱',
+ '简单', '复杂', '容易', '困难',
+ '靠近', '远离', '拥抱', '推开',
+ '说谎', '诚实', '隐藏', '坦白',
+ '醒来', '睡去', '清醒', '迷糊',
+ '选择', '犹豫', '决定', '后悔',
+ '珍惜', '浪费', '把握', '错过',
+ '等待', '追逐', '寻找', '遇见',
+ '重要', '无聊', '有趣', '平凡',
+ '特别', '普通', '独特', '相同',
+ '永远', '瞬间', '长久', '短暂',
+ '完整', '破碎', '圆满', '遗憾',
+ '美好', '糟糕', '幸福', '痛苦',
+ '想起', '遗忘', '回忆', '未来',
+ '过去', '此刻', '那时', '现在',
+ '或许', '肯定', '否定', '承认',
+ '否认', '相遇', '别离', '重逢',
+ '陌生', '熟悉', '新鲜', '厌倦',
+ '期待', '失望', '满足', '渴望',
+ '需要', '多余', '必须', '随意',
+ '认真', '敷衍', '真心', '假意',
+ '明白', '糊涂', '聪明', '愚蠢',
+ '清醒', '沉醉', '冷静', '疯狂'
+];
+
+function setup() {
+ createCanvas(800, 600);
+ textFont('Arial');
+
+ // 创建粒子
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
+ particles.push(new TextParticle(
+ random(-600, 600),
+ random(-600, 600)
+ ));
+ }
+}
+
+function draw() {
+ background(20, 25, 30);
+
+ // 更新粒子
+ for (let p of particles) {
+ p.update(offsetX, offsetY);
+ }
+
+ // 建立和断开连接
+ updateConnections();
+
+ // 应用轻微的吸引力
+ applyConnectionForces();
+
+ // 开始圆形遮罩
+ push();
+ drawingContext.save();
+ drawingContext.beginPath();
+ drawingContext.arc(width / 2, height / 2, maskRadius, 0, TWO_PI);
+ drawingContext.clip();
+
+ // 绘制连接线
+ stroke(120, 180, 255, 180);
+ strokeWeight(LINE_THICKNESS);
+ for (let group of phraseGroups) {
+ for (let i = 0; i < group.particles.length - 1; i++) {
+ let p1 = group.particles[i];
+ let p2 = group.particles[i + 1];
+ line(
+ p1.x + offsetX, p1.y + offsetY,
+ p2.x + offsetX, p2.y + offsetY
+ );
+ }
+ }
+
+ // 绘制粒子
+ for (let p of particles) {
+ p.display(offsetX, offsetY);
+ }
+
+ drawingContext.restore();
+ pop();
+
+ // 绘制圆形边界
+ push();
+ noFill();
+ stroke(100, 150, 255, 150);
+ strokeWeight(2);
+ circle(width / 2, height / 2, maskRadius * 2);
+ pop();
+
+ // 绘制提示信息
+ fill(255, 100);
+ noStroke();
+ textSize(14);
+ textAlign(LEFT);
+ text('拖拽鼠标查看不同区域', 10, 20);
+ text('当前文字数量: ' + PARTICLE_COUNT, 10, 40);
+}
+
+// 更新连接关系
+function updateConnections() {
+ // 更新现有短语组的计时器
+ for (let i = phraseGroups.length - 1; i >= 0; i--) {
+ let group = phraseGroups[i];
+ group.timer--;
+
+ if (group.timer <= 0) {
+ // 时间到,解除连接
+ for (let p of group.particles) {
+ p.isConnected = false;
+ p.connections = [];
+ p.alpha = p.baseAlpha;
+ }
+ phraseGroups.splice(i, 1);
+ }
+ }
+
+ // 提高连接频率:随机建立新的短语连接
+ if (random() < 0.08) {
+ // 选择一个随机短语
+ let phrase = random(meaningfulPhrases);
+ let phraseLength = phrase.length;
+
+ // 筛选在遮罩范围内且未连接的粒子
+ let centerX = width / 2;
+ let centerY = height / 2;
+ let availableParticles = particles.filter(p =>
+ !p.isConnected && p.isInMask(offsetX, offsetY, centerX, centerY, maskRadius - 50)
+ );
+
+ // 如果遮罩内粒子不够,放宽范围
+ if (availableParticles.length < phraseLength) {
+ availableParticles = particles.filter(p =>
+ !p.isConnected && p.isInMask(offsetX, offsetY, centerX, centerY, maskRadius + 100)
+ );
+ }
+
+ if (availableParticles.length < phraseLength) return;
+
+ // 选择一个起始粒子
+ let startParticle = random(availableParticles);
+ let selectedParticles = [startParticle];
+
+ // 移除已选择的粒子
+ availableParticles = availableParticles.filter(p => p !== startParticle);
+
+ // 选择附近的其他粒子
+ for (let i = 1; i < phraseLength; i++) {
+ if (availableParticles.length === 0) break;
+
+ // 找到距离最后选择的粒子较近的粒子
+ let lastSelected = selectedParticles[selectedParticles.length - 1];
+ let nearbyParticles = availableParticles.filter(p =>
+ lastSelected.distTo(p) < 200
+ );
+
+ if (nearbyParticles.length > 0) {
+ let nextParticle = random(nearbyParticles);
+ selectedParticles.push(nextParticle);
+ availableParticles = availableParticles.filter(p => p !== nextParticle);
+ } else {
+ // 如果没有附近的,就随机选择
+ let nextParticle = random(availableParticles);
+ selectedParticles.push(nextParticle);
+ availableParticles = availableParticles.filter(p => p !== nextParticle);
+ }
+ }
+
+ if (selectedParticles.length === phraseLength) {
+ // 设置每个粒子的文字为短语中的字,并提高透明度
+ for (let i = 0; i < phraseLength; i++) {
+ selectedParticles[i].char = phrase[i];
+ selectedParticles[i].isConnected = true;
+ selectedParticles[i].attractionTimer = 20;
+ selectedParticles[i].alpha = random(190, 210);
+ }
+
+ // 创建连接
+ for (let i = 0; i < phraseLength - 1; i++) {
+ selectedParticles[i].connections = [selectedParticles[i + 1]];
+ }
+
+ // 添加到短语组
+ phraseGroups.push({
+ particles: selectedParticles,
+ timer: random(60, 120)
+ });
+ }
+ }
+}
+
+// 应用轻微的吸引力
+function applyConnectionForces() {
+ for (let group of phraseGroups) {
+ for (let i = 0; i < group.particles.length - 1; i++) {
+ let p1 = group.particles[i];
+ let p2 = group.particles[i + 1];
+
+ // 只在吸引计时器大于0时应用力
+ if (p1.attractionTimer > 0 || p2.attractionTimer > 0) {
+ let dx = p2.x - p1.x;
+ let dy = p2.y - p1.y;
+ let d = sqrt(dx * dx + dy * dy);
+
+ if (d > 0 && d > 60) {
+ let force = 0.15;
+ p1.vx += (dx / d) * force;
+ p1.vy += (dy / d) * force;
+ p2.vx -= (dx / d) * force;
+ p2.vy -= (dy / d) * force;
+ }
+ }
+ }
+ }
+}
+
+// 鼠标按下
+function mousePressed() {
+ let d = dist(mouseX, mouseY, width / 2, height / 2);
+ if (d < maskRadius) {
+ isDragging = true;
+ lastMouseX = mouseX;
+ lastMouseY = mouseY;
+ }
+}
+
+// 鼠标拖拽
+function mouseDragged() {
+ if (isDragging) {
+ let dx = mouseX - lastMouseX;
+ let dy = mouseY - lastMouseY;
+ offsetX += dx;
+ offsetY += dy;
+ lastMouseX = mouseX;
+ lastMouseY = mouseY;
+ }
+}
+
+// 鼠标释放
+function mouseReleased() {
+ isDragging = false;
+}这样的么
\ No newline at end of file
diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt.meta b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt.meta
new file mode 100644
index 000000000..f260afc06
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c7cbe6739626ae5458cf9c27e963b310
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt
new file mode 100644
index 000000000..eb95947b7
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt
@@ -0,0 +1,1871 @@
+// ==================== 全局变量 ====================
+// 粒子数组
+let floatingTextParticles = []; // 背景浮动文字粒子数组
+let candidateParticles = []; // 候选粒子数组(可交互的粒子)
+let rippleEffects = []; // 涟漪效果数组
+let repelBursts = []; // 排斥爆发效果数组
+
+// 游戏参数
+let fontSize = 28; // 字体大小
+let floatingCount = 100; // 背景浮动文字数量
+let candidateCount = 50; // 候选粒子数量
+let redParticleCount = 8; // 红色粒子(目标粒子)数量
+let connectionDistance = 200; // 连接距离阈值(像素)
+
+// 游戏状态
+let isCompleted = false; // 游戏是否完成
+let completionPhase = 'none'; // 完成阶段:'none'(未完成), 'arranging'(排列中), 'revealing'(揭示中), 'completed'(已完成)
+let completionTimer = 0; // 完成动画计时器
+let fadeOutAlpha = 255; // 淡出透明度(0-255)
+let orderedRedParticles = []; // 按顺序排列的红色粒子数组
+let targetPositions = []; // 目标位置数组(用于完成动画)
+let targetSentence = "别过来我感觉害怕"; // 目标句子
+
+// 波形交互状态
+let calmProgress = 0; // 平静进度(0-1,影响文字的抖动和颜色)
+let waveformPoints = []; // 波形点数组(用于绘制波形)
+let waveformLength = 200; // 波形长度(点数)
+let waveformAmplitude = 30; // 波形振幅(像素)
+let waveformBaseY = 0; // 波形基准Y坐标
+let mouseInteractionActive = false; // 鼠标交互是否激活
+let interactionRadius = 50; // 交互影响半径
+let waveformParticles = []; // 波形粒子效果数组
+let waveformResistance = 0.15; // 波形抵抗力(0-1,越高越难抚平)
+let lastMouseX = 0; // 上一帧鼠标X位置(用于检测拖动速度)
+let lastMouseY = 0; // 上一帧鼠标Y位置
+let mouseDragSpeed = 0; // 鼠标拖动速度
+
+// 文字区域
+let textAreaMargin = 100; // 文字区域边距
+let textAreaX = 0; // 文字区域X坐标
+let textAreaY = 0; // 文字区域Y坐标
+let textAreaWidth = 0; // 文字区域宽度
+let textAreaHeight = 0; // 文字区域高度
+
+// 字符集
+let chineseChars = '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞';
+let chineseWords = ['不安', '紧张', '焦虑', '担忧', '烦躁', '恐慌', '恐惧', '绝望', '痛苦', '悲伤', '愤怒', '孤独', '无助', '迷茫', '困惑', '压抑', '沉重', '疲惫', '空虚', '失落'];
+
+// 连接状态跟踪
+let previousConnections = new Map(); // 上一帧的连接状态映射(用于检测连接变化)
+let effectDisplays = []; // 效果显示对象数组(用于传递效果)
+let initializationComplete = false; // 初始化是否完成
+let initializationFrames = 0; // 初始化帧数计数
+
+// 词组模式
+let phraseMode = true; // 是否启用词组模式
+let phraseGroups = []; // 词组分组数组
+
+// ==================== 初始化 ====================
+function setup() {
+ createCanvas(windowWidth, windowHeight);
+ textAlign(CENTER, CENTER);
+
+ textAreaX = textAreaMargin;
+ textAreaY = textAreaMargin;
+ textAreaWidth = width - textAreaMargin * 2;
+ textAreaHeight = height - textAreaMargin * 2;
+
+ for (let i = 0; i < floatingCount; i++) {
+ floatingTextParticles.push(new FloatingText(
+ random(textAreaX, textAreaX + textAreaWidth),
+ random(textAreaY, textAreaY + textAreaHeight)
+ ));
+ }
+ for (let i = 0; i < candidateCount; i++) {
+ candidateParticles.push(new CandidateParticle(
+ random(textAreaX, textAreaX + textAreaWidth),
+ random(textAreaY, textAreaY + textAreaHeight)
+ ));
+ }
+
+ selectRedParticles();
+}
+
+// ==================== 主循环 ====================
+/**
+ * 主绘制循环:更新和渲染所有游戏元素
+ */
+function draw() {
+ background(20, 25, 35);
+
+ // 初始化检查:等待一定帧数后再开始检测连接变化
+ if (!initializationComplete) {
+ initializationFrames++;
+ if (initializationFrames >= 30) {
+ initializationComplete = true;
+ }
+ }
+
+ // 更新词组分组
+ updatePhraseGroups();
+
+ // 检测连接变化(只在初始化完成后进行)
+ if (initializationComplete) {
+ detectConnectionChanges();
+ } else {
+ // 初始化阶段:建立初始连接状态(不触发效果)
+ if (initializationFrames >= 20) {
+ let currentConnections = new Map();
+ for (let i = 0; i < candidateParticles.length; i++) {
+ for (let j = i + 1; j < candidateParticles.length; j++) {
+ let distance = dist(candidateParticles[i].x, candidateParticles[i].y,
+ candidateParticles[j].x, candidateParticles[j].y);
+ if (distance < connectionDistance) {
+ let connectionKey = `${min(i, j)}-${max(i, j)}`;
+ currentConnections.set(connectionKey, {
+ p1: candidateParticles[i],
+ p2: candidateParticles[j],
+ d: distance
+ });
+ }
+ }
+ }
+ previousConnections = currentConnections;
+ }
+ }
+
+ // 更新和显示背景浮动文字粒子
+ for (let particle of floatingTextParticles) {
+ particle.update();
+ particle.display();
+ }
+
+ // 更新和显示候选粒子
+ for (let particle of candidateParticles) {
+ // 如果游戏完成且不是红色粒子,停止移动
+ if (isCompleted && !particle.isRed && completionPhase !== 'none') {
+ particle.vx = 0;
+ particle.vy = 0;
+ } else {
+ particle.update();
+ // 在正常游戏阶段应用涟漪和排斥效果
+ if (completionPhase === 'none') {
+ particle.applyRippleAndRepel();
+ }
+ }
+ particle.display();
+ }
+
+ // 更新和显示涟漪效果
+ for (let i = rippleEffects.length - 1; i >= 0; i--) {
+ rippleEffects[i].update();
+ rippleEffects[i].display();
+ if (rippleEffects[i].alpha <= 0) rippleEffects.splice(i, 1);
+ }
+
+ // 更新和显示排斥爆发效果
+ for (let i = repelBursts.length - 1; i >= 0; i--) {
+ repelBursts[i].update();
+ repelBursts[i].display();
+ if (repelBursts[i].alpha <= 0) repelBursts.splice(i, 1);
+ }
+
+ // 更新和显示效果传递(只在未完成或排列阶段)
+ if (completionPhase === 'none' || completionPhase === 'arranging') {
+ for (let i = effectDisplays.length - 1; i >= 0; i--) {
+ if (!effectDisplays[i].update()) {
+ // 效果结束,清理并移除
+ effectDisplays[i].cleanup();
+ effectDisplays.splice(i, 1);
+ } else {
+ // 效果仍在进行,显示效果
+ effectDisplays[i].display();
+ }
+ }
+ } else {
+ // 完成阶段:清理所有效果
+ for (let i = effectDisplays.length - 1; i >= 0; i--) {
+ effectDisplays[i].cleanup();
+ effectDisplays.splice(i, 1);
+ }
+ }
+
+ // 绘制连接线
+ drawConnections();
+
+ // 检测完成状态:所有红色粒子是否连接且与蓝色粒子分离
+ let newCompleted = checkRedParticlesConnected();
+ if (newCompleted && !isCompleted) {
+ isCompleted = true;
+ startCompletionSequence();
+ }
+
+ // 更新完成动画
+ if (isCompleted) {
+ updateCompletionAnimation();
+ }
+
+ // 绘制波形(在完成阶段)
+ if (completionPhase === 'revealing' || completionPhase === 'completed') {
+ drawWaveform();
+ }
+
+ // 显示波形交互提示和UI
+ if (completionPhase === 'completed') {
+ displayWaveformInteraction();
+ }
+}
+
+// ==================== 连接检测 ====================
+/**
+ * 检测粒子之间的连接变化,并在红色粒子和蓝色粒子新建立连接时触发效果传递
+ * 此函数会更新每个粒子的connections数组,记录真正连接的粒子
+ */
+function detectConnectionChanges() {
+ let currentConnections = new Map();
+
+ // 遍历所有候选粒子对,检测连接关系
+ for (let i = 0; i < candidateParticles.length; i++) {
+ let particle1 = candidateParticles[i];
+ particle1.connections = []; // 重置连接数组
+
+ for (let j = i + 1; j < candidateParticles.length; j++) {
+ let particle2 = candidateParticles[j];
+ let distance = dist(particle1.x, particle1.y, particle2.x, particle2.y);
+
+ // 如果距离小于连接阈值,则建立连接
+ if (distance < connectionDistance) {
+ let connectionKey = `${min(i, j)}-${max(i, j)}`;
+ currentConnections.set(connectionKey, {
+ p1: particle1,
+ p2: particle2,
+ d: distance
+ });
+
+ // 在双方的connections数组中记录连接关系
+ particle1.connections.push(particle2);
+ particle2.connections.push(particle1);
+ }
+ }
+ }
+
+ // 检测新建立的连接(红色粒子与蓝色粒子之间的连接)
+ if (initializationComplete && previousConnections.size > 0) {
+ for (let [connectionKey, connection] of currentConnections) {
+ // 如果这是一个新建立的连接
+ if (!previousConnections.has(connectionKey)) {
+ let particle1 = connection.p1;
+ let particle2 = connection.p2;
+
+ // 检查是否是红色粒子与蓝色粒子的新连接
+ // 条件:一个是红色粒子(原始红色),另一个是蓝色粒子
+ let isRedToBlueConnection =
+ (particle1.isRed && !particle2.isRed && !particle2.isOrange &&
+ !particle1.isStatic && !particle1.hasEffect && particle1.originalIsRed === true) ||
+ (!particle1.isRed && !particle1.isOrange && particle2.isRed &&
+ !particle2.isStatic && !particle2.hasEffect && particle2.originalIsRed === true);
+
+ if (isRedToBlueConnection) {
+ let redParticle = particle1.isRed ? particle1 : particle2;
+ let blueParticle = particle1.isRed ? particle2 : particle1;
+
+ // 确保蓝色粒子可以接收效果
+ if (!blueParticle.isStatic && !blueParticle.hasEffect && !blueParticle.isOrange) {
+ triggerEffectPropagation(redParticle, blueParticle);
+ }
+ }
+ }
+ }
+ }
+
+ // 更新上一帧的连接状态
+ previousConnections = currentConnections;
+}
+
+/**
+ * 触发效果传递(从红色粒子传递到蓝色粒子)
+ * @param {CandidateParticle} redParticle - 红色粒子(效果源)
+ * @param {CandidateParticle} blueParticle - 蓝色粒子(效果目标)
+ */
+function triggerEffectPropagation(redParticle, blueParticle) {
+ if (blueParticle) {
+ effectDisplays.push(new EffectDisplay(redParticle, blueParticle));
+ }
+}
+
+// ==================== 绘制连接线 ====================
+function drawConnections() {
+ // 蓝色粒子之间的连线
+ if (completionPhase === 'none' || completionPhase === 'arranging') {
+ strokeWeight(1.2);
+ for (let i = 0; i < candidateParticles.length; i++) {
+ for (let j = i + 1; j < candidateParticles.length; j++) {
+ if (candidateParticles[i].isRed && candidateParticles[j].isRed) continue;
+
+ let d = dist(candidateParticles[i].x, candidateParticles[i].y, candidateParticles[j].x, candidateParticles[j].y);
+ if (d < connectionDistance) {
+ let alpha = map(d, 0, connectionDistance, 180, 0) * (fadeOutAlpha / 255);
+ stroke(100, 200, 255, alpha);
+ line(candidateParticles[i].x, candidateParticles[i].y, candidateParticles[j].x, candidateParticles[j].y);
+ }
+ }
+ }
+ }
+
+ // 红色粒子之间的连线
+ if (completionPhase === 'none' || completionPhase === 'arranging') {
+ let redParticles = candidateParticles.filter(p => p.isRed);
+ strokeWeight(2.5);
+ for (let i = 0; i < redParticles.length; i++) {
+ for (let j = i + 1; j < redParticles.length; j++) {
+ let d = dist(redParticles[i].x, redParticles[i].y, redParticles[j].x, redParticles[j].y);
+ if (d < connectionDistance) {
+ let alpha = map(d, 0, connectionDistance, 255, 100);
+ stroke(255, 100, 100, alpha);
+ line(redParticles[i].x, redParticles[i].y, redParticles[j].x, redParticles[j].y);
+ }
+ }
+ }
+ }
+}
+
+// ==================== 完成序列 ====================
+function startCompletionSequence() {
+ completionPhase = 'arranging';
+ completionTimer = 0;
+ fadeOutAlpha = 255;
+
+ // 重置波形和平静进度
+ calmProgress = 0;
+ waveformPoints = [];
+
+ orderedRedParticles = getOrderedRedParticles();
+ let displayLength = min(orderedRedParticles.length, targetSentence.length);
+
+ let totalWidth = displayLength * (fontSize + 20);
+ let startX = (width - totalWidth) / 2 + fontSize / 2;
+ let centerY = height / 2;
+
+ targetPositions = [];
+ for (let i = 0; i < displayLength; i++) {
+ targetPositions.push({
+ x: startX + i * (fontSize + 20),
+ y: centerY
+ });
+ }
+
+ if (orderedRedParticles.length > displayLength) {
+ orderedRedParticles = orderedRedParticles.slice(0, displayLength);
+ }
+
+ effectDisplays = [];
+
+ for (let p of candidateParticles) {
+ if (!p.isRed) {
+ p.vx = 0;
+ p.vy = 0;
+ if (p.isOrange) {
+ p.hasEffect = false;
+ p.changeSpeedMultiplier = 1;
+ p.vibrationOffsetX = 0;
+ p.vibrationOffsetY = 0;
+ }
+ }
+ }
+}
+
+function getOrderedRedParticles() {
+ let redParticles = candidateParticles.filter(p => p.isRed);
+ if (redParticles.length === 0) return [];
+ if (redParticles.length === 1) return redParticles;
+
+ let startIdx = 0;
+ let minX = redParticles[0].x;
+ for (let i = 1; i < redParticles.length; i++) {
+ if (redParticles[i].x < minX) {
+ minX = redParticles[i].x;
+ startIdx = i;
+ }
+ }
+
+ let ordered = [];
+ let visited = new Set();
+ let stack = [startIdx];
+ visited.add(startIdx);
+
+ while (stack.length > 0) {
+ let currentIdx = stack.pop();
+ ordered.push(redParticles[currentIdx]);
+
+ for (let i = 0; i < redParticles.length; i++) {
+ if (!visited.has(i)) {
+ let d = dist(redParticles[currentIdx].x, redParticles[currentIdx].y,
+ redParticles[i].x, redParticles[i].y);
+ if (d < connectionDistance) {
+ visited.add(i);
+ stack.push(i);
+ }
+ }
+ }
+ }
+
+ return ordered;
+}
+
+/**
+ * 更新完成动画:包括文字揭示、抖动效果和波形交互
+ */
+function updateCompletionAnimation() {
+ completionTimer++;
+
+ // 更新波形交互
+ if (completionPhase === 'revealing' || completionPhase === 'completed') {
+ updateWaveform();
+ }
+
+ if (completionPhase === 'arranging') {
+ let allArrived = true;
+ for (let i = 0; i < orderedRedParticles.length; i++) {
+ let p = orderedRedParticles[i];
+ let target = targetPositions[i];
+
+ p.x = lerp(p.x, target.x, 0.1);
+ p.y = lerp(p.y, target.y, 0.1);
+
+ if (dist(p.x, p.y, target.x, target.y) > 1) {
+ allArrived = false;
+ }
+ }
+
+ fadeOutAlpha = max(0, fadeOutAlpha - 5);
+
+ if (allArrived && completionTimer > 30) {
+ completionPhase = 'revealing';
+ completionTimer = 0;
+
+ // 初始化波形(如果还没有初始化)
+ if (waveformPoints.length === 0) {
+ initializeWaveform();
+ }
+
+ // 初始化抖动和漂浮效果
+ for (let p of orderedRedParticles) {
+ p.changeTimer = 5;
+ p.changeSpeedMultiplier = 10;
+ p.isStatic = false;
+ // 初始化抖动参数
+ p.anxietyShakePhase = random(TWO_PI);
+ p.anxietyShakeIntensity = random(2, 4);
+ p.anxietyFloatPhase = random(TWO_PI);
+ p.anxietyFloatSpeed = random(0.02, 0.04);
+ }
+ }
+ } else if (completionPhase === 'revealing') {
+ let revealDelay = 15;
+ let currentIndex = floor(completionTimer / revealDelay);
+
+ for (let i = 0; i < orderedRedParticles.length; i++) {
+ let p = orderedRedParticles[i];
+
+ if (i < currentIndex) {
+ if (i < targetSentence.length) {
+ p.char = targetSentence.charAt(i);
+ }
+ p.isStatic = false; // 允许抖动
+ p.changeSpeedMultiplier = 1;
+ p.phraseGroup = null;
+ // 更新抖动和漂浮
+ updateAnxietyEffect(p);
+ } else if (i === currentIndex) {
+ p.changeTimer = max(0, p.changeTimer - 1);
+ p.changeSpeedMultiplier = 20;
+ p.isStatic = false;
+ p.phraseGroup = null;
+
+ if (completionTimer % revealDelay >= revealDelay - 5) {
+ if (i < targetSentence.length) {
+ p.char = targetSentence.charAt(i);
+ }
+ // 初始化抖动参数
+ if (p.anxietyShakePhase === undefined) {
+ p.anxietyShakePhase = random(TWO_PI);
+ p.anxietyShakeIntensity = random(2, 4);
+ p.anxietyFloatPhase = random(TWO_PI);
+ p.anxietyFloatSpeed = random(0.02, 0.04);
+ }
+ } else {
+ if (p.changeTimer <= 0) {
+ p.char = chineseChars.charAt(floor(random(chineseChars.length)));
+ p.changeTimer = 2;
+ }
+ }
+ } else {
+ p.changeTimer = max(0, p.changeTimer - 1);
+ p.changeSpeedMultiplier = 20;
+ p.isStatic = false;
+ p.phraseGroup = null;
+
+ if (p.changeTimer <= 0) {
+ p.char = chineseChars.charAt(floor(random(chineseChars.length)));
+ p.changeTimer = 2;
+ }
+ }
+ }
+
+ if (currentIndex >= orderedRedParticles.length) {
+ completionPhase = 'completed';
+ for (let i = 0; i < orderedRedParticles.length && i < targetSentence.length; i++) {
+ orderedRedParticles[i].char = targetSentence.charAt(i);
+ orderedRedParticles[i].isStatic = false; // 允许抖动
+ orderedRedParticles[i].changeSpeedMultiplier = 1;
+ orderedRedParticles[i].phraseGroup = null;
+ // 确保有抖动参数
+ if (orderedRedParticles[i].anxietyShakePhase === undefined) {
+ orderedRedParticles[i].anxietyShakePhase = random(TWO_PI);
+ orderedRedParticles[i].anxietyShakeIntensity = random(2, 4);
+ orderedRedParticles[i].anxietyFloatPhase = random(TWO_PI);
+ orderedRedParticles[i].anxietyFloatSpeed = random(0.02, 0.04);
+ }
+ }
+ }
+ } else if (completionPhase === 'completed') {
+ // 在完成阶段,更新所有文字的抖动和漂浮效果
+ for (let p of orderedRedParticles) {
+ updateAnxietyEffect(p);
+ }
+
+ // 检查所有文字是否都已消散完成
+ let allDissolved = true;
+ for (let p of orderedRedParticles) {
+ let dissolveProgress = p.dissolveProgress !== undefined ? p.dissolveProgress : 0;
+ if (dissolveProgress < 1) {
+ allDissolved = false;
+ break;
+ }
+ }
+
+ // 如果所有文字都已消散,触发游戏结束
+ if (allDissolved && orderedRedParticles.length > 0) {
+ // 可以在这里添加结束动画或回调
+ // 例如:延迟一段时间后重置游戏或显示结束画面
+ if (completionTimer > 60) { // 等待1秒(假设60fps)后重置
+ resetGame();
+ }
+ }
+ }
+}
+
+/**
+ * 更新焦虑效果(抖动和漂浮)- 根据对应波形点的抚平程度
+ * @param {CandidateParticle} particle - 要更新的粒子
+ */
+function updateAnxietyEffect(particle) {
+ if (particle.anxietyShakePhase === undefined) {
+ particle.anxietyShakePhase = random(TWO_PI);
+ particle.anxietyShakeIntensity = random(2, 4);
+ particle.anxietyFloatPhase = random(TWO_PI);
+ particle.anxietyFloatSpeed = random(0.02, 0.04);
+ }
+
+ // 获取对应波形点的抚平程度(如果没有关联,使用整体平静进度)
+ let localCalmProgress = particle.calmProgress !== undefined ? particle.calmProgress : calmProgress;
+
+ // 根据对应波形点的抚平程度减少抖动强度(0 = 完全抖动,1 = 完全平静)
+ let shakeIntensity = particle.anxietyShakeIntensity * (1 - localCalmProgress);
+ let floatAmplitude = 3 * (1 - localCalmProgress);
+
+ // 如果抚平程度高,停止抖动更新
+ if (localCalmProgress < 0.9) {
+ // 更新抖动相位
+ particle.anxietyShakePhase += 0.3;
+ particle.anxietyFloatPhase += particle.anxietyFloatSpeed;
+ }
+
+ // 计算抖动偏移(随机方向的小幅度抖动)
+ particle.anxietyShakeX = cos(particle.anxietyShakePhase) * shakeIntensity +
+ sin(particle.anxietyShakePhase * 1.7) * shakeIntensity * 0.5;
+ particle.anxietyShakeY = sin(particle.anxietyShakePhase * 1.3) * shakeIntensity +
+ cos(particle.anxietyShakePhase * 0.9) * shakeIntensity * 0.5;
+
+ // 计算漂浮偏移(缓慢的上下浮动)
+ particle.anxietyFloatY = sin(particle.anxietyFloatPhase) * floatAmplitude;
+}
+
+/**
+ * 初始化波形数据(与文字位置对应)
+ */
+function initializeWaveform() {
+ waveformPoints = [];
+ waveformParticles = [];
+
+ // 确保orderedRedParticles已经初始化
+ if (orderedRedParticles.length === 0) return;
+
+ // 波形显示在文字位置(与文字重叠)
+ waveformBaseY = height / 2; // 与文字中心对齐
+
+ // 为每个文字粒子创建一个对应的波形点
+ for (let i = 0; i < orderedRedParticles.length; i++) {
+ let particle = orderedRedParticles[i];
+ let x = particle.x;
+
+ // 异常波形:使用Perlin噪声生成真实的噪波效果
+ // 使用多个频率的噪声叠加,产生更自然的噪波
+ let noiseScale1 = 0.05; // 低频噪声(大范围变化)
+ let noiseScale2 = 0.15; // 中频噪声
+ let noiseScale3 = 0.4; // 高频噪声(细节变化)
+
+ // 使用p5.js的noise函数生成平滑的噪声值
+ let noiseValue1 = noise(x * noiseScale1, frameCount * 0.01);
+ let noiseValue2 = noise(x * noiseScale2, frameCount * 0.02 + 100);
+ let noiseValue3 = noise(x * noiseScale3, frameCount * 0.03 + 200);
+
+ // 将噪声值从[0,1]映射到[-1,1],并叠加多层
+ noiseValue1 = map(noiseValue1, 0, 1, -1, 1);
+ noiseValue2 = map(noiseValue2, 0, 1, -1, 1) * 0.6;
+ noiseValue3 = map(noiseValue3, 0, 1, -1, 1) * 0.3;
+
+ // 叠加多层噪声,产生更复杂的噪波
+ let combinedNoise = noiseValue1 + noiseValue2 + noiseValue3;
+ combinedNoise = constrain(combinedNoise, -1.5, 1.5);
+
+ // 添加一些随机性,使每个点的噪波特征不同
+ let noiseAmplitude = random(0.7, 1.0);
+ let noiseOffset = random(-0.2, 0.2);
+ let initialY = waveformBaseY + (combinedNoise + noiseOffset) * noiseAmplitude * waveformAmplitude;
+
+ // 抚平后的目标位置:规律的波形(正弦波)
+ let targetWavePhase = i * 0.3; // 波形相位
+ let targetWaveAmplitude = waveformAmplitude * 0.3; // 规律波形的振幅(较小)
+ let targetY = waveformBaseY + sin(targetWavePhase) * targetWaveAmplitude;
+
+ // 计算每个点的异常程度(距离规律波形的距离)
+ let anomalyAmount = abs(initialY - targetY) / waveformAmplitude;
+
+ waveformPoints.push({
+ x: x,
+ y: initialY, // 初始Y坐标(噪波)
+ targetY: targetY, // 目标位置(规律的波形)
+ smoothedY: initialY, // 平滑后的Y坐标
+ noiseOffset: noiseOffset, // 噪声偏移(用于动态噪波)
+ noiseScale1: noiseScale1, // 噪声缩放1
+ noiseScale2: noiseScale2, // 噪声缩放2
+ noiseScale3: noiseScale3, // 噪声缩放3
+ noiseAmplitude: noiseAmplitude, // 噪声振幅
+ noiseTime: random(0, 1000), // 噪声时间偏移(使每个点有不同的时间相位)
+ resistance: 0.1 + anomalyAmount * 0.25, // 抵抗力(异常程度越高,抵抗力越强)
+ calmAmount: 0, // 当前被抚平的程度(0-1)
+ isBeingCalmed: false, // 是否正在被抚平
+ calmTimer: 0, // 抚平计时器
+ particle: particle, // 关联的文字粒子
+ particleIndex: i // 粒子索引
+ });
+
+ // 在文字粒子上添加波形关联
+ particle.waveformPoint = waveformPoints[waveformPoints.length - 1];
+ particle.calmProgress = 0; // 文字粒子的平静进度
+ particle.dissolveProgress = 0; // 文字粒子的消散进度
+ particle.isCalmed = false; // 是否已被抚平
+ }
+
+ // 初始化鼠标位置
+ lastMouseX = mouseX;
+ lastMouseY = mouseY;
+}
+
+/**
+ * 更新波形:根据平静进度平滑波形,并处理鼠标交互(带抵抗力和愈合机制)
+ */
+function updateWaveform() {
+ if (waveformPoints.length === 0) {
+ initializeWaveform();
+ }
+
+ // 计算鼠标拖动速度
+ mouseDragSpeed = dist(mouseX, mouseY, lastMouseX, lastMouseY);
+ lastMouseX = mouseX;
+ lastMouseY = mouseY;
+
+ // 更新粒子效果
+ for (let i = waveformParticles.length - 1; i >= 0; i--) {
+ let p = waveformParticles[i];
+ p.x += p.vx;
+ p.y += p.vy;
+ p.vy += 0.1; // 重力
+ p.alpha -= 3;
+ p.size *= 0.98;
+
+ if (p.alpha <= 0 || p.size < 0.5) {
+ waveformParticles.splice(i, 1);
+ }
+ }
+
+ // 更新每个波形点
+ for (let point of waveformPoints) {
+ // 同步更新波形点的X位置(跟随文字位置)
+ if (point.particle) {
+ point.x = point.particle.x;
+ // 更新规律波形的目标位置(跟随文字位置)
+ let targetWavePhase = point.particleIndex * 0.3;
+ let targetWaveAmplitude = waveformAmplitude * 0.3;
+ point.targetY = waveformBaseY + sin(targetWavePhase) * targetWaveAmplitude;
+ }
+
+ // 重置状态
+ point.isBeingCalmed = false;
+
+ // 如果未抚平,添加动态噪波效果(表现异常波形的"生命力")
+ if (point.calmAmount < 0.9) {
+ // 使用Perlin噪声生成动态噪波
+ let currentTime = frameCount * 0.01 + point.noiseTime * 0.001;
+
+ // 生成多层噪声
+ let noiseValue1 = noise(point.x * point.noiseScale1, currentTime);
+ let noiseValue2 = noise(point.x * point.noiseScale2, currentTime * 1.5 + 100);
+ let noiseValue3 = noise(point.x * point.noiseScale3, currentTime * 2 + 200);
+
+ // 映射并叠加
+ noiseValue1 = map(noiseValue1, 0, 1, -1, 1);
+ noiseValue2 = map(noiseValue2, 0, 1, -1, 1) * 0.6;
+ noiseValue3 = map(noiseValue3, 0, 1, -1, 1) * 0.3;
+
+ let dynamicNoise = noiseValue1 + noiseValue2 + noiseValue3 + point.noiseOffset;
+ dynamicNoise = constrain(dynamicNoise, -1.5, 1.5);
+
+ // 根据抚平程度减少噪波强度(抚平程度越高,噪波越弱)
+ let noiseIntensity = (1 - point.calmAmount) * waveformAmplitude * point.noiseAmplitude;
+ point.smoothedY = point.y + dynamicNoise * noiseIntensity;
+ }
+
+ // 处理鼠标交互:如果鼠标在附近,尝试抚平该区域的波形
+ if (mouseIsPressed && mouseButton === LEFT) {
+ let distToMouse = dist(mouseX, mouseY, point.x, point.smoothedY);
+ if (distToMouse < interactionRadius) {
+ // 计算影响强度(距离越近影响越大,拖动速度越快影响越大)
+ let distanceInfluence = map(distToMouse, 0, interactionRadius, 1, 0);
+ let speedBonus = min(1, mouseDragSpeed / 5); // 拖动速度加成
+ let influence = distanceInfluence * (0.5 + speedBonus * 0.5);
+
+ // 标记为正在被抚平
+ point.isBeingCalmed = true;
+ point.calmTimer++;
+
+ // 需要持续按住才能抚平(抵抗机制)
+ // 抵抗力越高,需要按住的时间越长
+ let requiredTime = 30 + point.resistance * 60; // 需要按住30-90帧
+ let calmProgressLocal = min(1, point.calmTimer / requiredTime);
+
+ // 应用抚平效果(考虑抵抗力)
+ let calmStrength = influence * (1 - point.resistance) * 0.15;
+
+ // 计算目标位置(从噪波位置向规律波形位置过渡)
+ let targetY = lerp(point.y, point.targetY, calmProgressLocal);
+ point.smoothedY = lerp(point.smoothedY, targetY, calmStrength);
+
+ // 更新抚平程度(一旦抚平,就不会恢复)
+ if (calmProgressLocal > point.calmAmount) {
+ point.calmAmount = calmProgressLocal;
+ }
+
+ // 如果完全抚平,标记为已抚平
+ if (point.calmAmount >= 0.95) {
+ point.calmAmount = 1;
+ point.smoothedY = point.targetY; // 强制设置为目标位置
+ }
+
+ // 如果成功抚平了一点,生成粒子效果
+ if (calmProgressLocal > 0.3 && random() > 0.7) {
+ for (let j = 0; j < 2; j++) {
+ waveformParticles.push({
+ x: point.x,
+ y: point.smoothedY,
+ vx: random(-1, 1),
+ vy: random(-2, -0.5),
+ alpha: 200,
+ size: random(2, 4),
+ color: [150, 200, 255]
+ });
+ }
+ }
+
+ // 增加整体平静进度(需要持续交互)
+ if (calmProgressLocal > 0.5) {
+ calmProgress = min(1, calmProgress + influence * 0.0005);
+ }
+ } else {
+ // 不在交互范围内,保持当前状态(不重置)
+ }
+ }
+
+ // 关键:如果calmAmount接近1,强制让波形显示为规律波形
+ if (point.calmAmount > 0.8) {
+ // 高度抚平的点,强制向目标位置(规律波形)靠拢
+ let forceStrength = map(point.calmAmount, 0.8, 1, 0.1, 0.5);
+ point.smoothedY = lerp(point.smoothedY, point.targetY, forceStrength);
+ }
+
+ // 如果完全抚平,直接设置为目标位置(规律波形)
+ if (point.calmAmount >= 1) {
+ point.smoothedY = point.targetY;
+ }
+
+ // 限制波形点不会完全超出范围(抚平的点限制更严格)
+ let maxDeviation = waveformAmplitude * (1 - calmProgress * 0.5) * (1 - point.calmAmount * 0.8);
+ point.smoothedY = constrain(point.smoothedY,
+ waveformBaseY - maxDeviation,
+ waveformBaseY + maxDeviation);
+
+ // 同步更新对应文字粒子的平静进度
+ if (point.particle) {
+ let oldCalmProgress = point.particle.calmProgress || 0;
+ point.particle.calmProgress = point.calmAmount;
+
+ // 如果完全抚平(calmAmount >= 0.95),标记为已稳定
+ if (point.calmAmount >= 0.95 && !point.particle.isCalmed) {
+ point.particle.isCalmed = true;
+ // 停止字符变化和抖动
+ point.particle.isStatic = true;
+ point.particle.changeSpeedMultiplier = 0;
+ // 初始化消散进度(从0开始,等待波形完全稳定)
+ point.particle.dissolveProgress = 0;
+ point.particle.stableTimer = 0; // 稳定计时器
+ }
+
+ // 如果已抚平,检查波形是否完全稳定(接近目标位置)
+ if (point.particle.isCalmed) {
+ // 检查波形是否稳定(距离目标位置很近)
+ let distanceToTarget = abs(point.smoothedY - point.targetY);
+ let stabilityThreshold = waveformAmplitude * 0.05; // 稳定阈值
+
+ if (distanceToTarget < stabilityThreshold) {
+ // 波形已稳定,开始增加稳定计时器
+ if (point.particle.stableTimer === undefined) {
+ point.particle.stableTimer = 0;
+ }
+ point.particle.stableTimer++;
+
+ // 稳定一段时间后(例如30帧),开始消散
+ if (point.particle.stableTimer > 30 && point.particle.dissolveProgress < 1) {
+ // 消散速度:根据波形稳定程度决定
+ let dissolveSpeed = 0.02 + (point.calmAmount - 0.95) * 0.15; // 0.02到0.07之间
+ point.particle.dissolveProgress = min(1, point.particle.dissolveProgress + dissolveSpeed);
+ }
+ } else {
+ // 波形还未完全稳定,重置稳定计时器
+ point.particle.stableTimer = 0;
+ }
+ }
+ }
+ }
+
+ // 如果平静进度达到1,波形完全平滑
+ if (calmProgress >= 1) {
+ for (let point of waveformPoints) {
+ point.smoothedY = lerp(point.smoothedY, point.targetY, 0.05);
+ point.calmAmount = 1;
+ }
+ }
+}
+
+/**
+ * 绘制波形(带粒子效果和动态颜色)
+ */
+function drawWaveform() {
+ if (waveformPoints.length === 0) return;
+
+ // 绘制粒子效果
+ for (let p of waveformParticles) {
+ fill(p.color[0], p.color[1], p.color[2], p.alpha);
+ noStroke();
+ ellipse(p.x, p.y, p.size);
+
+ // 添加光晕效果
+ fill(p.color[0], p.color[1], p.color[2], p.alpha * 0.3);
+ ellipse(p.x, p.y, p.size * 2);
+ }
+
+ // 绘制波形线(根据抚平程度改变颜色:红色=异常,蓝色=正常)
+ noFill();
+ strokeWeight(2.5);
+
+ beginShape();
+ for (let i = 0; i < waveformPoints.length; i++) {
+ let point = waveformPoints[i];
+
+ // 同步更新波形点的X位置(跟随文字位置)
+ if (point.particle) {
+ point.x = point.particle.x;
+ }
+
+ // 获取对应文字的消散进度
+ let particleDissolve = point.particle && point.particle.dissolveProgress !== undefined ?
+ point.particle.dissolveProgress : 0;
+
+ // 根据抚平程度混合颜色:红色(255,100,100) -> 蓝色(150,200,255)
+ let redValue = lerp(255, 150, point.calmAmount);
+ let greenValue = lerp(100, 200, point.calmAmount);
+ let blueValue = lerp(100, 255, point.calmAmount);
+ let alphaValue = 200 * (1 - calmProgress * 0.3) * (1 - particleDissolve);
+
+ // 如果文字完全消散,不绘制该点
+ if (particleDissolve < 1) {
+ stroke(redValue, greenValue, blueValue, alphaValue);
+ vertex(point.x, point.smoothedY);
+ }
+ }
+ endShape();
+
+ // 绘制波形点(显示抵抗力和抚平进度)
+ for (let i = 0; i < waveformPoints.length; i += 3) {
+ let point = waveformPoints[i];
+
+ // 获取对应文字的消散进度
+ let particleDissolve = point.particle && point.particle.dissolveProgress !== undefined ?
+ point.particle.dissolveProgress : 0;
+
+ // 如果文字完全消散,不绘制
+ if (particleDissolve >= 1) continue;
+
+ // 如果正在被抚平,显示进度
+ if (point.isBeingCalmed && point.calmTimer > 0) {
+ let requiredTime = 30 + point.resistance * 60;
+ let progress = min(1, point.calmTimer / requiredTime);
+
+ // 绘制进度环
+ push();
+ translate(point.x, point.smoothedY);
+ noFill();
+ stroke(150, 200, 255, 150 * (1 - particleDissolve));
+ strokeWeight(1);
+ arc(0, 0, 8, 8, -HALF_PI, -HALF_PI + TWO_PI * progress);
+ pop();
+ }
+
+ // 绘制波形点(根据抚平程度和消散进度)
+ let pointAlpha = 150 * (1 - point.calmAmount) * (1 - calmProgress * 0.5) * (1 - particleDissolve);
+ if (pointAlpha > 10) {
+ let redValue = lerp(255, 150, point.calmAmount);
+ let greenValue = lerp(100, 200, point.calmAmount);
+ let blueValue = lerp(100, 255, point.calmAmount);
+ fill(redValue, greenValue, blueValue, pointAlpha);
+ noStroke();
+ ellipse(point.x, point.smoothedY, 3);
+ }
+ }
+
+ // 绘制交互反馈(如果鼠标按下)
+ if (mouseIsPressed && mouseButton === LEFT) {
+ // 内圈(交互范围)
+ fill(150, 200, 255, 30);
+ noStroke();
+ ellipse(mouseX, mouseY, interactionRadius * 2);
+
+ // 外圈(视觉反馈)
+ stroke(150, 200, 255, 150);
+ strokeWeight(2);
+ noFill();
+ ellipse(mouseX, mouseY, interactionRadius * 2);
+
+ // 根据拖动速度显示额外反馈
+ if (mouseDragSpeed > 2) {
+ stroke(150, 200, 255, 100);
+ strokeWeight(1);
+ noFill();
+ ellipse(mouseX, mouseY, interactionRadius * 2.5);
+ }
+ }
+
+ // 绘制基准线(目标线)
+ if (calmProgress < 0.9) {
+ stroke(150, 200, 255, 50 * (1 - calmProgress));
+ strokeWeight(1);
+ line(width * 0.2, waveformBaseY, width * 0.8, waveformBaseY);
+ }
+}
+
+// ==================== 游戏逻辑 ====================
+function updatePhraseGroups() {
+ phraseGroups = [];
+ let visited = new Set();
+
+ for (let i = 0; i < candidateParticles.length; i++) {
+ if (visited.has(i)) continue;
+
+ let group = [];
+ let stack = [i];
+ visited.add(i);
+
+ while (stack.length > 0) {
+ let currentIdx = stack.pop();
+ let current = candidateParticles[currentIdx];
+ group.push(current);
+
+ for (let j = 0; j < candidateParticles.length; j++) {
+ if (j === currentIdx || visited.has(j)) continue;
+ let d = dist(current.x, current.y, candidateParticles[j].x, candidateParticles[j].y);
+ if (d < connectionDistance) {
+ visited.add(j);
+ stack.push(j);
+ }
+ }
+ }
+
+ if (group.length >= 2) {
+ phraseGroups.push(group);
+ for (let p of group) {
+ p.phraseGroup = group;
+ p.phraseIndex = group.indexOf(p);
+ }
+ }
+ }
+}
+
+function selectRedParticles() {
+ let selected = [];
+ let minDistance = min(textAreaWidth, textAreaHeight) / (redParticleCount * 0.8);
+
+ let shuffled = [...candidateParticles];
+ for (let i = shuffled.length - 1; i > 0; i--) {
+ let j = floor(random(i + 1));
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+ }
+
+ for (let particle of shuffled) {
+ if (selected.length >= redParticleCount) break;
+
+ let tooClose = false;
+ for (let selectedP of selected) {
+ if (dist(particle.x, particle.y, selectedP.x, selectedP.y) < minDistance) {
+ tooClose = true;
+ break;
+ }
+ }
+
+ if (!tooClose || selected.length === 0) {
+ particle.isRed = true;
+ particle.originalIsRed = true;
+ selected.push(particle);
+ }
+ }
+
+ let remaining = redParticleCount - selected.length;
+ for (let i = 0; i < remaining && i < shuffled.length; i++) {
+ if (!shuffled[i].isRed) {
+ shuffled[i].isRed = true;
+ shuffled[i].originalIsRed = true;
+ selected.push(shuffled[i]);
+ }
+ }
+}
+
+function checkRedParticlesConnected() {
+ let redParticles = candidateParticles.filter(p => p.isRed);
+ if (redParticles.length === 0) return false;
+ if (redParticles.length === 1) return true;
+
+ let visited = new Set();
+ let stack = [0];
+ visited.add(0);
+
+ while (stack.length > 0) {
+ let current = stack.pop();
+ for (let i = 0; i < redParticles.length; i++) {
+ if (!visited.has(i)) {
+ let d = dist(redParticles[current].x, redParticles[current].y,
+ redParticles[i].x, redParticles[i].y);
+ if (d < connectionDistance) {
+ visited.add(i);
+ stack.push(i);
+ }
+ }
+ }
+ }
+
+ let allRedConnected = visited.size === redParticles.length;
+ if (!allRedConnected) return false;
+
+ for (let redP of redParticles) {
+ for (let p of candidateParticles) {
+ if (p.isRed) continue;
+
+ let d = dist(redP.x, redP.y, p.x, p.y);
+ if (d < connectionDistance) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+function resetGame() {
+ isCompleted = false;
+ completionPhase = 'none';
+ completionTimer = 0;
+ fadeOutAlpha = 255;
+ orderedRedParticles = [];
+ targetPositions = [];
+ effectDisplays = [];
+ previousConnections = new Map();
+ initializationComplete = false;
+ initializationFrames = 0;
+
+ // 重置波形交互状态
+ calmProgress = 0;
+ waveformPoints = [];
+ waveformParticles = [];
+ mouseInteractionActive = false;
+ lastMouseX = 0;
+ lastMouseY = 0;
+ mouseDragSpeed = 0;
+
+ for (let p of candidateParticles) {
+ p.isRed = false;
+ p.isOrange = false;
+ p.hasEffect = false;
+ p.isStatic = false;
+ p.originalIsRed = false;
+ p.changeSpeedMultiplier = 1;
+ p.vibrationOffsetX = 0;
+ p.vibrationOffsetY = 0;
+ p.vx = random(-0.5, 0.5);
+ p.vy = random(-0.5, 0.5);
+ p.x = random(textAreaX, textAreaX + textAreaWidth);
+ p.y = random(textAreaY, textAreaY + textAreaHeight);
+ p.alpha = p.baseAlpha + p.prob * 105;
+ p.phraseGroup = null;
+ p.phraseIndex = 0;
+
+ // 重置波形相关状态
+ p.calmProgress = undefined;
+ p.dissolveProgress = undefined;
+ p.isCalmed = false;
+ p.waveformPoint = undefined;
+ p.stableTimer = undefined;
+
+ // 重置抖动和漂浮参数
+ p.anxietyShakePhase = undefined;
+ p.anxietyShakeIntensity = undefined;
+ p.anxietyFloatPhase = undefined;
+ p.anxietyFloatSpeed = undefined;
+ p.anxietyShakeX = undefined;
+ p.anxietyShakeY = undefined;
+ p.anxietyFloatY = undefined;
+ }
+
+ selectRedParticles();
+ rippleEffects = [];
+ repelBursts = [];
+}
+
+// ==================== UI ====================
+/**
+ * 显示波形交互提示和UI
+ */
+function displayWaveformInteraction() {
+ // 显示提示文字
+ let hintY = height / 2 + 180;
+ fill(200, 220, 255, 180);
+ textSize(16);
+ textAlign(CENTER, CENTER);
+
+ // 检查是否有文字正在消散
+ let hasDissolving = false;
+ for (let p of orderedRedParticles) {
+ if (p.dissolveProgress !== undefined && p.dissolveProgress > 0 && p.dissolveProgress < 1) {
+ hasDissolving = true;
+ break;
+ }
+ }
+
+ // 检查是否所有文字都已消散
+ let allDissolved = true;
+ for (let p of orderedRedParticles) {
+ if (p.dissolveProgress === undefined || p.dissolveProgress < 1) {
+ allDissolved = false;
+ break;
+ }
+ }
+
+ if (allDissolved) {
+ fill(150, 255, 150, 200);
+ text("所有文字已消散,情绪已完全平静", width / 2, hintY);
+ } else if (hasDissolving) {
+ fill(150, 220, 255, 200);
+ text("波形已稳定,文字正在消散...", width / 2, hintY);
+ } else if (calmProgress < 1) {
+ if (mouseIsPressed && mouseButton === LEFT) {
+ text("持续按住并拖动,抚平波形(波形稳定后文字会消散)", width / 2, hintY);
+ } else {
+ text("按住鼠标左键持续拖动,抚平异常波形(波形稳定后文字会消散)", width / 2, hintY);
+ }
+
+ // 显示平静进度条
+ let progressBarX = width / 2;
+ let progressBarY = hintY + 25;
+ let progressBarWidth = 200;
+ let progressBarHeight = 8;
+
+ // 背景
+ fill(50, 50, 70, 150);
+ rectMode(CENTER);
+ rect(progressBarX, progressBarY, progressBarWidth, progressBarHeight, 4);
+
+ // 进度
+ fill(150, 200, 255, 200);
+ rectMode(CORNER);
+ rect(progressBarX - progressBarWidth / 2, progressBarY - progressBarHeight / 2,
+ progressBarWidth * calmProgress, progressBarHeight, 4);
+ } else {
+ fill(150, 255, 150, 200);
+ text("异常波形已抚平,等待文字消散...", width / 2, hintY);
+ }
+}
+
+function displayNextButton() {
+ // 不再显示下一步按钮,游戏会在所有文字消散后自动重置
+ // 保留函数以防其他地方调用
+}
+
+// ==================== 事件处理 ====================
+/**
+ * 鼠标按下事件
+ */
+function mousePressed() {
+ if (completionPhase === 'completed') {
+ // 在完成阶段,鼠标交互用于抚平波形(在updateWaveform中处理)
+ // 游戏会在所有文字消散后自动重置,不需要手动点击按钮
+ }
+
+ if (isCompleted && completionPhase !== 'completed') {
+ return;
+ }
+
+ // 正常游戏阶段的交互
+ if (mouseButton === LEFT) {
+ repelBursts.push(new RepelBurst(mouseX, mouseY));
+ } else if (mouseButton === RIGHT) {
+ rippleEffects.push(new Ripple(mouseX, mouseY));
+ }
+}
+
+/**
+ * 键盘按下事件
+ */
+function keyPressed() {
+ // 切换词组模式
+ if (key === 'p' || key === 'P') {
+ phraseMode = !phraseMode;
+ for (let p of candidateParticles) {
+ p.phraseGroup = null;
+ p.phraseIndex = 0;
+ }
+ }
+}
+
+// ==================== 类定义 ====================
+/**
+ * 效果显示类:管理从红色粒子到蓝色粒子的效果传递和显示
+ * 效果会沿着连接线传播,形成连锁反应
+ */
+class EffectDisplay {
+ /**
+ * 构造函数:初始化效果传递
+ * @param {CandidateParticle} redParticle - 红色粒子(效果源)
+ * @param {CandidateParticle} startBlueParticle - 起始蓝色粒子(第一个接收效果的粒子)
+ */
+ constructor(redParticle, startBlueParticle) {
+ this.redParticle = redParticle; // 红色粒子(效果源)
+ this.startBlueParticle = startBlueParticle; // 起始蓝色粒子
+ this.lifeTimer = 20; // 效果持续时间(帧数)
+
+ this.propagatedParticles = []; // 已传播的粒子数组(包含传播信息)
+ this.propagationQueue = []; // 传播队列(待传播的粒子)
+ this.propagationDelay = 3; // 传播延迟(帧数)
+ this.propagationCounter = 0; // 传播计数器
+ this.maxPropagationSteps = 8; // 最大传播步数
+ this.currentPropagationStep = 0; // 当前传播步数
+
+ // 从红色粒子开始,找到第一个连接的蓝色粒子并应用效果
+ let connectedBlueParticles = this.findConnectedBlues(this.redParticle);
+ if (connectedBlueParticles.length > 0 && this.currentPropagationStep < this.maxPropagationSteps) {
+ // 随机选择一个连接的蓝色粒子
+ let targetParticle = connectedBlueParticles[floor(random(connectedBlueParticles.length))];
+ this.applyEffectToParticle(targetParticle);
+
+ // 记录传播信息
+ this.propagatedParticles.push({
+ particle: targetParticle,
+ source: this.redParticle,
+ step: 1,
+ pulseProgress: 0 // 脉冲动画进度(0-1)
+ });
+
+ // 加入传播队列,用于下一步传播
+ this.propagationQueue.push(targetParticle);
+ this.currentPropagationStep = 1;
+ }
+ }
+
+ /**
+ * 对粒子应用效果(将其变为橙色并添加动画效果)
+ * @param {CandidateParticle} particle - 要应用效果的粒子
+ */
+ applyEffectToParticle(particle) {
+ // 如果粒子已经有效果,则跳过
+ if (particle.hasEffect) return;
+ // 如果是原始红色粒子,则跳过
+ if (particle.isRed && particle.originalIsRed === true) return;
+
+ // 标记粒子已有效果
+ particle.hasEffect = true;
+
+ // 保存原始状态
+ if (particle.originalIsRed === undefined) {
+ particle.originalIsRed = particle.isRed;
+ }
+ particle.originalChangeTimer = particle.changeTimer;
+ particle.originalX = particle.x;
+ particle.originalY = particle.y;
+
+ // 设置效果属性
+ particle.effectTimer = this.lifeTimer;
+ particle.isOrange = true; // 变为橙色
+ particle.isRed = false; // 不再是红色
+ particle.changeSpeedMultiplier = 3; // 字符变化速度加快
+ particle.vibrationOffsetX = 0; // 振动偏移X
+ particle.vibrationOffsetY = 0; // 振动偏移Y
+ particle.vibrationPhase = random(TWO_PI); // 振动相位(随机)
+ }
+
+ /**
+ * 查找与指定粒子真正连接的蓝色粒子(通过连接线连接)
+ * @param {CandidateParticle} particle - 源粒子
+ * @returns {Array} 连接的蓝色粒子数组
+ */
+ findConnectedBlues(particle) {
+ let connected = [];
+ let usedParticles = new Set();
+
+ // 记录已使用的粒子(红色粒子和已传播的粒子)
+ usedParticles.add(this.redParticle);
+ for (let prop of this.propagatedParticles) {
+ usedParticles.add(prop.particle);
+ }
+
+ // 只检查粒子connections数组中真正连接的粒子
+ // 这样可以确保只在有连接线的粒子之间传递效果
+ for (let connectedParticle of particle.connections) {
+ // 检查条件:
+ // 1. 粒子还没有效果
+ // 2. 粒子不在已使用列表中
+ // 3. 粒子不是红色
+ // 4. 粒子不是橙色(已受影响的粒子)
+ if (!connectedParticle.hasEffect &&
+ !usedParticles.has(connectedParticle) &&
+ !connectedParticle.isRed &&
+ !connectedParticle.isOrange) {
+ connected.push(connectedParticle);
+ }
+ }
+
+ return connected;
+ }
+
+ /**
+ * 更新效果传播和动画
+ * @returns {boolean} 效果是否仍然有效
+ */
+ update() {
+ this.lifeTimer--;
+ this.propagationCounter++;
+
+ // 每隔一定帧数进行一次传播
+ if (this.propagationCounter >= this.propagationDelay &&
+ this.currentPropagationStep < this.maxPropagationSteps) {
+ this.propagationCounter = 0;
+
+ let nextPropagationQueue = [];
+
+ // 从当前传播队列中的每个粒子继续传播
+ for (let sourceParticle of this.propagationQueue) {
+ if (this.currentPropagationStep >= this.maxPropagationSteps) break;
+
+ // 找到与源粒子连接的蓝色粒子(只查找真正有连接线的粒子)
+ let connectedBlueParticles = this.findConnectedBlues(sourceParticle);
+
+ if (connectedBlueParticles.length > 0) {
+ // 随机选择一个连接的蓝色粒子
+ let targetParticle = connectedBlueParticles[floor(random(connectedBlueParticles.length))];
+ this.applyEffectToParticle(targetParticle);
+
+ // 更新传播步数
+ this.currentPropagationStep++;
+
+ // 记录传播信息
+ this.propagatedParticles.push({
+ particle: targetParticle,
+ source: sourceParticle,
+ step: this.currentPropagationStep,
+ pulseProgress: 0
+ });
+
+ // 如果还有传播步数,加入下一轮传播队列
+ if (this.currentPropagationStep < this.maxPropagationSteps) {
+ nextPropagationQueue.push(targetParticle);
+ }
+ }
+ }
+
+ // 更新传播队列
+ this.propagationQueue = nextPropagationQueue.length > 0 ? nextPropagationQueue : [];
+ }
+
+ // 更新所有已传播粒子的效果动画
+ for (let propagationInfo of this.propagatedParticles) {
+ this.updateParticleEffectAnimation(propagationInfo.particle);
+ // 更新脉冲动画进度
+ propagationInfo.pulseProgress += 0.15;
+ if (propagationInfo.pulseProgress > 1) {
+ propagationInfo.pulseProgress = 1;
+ }
+ }
+
+ // 返回效果是否仍然有效
+ return this.lifeTimer > 0;
+ }
+
+ /**
+ * 更新粒子的效果动画(振动、透明度、缩放、发光)
+ * @param {CandidateParticle} particle - 要更新动画的粒子
+ */
+ updateParticleEffectAnimation(particle) {
+ if (!particle.hasEffect) return;
+
+ particle.effectTimer--;
+
+ // 振动效果:粒子位置轻微振动
+ let vibrationIntensity = 2;
+ particle.vibrationPhase += 0.4;
+ particle.vibrationOffsetX = cos(particle.vibrationPhase) * vibrationIntensity;
+ particle.vibrationOffsetY = sin(particle.vibrationPhase * 1.3) * vibrationIntensity;
+
+ // 透明度动画:周期性变化
+ let alphaPhase = (frameCount * 0.1 + particle.vibrationPhase) % (TWO_PI);
+ particle.effectAlpha = map(sin(alphaPhase), -1, 1, 150, 255);
+
+ // 缩放动画:周期性缩放
+ let scalePhase = (frameCount * 0.15 + particle.vibrationPhase) % (TWO_PI);
+ particle.effectScale = map(sin(scalePhase), -1, 1, 0.95, 1.05);
+
+ // 发光效果:根据透明度计算发光强度
+ particle.effectGlow = map(particle.effectAlpha, 150, 255, 15, 25);
+ }
+
+ /**
+ * 显示效果传播的视觉效果(连接线、脉冲、箭头)
+ */
+ display() {
+ strokeWeight(2);
+
+ // 遍历所有已传播的粒子,绘制传播效果
+ for (let propagationInfo of this.propagatedParticles) {
+ if (propagationInfo.source) {
+ // 计算透明度:根据传播步数和剩余时间
+ let stepAlpha = map(propagationInfo.step, 1, this.maxPropagationSteps, 255, 100);
+ let timeAlpha = map(this.lifeTimer, 0, 20, 0, 255);
+ let alpha = min(stepAlpha, timeAlpha);
+
+ // 绘制连接线(从源粒子到目标粒子)
+ stroke(255, 150, 50, alpha * 0.3);
+ line(propagationInfo.source.x, propagationInfo.source.y,
+ propagationInfo.particle.x, propagationInfo.particle.y);
+
+ // 绘制脉冲动画(沿着连接线移动的光点)
+ if (propagationInfo.pulseProgress < 1) {
+ let pulseX = lerp(propagationInfo.source.x, propagationInfo.particle.x,
+ propagationInfo.pulseProgress);
+ let pulseY = lerp(propagationInfo.source.y, propagationInfo.particle.y,
+ propagationInfo.pulseProgress);
+
+ let pulseSize = map(propagationInfo.pulseProgress, 0, 1, 3, 8);
+ let pulseAlpha = map(propagationInfo.pulseProgress, 0, 1, 255, 0);
+
+ // 绘制脉冲光点
+ noStroke();
+ fill(255, 200, 100, pulseAlpha);
+ ellipse(pulseX, pulseY, pulseSize);
+
+ // 绘制脉冲光晕
+ fill(255, 180, 80, pulseAlpha * 0.5);
+ ellipse(pulseX, pulseY, pulseSize * 2);
+ }
+
+ // 绘制箭头(在连接线中间,表示传播方向)
+ if (propagationInfo.pulseProgress > 0.5 && propagationInfo.pulseProgress < 1) {
+ let arrowX = lerp(propagationInfo.source.x, propagationInfo.particle.x, 0.5);
+ let arrowY = lerp(propagationInfo.source.y, propagationInfo.particle.y, 0.5);
+ let angle = atan2(propagationInfo.particle.y - propagationInfo.source.y,
+ propagationInfo.particle.x - propagationInfo.source.x);
+
+ push();
+ translate(arrowX, arrowY);
+ rotate(angle);
+ stroke(255, 200, 100, alpha * 0.8);
+ strokeWeight(1.5);
+ fill(255, 200, 100, alpha * 0.8);
+ triangle(0, 0, -8, -4, -8, 4);
+ pop();
+ }
+ }
+ }
+ }
+
+ /**
+ * 清理效果:移除所有粒子的效果
+ */
+ cleanup() {
+ for (let propagationInfo of this.propagatedParticles) {
+ this.removeEffectFromParticle(propagationInfo.particle);
+ }
+ }
+
+ /**
+ * 从粒子移除效果,恢复原始状态
+ * @param {CandidateParticle} particle - 要移除效果的粒子
+ */
+ removeEffectFromParticle(particle) {
+ if (!particle.hasEffect) return;
+
+ // 移除效果标记
+ particle.hasEffect = false;
+
+ // 恢复颜色状态
+ if (particle.isOrange) {
+ particle.isOrange = false;
+ particle.isRed = false;
+ }
+
+ // 恢复属性
+ particle.changeSpeedMultiplier = 1;
+ particle.vibrationOffsetX = 0;
+ particle.vibrationOffsetY = 0;
+
+ // 恢复原始计时器
+ if (particle.originalChangeTimer !== undefined) {
+ particle.changeTimer = particle.originalChangeTimer;
+ }
+ }
+}
+
+class FloatingText {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ this.vx = random(-0.3, 0.3);
+ this.vy = random(-0.3, 0.3);
+ this.char = this.randomChar();
+ this.alpha = random(10, 80);
+ this.size = random(fontSize - 10, fontSize + 2);
+ this.changeTimer = int(random(30, 80));
+ }
+
+ randomChar() {
+ return chineseChars.charAt(floor(random(chineseChars.length)));
+ }
+
+ update() {
+ this.x += this.vx;
+ this.y += this.vy;
+
+ if (this.x < textAreaX || this.x > textAreaX + textAreaWidth) {
+ this.vx *= -1;
+ this.x = constrain(this.x, textAreaX, textAreaX + textAreaWidth);
+ }
+ if (this.y < textAreaY || this.y > textAreaY + textAreaHeight) {
+ this.vy *= -1;
+ this.y = constrain(this.y, textAreaY, textAreaY + textAreaHeight);
+ }
+
+ this.changeTimer--;
+ if (this.changeTimer <= 0) {
+ this.char = this.randomChar();
+ this.changeTimer = int(random(30, 80));
+ }
+ }
+
+ display() {
+ let displayAlpha = this.alpha;
+ if (isCompleted && completionPhase !== 'none') {
+ displayAlpha = this.alpha * (fadeOutAlpha / 255);
+ }
+
+ noStroke();
+ fill(180, 200, 255, displayAlpha);
+ textSize(this.size);
+ text(this.char, this.x, this.y);
+ }
+}
+
+class CandidateParticle {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ this.vx = random(-0.5, 0.5);
+ this.vy = random(-0.5, 0.5);
+ this.char = this.randomChar();
+ this.baseAlpha = 150;
+ this.size = fontSize;
+ this.prob = random(0.1, 1.0);
+ this.isRed = false;
+ this.isOrange = false;
+ this.changeTimer = int(random(30, 80));
+ this.phraseGroup = null;
+ this.phraseIndex = 0;
+ this.isTemporaryRed = false;
+ this.tempRedTimer = 0;
+ this.connections = [];
+ this.isStatic = false;
+ this.originalIsRed = false;
+ this.hasEffect = false;
+ this.changeSpeedMultiplier = 1;
+ this.vibrationOffsetX = 0;
+ this.vibrationOffsetY = 0;
+ this.vibrationPhase = 0;
+ }
+
+ randomChar() {
+ return chineseChars.charAt(floor(random(chineseChars.length)));
+ }
+
+ randomWord() {
+ return chineseWords[floor(random(chineseWords.length))];
+ }
+
+ update() {
+ this.x += this.vx;
+ this.y += this.vy;
+
+ if (this.x < textAreaX || this.x > textAreaX + textAreaWidth) {
+ this.vx *= -1;
+ this.x = constrain(this.x, textAreaX, textAreaX + textAreaWidth);
+ }
+ if (this.y < textAreaY || this.y > textAreaY + textAreaHeight) {
+ this.vy *= -1;
+ this.y = constrain(this.y, textAreaY, textAreaY + textAreaHeight);
+ }
+
+ this.alpha = this.baseAlpha + this.prob * 105;
+ this.displaySize = this.size * (0.5 + this.prob);
+
+ this.vx *= 0.95;
+ this.vy *= 0.95;
+
+ if (!this.isStatic && !this.isCalmed) {
+ // 获取对应波形点的抚平程度(如果没有关联,使用整体平静进度)
+ let localCalmProgress = this.calmProgress !== undefined ? this.calmProgress : calmProgress;
+
+ // 如果抚平程度高,停止字符变化
+ if (localCalmProgress < 0.7) {
+ let changeSpeed = this.changeSpeedMultiplier || 1;
+ // 根据抚平程度减慢变化速度
+ changeSpeed *= (1 - localCalmProgress);
+ this.changeTimer -= changeSpeed;
+
+ if (this.changeTimer <= 0) {
+ if (phraseMode && this.phraseGroup) {
+ let word = this.randomWord();
+ for (let i = 0; i < this.phraseGroup.length; i++) {
+ if (!this.phraseGroup[i].isStatic && !this.phraseGroup[i].isCalmed) {
+ let groupCalmProgress = this.phraseGroup[i].calmProgress !== undefined ?
+ this.phraseGroup[i].calmProgress : calmProgress;
+ if (groupCalmProgress < 0.7) {
+ if (i < word.length) {
+ this.phraseGroup[i].char = word.charAt(i);
+ } else {
+ this.phraseGroup[i].char = this.phraseGroup[i].randomChar();
+ }
+ this.phraseGroup[i].changeTimer = int(random(30, 80));
+ }
+ }
+ }
+ } else {
+ this.char = this.randomChar();
+ this.changeTimer = int(random(30, 80));
+ }
+ }
+ }
+ }
+
+ if (this.hasEffect) {
+ if (this.changeSpeedMultiplier === undefined) {
+ this.changeSpeedMultiplier = 1;
+ }
+ if (this.vibrationOffsetX === undefined) {
+ this.vibrationOffsetX = 0;
+ this.vibrationOffsetY = 0;
+ this.vibrationPhase = random(TWO_PI);
+ }
+ }
+
+ if (this.isTemporaryRed) {
+ this.tempRedTimer--;
+ if (this.tempRedTimer <= 0) {
+ this.isTemporaryRed = false;
+ }
+ }
+ }
+
+ applyRippleAndRepel() {
+ for (let other of candidateParticles) {
+ if (other === this) continue;
+ let d = dist(this.x, this.y, other.x, other.y);
+ let minDist = (this.displaySize + other.displaySize) * 0.4;
+ if (d < minDist && d > 0) {
+ let angle = atan2(this.y - other.y, this.x - other.x);
+ let force = map(d, 0, minDist, 1.5, 0);
+ this.vx += cos(angle) * force * 0.1;
+ this.vy += sin(angle) * force * 0.1;
+ }
+ }
+
+ for (let ripple of rippleEffects) {
+ let rd = dist(ripple.x, ripple.y, this.x, this.y);
+ if (rd < ripple.r + 60) {
+ let angle = atan2(ripple.y - this.y, ripple.x - this.x);
+ let force = map(ripple.r + 60 - rd, 0, ripple.r + 60, 0, 0.3);
+ this.vx += cos(angle) * force;
+ this.vy += sin(angle) * force;
+ }
+ }
+
+ for (let repel of repelBursts) {
+ let d = dist(repel.x, repel.y, this.x, this.y);
+ if (d < repel.r) {
+ let angle = atan2(this.y - repel.y, this.x - repel.x);
+ let force = map(repel.r - d, 0, repel.r, 0, 1.2);
+ this.vx += cos(angle) * force;
+ this.vy += sin(angle) * force;
+ }
+ }
+ }
+
+ display() {
+ noStroke();
+
+ // 计算显示位置:基础位置 + 效果振动 + 焦虑抖动 + 焦虑漂浮
+ let displayX = this.x + (this.vibrationOffsetX || 0);
+ let displayY = this.y + (this.vibrationOffsetY || 0);
+
+ // 获取对应波形点的抚平程度(如果没有关联,使用整体平静进度)
+ let localCalmProgress = this.calmProgress !== undefined ? this.calmProgress : calmProgress;
+ let dissolveProgress = this.dissolveProgress !== undefined ? this.dissolveProgress : 0;
+
+ // 如果是红色粒子且在完成阶段,应用抖动和漂浮效果(根据抚平程度减少)
+ if (this.isRed && (completionPhase === 'revealing' || completionPhase === 'completed')) {
+ if (this.anxietyShakeX !== undefined && this.anxietyShakeY !== undefined) {
+ // 根据抚平程度减少抖动
+ displayX += (this.anxietyShakeX || 0) * (1 - localCalmProgress);
+ displayY += ((this.anxietyShakeY || 0) + (this.anxietyFloatY || 0)) * (1 - localCalmProgress);
+ }
+ }
+
+ let displaySize = this.displaySize;
+ if (this.hasEffect && this.effectScale !== undefined) {
+ displaySize = this.displaySize * this.effectScale;
+ }
+
+ let glowBlur = 12 * this.prob;
+ if (this.hasEffect && this.effectGlow !== undefined) {
+ glowBlur = this.effectGlow;
+ }
+ drawingContext.shadowBlur = glowBlur;
+
+ let displayAlpha = this.alpha;
+ if (this.hasEffect && this.effectAlpha !== undefined) {
+ displayAlpha = this.effectAlpha;
+ }
+
+ // 根据消散进度减少透明度
+ displayAlpha *= (1 - dissolveProgress);
+
+ // 根据平静进度调整红色粒子的颜色(逐渐变淡)
+ if (this.isRed) {
+ if (completionPhase === 'revealing' || completionPhase === 'completed') {
+ // 在完成阶段,根据对应波形点的抚平程度调整颜色
+ let redValue = map(localCalmProgress, 0, 1, 255, 200);
+ let greenValue = map(localCalmProgress, 0, 1, 100, 150);
+ let blueValue = map(localCalmProgress, 0, 1, 100, 150);
+ drawingContext.shadowColor = `rgba(${redValue},${greenValue},${blueValue},${this.prob * (1 - localCalmProgress * 0.5) * (1 - dissolveProgress)})`;
+ fill(redValue, greenValue, blueValue, displayAlpha);
+ } else {
+ // 正常游戏阶段,显示正常的红色
+ drawingContext.shadowColor = `rgba(255,100,100,${this.prob})`;
+ fill(255, 100, 100, displayAlpha);
+ }
+ } else if (this.isOrange) {
+ if (isCompleted && completionPhase !== 'none') {
+ displayAlpha = displayAlpha * (fadeOutAlpha / 255);
+ }
+ drawingContext.shadowColor = `rgba(255,150,50,${this.prob * (fadeOutAlpha / 255)})`;
+ fill(255, 150, 50, displayAlpha);
+ } else if (this.isTemporaryRed) {
+ drawingContext.shadowColor = `rgba(255,150,150,${this.prob})`;
+ fill(255, 150, 150, displayAlpha);
+ } else {
+ if (isCompleted && completionPhase !== 'none') {
+ displayAlpha = displayAlpha * (fadeOutAlpha / 255);
+ }
+ drawingContext.shadowColor = `rgba(50,200,255,${this.prob * (fadeOutAlpha / 255)})`;
+ fill(50, 200, 255, displayAlpha);
+ }
+
+ if (this.hasEffect && this.changeTimer !== undefined && this.changeTimer > 0 && this.changeTimer < 5) {
+ let highlightAlpha = map(this.changeTimer, 0, 5, 255, displayAlpha);
+ if (this.isOrange) {
+ fill(255, 180, 80, highlightAlpha);
+ } else if (this.isRed) {
+ fill(255, 150, 150, highlightAlpha);
+ } else {
+ fill(50, 200, 255, highlightAlpha);
+ }
+ }
+
+ textSize(displaySize);
+ text(this.char, displayX, displayY);
+ drawingContext.shadowBlur = 0;
+ }
+}
+
+class Ripple {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ this.r = 10;
+ this.alpha = 200;
+ }
+
+ update() {
+ this.r += 3;
+ this.alpha -= 4;
+ }
+
+ display() {
+ noFill();
+ stroke(100, 200, 255, this.alpha);
+ strokeWeight(2);
+ ellipse(this.x, this.y, this.r * 2);
+ }
+}
+
+class RepelBurst {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ this.r = 10;
+ this.alpha = 180;
+ }
+
+ update() {
+ this.r += 6;
+ this.alpha -= 5;
+ }
+
+ display() {
+ noFill();
+ stroke(255, 150, 100, this.alpha);
+ strokeWeight(2);
+ ellipse(this.x, this.y, this.r * 2);
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt.meta b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt.meta
new file mode 100644
index 000000000..46d227e23
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 632d2ea6fbd2c684e90cbd7f7e77036a
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language.meta b/Assets/Scripts/MiniGame/HuoShan/Language.meta
new file mode 100644
index 000000000..7569e51dd
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Language.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a1958c693a891564ca52d09de765d438
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs
new file mode 100644
index 000000000..323a8022a
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs
@@ -0,0 +1,203 @@
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace AibisDream.MiniGame.Language
+{
+ ///
+ /// 候选粒子(可交互的主要粒子)
+ ///
+ public class CandidateParticle : TextParticle
+ {
+ [Header("粒子状态")]
+ public bool isRed = false; // 是否是红色目标粒子
+ public bool isOrange = false; // 是否受到效果影响(橙色)
+ public bool originalIsRed = false; // 原始是否为红色
+ public bool hasEffect = false; // 是否有效果
+ public bool isTemporaryRed = false; // 临时红色状态
+
+ [Header("连接")]
+ public List connections = new List();
+
+ [Header("效果")]
+ public float changeSpeedMultiplier = 1f;
+ public Vector2 vibrationOffset;
+ public float vibrationPhase;
+ public float effectTimer;
+ public float effectAlpha;
+ public float effectScale = 1f;
+ public float effectGlow;
+
+ [Header("焦虑效果")]
+ public float anxietyShakePhase;
+ public float anxietyShakeIntensity;
+ public float anxietyFloatPhase;
+ public float anxietyFloatSpeed;
+ public Vector2 anxietyShakeOffset;
+ public float anxietyFloatY;
+
+ [Header("波形关联")]
+ public float calmProgress = 0f; // 平静进度(0-1)
+ public float dissolveProgress = 0f; // 消散进度(0-1)
+ public WaveformPoint waveformPoint; // 关联的波形点
+
+ protected override void Awake()
+ {
+ base.Awake();
+
+ baseAlpha = 0.6f; // 0-1 范围的透明度
+ probability = Random.Range(0.1f, 1f);
+ alpha = baseAlpha + probability * 0.4f; // 最大 1.0
+ size = 3f;
+ velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
+ changeInterval = Random.Range(1f, 2.5f);
+
+ if (textMesh != null)
+ {
+ textMesh.fontSize = size;
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // 更新效果动画
+ if (hasEffect)
+ {
+ UpdateEffectAnimation();
+ }
+
+ // 更新焦虑效果
+ if (isRed && (anxietyShakeIntensity > 0))
+ {
+ UpdateAnxietyEffect();
+ }
+ }
+
+ private void UpdateEffectAnimation()
+ {
+ if (effectTimer > 0)
+ {
+ effectTimer -= Time.deltaTime;
+
+ // 振动效果
+ vibrationPhase += Time.deltaTime * 15f;
+ float vibrationIntensity = 0.02f;
+ vibrationOffset = new Vector2(
+ Mathf.Cos(vibrationPhase) * vibrationIntensity,
+ Mathf.Sin(vibrationPhase * 1.3f) * vibrationIntensity
+ );
+
+ // 透明度动画(0-1 范围)
+ effectAlpha = Mathf.Lerp(0.6f, 1f, (Mathf.Sin(Time.time * 3f) + 1f) / 2f);
+
+ // 缩放动画
+ effectScale = Mathf.Lerp(0.95f, 1.05f, (Mathf.Sin(Time.time * 4.5f) + 1f) / 2f);
+
+ // 发光强度
+ effectGlow = Mathf.Lerp(0.06f, 0.1f, (effectAlpha - 0.6f) / 0.4f);
+ }
+ }
+
+ private void UpdateAnxietyEffect()
+ {
+ // 根据平静进度减少抖动强度
+ float shakeIntensity = anxietyShakeIntensity * (1 - calmProgress) * 0.01f;
+ float floatAmplitude = 0.03f * (1 - calmProgress);
+
+ if (calmProgress < 0.9f)
+ {
+ anxietyShakePhase += Time.deltaTime * 10f;
+ anxietyFloatPhase += anxietyFloatSpeed;
+ }
+
+ // 计算抖动偏移
+ anxietyShakeOffset = new Vector2(
+ Mathf.Cos(anxietyShakePhase) * shakeIntensity + Mathf.Sin(anxietyShakePhase * 1.7f) * shakeIntensity * 0.5f,
+ Mathf.Sin(anxietyShakePhase * 1.3f) * shakeIntensity + Mathf.Cos(anxietyShakePhase * 0.9f) * shakeIntensity * 0.5f
+ );
+
+ // 计算漂浮偏移
+ anxietyFloatY = Mathf.Sin(anxietyFloatPhase) * floatAmplitude;
+ }
+
+ protected override void UpdateVisuals()
+ {
+ if (textMesh == null) return;
+
+ // 计算显示位置
+ Vector3 displayOffset = (Vector3)(vibrationOffset + anxietyShakeOffset) + Vector3.up * anxietyFloatY;
+ textMesh.transform.localPosition = displayOffset;
+
+ // 计算颜色和透明度
+ Color color = Color.white;
+ float displayAlpha = alpha;
+
+ if (hasEffect && effectTimer > 0)
+ {
+ displayAlpha = effectAlpha;
+ textMesh.transform.localScale = Vector3.one * effectScale;
+ }
+
+ // 根据消散进度减少透明度
+ displayAlpha *= (1 - dissolveProgress);
+
+ if (isRed)
+ {
+ // 红色粒子:根据平静进度调整颜色
+ float r = Mathf.Lerp(1f, 0.78f, calmProgress);
+ float g = Mathf.Lerp(0.39f, 0.59f, calmProgress);
+ float b = Mathf.Lerp(0.39f, 0.59f, calmProgress);
+ color = new Color(r, g, b, displayAlpha); // displayAlpha 已经是 0-1 范围
+ }
+ else if (isOrange)
+ {
+ color = new Color(1f, 0.59f, 0.2f, displayAlpha);
+ }
+ else
+ {
+ color = new Color(0.2f, 0.78f, 1f, displayAlpha);
+ }
+
+ textMesh.color = color;
+
+ // 更新光晕
+ if (glowSprite != null)
+ {
+ glowSprite.color = new Color(color.r, color.g, color.b, color.a * 0.5f);
+ float glowScale = probability * 0.5f;
+ if (hasEffect)
+ glowScale += effectGlow;
+ glowSprite.transform.localScale = Vector3.one * glowScale;
+ }
+ }
+
+ public void InitializeAnxietyEffect()
+ {
+ anxietyShakePhase = Random.Range(0f, Mathf.PI * 2f);
+ anxietyShakeIntensity = Random.Range(2f, 4f);
+ anxietyFloatPhase = Random.Range(0f, Mathf.PI * 2f);
+ anxietyFloatSpeed = Random.Range(0.02f, 0.04f);
+ }
+
+ public void ApplyEffect(float duration = 1f)
+ {
+ hasEffect = true;
+ effectTimer = duration;
+ isOrange = true;
+ isRed = false;
+ changeSpeedMultiplier = 3f;
+ vibrationPhase = Random.Range(0f, Mathf.PI * 2f);
+ }
+
+ public void RemoveEffect()
+ {
+ hasEffect = false;
+ isOrange = false;
+ isRed = false;
+ changeSpeedMultiplier = 1f;
+ vibrationOffset = Vector2.zero;
+ effectTimer = 0;
+ }
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs.meta
similarity index 83%
rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta
rename to Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs.meta
index 0959e71e6..b69762fce 100644
--- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: bc6c154e0dac2e6469211e12b1f05fce
+guid: fe9b66f677d7a024c8f232d03839b2ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs
new file mode 100644
index 000000000..48b50169d
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs
@@ -0,0 +1,210 @@
+using UnityEngine;
+using System.Collections.Generic;
+using Shapes;
+
+namespace AibisDream.MiniGame.Language
+{
+ ///
+ /// 连接线渲染器:使用Shapes绘制粒子间的连接线
+ ///
+ [ExecuteAlways]
+ public class ConnectionRenderer : ImmediateModeShapeDrawer
+ {
+ [Header("引用")]
+ public LanguageParticleManager manager;
+
+ [Header("连接线设置")]
+ public float connectionDistance = 2f;
+ public Color blueConnectionColor = new Color(0.39f, 0.78f, 1f, 0.7f);
+ public Color redConnectionColor = new Color(1f, 0.39f, 0.39f, 1f);
+ public float blueLineThickness = 0.02f;
+ public float redLineThickness = 0.04f;
+
+ [Header("效果传播设置")]
+ public Color propagationColor = new Color(1f, 0.59f, 0.2f, 1f);
+ public float propagationLineThickness = 0.03f;
+
+ private List candidateParticles;
+ private List effectPropagations;
+ private CompletionPhase completionPhase;
+ private float fadeOutAlpha;
+
+ public void SetData(
+ List candidates,
+ List propagations,
+ CompletionPhase phase,
+ float fadeAlpha)
+ {
+ candidateParticles = candidates;
+ effectPropagations = propagations;
+ completionPhase = phase;
+ fadeOutAlpha = fadeAlpha;
+ }
+
+ public override void DrawShapes(Camera cam)
+ {
+ if (candidateParticles == null || candidateParticles.Count == 0)
+ return;
+
+ using (Draw.Command(cam))
+ {
+ Draw.BlendMode = ShapesBlendMode.Transparent;
+ Draw.ZTest = UnityEngine.Rendering.CompareFunction.Always;
+
+ // 绘制蓝色粒子间的连线
+ if (completionPhase == CompletionPhase.None || completionPhase == CompletionPhase.Arranging)
+ {
+ DrawBlueConnections();
+ }
+
+ // 绘制红色粒子间的连线
+ if (completionPhase == CompletionPhase.None || completionPhase == CompletionPhase.Arranging)
+ {
+ DrawRedConnections();
+ }
+
+ // 绘制效果传播线
+ if (effectPropagations != null && effectPropagations.Count > 0)
+ {
+ DrawPropagationEffects();
+ }
+ }
+ }
+
+ private void DrawBlueConnections()
+ {
+ Draw.LineGeometry = LineGeometry.Flat2D;
+ Draw.Thickness = blueLineThickness;
+
+ for (int i = 0; i < candidateParticles.Count; i++)
+ {
+ for (int j = i + 1; j < candidateParticles.Count; j++)
+ {
+ var p1 = candidateParticles[i];
+ var p2 = candidateParticles[j];
+
+ // 跳过红色粒子
+ if (p1.isRed && p2.isRed) continue;
+
+ float distance = Vector3.Distance(p1.transform.position, p2.transform.position);
+ if (distance < connectionDistance)
+ {
+ float alpha = Mathf.Lerp(0.7f, 0f, distance / connectionDistance) * fadeOutAlpha;
+ Color lineColor = blueConnectionColor;
+ lineColor.a = alpha;
+
+ Draw.Line(p1.transform.position, p2.transform.position, lineColor);
+ }
+ }
+ }
+ }
+
+ private void DrawRedConnections()
+ {
+ Draw.LineGeometry = LineGeometry.Flat2D;
+ Draw.Thickness = redLineThickness;
+
+ var redParticles = new List();
+ foreach (var p in candidateParticles)
+ {
+ if (p.isRed)
+ redParticles.Add(p);
+ }
+
+ for (int i = 0; i < redParticles.Count; i++)
+ {
+ for (int j = i + 1; j < redParticles.Count; j++)
+ {
+ float distance = Vector3.Distance(
+ redParticles[i].transform.position,
+ redParticles[j].transform.position
+ );
+
+ if (distance < connectionDistance)
+ {
+ float alpha = Mathf.Lerp(1f, 0.4f, distance / connectionDistance);
+ Color lineColor = redConnectionColor;
+ lineColor.a = alpha;
+
+ Draw.Line(redParticles[i].transform.position, redParticles[j].transform.position, lineColor);
+ }
+ }
+ }
+ }
+
+ private void DrawPropagationEffects()
+ {
+ Draw.LineGeometry = LineGeometry.Flat2D;
+ Draw.Thickness = propagationLineThickness;
+
+ foreach (var propagation in effectPropagations)
+ {
+ if (propagation.PropagatedParticles == null) continue;
+
+ foreach (var info in propagation.PropagatedParticles)
+ {
+ if (info.source == null || info.particle == null) continue;
+
+ // 绘制连接线
+ Color lineColor = propagationColor;
+ lineColor.a = 0.3f;
+ Draw.Line(info.source.transform.position, info.particle.transform.position, lineColor);
+
+ // 绘制脉冲光点
+ if (info.pulseProgress < 1f)
+ {
+ Vector3 pulsePos = Vector3.Lerp(
+ info.source.transform.position,
+ info.particle.transform.position,
+ info.pulseProgress
+ );
+
+ float pulseSize = Mathf.Lerp(0.03f, 0.08f, info.pulseProgress);
+ float pulseAlpha = Mathf.Lerp(1f, 0f, info.pulseProgress);
+
+ Color pulseColor = new Color(1f, 0.78f, 0.39f, pulseAlpha);
+ Draw.Ring(pulsePos, pulseSize, pulseSize * 0.5f, pulseColor);
+
+ // 光晕
+ pulseColor.a = pulseAlpha * 0.5f;
+ Draw.Disc(pulsePos, pulseSize * 1.5f, pulseColor);
+ }
+
+ // 绘制箭头
+ if (info.pulseProgress > 0.5f && info.pulseProgress < 1f)
+ {
+ Vector3 arrowPos = Vector3.Lerp(
+ info.source.transform.position,
+ info.particle.transform.position,
+ 0.5f
+ );
+
+ Vector3 direction = (info.particle.transform.position - info.source.transform.position).normalized;
+ float angle = Mathf.Atan2(direction.y, direction.x);
+
+ Color arrowColor = new Color(1f, 0.78f, 0.39f, 0.8f);
+ DrawArrow(arrowPos, angle, 0.1f, arrowColor);
+ }
+ }
+ }
+ }
+
+ private void DrawArrow(Vector3 position, float angle, float size, Color color)
+ {
+ Vector3 tip = position + new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * size;
+ Vector3 left = position + new Vector3(
+ Mathf.Cos(angle + 2.5f),
+ Mathf.Sin(angle + 2.5f),
+ 0
+ ) * size * 0.5f;
+ Vector3 right = position + new Vector3(
+ Mathf.Cos(angle - 2.5f),
+ Mathf.Sin(angle - 2.5f),
+ 0
+ ) * size * 0.5f;
+
+ Draw.Triangle(tip, left, right, color);
+ }
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs.meta
similarity index 83%
rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta
rename to Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs.meta
index b2b973262..5c9a8c9e1 100644
--- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: c5da4de45a1d4c741b06d9c9c0519ff4
+guid: d7feb80748ec75e4eb986d0d676dfa04
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs
new file mode 100644
index 000000000..66ff537f9
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs
@@ -0,0 +1,154 @@
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace AibisDream.MiniGame.Language
+{
+ ///
+ /// 效果传播信息
+ ///
+ public class PropagationInfo
+ {
+ public CandidateParticle particle;
+ public CandidateParticle source;
+ public int step;
+ public float pulseProgress;
+
+ public PropagationInfo(CandidateParticle particle, CandidateParticle source, int step)
+ {
+ this.particle = particle;
+ this.source = source;
+ this.step = step;
+ this.pulseProgress = 0f;
+ }
+ }
+
+ ///
+ /// 效果传播系统:管理从红色粒子到蓝色粒子的效果传递
+ ///
+ public class EffectPropagation
+ {
+ private CandidateParticle redParticle;
+ private List propagatedParticles = new List();
+ private List propagationQueue = new List();
+
+ private float lifeTimer;
+ private float propagationDelay = 0.05f;
+ private float propagationCounter = 0f;
+ private int maxPropagationSteps = 8;
+ private int currentPropagationStep = 0;
+
+ public bool IsActive => lifeTimer > 0;
+ public List PropagatedParticles => propagatedParticles;
+
+ public EffectPropagation(CandidateParticle redParticle, float duration = 1f)
+ {
+ this.redParticle = redParticle;
+ this.lifeTimer = duration;
+
+ // 从红色粒子开始传播
+ var connectedBlues = FindConnectedBlues(redParticle);
+ if (connectedBlues.Count > 0 && currentPropagationStep < maxPropagationSteps)
+ {
+ // 随机选择一个连接的蓝色粒子
+ var targetParticle = connectedBlues[Random.Range(0, connectedBlues.Count)];
+ ApplyEffectToParticle(targetParticle);
+
+ // 记录传播信息
+ propagatedParticles.Add(new PropagationInfo(targetParticle, redParticle, 1));
+
+ // 加入传播队列
+ propagationQueue.Add(targetParticle);
+ currentPropagationStep = 1;
+ }
+ }
+
+ public void Update(float deltaTime)
+ {
+ lifeTimer -= deltaTime;
+ propagationCounter += deltaTime;
+
+ // 定期传播
+ if (propagationCounter >= propagationDelay && currentPropagationStep < maxPropagationSteps)
+ {
+ propagationCounter = 0f;
+
+ List nextQueue = new List();
+
+ foreach (var sourceParticle in propagationQueue)
+ {
+ if (currentPropagationStep >= maxPropagationSteps) break;
+
+ var connectedBlues = FindConnectedBlues(sourceParticle);
+ if (connectedBlues.Count > 0)
+ {
+ var targetParticle = connectedBlues[Random.Range(0, connectedBlues.Count)];
+ ApplyEffectToParticle(targetParticle);
+
+ currentPropagationStep++;
+
+ propagatedParticles.Add(new PropagationInfo(targetParticle, sourceParticle, currentPropagationStep));
+
+ if (currentPropagationStep < maxPropagationSteps)
+ {
+ nextQueue.Add(targetParticle);
+ }
+ }
+ }
+
+ propagationQueue = nextQueue;
+ }
+
+ // 更新所有传播粒子的脉冲动画
+ foreach (var info in propagatedParticles)
+ {
+ info.pulseProgress += deltaTime * 5f;
+ if (info.pulseProgress > 1f)
+ info.pulseProgress = 1f;
+ }
+ }
+
+ private List FindConnectedBlues(CandidateParticle particle)
+ {
+ List connected = new List();
+ HashSet usedParticles = new HashSet();
+
+ // 记录已使用的粒子
+ usedParticles.Add(redParticle);
+ foreach (var prop in propagatedParticles)
+ {
+ usedParticles.Add(prop.particle);
+ }
+
+ // 只检查真正连接的粒子
+ foreach (var connectedParticle in particle.connections)
+ {
+ if (!connectedParticle.hasEffect &&
+ !usedParticles.Contains(connectedParticle) &&
+ !connectedParticle.isRed &&
+ !connectedParticle.isOrange)
+ {
+ connected.Add(connectedParticle);
+ }
+ }
+
+ return connected;
+ }
+
+ private void ApplyEffectToParticle(CandidateParticle particle)
+ {
+ if (particle.hasEffect) return;
+ if (particle.isRed && particle.originalIsRed) return;
+
+ particle.ApplyEffect(lifeTimer);
+ }
+
+ public void Cleanup()
+ {
+ foreach (var info in propagatedParticles)
+ {
+ info.particle.RemoveEffect();
+ }
+ }
+ }
+}
+
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs.meta
similarity index 83%
rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta
rename to Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs.meta
index 8dbc7fa7f..b2fcf2d61 100644
--- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: ce8085db413e67d4c9ddd8a3415ae3cf
+guid: 13d2bba97b998f049bb52f097b0f597c
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs
new file mode 100644
index 000000000..a43b3fd9b
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs
@@ -0,0 +1,42 @@
+using UnityEngine;
+
+namespace AibisDream.MiniGame.Language
+{
+ ///
+ /// 背景浮动文字粒子
+ ///
+ public class FloatingTextParticle : TextParticle
+ {
+ protected override void Awake()
+ {
+ base.Awake();
+
+ // 背景文字特性
+ baseAlpha = Random.Range(0.04f, 0.1f); // 0-1 范围的透明度
+ alpha = baseAlpha;
+ size = Random.Range(1.5f, 2.5f);
+ velocity = new Vector2(Random.Range(-0.3f, 0.3f), Random.Range(-0.3f, 0.3f));
+ changeInterval = Random.Range(0.5f, 0.7f);
+
+ if (textMesh != null)
+ {
+ textMesh.fontSize = size;
+ textMesh.color = new Color(0.7f, 0.78f, 1f, alpha);
+ }
+ }
+
+ protected override void UpdateVisuals()
+ {
+ if (textMesh != null)
+ {
+ Color color = new Color(0.7f, 0.78f, 1f, alpha);
+ textMesh.color = color;
+ }
+
+ if (glowSprite != null)
+ {
+ glowSprite.color = new Color(0.7f, 0.78f, 1f, alpha * 0.3f);
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs.meta
similarity index 83%
rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta
rename to Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs.meta
index 70d934783..93cfa4990 100644
--- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 68f10137fe541b44485fa63022e29524
+guid: f7f33e95ffe9c50418758691861b6ebd
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
new file mode 100644
index 000000000..b86560dd2
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
@@ -0,0 +1,795 @@
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+using DG.Tweening;
+using Shapes;
+using TMPro;
+
+namespace AibisDream.MiniGame.Language
+{
+ ///
+ /// 游戏完成阶段
+ ///
+ public enum CompletionPhase
+ {
+ None, // 未完成
+ Arranging, // 排列中
+ Revealing, // 揭示中
+ Completed // 已完成
+ }
+
+ ///
+ /// 语言粒子系统主管理器
+ ///
+ public class LanguageParticleManager : MonoBehaviour
+ {
+ [Header("Canvas设置")]
+ [SerializeField] private Canvas worldCanvas;
+ [SerializeField] private float canvasSize = 10f;
+
+ [Header("字体设置")]
+ [SerializeField] private TMP_FontAsset chineseFontAsset;
+
+ [Header("粒子预制体")]
+ [SerializeField] private GameObject floatingParticlePrefab;
+ [SerializeField] private GameObject candidateParticlePrefab;
+
+ [Header("粒子数量")]
+ [SerializeField] private int floatingCount = 100;
+ [SerializeField] private int candidateCount = 50;
+ [SerializeField] private int redParticleCount = 8;
+
+ [Header("游戏参数")]
+ [SerializeField] private float connectionDistance = 2f; // Unity单位
+ [SerializeField] private float textMargin = 1f;
+
+ [Header("目标句子")]
+ [SerializeField] private string targetSentence = "别过来我感觉害怕";
+
+ [Header("波形参数")]
+ [SerializeField] private float waveformAmplitude = 0.3f;
+ [SerializeField] private float interactionRadius = 0.5f;
+
+ [Header("鼠标交互参数")]
+ [SerializeField] private float mouseInteractionRadius = 1.5f;
+ [SerializeField] private float repulsionForce = 2f;
+ [SerializeField] private float attractionForce = 1.5f;
+
+ // 粒子列表
+ private List floatingParticles = new List();
+ private List candidateParticles = new List();
+ private List redParticles = new List();
+
+ // 效果系统
+ private List effectPropagations = new List();
+ private Dictionary previousConnections = new Dictionary();
+ private bool initializationComplete = false;
+ private int initializationFrames = 0;
+
+ // 游戏状态
+ private bool isCompleted = false;
+ private CompletionPhase completionPhase = CompletionPhase.None;
+ private float completionTimer = 0f;
+ private float fadeOutAlpha = 1f;
+ private List orderedRedParticles = new List();
+ private List targetPositions = new List();
+
+ // 波形系统
+ private List waveformPoints = new List();
+ private float calmProgress = 0f;
+ private Vector2 lastMousePos;
+ private float mouseDragSpeed;
+
+ // 边界
+ private Bounds movementBounds;
+
+ // 连接渲染(使用Shapes)
+ private ConnectionRenderer connectionRenderer;
+ private WaveformRenderer waveformRenderer;
+
+ private void Start()
+ {
+ InitializeCanvas();
+ InitializeParticles();
+ InitializeRenderers();
+ }
+
+ private void InitializeCanvas()
+ {
+ // 如果没有指定canvas,创建一个
+ if (worldCanvas == null)
+ {
+ GameObject canvasObj = new GameObject("Language Canvas");
+ canvasObj.transform.SetParent(transform);
+ canvasObj.transform.localPosition = Vector3.zero;
+ worldCanvas = canvasObj.AddComponent