特效新内容。修复gameplay致命bug和分数保存bug
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
@@ -88,12 +88,19 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
private Vector3[] skillIconBaseScales;
|
||||
private bool[] skillIconBaseScaleCached;
|
||||
|
||||
[Tooltip("最多显示的已释放技能图标数量 (默认1)")]
|
||||
public int maxSkillHistoryCount = 1;
|
||||
[Tooltip("无新技能释放时,技能图标自动渐隐的时间 (秒)")]
|
||||
public float skillIconAutoFadeTime = 2f;
|
||||
|
||||
private Coroutine fadeHealthCoroutine;
|
||||
private Coroutine fadeManaCoroutine;
|
||||
private List<Buff> activeBuffs = new List<Buff>();
|
||||
|
||||
private Coroutine skillIconEnterCoroutine;
|
||||
private Image skillIconEnterAnimatingImage;
|
||||
private Coroutine skillIconAutoFadeCoroutine;
|
||||
private float lastSkillIconPushTime = -1f;
|
||||
|
||||
// Track skills that are currently active because HP percent condition is satisfied.
|
||||
private HashSet<string> _activeHpPercentSkills = new HashSet<string>();
|
||||
@@ -146,6 +153,32 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (GameConfig.verboseLogs) Debug.Log(message);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Check for skill icon auto-fade
|
||||
if (Application.isPlaying && skillIconAutoFadeTime > 0f && lastSkillIconPushTime > 0f)
|
||||
{
|
||||
// Only start coroutine if not already running and time exceeded
|
||||
if (skillIconAutoFadeCoroutine == null && Time.time - lastSkillIconPushTime >= skillIconAutoFadeTime)
|
||||
{
|
||||
// Double check visibility
|
||||
bool anyVisible = false;
|
||||
if (skillIconOccupied != null)
|
||||
{
|
||||
for (int i = 0; i < skillIconOccupied.Length; i++)
|
||||
{
|
||||
if (skillIconOccupied[i]) { anyVisible = true; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (anyVisible)
|
||||
{
|
||||
skillIconAutoFadeCoroutine = StartCoroutine(SkillIconAutoFadeRoutine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Resolve UI and optionally pull data from SO
|
||||
@@ -482,6 +515,13 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
Image slot = skillIconSlots[i];
|
||||
if (slot == null) continue;
|
||||
|
||||
// Enforce max count
|
||||
if (i >= maxSkillHistoryCount)
|
||||
{
|
||||
slot.enabled = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool occupied = i < skillIconOccupied.Length && skillIconOccupied[i];
|
||||
slot.enabled = occupied || slot.sprite != null;
|
||||
|
||||
@@ -554,16 +594,29 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (icon == null) return;
|
||||
if (skillIconSlots == null || skillIconSlots.Length == 0) ResolveHudExtras();
|
||||
if (skillIconSlots == null || skillIconSlots.Length == 0) return;
|
||||
|
||||
// Reset auto-fade logic
|
||||
lastSkillIconPushTime = Time.time;
|
||||
if (skillIconAutoFadeCoroutine != null)
|
||||
{
|
||||
StopCoroutine(skillIconAutoFadeCoroutine);
|
||||
skillIconAutoFadeCoroutine = null;
|
||||
}
|
||||
|
||||
int limit = Mathf.Clamp(maxSkillHistoryCount, 0, skillIconSlots.Length);
|
||||
if (limit <= 0) return;
|
||||
|
||||
EnsureSkillIconSlotScaleCache();
|
||||
NormalizeSkillIconSlotsVisualState();
|
||||
|
||||
// Capture the icon being pushed out (oldest) for exit animation.
|
||||
Image lastSlot = skillIconSlots[skillIconSlots.Length - 1];
|
||||
// Capture the icon being pushed out (oldest active within limit) for exit animation.
|
||||
int lastIndex = limit - 1;
|
||||
Image lastSlot = skillIconSlots[lastIndex];
|
||||
Sprite removedSprite = null;
|
||||
Color removedColor = Color.white;
|
||||
bool removedOccupied = skillIconOccupied != null &&
|
||||
skillIconSlots.Length - 1 < skillIconOccupied.Length &&
|
||||
skillIconOccupied[skillIconSlots.Length - 1];
|
||||
lastIndex < skillIconOccupied.Length &&
|
||||
skillIconOccupied[lastIndex];
|
||||
if (removedOccupied && lastSlot != null && lastSlot.sprite != null)
|
||||
{
|
||||
removedSprite = lastSlot.sprite;
|
||||
@@ -571,7 +624,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
|
||||
// Shift down (oldest drops off the end).
|
||||
for (int i = skillIconSlots.Length - 1; i > 0; i--)
|
||||
for (int i = lastIndex; i > 0; i--)
|
||||
{
|
||||
var prev = skillIconSlots[i - 1];
|
||||
var cur = skillIconSlots[i];
|
||||
@@ -601,6 +654,19 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
skillIconOccupied[0] = true;
|
||||
PlaySkillIconEnterAnim(first);
|
||||
}
|
||||
|
||||
// Clear slots beyond limit
|
||||
for (int i = limit; i < skillIconSlots.Length; i++)
|
||||
{
|
||||
if (skillIconSlots[i] != null)
|
||||
{
|
||||
skillIconSlots[i].sprite = null;
|
||||
skillIconSlots[i].enabled = false;
|
||||
}
|
||||
if (skillIconOccupied != null && i < skillIconOccupied.Length)
|
||||
skillIconOccupied[i] = false;
|
||||
}
|
||||
|
||||
ApplySkillIconSlotVisibility();
|
||||
|
||||
// Play exit animation for the removed icon (if any).
|
||||
@@ -610,6 +676,45 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SkillIconAutoFadeRoutine()
|
||||
{
|
||||
if (skillIconSlots == null || skillIconOccupied == null)
|
||||
{
|
||||
skillIconAutoFadeCoroutine = null;
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Fading from Left to Right (0 -> N)
|
||||
// Assuming slot 0 is the newest and leftmost.
|
||||
// If the user meant visual order, 0 is usually left.
|
||||
int limit = Mathf.Clamp(maxSkillHistoryCount, 0, skillIconSlots.Length);
|
||||
|
||||
for (int i = 0; i < limit; i++)
|
||||
{
|
||||
if (i >= skillIconOccupied.Length) break;
|
||||
|
||||
// If new skill interrupts, this coroutine is stopped by PushSkillIcon
|
||||
if (skillIconOccupied[i] && skillIconSlots[i] != null && skillIconSlots[i].sprite != null)
|
||||
{
|
||||
// Play exit animation for this slot
|
||||
PlaySkillIconExitAnim(skillIconSlots[i], skillIconSlots[i].sprite, skillIconSlots[i].color);
|
||||
|
||||
// Clear the slot
|
||||
skillIconSlots[i].sprite = null;
|
||||
skillIconSlots[i].enabled = false;
|
||||
skillIconOccupied[i] = false;
|
||||
|
||||
// Wait small interval between fades
|
||||
yield return new WaitForSeconds(0.15f);
|
||||
}
|
||||
}
|
||||
|
||||
ApplySkillIconSlotVisibility();
|
||||
skillIconAutoFadeCoroutine = null;
|
||||
// Reset time so it doesn't loop immediately (though checks anyVisible)
|
||||
lastSkillIconPushTime = -1f;
|
||||
}
|
||||
|
||||
private void PlaySkillIconEnterAnim(Image img)
|
||||
{
|
||||
if (img == null) return;
|
||||
|
||||
@@ -82,12 +82,24 @@ public class EffectSystem : MonoBehaviour
|
||||
// Captured target for callback
|
||||
GameObject targetToHit = t;
|
||||
bool isEnemyTarget = targetIsEnemy;
|
||||
|
||||
// Calculate damage info for explosion effect (Ally -> Enemy only)
|
||||
(float damage, float maxHealth)? damageInfo = null;
|
||||
if (isEnemyTarget && effectType == EffectType.DamageSingleEnemy)
|
||||
{
|
||||
var ec = targetToHit.GetComponent<EnemyCombatant>();
|
||||
if (ec != null)
|
||||
{
|
||||
float effectiveDamage = amount * (1f - ec.damageResistance);
|
||||
damageInfo = (effectiveDamage, (float)ec.maxHP);
|
||||
}
|
||||
}
|
||||
|
||||
GfxController.Instance.PlayProjectile(source, t, (finalPos) => {
|
||||
// Only play hit FX when projectile arrives
|
||||
if (targetToHit != null && GfxController.Instance != null)
|
||||
{
|
||||
GfxController.Instance.PlayHitFX(targetToHit, null, isEnemyTarget, finalPos);
|
||||
GfxController.Instance.PlayHitFX(targetToHit, null, isEnemyTarget, finalPos, null, damageInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
@@ -29,10 +29,13 @@ namespace Bansonic
|
||||
int totalFiles = 0;
|
||||
int corruptedFiles = 0;
|
||||
|
||||
// 在主线程提前获取路径,避免子线程调用 Unity API 报错
|
||||
string persistentPath = Application.persistentDataPath;
|
||||
|
||||
// 执行异步检查任务
|
||||
await Task.Run(() =>
|
||||
{
|
||||
string path = Application.persistentDataPath;
|
||||
string path = persistentPath;
|
||||
string[] files = Directory.GetFiles(path, "SongData_*.json");
|
||||
totalFiles = files.Length;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
|
||||
public class BeatmapManager : MonoBehaviour
|
||||
{
|
||||
@@ -34,6 +35,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
assignedSongData = song;
|
||||
assignedDifficulty = difficulty;
|
||||
Debug.LogWarning($"BeatmapManager.AcceptSongData: accepted song={(song==null?"NULL":song.songName)}, difficulty={difficulty}");
|
||||
UpdateGameplayUI();
|
||||
}
|
||||
|
||||
public Beatmap beatmap; // Documentation text normalized.
|
||||
@@ -44,10 +46,14 @@ public class BeatmapManager : MonoBehaviour
|
||||
// Documentation text normalized.
|
||||
public teamUIController uiController;
|
||||
|
||||
public Image bgSpriteImage; // Documentation text normalized.
|
||||
[Header("UI Display")]
|
||||
public Text gameplay_songname;
|
||||
public TextMeshProUGUI gameplay_difficultyID;
|
||||
public TextMeshProUGUI gameplay_difficultyName;
|
||||
|
||||
public GameObject bgSpriteObject;
|
||||
public Image bgMainImage; // 场景主背景图 (UI Image)
|
||||
public Image startCanvas_image;
|
||||
public SpriteRenderer bgSpriteRenderer; // 场景 3D 背景图 (SpriteRenderer)
|
||||
|
||||
// Extra fields from beatmap JSON (stored temporarily)
|
||||
[HideInInspector] public string parsedTitle;
|
||||
@@ -347,38 +353,36 @@ public class BeatmapManager : MonoBehaviour
|
||||
Debug.LogWarning("GameManager or its musicSource not found; audio not assigned");
|
||||
}
|
||||
|
||||
// Assign fullscreen image from SongData to bgSprite if available
|
||||
// Assign fullscreen image from SongData to background images if available
|
||||
if (assignedSongData != null && assignedSongData.fullscreen_songPicture != null)
|
||||
{
|
||||
// Prefer setting a Scene GameObject's SpriteRenderer if provided
|
||||
if (bgSpriteObject != null)
|
||||
Sprite bgSprite = assignedSongData.fullscreen_songPicture;
|
||||
|
||||
if (bgMainImage != null)
|
||||
{
|
||||
var sr = bgSpriteObject.GetComponentInChildren<SpriteRenderer>();
|
||||
if (sr != null)
|
||||
{
|
||||
sr.sprite = assignedSongData.fullscreen_songPicture;
|
||||
// ensure fully visible
|
||||
sr.color = new Color(sr.color.r, sr.color.g, sr.color.b, 1f);
|
||||
if (startCanvas_image != null)
|
||||
{
|
||||
startCanvas_image.sprite = assignedSongData.fullscreen_songPicture;
|
||||
}
|
||||
sr = null;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to SpriteRenderer on bgSpriteObject for song {assignedSongData.songName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("bgSpriteObject has no SpriteRenderer in children; cannot assign fullscreen sprite");
|
||||
}
|
||||
bgMainImage.sprite = bgSprite;
|
||||
// ensure fully visible
|
||||
bgMainImage.color = new Color(bgMainImage.color.r, bgMainImage.color.g, bgMainImage.color.b, 1f);
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgMainImage for song {assignedSongData.songName}");
|
||||
}
|
||||
else if (bgSpriteImage != null)
|
||||
|
||||
if (bgSpriteRenderer != null)
|
||||
{
|
||||
bgSpriteImage.sprite = assignedSongData.fullscreen_songPicture;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgSpriteImage for song {assignedSongData.songName}");
|
||||
bgSpriteRenderer.sprite = bgSprite;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgSpriteRenderer for song {assignedSongData.songName}");
|
||||
// 释放引用以节省内存(按要求:完毕后令 sprite renderer = null)
|
||||
bgSpriteRenderer = null;
|
||||
}
|
||||
else
|
||||
|
||||
if (startCanvas_image != null)
|
||||
{
|
||||
Debug.LogWarning("No bg target (bgSpriteObject or bgSpriteImage) assigned; fullscreen sprite not applied");
|
||||
startCanvas_image.sprite = bgSprite;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to startCanvas_image for song {assignedSongData.songName}");
|
||||
}
|
||||
|
||||
if (bgMainImage == null && startCanvas_image == null)
|
||||
{
|
||||
Debug.LogWarning("No background Image targets assigned in BeatmapManager; fullscreen sprite not applied");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,8 +412,52 @@ public class BeatmapManager : MonoBehaviour
|
||||
else
|
||||
{
|
||||
// Documentation text normalized.
|
||||
// Documentation text normalized.
|
||||
Debug.LogWarning("Default automatic demo beatmap load is commented out to prioritize SO-provided charts.");
|
||||
// Documentation text normalized.
|
||||
Debug.LogWarning("Default automatic demo beatmap load is commented out to prioritize SO-provided charts.");
|
||||
}
|
||||
|
||||
UpdateGameplayUI();
|
||||
|
||||
}
|
||||
|
||||
private void UpdateGameplayUI()
|
||||
{
|
||||
if (assignedSongData == null) return;
|
||||
|
||||
if (gameplay_songname != null)
|
||||
{
|
||||
gameplay_songname.text = assignedSongData.songName;
|
||||
}
|
||||
|
||||
if (gameplay_difficultyID != null)
|
||||
{
|
||||
// Find the ChartFileEntry matching the assignedDifficulty to get difficultyLEVEL (star rating)
|
||||
float diffLevel = 0;
|
||||
if (assignedSongData.chartFiles != null)
|
||||
{
|
||||
foreach (var entry in assignedSongData.chartFiles)
|
||||
{
|
||||
if (entry.difficulty == assignedDifficulty)
|
||||
{
|
||||
diffLevel = entry.difficultyLEVEL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
gameplay_difficultyID.text = diffLevel.ToString();
|
||||
}
|
||||
|
||||
if (gameplay_difficultyName != null)
|
||||
{
|
||||
string diffText = "Unknown";
|
||||
switch (assignedDifficulty)
|
||||
{
|
||||
case 0: diffText = "Easy"; break;
|
||||
case 1: diffText = "Hard"; break;
|
||||
case 2: diffText = "Incredible"; break;
|
||||
case 3: diffText = "Impossible"; break;
|
||||
}
|
||||
gameplay_difficultyName.text = diffText;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections;
|
||||
@@ -204,18 +204,18 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
try
|
||||
{
|
||||
// Reset audio time to 0 before playing to ensure playback starts from the beginning
|
||||
musicSource.time = 0f;
|
||||
float startTime = 0f;
|
||||
if (delaySeconds < 0f)
|
||||
startTime = -delaySeconds;
|
||||
|
||||
if (delaySeconds > 0f)
|
||||
{
|
||||
// Use coroutine instead of PlayDelayed so pause state can interrupt it
|
||||
playbackDelayCoroutine = StartCoroutine(DelayAndPlayMusic(delaySeconds));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Immediate playback (no delay)
|
||||
musicSource.time = 0f;
|
||||
float clampedStartTime = ClampAudioStartTime(startTime);
|
||||
musicSource.time = clampedStartTime;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
Debug.Log("GameManager.PlayMusicWithDelay: Music playback started immediately (no delay)");
|
||||
@@ -226,7 +226,8 @@ public class GameManager : MonoBehaviour
|
||||
Debug.LogWarning($"PlayMusicWithDelay failed: {ex}");
|
||||
try
|
||||
{
|
||||
musicSource.time = 0f;
|
||||
float clampedStartTime = ClampAudioStartTime(delaySeconds < 0f ? -delaySeconds : 0f);
|
||||
musicSource.time = clampedStartTime;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
}
|
||||
@@ -234,6 +235,14 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private float ClampAudioStartTime(float startTime)
|
||||
{
|
||||
if (musicSource == null || musicSource.clip == null)
|
||||
return Mathf.Max(0f, startTime);
|
||||
|
||||
return Mathf.Clamp(startTime, 0f, musicSource.clip.length);
|
||||
}
|
||||
|
||||
private IEnumerator DelayAndPlayMusic(float delaySeconds)
|
||||
{
|
||||
Debug.Log($"GameManager.DelayAndPlayMusic: waiting {delaySeconds} seconds before playing (respects pause state)");
|
||||
|
||||
@@ -37,6 +37,13 @@ public class GfxController : MonoBehaviour
|
||||
public GameObject k_o_fx;
|
||||
public GameObject koFatherObject;
|
||||
|
||||
[Header("Explosion Effect Settings")]
|
||||
[Tooltip("当单次伤害超过敌人最大生命值的百分比时触发爆炸 (0.2 = 20%)")]
|
||||
public float explosionDamageThresholdRatio = 0.2f;
|
||||
[Tooltip("爆炸特效是否使用独立的缩放设置 (否则跟随Hit特效缩放)")]
|
||||
public bool useExplosionSeparateScale = false;
|
||||
public float explosionFxScale = 1.5f;
|
||||
|
||||
[Header("Hit Effect Settings - Enemy")]
|
||||
public float enemyOffsetX = 0f;
|
||||
public float enemyOffsetY = 0f;
|
||||
@@ -123,11 +130,26 @@ public class GfxController : MonoBehaviour
|
||||
/// <param name="isEnemy">是否为敌人受击</param>
|
||||
/// <param name="customWorldPos">可选:自定义世界坐标播放(如匹配弹道终点)</param>
|
||||
/// <param name="scaleOverride">可选:强制指定特效缩放倍率</param>
|
||||
public void PlayHitFX(GameObject target, GameObject parent = null, bool isEnemy = false, Vector3? customWorldPos = null, float? scaleOverride = null)
|
||||
/// <param name="damageInfo">可选:传入伤害信息以判断是否触发爆炸特效 (damage, maxHealth)</param>
|
||||
public void PlayHitFX(GameObject target, GameObject parent = null, bool isEnemy = false, Vector3? customWorldPos = null, float? scaleOverride = null, (float damage, float maxHealth)? damageInfo = null)
|
||||
{
|
||||
if (target == null || hitFX == null)
|
||||
// Determine which FX prefab to use (Hit or Explosion)
|
||||
GameObject fxPrefab = hitFX;
|
||||
bool isExplosion = false;
|
||||
|
||||
if (isEnemy && damageInfo.HasValue && explosionFX != null)
|
||||
{
|
||||
if (hitFX == null) Debug.LogWarning("[GfxController] hitFX Prefab 未在 Inspector 中分配!");
|
||||
float ratio = damageInfo.Value.damage / damageInfo.Value.maxHealth;
|
||||
if (ratio >= explosionDamageThresholdRatio)
|
||||
{
|
||||
fxPrefab = explosionFX;
|
||||
isExplosion = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (target == null || fxPrefab == null)
|
||||
{
|
||||
if (fxPrefab == null) Debug.LogWarning($"[GfxController] {(isExplosion ? "ExplosionFX" : "HitFX")} Prefab 未在 Inspector 中分配!");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,7 +198,14 @@ public class GfxController : MonoBehaviour
|
||||
}
|
||||
else if (isEnemy)
|
||||
{
|
||||
fxScale = useEnemyRandomScale ? Random.Range(enemyMinFxScale, enemyMaxFxScale) : enemyFxScale;
|
||||
if (isExplosion && useExplosionSeparateScale)
|
||||
{
|
||||
fxScale = explosionFxScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
fxScale = useEnemyRandomScale ? Random.Range(enemyMinFxScale, enemyMaxFxScale) : enemyFxScale;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -194,7 +223,7 @@ public class GfxController : MonoBehaviour
|
||||
visualParent.transform.localScale = Vector3.one * fxScale;
|
||||
|
||||
// 作为子物体生成
|
||||
fx = Instantiate(hitFX, visualParent.transform);
|
||||
fx = Instantiate(fxPrefab, visualParent.transform);
|
||||
|
||||
// 确保特效在 UI 层级中显示在最前方
|
||||
fx.transform.SetAsLastSibling();
|
||||
@@ -215,7 +244,7 @@ public class GfxController : MonoBehaviour
|
||||
|
||||
rt.localRotation = Quaternion.identity;
|
||||
// 确保 UI 特效的原始大小正确,且 localScale 为 1(继承 parent 的缩放)
|
||||
rt.sizeDelta = hitFX.GetComponent<RectTransform>().sizeDelta;
|
||||
rt.sizeDelta = fxPrefab.GetComponent<RectTransform>().sizeDelta;
|
||||
rt.localScale = Vector3.one;
|
||||
}
|
||||
else
|
||||
@@ -235,7 +264,7 @@ public class GfxController : MonoBehaviour
|
||||
else
|
||||
{
|
||||
Vector3 spawnPos = customWorldPos.HasValue ? customWorldPos.Value : (target.transform.position + offsetVector);
|
||||
fx = Instantiate(hitFX, spawnPos, Quaternion.identity);
|
||||
fx = Instantiate(fxPrefab, spawnPos, Quaternion.identity);
|
||||
fx.transform.localScale = Vector3.one * fxScale;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Pool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class groundParticularController : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct MaterialConfig
|
||||
{
|
||||
public Material material;
|
||||
public bool isEnabled;
|
||||
[Tooltip("是否为该材质启用渐变颜色覆盖")]
|
||||
public bool useGradientOverride;
|
||||
[Tooltip("粒子生命周期内的颜色渐变 (仅在 useGradientOverride 开启时有效)")]
|
||||
public Gradient colorGradient;
|
||||
}
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("粒子的起点")]
|
||||
[SerializeField] private Transform startPoint;
|
||||
[Tooltip("粒子的终点")]
|
||||
[SerializeField] private Transform endPoint;
|
||||
[Tooltip("作为粒子的预制体")]
|
||||
[SerializeField] private GameObject particlePrefab;
|
||||
|
||||
[Header("Visuals")]
|
||||
[Tooltip("粒子可用的材质配置列表")]
|
||||
[SerializeField] private List<MaterialConfig> particleMaterials = new List<MaterialConfig>();
|
||||
[Tooltip("粒子最小缩放比例")]
|
||||
[SerializeField] private float minParticleScale = 0.8f;
|
||||
[Tooltip("粒子最大缩放比例")]
|
||||
[SerializeField] private float maxParticleScale = 1.2f;
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("性能门控:是否启用粒子生成")]
|
||||
public bool enableParticles = true;
|
||||
[Tooltip("粒子的物理层级 (Layer Name)")]
|
||||
[SerializeField] private string physicsLayerName = "particles";
|
||||
[Tooltip("粒子的渲染层级名称 (Sorting Layer)")]
|
||||
[SerializeField] private string sortingLayerName = "Default";
|
||||
[Tooltip("粒子的渲染层级编号 (Order in Layer)")]
|
||||
[SerializeField] private int sortingOrder = 0;
|
||||
[Tooltip("渲染队列偏移 (Render Queue Offset),用于解决 3D 物体与 UI 遮挡问题。数值越小越先渲染(被遮挡)")]
|
||||
[SerializeField] private int renderQueueOffset = 0;
|
||||
|
||||
[Header("Spawn Quantity")]
|
||||
[Tooltip("基础发射频率(每秒发射次数)")]
|
||||
[SerializeField] private float baseEmitRate = 10f;
|
||||
[Tooltip("每次发射生成的粒子数量 (Batch Size) - 增加此数值可显著增加粒子密度")]
|
||||
[SerializeField] private int emitBatchSize = 1;
|
||||
|
||||
[Header("Spawn Randomness")]
|
||||
[Tooltip("混乱度:随机时间偏移范围")]
|
||||
[SerializeField] private float chaosRandomDelay = 0.05f;
|
||||
[Tooltip("出生点 X 轴随机偏移范围")]
|
||||
[SerializeField] private float spawnOffsetX = 0.5f;
|
||||
[Tooltip("出生点 Z 轴 (深度) 随机偏移范围")]
|
||||
[SerializeField] private float spawnOffsetY = 0.5f;
|
||||
[Tooltip("速率系数,用于调节基础移动速度")]
|
||||
[SerializeField] private float speedMultiplier = 0.1f;
|
||||
|
||||
[Tooltip("起点缩放抖动强度 (基于音频电平)")]
|
||||
[SerializeField] private float shakeIntensity = 0.1f;
|
||||
|
||||
[Header("External References")]
|
||||
[Tooltip("粒子生成的父物体")]
|
||||
[SerializeField] private Transform particleParent;
|
||||
[Tooltip("主音频源,用于同步音乐状态")]
|
||||
public AudioSource musicSource;
|
||||
[Tooltip("赋值对应的 BeatmapManager 以获取当前歌曲的 BPM")]
|
||||
public BeatmapManager beatmapManager;
|
||||
|
||||
[Header("Audio Analysis")]
|
||||
public bool useAudioAnalysis = true;
|
||||
[Range(0, 1)] public float audioSensitivity = 0.5f;
|
||||
[Tooltip("音频对发射频率的影响强度")]
|
||||
[SerializeField] private float audioEmitBoost = 50f;
|
||||
|
||||
private float[] _audioSamples = new float[128];
|
||||
private float _currentAverageVolume = 0f;
|
||||
private float _emitTimer = 0f;
|
||||
|
||||
// 材质均衡分配逻辑
|
||||
private List<MaterialConfig> _cachedEnabledConfigs = new List<MaterialConfig>();
|
||||
private int _materialIndex = 0;
|
||||
|
||||
private IObjectPool<GameObject> _pool;
|
||||
private Vector3 _originalStartScale;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (startPoint != null) _originalStartScale = startPoint.localScale;
|
||||
|
||||
_pool = new ObjectPool<GameObject>(
|
||||
createFunc: () => {
|
||||
GameObject go = Instantiate(particlePrefab);
|
||||
go.SetActive(false);
|
||||
return go;
|
||||
},
|
||||
actionOnGet: (obj) => {
|
||||
obj.SetActive(true);
|
||||
// 解决对象池拉线问题:如果球体带拖尾,获取时必须清除旧路径
|
||||
var trail = obj.GetComponent<TrailRenderer>();
|
||||
if (trail != null) trail.Clear();
|
||||
},
|
||||
actionOnRelease: (obj) => obj.SetActive(false),
|
||||
actionOnDestroy: (obj) => Destroy(obj),
|
||||
defaultCapacity: 30
|
||||
);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 执行实时音频 analysis(获取音量强度)
|
||||
AnalyzeAudio();
|
||||
|
||||
// 持续发射逻辑
|
||||
UpdateContinuousEmit();
|
||||
|
||||
// 强制打印当前状态
|
||||
if (Time.frameCount % 60 == 0)
|
||||
{
|
||||
string audioStatus = musicSource != null ? (musicSource.isPlaying ? "Playing" : "Paused/Stopped") : "Null";
|
||||
Debug.Log($"[GroundParticular] Vol: {_currentAverageVolume:F3}, Status: {audioStatus}");
|
||||
}
|
||||
|
||||
// 起点缩放抖动
|
||||
if (startPoint != null)
|
||||
{
|
||||
float shakeEffect = _currentAverageVolume * shakeIntensity;
|
||||
startPoint.localScale = _originalStartScale * (1f + shakeEffect);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateContinuousEmit()
|
||||
{
|
||||
// 门控检查:如果玩家禁用了粒子,则不执行生成逻辑
|
||||
if (!enableParticles || musicSource == null || !musicSource.isPlaying) return;
|
||||
|
||||
// 计算当前瞬间的发射频率:基础频率 + 音频增益
|
||||
float currentRate = baseEmitRate + (_currentAverageVolume * audioEmitBoost);
|
||||
if (currentRate <= 0) return;
|
||||
|
||||
float interval = 1f / currentRate;
|
||||
_emitTimer += Time.deltaTime;
|
||||
|
||||
if (_emitTimer >= interval)
|
||||
{
|
||||
_emitTimer = 0;
|
||||
|
||||
// 批量生成
|
||||
for (int i = 0; i < emitBatchSize; i++)
|
||||
{
|
||||
// 引入混乱度:随机延迟发射
|
||||
if (chaosRandomDelay > 0)
|
||||
{
|
||||
Invoke(nameof(DelayedEmit), Random.Range(0f, chaosRandomDelay));
|
||||
}
|
||||
else
|
||||
{
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DelayedEmit()
|
||||
{
|
||||
Emit();
|
||||
}
|
||||
|
||||
private void AnalyzeAudio()
|
||||
{
|
||||
if (musicSource == null || !musicSource.isPlaying || !useAudioAnalysis)
|
||||
{
|
||||
_currentAverageVolume = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 GetOutputData 获取实时波形采样数据(电平)
|
||||
musicSource.GetOutputData(_audioSamples, 0);
|
||||
|
||||
float sum = 0;
|
||||
for (int i = 0; i < _audioSamples.Length; i++)
|
||||
{
|
||||
sum += Mathf.Abs(_audioSamples[i]);
|
||||
}
|
||||
|
||||
// 计算平均振幅并应用敏感度
|
||||
_currentAverageVolume = (sum / _audioSamples.Length) * audioSensitivity * 2.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射一个粒子
|
||||
/// </summary>
|
||||
public void Emit()
|
||||
{
|
||||
if (startPoint == null || endPoint == null || particlePrefab == null) return;
|
||||
|
||||
// 1. 获取粒子
|
||||
GameObject particle = _pool.Get();
|
||||
|
||||
// 2. 设置层级
|
||||
particle.transform.SetParent(particleParent != null ? particleParent : null);
|
||||
|
||||
// 3. 计算缩放
|
||||
float randomScale = Random.Range(minParticleScale, maxParticleScale);
|
||||
float audioBoost = useAudioAnalysis ? (1f + _currentAverageVolume) : 1f;
|
||||
particle.transform.localScale = particlePrefab.transform.localScale * randomScale * audioBoost;
|
||||
|
||||
// 4. 计算位置 (全表面随机 - XZ 平面)
|
||||
// 修改为 X 和 Z 轴随机,以覆盖地面 (Ground) 的水平和深度方向
|
||||
Vector3 surfaceRandomPos = new Vector3(Random.Range(-0.5f, 0.5f), 0f, Random.Range(-0.5f, 0.5f));
|
||||
Vector3 finalLocalPos = surfaceRandomPos + new Vector3(
|
||||
Random.Range(-spawnOffsetX, spawnOffsetX),
|
||||
0f,
|
||||
Random.Range(-spawnOffsetY, spawnOffsetY)
|
||||
);
|
||||
particle.transform.position = startPoint.TransformPoint(finalLocalPos);
|
||||
particle.transform.rotation = startPoint.rotation;
|
||||
|
||||
// 5. 材质均衡选取逻辑
|
||||
MaterialConfig? selectedConfig = GetBalancedMaterialConfig();
|
||||
|
||||
// 设置物理层级
|
||||
int layer = LayerMask.NameToLayer(physicsLayerName);
|
||||
if (layer != -1)
|
||||
{
|
||||
particle.layer = layer;
|
||||
}
|
||||
|
||||
var renderer = particle.GetComponent<Renderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
// 设置渲染层级
|
||||
renderer.sortingLayerName = sortingLayerName;
|
||||
renderer.sortingOrder = sortingOrder;
|
||||
|
||||
if (selectedConfig != null)
|
||||
{
|
||||
// 使用实例化材质以允许独立修改 RenderQueue
|
||||
renderer.material = selectedConfig.Value.material;
|
||||
if (renderQueueOffset != 0)
|
||||
{
|
||||
renderer.material.renderQueue = selectedConfig.Value.material.renderQueue + renderQueueOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 开始移动
|
||||
StartCoroutine(MoveRoutine(particle, finalLocalPos, selectedConfig));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轮询式均衡选取材质,确保每种材质数量大致相同
|
||||
/// </summary>
|
||||
private MaterialConfig? GetBalancedMaterialConfig()
|
||||
{
|
||||
if (particleMaterials == null || particleMaterials.Count == 0) return null;
|
||||
|
||||
// 过滤出所有启用的材质
|
||||
_cachedEnabledConfigs.Clear();
|
||||
foreach (var config in particleMaterials)
|
||||
{
|
||||
if (config.isEnabled && config.material != null)
|
||||
_cachedEnabledConfigs.Add(config);
|
||||
}
|
||||
|
||||
if (_cachedEnabledConfigs.Count == 0) return null;
|
||||
|
||||
// 轮询选取
|
||||
_materialIndex = (_materialIndex + 1) % _cachedEnabledConfigs.Count;
|
||||
return _cachedEnabledConfigs[_materialIndex];
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator MoveRoutine(GameObject particle, Vector3 initialLocalPos, MaterialConfig? config)
|
||||
{
|
||||
if (startPoint == null || endPoint == null) yield break;
|
||||
|
||||
// 【关键修复】在协程开始时,缓存起点的世界坐标
|
||||
Vector3 spawnWorldPos = startPoint.TransformPoint(initialLocalPos);
|
||||
|
||||
Vector3 currentStartPos = startPoint.position;
|
||||
Vector3 currentEndPos = endPoint.position;
|
||||
Vector3 moveDir = (currentEndPos - currentStartPos).normalized;
|
||||
float totalDist = Vector3.Distance(currentStartPos, currentEndPos);
|
||||
float traveledDist = 0f;
|
||||
|
||||
particle.transform.position = spawnWorldPos;
|
||||
|
||||
// 获取材质属性块用于动态修改颜色 (不产生材质实例 GC)
|
||||
MaterialPropertyBlock propBlock = new MaterialPropertyBlock();
|
||||
var renderer = particle.GetComponent<Renderer>();
|
||||
|
||||
while (particle != null && particle.activeInHierarchy)
|
||||
{
|
||||
// 如果游戏暂停,则等待直到取消暂停
|
||||
if (PauseManager.Instance != null && PauseManager.Instance.IsPaused)
|
||||
{
|
||||
yield return new WaitUntil(() => !PauseManager.Instance.IsPaused);
|
||||
}
|
||||
|
||||
float currentBPM = GetCurrentBPM();
|
||||
float noteSpawnerSpeedMultiplier = 1.0f;
|
||||
if (beatmapManager != null && beatmapManager.noteSpawner != null)
|
||||
{
|
||||
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.speedMultiplier;
|
||||
}
|
||||
|
||||
float effectiveBPM = Mathf.Max(currentBPM, 60f);
|
||||
float noteSpeed = (totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f;
|
||||
|
||||
float step = noteSpeed * speedMultiplier * Time.deltaTime;
|
||||
if (step <= 0) step = 0.01f;
|
||||
|
||||
traveledDist += step;
|
||||
particle.transform.position += moveDir * step;
|
||||
|
||||
// 【动态空间渐变色逻辑】
|
||||
if (config != null && config.Value.useGradientOverride && renderer != null)
|
||||
{
|
||||
// 获取渐变的首尾颜色
|
||||
Color headColor = config.Value.colorGradient.Evaluate(1f);
|
||||
Color tailColor = config.Value.colorGradient.Evaluate(0f);
|
||||
|
||||
renderer.GetPropertyBlock(propBlock);
|
||||
// 对应 Shader 中的属性
|
||||
propBlock.SetColor("_HeadColor", headColor);
|
||||
propBlock.SetColor("_TailColor", tailColor);
|
||||
propBlock.SetVector("_MoveDir", moveDir);
|
||||
renderer.SetPropertyBlock(propBlock);
|
||||
}
|
||||
|
||||
if (traveledDist >= totalDist)
|
||||
{
|
||||
if (particle.activeInHierarchy) _pool.Release(particle);
|
||||
yield break;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetCurrentBPM()
|
||||
{
|
||||
if (beatmapManager != null && beatmapManager.assignedSongData != null)
|
||||
{
|
||||
return beatmapManager.assignedSongData.bpm;
|
||||
}
|
||||
return 120f;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (startPoint == null) return;
|
||||
|
||||
// 绘制起点的发射区域(完全匹配 Cube 的视觉大小)
|
||||
Gizmos.matrix = startPoint.localToWorldMatrix;
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
|
||||
|
||||
if (endPoint != null)
|
||||
{
|
||||
Gizmos.matrix = Matrix4x4.identity; // 切回世界坐标画线
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawLine(startPoint.position, endPoint.position);
|
||||
|
||||
Gizmos.matrix = endPoint.localToWorldMatrix;
|
||||
Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee19a6040a62c2248b337879e955533a
|
||||
@@ -304,36 +304,7 @@ public class settlementController : MonoBehaviour
|
||||
{
|
||||
int pm = sm != null ? sm.allSum_pmScore : 0;
|
||||
int idol = sm != null ? sm.allSum_idolScore : 0;
|
||||
int total = pm + idol;
|
||||
|
||||
// Documentation text normalized.
|
||||
thisSong_so.UpdateDifficultyRecord(diff, pm, idol);
|
||||
|
||||
// Documentation text normalized.
|
||||
thisSong_so.UpdateChartScore(diff, total);
|
||||
|
||||
// Documentation text normalized.
|
||||
if (thisSong_so.chartFiles != null)
|
||||
{
|
||||
var entry = thisSong_so.chartFiles.Find(e => e != null && e.difficulty == diff);
|
||||
if (entry != null)
|
||||
{
|
||||
// Documentation text normalized.
|
||||
float newProgress = Mathf.Clamp01((float)total / (float)maxScore_sum);
|
||||
if (newProgress > entry.levelProgressForThisDifficulty)
|
||||
{
|
||||
entry.levelProgressForThisDifficulty = newProgress;
|
||||
}
|
||||
|
||||
if (thisSong_so.thisLevel_selectedDifficultyID == diff)
|
||||
{
|
||||
thisSong_so.current_levelProgress = Mathf.Max(thisSong_so.current_levelProgress, entry.levelProgressForThisDifficulty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 更新:保存到持久化存储(Build包必需) ---
|
||||
thisSong_so.SavePersistent();
|
||||
thisSong_so.ApplySettlementResult(diff, pm, idol, maxScore_sum);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,4 +797,3 @@ public class settlementController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
@@ -71,6 +71,25 @@ public class teamUIController : MonoBehaviour
|
||||
public GameObject ally04_object;
|
||||
public GameObject ally05_object;
|
||||
|
||||
[Header("Victory Animation")]
|
||||
[Tooltip("The object to move when all enemies are defeated")]
|
||||
public GameObject victoryMoveObject;
|
||||
[Tooltip("Target X position (AnchoredPosition X for UI, World X for others)")]
|
||||
public float victoryTargetX;
|
||||
[Tooltip("Movement curve (Time 0->1, Value 0->1 recommended)")]
|
||||
public AnimationCurve victoryMoveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
[Tooltip("Duration of the movement in seconds")]
|
||||
public float victoryMoveDuration = 2f;
|
||||
[Tooltip("Delay before the victory animation starts (seconds)")]
|
||||
public float victoryAnimationDelay = 3f;
|
||||
[Tooltip("The image to fade saturation on victory")]
|
||||
public Image victorySaturationImage;
|
||||
[Tooltip("Duration for the saturation fade (seconds)")]
|
||||
public float victorySaturationDuration = 2f;
|
||||
|
||||
private Coroutine _victoryMoveCoroutine;
|
||||
private Coroutine _victorySaturationCoroutine;
|
||||
|
||||
[Header("Ally Statistics (Real-time)")]
|
||||
[Tooltip("Total damage dealt to enemies by each of the 5 ally slots.")]
|
||||
public float[] totalDamageDealt = new float[5];
|
||||
@@ -997,6 +1016,18 @@ public class teamUIController : MonoBehaviour
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
// Saturation material assignment moved to PlayVictoryAnimation as requested
|
||||
/*
|
||||
// Initialize saturation image if assigned
|
||||
if (victorySaturationImage != null && grayScaleMaterial != null)
|
||||
{
|
||||
// Assign the grayscale material instance to the image
|
||||
victorySaturationImage.material = new Material(grayScaleMaterial);
|
||||
// Use _Saturation property ID
|
||||
victorySaturationImage.material.SetFloat(SaturationID, 1f);
|
||||
}
|
||||
*/
|
||||
|
||||
// Load ally slot IDs from PlayerPrefs
|
||||
allySlotIds[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
|
||||
allySlotIds[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
|
||||
@@ -1371,7 +1402,7 @@ public class teamUIController : MonoBehaviour
|
||||
// All enemies processed: show empty/finished state but keep UI visible
|
||||
if (objectFather_enemy != null) objectFather_enemy.SetActive(true);
|
||||
if (currentEnemy_nameText != null) currentEnemy_nameText.text = "(All Defeated)";
|
||||
if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
|
||||
// if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
|
||||
if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = 0f;
|
||||
if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = 0f;
|
||||
if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = "0/0";
|
||||
@@ -1379,8 +1410,11 @@ public class teamUIController : MonoBehaviour
|
||||
if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = 0f;
|
||||
if (currentEnemy_typeText != null) currentEnemy_typeText.text = "(普通敌人)";
|
||||
UpdateEnemyListText();
|
||||
// Documentation text normalized.
|
||||
// Update total health bar to 0
|
||||
UpdateAllEnemyTotalHealthUIImmediate();
|
||||
|
||||
// Trigger victory animation
|
||||
PlayVictoryAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1451,14 +1485,14 @@ public class teamUIController : MonoBehaviour
|
||||
{
|
||||
if (enemyList_rateText == null) return;
|
||||
|
||||
// 使用实际识别出的敌人数组长度作为总数
|
||||
// Use actual recognized enemy count
|
||||
int total = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0;
|
||||
int displayIndex = 0;
|
||||
|
||||
if (total > 0)
|
||||
{
|
||||
// enemyCurrentCount 是从 0 开始的索引
|
||||
// 确保显示编号在 1 到 total 之间
|
||||
// enemyCurrentCount is 0-based index
|
||||
// ensure display is 1 to total
|
||||
displayIndex = Mathf.Clamp(enemyCurrentCount + 1, 1, total);
|
||||
}
|
||||
|
||||
@@ -1467,6 +1501,96 @@ public class teamUIController : MonoBehaviour
|
||||
enemyList_rateText.text = _sb.ToString();
|
||||
}
|
||||
|
||||
private void PlayVictoryAnimation()
|
||||
{
|
||||
// 1. Victory Movement (Delayed)
|
||||
if (victoryMoveObject != null)
|
||||
{
|
||||
if (_victoryMoveCoroutine != null) StopCoroutine(_victoryMoveCoroutine);
|
||||
_victoryMoveCoroutine = StartCoroutine(VictoryMoveRoutine());
|
||||
}
|
||||
|
||||
// 2. Victory Saturation Set (Immediate)
|
||||
if (victorySaturationImage != null)
|
||||
{
|
||||
if (_victorySaturationCoroutine != null) StopCoroutine(_victorySaturationCoroutine);
|
||||
|
||||
// Assign material only when victory occurs
|
||||
if (grayScaleMaterial != null)
|
||||
{
|
||||
victorySaturationImage.material = new Material(grayScaleMaterial);
|
||||
}
|
||||
|
||||
// Immediately set saturation to 0 (Grayscale)
|
||||
victorySaturationImage.material.SetFloat(SaturationID, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator VictoryMoveRoutine()
|
||||
{
|
||||
if (victoryMoveObject == null) yield break;
|
||||
|
||||
// Wait for the specified delay before starting movement
|
||||
if (victoryAnimationDelay > 0f)
|
||||
yield return new WaitForSeconds(victoryAnimationDelay);
|
||||
|
||||
float timer = 0f;
|
||||
float duration = victoryMoveDuration > 0 ? victoryMoveDuration : 2f;
|
||||
|
||||
RectTransform rt = victoryMoveObject.GetComponent<RectTransform>();
|
||||
Transform tf = victoryMoveObject.transform;
|
||||
|
||||
float startX = 0f;
|
||||
bool isUI = (rt != null);
|
||||
|
||||
if (isUI) startX = rt.anchoredPosition.x;
|
||||
else startX = tf.position.x;
|
||||
|
||||
// If curve is not set, default to EaseInOut
|
||||
if (victoryMoveCurve == null || victoryMoveCurve.length == 0)
|
||||
victoryMoveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
while (timer < duration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
float progress = Mathf.Clamp01(timer / duration);
|
||||
float curveValue = victoryMoveCurve.Evaluate(progress);
|
||||
|
||||
float newX = Mathf.LerpUnclamped(startX, victoryTargetX, curveValue);
|
||||
|
||||
if (isUI)
|
||||
{
|
||||
Vector2 pos = rt.anchoredPosition;
|
||||
pos.x = newX;
|
||||
rt.anchoredPosition = pos;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 pos = tf.position;
|
||||
pos.x = newX;
|
||||
tf.position = pos;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Final set
|
||||
if (isUI)
|
||||
{
|
||||
Vector2 pos = rt.anchoredPosition;
|
||||
pos.x = victoryTargetX;
|
||||
rt.anchoredPosition = pos;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 pos = tf.position;
|
||||
pos.x = victoryTargetX;
|
||||
tf.position = pos;
|
||||
}
|
||||
}
|
||||
|
||||
// VictorySaturationRoutine removed as requested (immediate set)
|
||||
|
||||
private void OnEnemyDiedHandler(EnemyCombatant e)
|
||||
{
|
||||
// advance to next enemy after death
|
||||
|
||||
Reference in New Issue
Block a user