feat(fix-system): 添加维修模块运行状态表现
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user