feat(fix-system): 添加维修模块运行状态表现

This commit is contained in:
2026-07-15 19:34:50 +08:00
parent 161fe6ac7d
commit d075d1e0f8
10 changed files with 9414 additions and 3 deletions
@@ -91,6 +91,8 @@ namespace AibisDream.FixSystem
var name = module.data.moduleName;
if (module.IsHighlighted) dto.highlightedModules.Add(name);
if (module.IsAlarmed) dto.alarmedModules.Add(name);
if (module.SupportsConditionVisual)
dto.moduleConditions[name] = module.Condition.ToString();
}
return dto;
@@ -115,12 +117,26 @@ namespace AibisDream.FixSystem
var highlighted = new HashSet<string>(dto.highlightedModules ?? Enumerable.Empty<string>());
var alarmed = new HashSet<string>(dto.alarmedModules ?? Enumerable.Empty<string>());
var moduleConditions = dto.moduleConditions ?? new Dictionary<string, string>();
foreach (var module in EnumerateAllBodyModules())
{
var name = module.data.moduleName;
module.Highlight(highlighted.Contains(name));
module.Alarm(alarmed.Contains(name));
if (!module.SupportsConditionVisual)
continue;
var condition = ModuleCondition.Normal;
if (moduleConditions.TryGetValue(name, out var conditionName)
&& !Enum.TryParse(conditionName, true, out condition))
{
Debug.LogWarning($"[BodyModuleSystem] Unknown saved module condition '{conditionName}' for {name}; using Normal.");
condition = ModuleCondition.Normal;
}
module.SetCondition(condition);
}
}
@@ -220,6 +236,15 @@ namespace AibisDream.FixSystem
return BodyModules?.FirstOrDefault(module => module.data.moduleName == modulename);
}
/// <summary>
/// 查找任意分组中的模块,包含 inactive 对象。用于可在模块显示前预设状态的 Yarn 命令。
/// </summary>
public BodyModule FindAnyBodyModuleByName(string moduleName)
{
return EnumerateAllBodyModules()
.FirstOrDefault(module => module.data.moduleName == moduleName);
}
/// <summary>
/// 寻找指定类型的BodyModule
/// </summary>
@@ -463,4 +488,4 @@ namespace AibisDream.FixSystem
Body,
Head
}
}
}
@@ -131,6 +131,25 @@ namespace AibisDream
targetModule.Alarm(enable);
}
[YarnCommand("SetModuleState")]
public static void SetModuleState(string moduleName, string stateName)
{
if (!System.Enum.TryParse(stateName, true, out ModuleCondition condition))
{
Debug.LogWarning($"[Yarn] Unknown module state '{stateName}'. Expected Normal, Damaged, or PoweredOff.");
return;
}
var targetModule = BodyModuleSystem?.FindAnyBodyModuleByName(moduleName);
if (targetModule == null)
{
Debug.LogWarning($"[Yarn] Module '{moduleName}' not found.");
return;
}
targetModule.SetCondition(condition);
}
[YarnCommand("DropCable")]
public static void DropCable()
{
@@ -173,4 +192,4 @@ namespace AibisDream
BodyModuleSystem.StopContinuousPain();
}
}
}
}
@@ -0,0 +1,238 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.VFX;
namespace AibisDream.FixSystem
{
/// <summary>
/// 模块运行状态的纯表现层:Module Pic 始终显示,文字负责常亮/闪烁/熄灭,
/// 火花依据 Module Pic 的世界包围盒生成在外围并渲染在本体下层。
/// </summary>
public class ModuleConditionVisual : MonoBehaviour
{
private static readonly Vector2[] SparkDirections =
{
new(-1f, -1f), new(0f, -1f), new(1f, -1f),
new(-1f, 0f), new(1f, 0f),
new(-1f, 1f), new(0f, 1f), new(1f, 1f)
};
[Header("模块引用")]
[SerializeField] private SpriteRenderer modulePicRenderer;
[SerializeField] private SpriteRenderer moduleTextRenderer;
[Header("损坏文字闪烁")]
[SerializeField, Min(0.01f)] private float flickerMinInterval = 0.06f;
[SerializeField, Min(0.01f)] private float flickerMaxInterval = 0.18f;
[Header("损坏火花")]
[SerializeField] private VisualEffectAsset sparkAsset;
[SerializeField, Min(0.01f)] private float sparkMinInterval = 0.35f;
[SerializeField, Min(0.01f)] private float sparkMaxInterval = 1.2f;
[SerializeField, Min(0f)] private float sparkBoundsPadding = 0.06f;
[SerializeField] private Vector2 sparkBoundsScale = Vector2.one;
[SerializeField] private Vector2 sparkCenterOffset = Vector2.zero;
[SerializeField] private Vector2 sparkBurstCount = new(2f, 5f);
[SerializeField] private Vector2 sparkLifetime = new(0.08f, 0.22f);
[SerializeField] private Vector2 sparkSizeFactor = new(0.025f, 0.055f);
[SerializeField, Min(0f)] private float sparkVelocityFactor = 1.5f;
private readonly List<VisualEffect> _sparkEmitters = new();
private Coroutine _textFlickerCoroutine;
private Coroutine _sparkCoroutine;
private Color _originalTextColor = Color.white;
private ModuleCondition _condition = ModuleCondition.Normal;
private bool _initialized;
private bool _hasAppliedCondition;
public bool IsConfigured => modulePicRenderer != null && moduleTextRenderer != null;
private void Awake()
{
Initialize();
}
private void OnEnable()
{
if (_initialized && _hasAppliedCondition)
ApplyConditionInternal(_condition, true);
}
private void OnDisable()
{
StopRunningEffects();
}
public void ApplyCondition(ModuleCondition condition)
{
Initialize();
ApplyConditionInternal(condition, false);
}
private void Initialize()
{
if (_initialized) return;
_initialized = true;
if (moduleTextRenderer != null)
_originalTextColor = moduleTextRenderer.color;
CreateSparkEmitters();
}
private void ApplyConditionInternal(ModuleCondition condition, bool force)
{
if (!force && _hasAppliedCondition && _condition == condition)
return;
_condition = condition;
_hasAppliedCondition = true;
StopRunningEffects();
if (!IsConfigured)
return;
switch (condition)
{
case ModuleCondition.Normal:
SetTextVisible(true);
break;
case ModuleCondition.Damaged:
SetTextVisible(true);
if (isActiveAndEnabled)
{
_textFlickerCoroutine = StartCoroutine(FlickerText());
if (_sparkEmitters.Count > 0)
_sparkCoroutine = StartCoroutine(PlayIntermittentSparks());
}
break;
case ModuleCondition.PoweredOff:
SetTextVisible(false);
break;
}
}
private void StopRunningEffects()
{
if (_textFlickerCoroutine != null)
{
StopCoroutine(_textFlickerCoroutine);
_textFlickerCoroutine = null;
}
if (_sparkCoroutine != null)
{
StopCoroutine(_sparkCoroutine);
_sparkCoroutine = null;
}
foreach (var effect in _sparkEmitters)
{
if (effect == null) continue;
effect.Stop();
effect.gameObject.SetActive(false);
}
}
private IEnumerator FlickerText()
{
while (_condition == ModuleCondition.Damaged)
{
SetTextVisible(Random.value > 0.4f);
yield return new WaitForSeconds(Random.Range(
Mathf.Min(flickerMinInterval, flickerMaxInterval),
Mathf.Max(flickerMinInterval, flickerMaxInterval)));
}
}
private IEnumerator PlayIntermittentSparks()
{
yield return new WaitForSeconds(Random.Range(0.05f, 0.25f));
while (_condition == ModuleCondition.Damaged)
{
PlaySparkBurst();
yield return new WaitForSeconds(Random.Range(
Mathf.Min(sparkMinInterval, sparkMaxInterval),
Mathf.Max(sparkMinInterval, sparkMaxInterval)));
}
}
private void PlaySparkBurst()
{
if (_sparkEmitters.Count == 0) return;
var effect = _sparkEmitters[Random.Range(0, _sparkEmitters.Count)];
if (effect == null) return;
effect.gameObject.SetActive(true);
effect.Reinit();
effect.Play();
}
private void SetTextVisible(bool visible)
{
if (moduleTextRenderer == null) return;
moduleTextRenderer.color = _originalTextColor;
moduleTextRenderer.enabled = visible;
}
private void CreateSparkEmitters()
{
if (sparkAsset == null || modulePicRenderer == null)
return;
var bounds = modulePicRenderer.bounds;
var scaledExtents = new Vector2(
bounds.extents.x * Mathf.Max(0.01f, sparkBoundsScale.x),
bounds.extents.y * Mathf.Max(0.01f, sparkBoundsScale.y));
var sizeBase = Mathf.Max(0.01f, Mathf.Min(bounds.size.x, bounds.size.y));
foreach (var rawDirection in SparkDirections)
{
var direction = rawDirection.normalized;
var worldPosition = (Vector2)bounds.center + sparkCenterOffset + new Vector2(
rawDirection.x * scaledExtents.x,
rawDirection.y * scaledExtents.y) + direction * sparkBoundsPadding;
var emitterObject = new GameObject($"Damage Spark {rawDirection.x:0},{rawDirection.y:0}");
emitterObject.transform.SetParent(transform, false);
emitterObject.transform.localPosition = transform.InverseTransformPoint(worldPosition);
var effect = emitterObject.AddComponent<VisualEffect>();
effect.visualEffectAsset = sparkAsset;
ConfigureSparkEffect(effect, direction, sizeBase);
var renderer = emitterObject.GetComponent<Renderer>();
if (renderer != null)
{
renderer.sortingLayerID = modulePicRenderer.sortingLayerID;
renderer.sortingOrder = modulePicRenderer.sortingOrder - 1;
}
effect.Stop();
emitterObject.SetActive(false);
_sparkEmitters.Add(effect);
}
}
private void ConfigureSparkEffect(VisualEffect effect, Vector2 direction, float sizeBase)
{
if (effect.HasFloat("Spawn Rate"))
effect.SetFloat("Spawn Rate", 0f);
if (effect.HasVector2("Burst Count Min/Max"))
effect.SetVector2("Burst Count Min/Max", sparkBurstCount);
if (effect.HasVector2("Lifetime Min/Max"))
effect.SetVector2("Lifetime Min/Max", sparkLifetime);
if (effect.HasVector2("Size Min/Max"))
effect.SetVector2("Size Min/Max", sparkSizeFactor * sizeBase);
if (effect.HasVector3("Initial Velocity_vector"))
{
var speed = sizeBase * sparkVelocityFactor;
effect.SetVector3("Initial Velocity_vector", new Vector3(direction.x, direction.y, 0f) * speed);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af1c874964b34b4488243b5730d70962
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -36,6 +36,13 @@ namespace AibisDream.FixSystem
[SerializeField]
private SpriteRenderer targetSpriteRenderer; // 目标的 SpriteRenderer
[Header("模块运行状态")]
[SerializeField] private ModuleConditionVisual conditionVisual;
[SerializeField] private ModuleCondition condition = ModuleCondition.Normal;
public ModuleCondition Condition => condition;
public bool SupportsConditionVisual => conditionVisual != null && conditionVisual.IsConfigured;
private SpriteRenderer _cutSpriteRenderer;
private SpriteRenderer _cutTextRenderer; // 添加cutText的SpriteRenderer引用
@@ -60,6 +67,7 @@ namespace AibisDream.FixSystem
private void Start()
{
StartCoroutine(LoadMaterial());
conditionVisual?.ApplyCondition(condition);
}
protected virtual void InitReference()
@@ -192,6 +200,26 @@ namespace AibisDream.FixSystem
#endregion
#region
/// <summary>
/// 设置模块表现状态。状态只影响独立文字与火花,不影响 Module Pic、插线或切割逻辑。
/// </summary>
public bool SetCondition(ModuleCondition newCondition)
{
if (conditionVisual == null)
{
Debug.LogWarning($"[BodyModule] Module {data.moduleName} has no ModuleConditionVisual configured.", this);
return false;
}
condition = newCondition;
conditionVisual.ApplyCondition(newCondition);
return true;
}
#endregion
#region
public void OnCuttingDown()
@@ -295,4 +323,11 @@ namespace AibisDream.FixSystem
// 只读
[JsonIgnore] public string PlugInNodeName => string.Format(PlugInNodeTemplate, moduleName);
}
}
public enum ModuleCondition
{
Normal,
Damaged,
PoweredOff
}
}