ui基本完毕,修了一大把的bug
This commit is contained in:
@@ -1266,6 +1266,18 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
|
||||
// Trigger HP changed events
|
||||
int delta = currentHP - old;
|
||||
if (delta != 0 && teamUIController.Instance != null)
|
||||
{
|
||||
if (delta > 0)
|
||||
{
|
||||
teamUIController.Instance.RecordHeal(slotIndex, delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
teamUIController.Instance.RecordDamageTaken(slotIndex, -delta);
|
||||
}
|
||||
}
|
||||
|
||||
if (delta != 0 && !skipPopup)
|
||||
{
|
||||
if (iNumberPrefabController.Instance != null)
|
||||
@@ -1387,6 +1399,11 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
|
||||
// Trigger mana changed events
|
||||
int delta = currentMana - old;
|
||||
if (delta > 0 && teamUIController.Instance != null)
|
||||
{
|
||||
teamUIController.Instance.RecordMana(slotIndex, delta);
|
||||
}
|
||||
|
||||
if (delta != 0)
|
||||
{
|
||||
if (iNumberPrefabController.Instance != null)
|
||||
|
||||
@@ -1078,22 +1078,6 @@ public class EffectSystem : MonoBehaviour
|
||||
{
|
||||
// Use deferPopup: true for synchronization with hit fx
|
||||
comp.ReceiveDamage(amount, source, true);
|
||||
|
||||
// Record stats if source is an ally
|
||||
if (source != null && target.GetComponent<EnemyCombatant>() != null)
|
||||
{
|
||||
var ally = source.GetComponent<AllyCombatant>();
|
||||
if (ally != null && teamUIController.Instance != null)
|
||||
{
|
||||
teamUIController.Instance.RecordDamage(ally.slotIndex, amount);
|
||||
}
|
||||
}
|
||||
|
||||
// Record damage taken if target is an ally
|
||||
if (targetAlly != null && teamUIController.Instance != null)
|
||||
{
|
||||
teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, amount);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1109,16 +1093,6 @@ public class EffectSystem : MonoBehaviour
|
||||
{
|
||||
// Use deferPopup: false for heals (usually immediate)
|
||||
comp.ReceiveHeal(amount, source, false);
|
||||
|
||||
// Record stats if source is an ally
|
||||
if (source != null)
|
||||
{
|
||||
var ally = source.GetComponent<AllyCombatant>();
|
||||
if (ally != null && teamUIController.Instance != null)
|
||||
{
|
||||
teamUIController.Instance.RecordHeal(ally.slotIndex, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1175,13 +1149,6 @@ public class EffectSystem : MonoBehaviour
|
||||
|
||||
comp.ReceiveDamage(totalAmount, source, false);
|
||||
// No need to call TriggerQueuedPopups manually as deferPopup is false
|
||||
|
||||
if (source != null && targetEnemy != null)
|
||||
{
|
||||
var ally = source.GetComponent<AllyCombatant>();
|
||||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, totalAmount);
|
||||
}
|
||||
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, totalAmount);
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -1224,13 +1191,6 @@ public class EffectSystem : MonoBehaviour
|
||||
|
||||
comp.ReceiveDamage(perTick, source, false);
|
||||
// No need to call TriggerQueuedPopups manually as deferPopup is false
|
||||
|
||||
if (source != null && targetEnemy != null)
|
||||
{
|
||||
var ally = source.GetComponent<AllyCombatant>();
|
||||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamage(ally.slotIndex, perTick);
|
||||
}
|
||||
if (targetAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordDamageTaken(targetAlly.slotIndex, perTick);
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
}
|
||||
@@ -1265,13 +1225,6 @@ public class EffectSystem : MonoBehaviour
|
||||
|
||||
comp.ReceiveHeal(totalAmount, source, false);
|
||||
// No need to call TriggerQueuedPopups manually as deferPopup is false
|
||||
|
||||
// Record stats
|
||||
if (source != null)
|
||||
{
|
||||
var ally = source.GetComponent<AllyCombatant>();
|
||||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordHeal(ally.slotIndex, totalAmount);
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -1286,13 +1239,6 @@ public class EffectSystem : MonoBehaviour
|
||||
|
||||
comp.ReceiveHeal(perTick, source, false);
|
||||
// No need to call TriggerQueuedPopups manually as deferPopup is false
|
||||
|
||||
// Record stats
|
||||
if (source != null)
|
||||
{
|
||||
var ally = source.GetComponent<AllyCombatant>();
|
||||
if (ally != null && teamUIController.Instance != null) teamUIController.Instance.RecordHeal(ally.slotIndex, perTick);
|
||||
}
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
}
|
||||
@@ -1345,12 +1291,6 @@ public class EffectSystem : MonoBehaviour
|
||||
if (ally != null)
|
||||
{
|
||||
ally.ModifyMana(Mathf.CeilToInt(totalAmount), true, true);
|
||||
// Record stats
|
||||
if (totalAmount > 0f && source != null)
|
||||
{
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, totalAmount);
|
||||
}
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
@@ -1364,12 +1304,6 @@ public class EffectSystem : MonoBehaviour
|
||||
if (ally != null)
|
||||
{
|
||||
ally.ModifyMana(Mathf.CeilToInt(perTick), true, true);
|
||||
// Record stats
|
||||
if (perTick > 0f && source != null)
|
||||
{
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null && teamUIController.Instance != null) teamUIController.Instance.RecordMana(sourceAlly.slotIndex, perTick);
|
||||
}
|
||||
}
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
|
||||
@@ -159,6 +159,15 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
ModifyHP(-delta, true);
|
||||
|
||||
int actual = before - currentHP;
|
||||
if (actual > 0 && source != null && teamUIController.Instance != null)
|
||||
{
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null)
|
||||
{
|
||||
teamUIController.Instance.RecordDamage(sourceAlly.slotIndex, actual);
|
||||
}
|
||||
}
|
||||
|
||||
GameplaySkillLogger.RecordEnemyHpEvent(
|
||||
gameObject.name,
|
||||
"Damage",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
public static class GameplaySkillLogger
|
||||
@@ -18,6 +20,24 @@ public static class GameplaySkillLogger
|
||||
|
||||
private const float RapidDuplicateSkillThresholdSeconds = 0.05f;
|
||||
|
||||
// Off-thread writer: judge/skill events push formatted lines into this queue and a
|
||||
// single background thread writes them to disk in FIFO order. This keeps the
|
||||
// synchronous file I/O off the gameplay hot path (first-note hitch) while producing
|
||||
// byte-identical output: the queue preserves enqueue order (enqueues happen under
|
||||
// s_fileLock), and the writer thread is the sole owner of the file so a session
|
||||
// truncate (header) can never interleave with an append.
|
||||
private struct LogMessage
|
||||
{
|
||||
public bool Truncate; // true => WriteAllText (new session header); false => AppendAllText
|
||||
public string Text;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentQueue<LogMessage> s_pendingLines = new ConcurrentQueue<LogMessage>();
|
||||
private static readonly AutoResetEvent s_writeSignal = new AutoResetEvent(false);
|
||||
private static Thread s_writerThread;
|
||||
private static volatile bool s_writerRunning;
|
||||
private static readonly object s_writerStartLock = new object();
|
||||
|
||||
public static string LogFilePath
|
||||
{
|
||||
get { return EnsureLogFilePath(); }
|
||||
@@ -256,14 +276,9 @@ public static class GameplaySkillLogger
|
||||
"# Format: [elapsedSec] [CATEGORY] key=value | key=value" + Environment.NewLine +
|
||||
Environment.NewLine;
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, header, s_utf8NoBom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GameplaySkillLogger] Failed to create session log: " + ex.Message);
|
||||
}
|
||||
EnsureWriterThread();
|
||||
s_pendingLines.Enqueue(new LogMessage { Truncate = true, Text = header });
|
||||
s_writeSignal.Set();
|
||||
}
|
||||
|
||||
private static float AppendEventLineLocked(string category, string payload)
|
||||
@@ -286,18 +301,70 @@ public static class GameplaySkillLogger
|
||||
.Append("] ")
|
||||
.AppendLine(payload);
|
||||
|
||||
try
|
||||
{
|
||||
File.AppendAllText(EnsureLogFilePath(), builder.ToString(), s_utf8NoBom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GameplaySkillLogger] Failed to append log: " + ex.Message);
|
||||
}
|
||||
EnsureWriterThread();
|
||||
s_pendingLines.Enqueue(new LogMessage { Truncate = false, Text = builder.ToString() });
|
||||
s_writeSignal.Set();
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
private static void EnsureWriterThread()
|
||||
{
|
||||
if (s_writerRunning)
|
||||
return;
|
||||
|
||||
lock (s_writerStartLock)
|
||||
{
|
||||
if (s_writerRunning)
|
||||
return;
|
||||
|
||||
s_writerRunning = true;
|
||||
s_writerThread = new Thread(WriterLoop)
|
||||
{
|
||||
Name = "GameplaySkillLoggerWriter",
|
||||
IsBackground = true
|
||||
};
|
||||
s_writerThread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriterLoop()
|
||||
{
|
||||
while (s_writerRunning)
|
||||
{
|
||||
s_writeSignal.WaitOne(200);
|
||||
DrainPendingLines();
|
||||
}
|
||||
|
||||
// Final drain so nothing queued right before shutdown is lost.
|
||||
DrainPendingLines();
|
||||
}
|
||||
|
||||
private static void DrainPendingLines()
|
||||
{
|
||||
while (s_pendingLines.TryDequeue(out LogMessage message))
|
||||
{
|
||||
// Read the cached path directly: it is always resolved on the main thread
|
||||
// (BeginSessionInternal -> EnsureLogFilePath) before the writer thread starts,
|
||||
// so we must never call Application.dataPath from this background thread.
|
||||
string path = s_logFilePath;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
if (message.Truncate)
|
||||
File.WriteAllText(path, message.Text, s_utf8NoBom);
|
||||
else
|
||||
File.AppendAllText(path, message.Text, s_utf8NoBom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GameplaySkillLogger] Failed to write log: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureSessionLocked()
|
||||
{
|
||||
if (!s_sessionActive)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
|
||||
public class ScoreManager : MonoBehaviour
|
||||
{
|
||||
@@ -56,6 +57,12 @@ public class ScoreManager : MonoBehaviour
|
||||
private bool perfectBonusHooked = false;
|
||||
private int perfectClearBonusPm = 0;
|
||||
private bool perfectClearBonusApplied = false;
|
||||
private int comboRecentPlusAmount = 0;
|
||||
private int displayedRecentPlusAmount = 0;
|
||||
private int lastKnownCombo = 0;
|
||||
private float lastRecentPlusScoreTime = float.NegativeInfinity;
|
||||
private Tween recentPlusAmountValueTween;
|
||||
private const float RecentPlusResetDelaySeconds = 5f;
|
||||
|
||||
[Header("Judgement Statistics")]
|
||||
public int countPerfect = 0;
|
||||
@@ -126,6 +133,10 @@ public class ScoreManager : MonoBehaviour
|
||||
}
|
||||
|
||||
ResetStatistics();
|
||||
comboRecentPlusAmount = 0;
|
||||
displayedRecentPlusAmount = 0;
|
||||
lastKnownCombo = 0;
|
||||
lastRecentPlusScoreTime = float.NegativeInfinity;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -140,6 +151,7 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
KillRecentPlusAmountValueTween();
|
||||
UnhookPerfectBonusEvent();
|
||||
}
|
||||
|
||||
@@ -148,11 +160,13 @@ public class ScoreManager : MonoBehaviour
|
||||
// Ensure the progress keys start at 0.
|
||||
UpdateIdolscoreKeyScales();
|
||||
TryHookPerfectBonusEvent();
|
||||
RefreshAllScoreUi();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
EnsureIdolscoreKeys();
|
||||
UpdateRecentPlusAmountTimeout();
|
||||
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
if (dt <= 0f) return;
|
||||
@@ -428,6 +442,8 @@ public class ScoreManager : MonoBehaviour
|
||||
if (JudgeManager.IsDebugEnabled)
|
||||
Debug.Log($"[ScoreManager] Added {pmDelta} to track {trackIndex} pm sum, idol delta {idolDelta}. New pm sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSumPm={allSum_pmScore}. Idol sums: R{red_idolScore_sum} G{green_idolScore_sum} Y{yellow_idolScore_sum} P{purple_idolScore_sum} B{blue_idolScore_sum} -> allSumIdol={allSum_idolScore}");
|
||||
|
||||
UpdateRecentPlusAmountUi(pmActualDelta + idolActualDelta);
|
||||
|
||||
// Update UI in teamUIController if available (per-track pm sums + aggregate)
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null)
|
||||
@@ -514,6 +530,8 @@ public class ScoreManager : MonoBehaviour
|
||||
iNumberPrefabController.SpawnForAllyStatic(trackIndex, type, actual);
|
||||
}
|
||||
|
||||
UpdateRecentPlusAmountUi(actual);
|
||||
|
||||
red_idolScore_sum = idolScoreSums[0];
|
||||
green_idolScore_sum = idolScoreSums[1];
|
||||
yellow_idolScore_sum = idolScoreSums[2];
|
||||
@@ -694,10 +712,172 @@ public class ScoreManager : MonoBehaviour
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("[ScoreManager] teamUIController.currentTotalScore is null; cannot display total score.");
|
||||
}
|
||||
|
||||
if (ui.currentScoreRankImage != null && ui.currentScoreRankConfig != null)
|
||||
{
|
||||
ui.currentScoreRankImage.sprite = ui.currentScoreRankConfig.GetRankSprite(totalScore);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("[ScoreManager] teamUIController.Instance is null; cannot update UI.");
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyExternalTotalScoreDelta(int delta)
|
||||
{
|
||||
if (delta == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long next = (long)totalScore + delta;
|
||||
if (next < 0) next = 0;
|
||||
if (next > int.MaxValue - 1L) next = int.MaxValue - 1L;
|
||||
totalScore = (int)next;
|
||||
|
||||
UpdateRecentPlusAmountUi(delta);
|
||||
RefreshAllScoreUi();
|
||||
}
|
||||
|
||||
private void UpdateRecentPlusAmountUi(int scoreDelta)
|
||||
{
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui == null || ui.recentPlusAmountText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int comboNow = ui.CurrentCombo;
|
||||
bool comboBroken = comboNow <= 0;
|
||||
bool comboRestarted = comboNow > 0 && lastKnownCombo <= 0;
|
||||
|
||||
if (comboBroken)
|
||||
{
|
||||
ResetRecentPlusAmountState(ui, true);
|
||||
lastKnownCombo = comboNow;
|
||||
return;
|
||||
}
|
||||
|
||||
if (comboRestarted)
|
||||
{
|
||||
ResetRecentPlusAmountState(ui, false);
|
||||
}
|
||||
|
||||
if (scoreDelta != 0)
|
||||
{
|
||||
comboRecentPlusAmount += scoreDelta;
|
||||
lastRecentPlusScoreTime = Time.unscaledTime;
|
||||
}
|
||||
|
||||
ui.ResetRecentPlusAmountVisual();
|
||||
AnimateRecentPlusAmountText(ui, comboRecentPlusAmount);
|
||||
lastKnownCombo = comboNow;
|
||||
}
|
||||
|
||||
public void RefreshAllScoreUi()
|
||||
{
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ui.currentTotalScore != null)
|
||||
{
|
||||
ui.currentTotalScore.text = totalScore.ToString();
|
||||
}
|
||||
|
||||
if (ui.currentScoreRankImage != null && ui.currentScoreRankConfig != null)
|
||||
{
|
||||
ui.currentScoreRankImage.sprite = ui.currentScoreRankConfig.GetRankSprite(totalScore);
|
||||
}
|
||||
}
|
||||
|
||||
private void AnimateRecentPlusAmountText(teamUIController ui, int targetValue)
|
||||
{
|
||||
if (ui == null || ui.recentPlusAmountText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KillRecentPlusAmountValueTween();
|
||||
|
||||
int startValue = displayedRecentPlusAmount;
|
||||
if (startValue == targetValue)
|
||||
{
|
||||
ui.recentPlusAmountText.text = FormatSignedScoreDelta(targetValue);
|
||||
return;
|
||||
}
|
||||
|
||||
recentPlusAmountValueTween = DOTween
|
||||
.To(() => startValue, value =>
|
||||
{
|
||||
startValue = value;
|
||||
displayedRecentPlusAmount = value;
|
||||
ui.recentPlusAmountText.text = FormatSignedScoreDelta(value);
|
||||
}, targetValue, 0.2f)
|
||||
.SetEase(Ease.OutQuad)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
displayedRecentPlusAmount = targetValue;
|
||||
if (ui.recentPlusAmountText != null)
|
||||
{
|
||||
ui.recentPlusAmountText.text = FormatSignedScoreDelta(targetValue);
|
||||
}
|
||||
recentPlusAmountValueTween = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void KillRecentPlusAmountValueTween()
|
||||
{
|
||||
if (recentPlusAmountValueTween == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
recentPlusAmountValueTween.Kill();
|
||||
recentPlusAmountValueTween = null;
|
||||
}
|
||||
|
||||
private void UpdateRecentPlusAmountTimeout()
|
||||
{
|
||||
if (comboRecentPlusAmount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time.unscaledTime - lastRecentPlusScoreTime < RecentPlusResetDelaySeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResetRecentPlusAmountState(teamUIController.Instance, false);
|
||||
}
|
||||
|
||||
private void ResetRecentPlusAmountState(teamUIController ui, bool killTweenOnly)
|
||||
{
|
||||
comboRecentPlusAmount = 0;
|
||||
displayedRecentPlusAmount = 0;
|
||||
lastRecentPlusScoreTime = float.NegativeInfinity;
|
||||
KillRecentPlusAmountValueTween();
|
||||
|
||||
if (ui == null || ui.recentPlusAmountText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ui.recentPlusAmountText.text = FormatSignedScoreDelta(0);
|
||||
|
||||
if (killTweenOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatSignedScoreDelta(int value)
|
||||
{
|
||||
return value >= 0 ? $"+{value}" : value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,14 +535,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
public void ModifyTotalScore(int delta)
|
||||
{
|
||||
if (ScoreManager.Instance == null) { Debug.LogWarning("ModifyTotalScore: ScoreManager.Instance is null"); return; }
|
||||
// adjust total and attempt to update UI
|
||||
ScoreManager.Instance.totalScore += delta;
|
||||
// try to force UI update via ScoreManager.RecalculateTotal() is not appropriate because it recalculates from allies
|
||||
// so instead update displayed total directly if teamUIController present
|
||||
if (teamUIController.Instance != null && teamUIController.Instance.currentTotalScore != null)
|
||||
{
|
||||
teamUIController.Instance.currentTotalScore.text = ScoreManager.Instance.totalScore.ToString();
|
||||
}
|
||||
ScoreManager.Instance.ApplyExternalTotalScoreDelta(delta);
|
||||
}
|
||||
|
||||
// Documentation text normalized.
|
||||
@@ -1238,24 +1231,12 @@ public class SkillBuilder : MonoBehaviour
|
||||
var comp = target.GetComponent<ICombatant>();
|
||||
if (comp == null) return false;
|
||||
comp.ReceiveHeal(amount, source);
|
||||
if (source != null)
|
||||
{
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null && teamUIController.Instance != null)
|
||||
teamUIController.Instance.RecordHeal(sourceAlly.slotIndex, amount);
|
||||
}
|
||||
return true;
|
||||
|
||||
case EffectType.IncreaseManaOverTime:
|
||||
var ally = target.GetComponent<AllyCombatant>();
|
||||
if (ally == null) return false;
|
||||
ally.ModifyMana(Mathf.CeilToInt(amount), true, true);
|
||||
if (amount > 0f && source != null)
|
||||
{
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null && teamUIController.Instance != null)
|
||||
teamUIController.Instance.RecordMana(sourceAlly.slotIndex, amount);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ public class AllyHero_SO : ScriptableObject
|
||||
public Sprite ally_hero_HD_image;
|
||||
[Header("Inspector")]
|
||||
public Sprite ally_hero_squareProfile;
|
||||
[Header("Inspector")]
|
||||
public Sprite ally_hero_settleDisplay;
|
||||
|
||||
[Header("spine")]
|
||||
public SkeletonDataAsset ally_heroSpineData;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
// NOTE: Windows-only. Launches an external WebView2Host.exe via Process.Start.
|
||||
// Wrapped in UNITY_STANDALONE_WIN so the Windows build/logic is unchanged while
|
||||
// Android compiles this class out.
|
||||
#if UNITY_STANDALONE_WIN
|
||||
using System.Diagnostics;
|
||||
using UnityEngine;
|
||||
|
||||
public class LaunchH5 : MonoBehaviour
|
||||
@@ -22,3 +26,4 @@ public class LaunchH5 : MonoBehaviour
|
||||
Process.Start(psi);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -29,7 +29,7 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 1
|
||||
m_Children:
|
||||
- {fileID: 3426810894855294520}
|
||||
- {fileID: 4787860189400675959}
|
||||
- {fileID: 4600183637760829555}
|
||||
- {fileID: 6563389421309098651}
|
||||
- {fileID: 7481797042088762975}
|
||||
- {fileID: 1351079257240028571}
|
||||
@@ -39,7 +39,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 135.9, y: 0}
|
||||
m_AnchoredPosition: {x: 135.9, y: -54}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &312614768471175586
|
||||
@@ -142,7 +142,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 35, y: 25}
|
||||
m_SizeDelta: {x: 30, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4734727163133092417
|
||||
CanvasRenderer:
|
||||
@@ -217,7 +217,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 30}
|
||||
m_SizeDelta: {x: 129.2, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1347893851219428802
|
||||
CanvasRenderer:
|
||||
@@ -249,11 +249,11 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 25
|
||||
m_FontSize: 28
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 1
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 25
|
||||
m_MinSize: 14
|
||||
m_MaxSize: 28
|
||||
m_Alignment: 5
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
@@ -296,7 +296,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 30}
|
||||
m_SizeDelta: {x: 129.2, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8957605422153280152
|
||||
CanvasRenderer:
|
||||
@@ -328,11 +328,11 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 25
|
||||
m_FontSize: 28
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 1
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 25
|
||||
m_MinSize: 14
|
||||
m_MaxSize: 28
|
||||
m_Alignment: 5
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
@@ -340,81 +340,6 @@ MonoBehaviour:
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: 350234
|
||||
--- !u!1 &1300686028162242880
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7276837415997336770}
|
||||
- component: {fileID: 2417251892043802145}
|
||||
- component: {fileID: 7331213209788149531}
|
||||
m_Layer: 0
|
||||
m_Name: bottom
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7276837415997336770
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1300686028162242880}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -0.8}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4787860189400675959}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -3.1, y: 3.8}
|
||||
m_SizeDelta: {x: 145.324, y: 141.255}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2417251892043802145
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1300686028162242880}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &7331213209788149531
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1300686028162242880}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 0.6367924, b: 0.64803755, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1.5
|
||||
--- !u!1 &1365303792903341059
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -451,7 +376,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 35, y: 25}
|
||||
m_SizeDelta: {x: 30, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4210288393527682798
|
||||
CanvasRenderer:
|
||||
@@ -525,8 +450,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -65, y: 0}
|
||||
m_SizeDelta: {x: 90, y: 30}
|
||||
m_AnchoredPosition: {x: -71.489944, y: 0}
|
||||
m_SizeDelta: {x: 102.9798, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8683684367698790289
|
||||
CanvasRenderer:
|
||||
@@ -549,7 +474,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -558,7 +483,7 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 24
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -605,7 +530,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 30}
|
||||
m_SizeDelta: {x: 129.2, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8186726174179685067
|
||||
CanvasRenderer:
|
||||
@@ -637,11 +562,11 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 25
|
||||
m_FontSize: 28
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 1
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 25
|
||||
m_MinSize: 14
|
||||
m_MaxSize: 28
|
||||
m_Alignment: 5
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
@@ -675,16 +600,16 @@ RectTransform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1803836327666263803}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -4.84}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4787860189400675959}
|
||||
m_Father: {fileID: 4148755058004053777}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -3, y: 4.3}
|
||||
m_SizeDelta: {x: 123.437, y: 123.437}
|
||||
m_AnchoredPosition: {x: -213.69046, y: 54.585762}
|
||||
m_SizeDelta: {x: 600, y: 165}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1406633831322811249
|
||||
CanvasRenderer:
|
||||
@@ -714,7 +639,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: eec334d03c52af847bfa12dc839ac536, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: f1d5deba8c5a71c439be778c5bd9a61b, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -789,7 +714,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 4573847525859265706, guid: c0dab60f28c13404fbad49de3cf86dd0, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: 7b78ab1a3117c6243a284471316b0c65, type: 3}
|
||||
m_Type: 3
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -898,7 +823,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 30}
|
||||
m_SizeDelta: {x: 129.2, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4306892612456958120
|
||||
CanvasRenderer:
|
||||
@@ -930,11 +855,11 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 25
|
||||
m_FontSize: 28
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 1
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 25
|
||||
m_MinSize: 14
|
||||
m_MaxSize: 28
|
||||
m_Alignment: 5
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
@@ -1023,7 +948,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &3426810894855294520
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1117,7 +1042,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 35, y: 25}
|
||||
m_SizeDelta: {x: 30, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7316102496420409225
|
||||
CanvasRenderer:
|
||||
@@ -1356,7 +1281,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.8962264, g: 0.4523407, b: 0.768059, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1408,7 +1333,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 30}
|
||||
m_SizeDelta: {x: 107.4343, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1151234173233781149
|
||||
CanvasRenderer:
|
||||
@@ -1431,7 +1356,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1440,11 +1365,11 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 24
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 1
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 22
|
||||
m_MaxSize: 24
|
||||
m_Alignment: 5
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
@@ -1486,8 +1411,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -65, y: 0}
|
||||
m_SizeDelta: {x: 90, y: 30}
|
||||
m_AnchoredPosition: {x: -71.489944, y: 0}
|
||||
m_SizeDelta: {x: 102.9798, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8439903389052874367
|
||||
CanvasRenderer:
|
||||
@@ -1510,7 +1435,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1519,7 +1444,7 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 24
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -1629,8 +1554,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -77.114, y: 0}
|
||||
m_SizeDelta: {x: 95.773, y: 30}
|
||||
m_AnchoredPosition: {x: -81.71184, y: 0}
|
||||
m_SizeDelta: {x: 104.9686, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3533238549364255986
|
||||
CanvasRenderer:
|
||||
@@ -1662,7 +1587,7 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 26
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -1745,8 +1670,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -77.114, y: 0}
|
||||
m_SizeDelta: {x: 95.773, y: 30}
|
||||
m_AnchoredPosition: {x: -81.71184, y: 0}
|
||||
m_SizeDelta: {x: 104.9686, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1513130722049452815
|
||||
CanvasRenderer:
|
||||
@@ -1778,7 +1703,7 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 26
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -1936,7 +1861,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -324.5, y: 45.2}
|
||||
m_AnchoredPosition: {x: -217.4, y: 50.8}
|
||||
m_SizeDelta: {x: 233, y: 36}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4531879398774622215
|
||||
@@ -1981,8 +1906,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -326.8, y: 95.52118}
|
||||
m_SizeDelta: {x: 290.97, y: 40}
|
||||
m_AnchoredPosition: {x: -115.5, y: 95.52118}
|
||||
m_SizeDelta: {x: 336.6112, y: 40}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3659676079848674504
|
||||
CanvasRenderer:
|
||||
@@ -2005,7 +1930,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -2060,8 +1985,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 1.6662}
|
||||
m_SizeDelta: {x: 0, y: -3.3324}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7287820284985504367
|
||||
CanvasRenderer:
|
||||
@@ -2084,7 +2009,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 0.09803922, g: 0.32941177, b: 0.6, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -2092,8 +2017,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 20
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 25
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -2316,7 +2241,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 1}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchoredPosition: {x: -293.1, y: -8.57}
|
||||
m_AnchoredPosition: {x: -97.1, y: -14}
|
||||
m_SizeDelta: {x: 200, y: 90}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &685427306910703188
|
||||
@@ -2443,8 +2368,8 @@ RectTransform:
|
||||
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: 250, y: 100}
|
||||
m_AnchoredPosition: {x: 0, y: 69.49}
|
||||
m_SizeDelta: {x: 250, y: 165}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &6539971250549974765
|
||||
MonoBehaviour:
|
||||
@@ -2470,83 +2395,6 @@ MonoBehaviour:
|
||||
this_allyManaRestored: {fileID: 6381579454208282139}
|
||||
displayDetailContributionButton: {fileID: 2479934731397186594}
|
||||
detailContributionGO: {fileID: 7749677393578589965}
|
||||
--- !u!1 &6682492808549074205
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4787860189400675959}
|
||||
- component: {fileID: 2643565460332953122}
|
||||
- component: {fileID: 7400641254697929517}
|
||||
m_Layer: 0
|
||||
m_Name: shadow
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4787860189400675959
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6682492808549074205}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -4.402}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 7276837415997336770}
|
||||
- {fileID: 4600183637760829555}
|
||||
m_Father: {fileID: 4148755058004053777}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -67.251, y: 53.9}
|
||||
m_SizeDelta: {x: 147.235, y: 143.112}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2643565460332953122
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6682492808549074205}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &7400641254697929517
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6682492808549074205}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 0.4745098}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1.5
|
||||
--- !u!1 &6853683024340712328
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2580,7 +2428,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -177.5, y: -5.2}
|
||||
m_AnchoredPosition: {x: 71.38, y: -34.55}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &7151601246212580972
|
||||
@@ -2684,7 +2532,7 @@ RectTransform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7380227020462892773}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0.0001}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -2692,8 +2540,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0.17, y: -38.91}
|
||||
m_SizeDelta: {x: 301.5, y: 22.17}
|
||||
m_AnchoredPosition: {x: 57.903595, y: -43.9901}
|
||||
m_SizeDelta: {x: 255.7965, y: 12.0376}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7024596005729876765
|
||||
CanvasRenderer:
|
||||
@@ -2723,8 +2571,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Type: 1
|
||||
m_Sprite: {fileID: 21300000, guid: 3dc2408de993d094695786359ef267aa, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@@ -2768,7 +2616,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 30}
|
||||
m_SizeDelta: {x: 107.4343, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4931104231077369669
|
||||
CanvasRenderer:
|
||||
@@ -2791,7 +2639,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -2800,11 +2648,11 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 24
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 1
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 22
|
||||
m_MaxSize: 24
|
||||
m_Alignment: 5
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
@@ -2876,7 +2724,7 @@ RectTransform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7692321193941902280}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0.0001}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
@@ -2885,8 +2733,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0.01, y: -38.91}
|
||||
m_SizeDelta: {x: 301.5, y: 22.17}
|
||||
m_AnchoredPosition: {x: 57.67989, y: -43.9901}
|
||||
m_SizeDelta: {x: 255.7965, y: 12.0376}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4552223981125847129
|
||||
CanvasRenderer:
|
||||
@@ -2916,8 +2764,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Type: 1
|
||||
m_Sprite: {fileID: 21300000, guid: 3dc2408de993d094695786359ef267aa, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@@ -2965,7 +2813,7 @@ RectTransform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7749677393578589965}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -5.087}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
@@ -2974,8 +2822,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -670.5, y: 54.942}
|
||||
m_SizeDelta: {x: 302.667, y: 182.657}
|
||||
m_AnchoredPosition: {x: 243.65, y: 53.535202}
|
||||
m_SizeDelta: {x: 302.667, y: 170.0029}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &810551395488659022
|
||||
CanvasRenderer:
|
||||
@@ -3005,7 +2853,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: 45682aabe328fec4a8feb3e055f1dfb2, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -3051,8 +2899,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -30.02781, y: 51.42924}
|
||||
m_SizeDelta: {x: 60.056, y: 42}
|
||||
m_AnchoredPosition: {x: -50.136425, y: 53.8}
|
||||
m_SizeDelta: {x: 94.3793, y: 42}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &79757123884187993
|
||||
CanvasRenderer:
|
||||
@@ -3082,8 +2930,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Type: 1
|
||||
m_Sprite: {fileID: -566881566719626471, guid: aee8077d170e84b44afd422763951685, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@@ -3232,7 +3080,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.8773585, g: 0.59571314, b: 0.09518514, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -3283,8 +3131,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -77.114, y: 0}
|
||||
m_SizeDelta: {x: 95.773, y: 30}
|
||||
m_AnchoredPosition: {x: -81.71184, y: 0}
|
||||
m_SizeDelta: {x: 104.9686, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &492154790085150397
|
||||
CanvasRenderer:
|
||||
@@ -3316,7 +3164,7 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 26
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -3514,7 +3362,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 35, y: 25}
|
||||
m_SizeDelta: {x: 30, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6314649216240834215
|
||||
CanvasRenderer:
|
||||
@@ -3663,8 +3511,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -77.114, y: 0}
|
||||
m_SizeDelta: {x: 95.773, y: 30}
|
||||
m_AnchoredPosition: {x: -81.71184, y: 0}
|
||||
m_SizeDelta: {x: 104.9686, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1247215313835421715
|
||||
CanvasRenderer:
|
||||
@@ -3696,7 +3544,7 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 26
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
|
||||
@@ -151,14 +151,14 @@ public class loadSettlementTeamPrefab : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Populate profile and name: prefer AllyHero_SO.ally_hero_squareProfile when available
|
||||
// Populate profile and name: prefer AllyHero_SO.ally_hero_settleDisplay when available
|
||||
bool setProfile = false;
|
||||
|
||||
if (resolvedHeroSO != null && card.ally_profile != null)
|
||||
{
|
||||
if (resolvedHeroSO.ally_hero_squareProfile != null)
|
||||
if (resolvedHeroSO.ally_hero_settleDisplay != null)
|
||||
{
|
||||
card.ally_profile.sprite = resolvedHeroSO.ally_hero_squareProfile;
|
||||
card.ally_profile.sprite = resolvedHeroSO.ally_hero_settleDisplay;
|
||||
card.ally_profile.color = new Color(1f, 1f, 1f, 1f);
|
||||
if (card.ally_name != null)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,8 @@ class UI_Panel_Character : MonoBehaviour
|
||||
{
|
||||
[SerializeField] float anim_Time = 0.5f;
|
||||
[SerializeField, Range(0f, 1f)] float illustrationFadeStartAlpha = 0.25f;
|
||||
[SerializeField] float illustrationBgTargetXOffset = 0f;
|
||||
[SerializeField] float illustrationBgTargetYOffset = 0f;
|
||||
float Anim_Speed => 1f / anim_Time;
|
||||
|
||||
[SerializeField] List<Animator> ui_Anim;
|
||||
@@ -287,6 +289,8 @@ class UI_Panel_Character : MonoBehaviour
|
||||
Vector2 targetPosBG = hasCachedPositions
|
||||
? originalIllustrationBGPos
|
||||
: Image_Character_Illustration_BG.rectTransform.anchoredPosition;
|
||||
targetPosBG.x += illustrationBgTargetXOffset;
|
||||
targetPosBG.y += illustrationBgTargetYOffset;
|
||||
Image_Character_Illustration_BG.sprite = data.ally_hero_HD_image;
|
||||
|
||||
float startXBG = targetPosBG.x - 200;
|
||||
|
||||
@@ -9,11 +9,18 @@ using Bansonic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.UI;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
public class UI_Panel_Mail : MonoBehaviour
|
||||
{
|
||||
public static event Action OnUnreadStateChanged;
|
||||
private static readonly Dictionary<string, Sprite> ServerMailImageCache = new Dictionary<string, Sprite>(StringComparer.Ordinal);
|
||||
private static readonly HashSet<string> ServerMailImageLoadsInFlight = new HashSet<string>(StringComparer.Ordinal);
|
||||
#if UNITY_EDITOR
|
||||
private const string DefaultCoinRewardIconPath = "Assets/__UI_NEW/hallway/icon.90.90/icon_coin.png";
|
||||
private const string DefaultMaterialRewardIconPath = "Assets/__UI_NEW/hallway/icon.90.90/icon_piece.png";
|
||||
#endif
|
||||
|
||||
//1234567890
|
||||
[SerializeField] float anim_Time = 0.5f;
|
||||
@@ -31,6 +38,10 @@ public class UI_Panel_Mail : MonoBehaviour
|
||||
[Header("rewards objects")]
|
||||
public GameObject rewardSlotPrefab;
|
||||
[SerializeField] Transform content_Reward_Slot;
|
||||
[Header("reward fallback icons")]
|
||||
[SerializeField] private Sprite playerExpRewardIcon;
|
||||
[SerializeField] private Sprite coinRewardIcon;
|
||||
[SerializeField] private Sprite materialRewardIcon;
|
||||
|
||||
[Header("texts")]
|
||||
public Text thisMail_title;
|
||||
@@ -47,11 +58,12 @@ public class UI_Panel_Mail : MonoBehaviour
|
||||
|
||||
[Header("buttons")]
|
||||
public Button receive_this_mail;
|
||||
public Button backButton;
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Escape))
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
CloseMailPanel();
|
||||
}
|
||||
}
|
||||
void Start()
|
||||
@@ -60,12 +72,14 @@ public class UI_Panel_Mail : MonoBehaviour
|
||||
{
|
||||
item.speed = Anim_Speed;
|
||||
}
|
||||
MailRewardGrantService.ConfigureBasicRewardIcons(playerExpRewardIcon, coinRewardIcon, materialRewardIcon);
|
||||
BindTopLevelButtons();
|
||||
InitializeMailSource();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
MailRewardGrantService.ConfigureBasicRewardIcons(playerExpRewardIcon, coinRewardIcon, materialRewardIcon);
|
||||
InitializeMailSource();
|
||||
}
|
||||
|
||||
@@ -74,6 +88,48 @@ public class UI_Panel_Mail : MonoBehaviour
|
||||
UnsubscribeMailService();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
TryAssignDefaultRewardIcons();
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
TryAssignDefaultRewardIcons();
|
||||
}
|
||||
|
||||
private void TryAssignDefaultRewardIcons()
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
if (coinRewardIcon == null)
|
||||
{
|
||||
Sprite coinIcon = AssetDatabase.LoadAssetAtPath<Sprite>(DefaultCoinRewardIconPath);
|
||||
if (coinIcon != null)
|
||||
{
|
||||
coinRewardIcon = coinIcon;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (materialRewardIcon == null)
|
||||
{
|
||||
Sprite materialIcon = AssetDatabase.LoadAssetAtPath<Sprite>(DefaultMaterialRewardIconPath);
|
||||
if (materialIcon != null)
|
||||
{
|
||||
materialRewardIcon = materialIcon;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed && !Application.isPlaying)
|
||||
{
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void InitializeMailSource()
|
||||
{
|
||||
if (!isActiveAndEnabled)
|
||||
@@ -445,14 +501,18 @@ public class UI_Panel_Mail : MonoBehaviour
|
||||
|
||||
void BindTopLevelButtons()
|
||||
{
|
||||
if (receive_this_mail == null)
|
||||
if (receive_this_mail != null)
|
||||
{
|
||||
return;
|
||||
receive_this_mail.onClick.RemoveListener(HandleReceiveCurrentMailClicked);
|
||||
receive_this_mail.onClick.AddListener(HandleReceiveCurrentMailClicked);
|
||||
RefreshReceiveButton();
|
||||
}
|
||||
|
||||
receive_this_mail.onClick.RemoveListener(HandleReceiveCurrentMailClicked);
|
||||
receive_this_mail.onClick.AddListener(HandleReceiveCurrentMailClicked);
|
||||
RefreshReceiveButton();
|
||||
if (backButton != null)
|
||||
{
|
||||
backButton.onClick.RemoveListener(HandleBackButtonClicked);
|
||||
backButton.onClick.AddListener(HandleBackButtonClicked);
|
||||
}
|
||||
}
|
||||
|
||||
void HandleReceiveCurrentMailClicked()
|
||||
@@ -465,6 +525,16 @@ public class UI_Panel_Mail : MonoBehaviour
|
||||
OnMailReceived(selectedSlot, selectedMailData);
|
||||
}
|
||||
|
||||
void HandleBackButtonClicked()
|
||||
{
|
||||
CloseMailPanel();
|
||||
}
|
||||
|
||||
void CloseMailPanel()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void RefreshReceiveButton()
|
||||
{
|
||||
if (receive_this_mail == null)
|
||||
@@ -1282,6 +1352,27 @@ public static class MailRewardGrantService
|
||||
private static readonly Dictionary<int, storeItemSO> StoreItemsById = new Dictionary<int, storeItemSO>();
|
||||
private static readonly Dictionary<string, storeItemSO> StoreItemsByName = new Dictionary<string, storeItemSO>(StringComparer.OrdinalIgnoreCase);
|
||||
private static bool _assetsLoaded;
|
||||
private static Sprite _playerExpRewardIcon;
|
||||
private static Sprite _coinRewardIcon;
|
||||
private static Sprite _materialRewardIcon;
|
||||
|
||||
public static void ConfigureBasicRewardIcons(Sprite playerExpIcon, Sprite coinIcon, Sprite materialIcon)
|
||||
{
|
||||
if (playerExpIcon != null)
|
||||
{
|
||||
_playerExpRewardIcon = playerExpIcon;
|
||||
}
|
||||
|
||||
if (coinIcon != null)
|
||||
{
|
||||
_coinRewardIcon = coinIcon;
|
||||
}
|
||||
|
||||
if (materialIcon != null)
|
||||
{
|
||||
_materialRewardIcon = materialIcon;
|
||||
}
|
||||
}
|
||||
|
||||
public static void PopulateRewardDisplay(mail_so.rewardItem reward)
|
||||
{
|
||||
@@ -1296,15 +1387,15 @@ public static class MailRewardGrantService
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(reward.rewardName))
|
||||
if (ShouldOverrideRewardDisplayName(reward, resolved))
|
||||
{
|
||||
reward.rewardName = resolved.DisplayName;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(reward.reward_description))
|
||||
if (ShouldOverrideRewardDescription(reward, resolved))
|
||||
{
|
||||
reward.reward_description = resolved.Description;
|
||||
}
|
||||
if (reward.reward_image == null)
|
||||
if (resolved.Icon != null)
|
||||
{
|
||||
reward.reward_image = resolved.Icon;
|
||||
}
|
||||
@@ -1585,13 +1676,13 @@ public static class MailRewardGrantService
|
||||
switch (reward.reward_Type)
|
||||
{
|
||||
case mail_so.reward_type.exp_user:
|
||||
resolved = CreateBasicResolved(ResolvedKind.PlayerExp, amount, rewardName, reward.reward_image, reward.reward_description, reward, "\u73a9\u5bb6\u7ecf\u9a8c");
|
||||
resolved = CreateBasicResolved(ResolvedKind.PlayerExp, amount, rewardName, reward.reward_image != null ? reward.reward_image : _playerExpRewardIcon, reward.reward_description, reward, "\u73a9\u5bb6\u7ecf\u9a8c");
|
||||
return true;
|
||||
case mail_so.reward_type.money:
|
||||
resolved = CreateBasicResolved(ResolvedKind.Coins, amount, rewardName, reward.reward_image, reward.reward_description, reward, "\u91d1\u5e01");
|
||||
resolved = CreateBasicResolved(ResolvedKind.Coins, amount, rewardName, reward.reward_image != null ? reward.reward_image : _coinRewardIcon, reward.reward_description, reward, "\u91d1\u5e01");
|
||||
return true;
|
||||
case mail_so.reward_type.metarial:
|
||||
resolved = CreateBasicResolved(ResolvedKind.Material, amount, rewardName, reward.reward_image, reward.reward_description, reward, "\u8bb0\u5fc6\u788e\u7247");
|
||||
resolved = CreateBasicResolved(ResolvedKind.Material, amount, rewardName, reward.reward_image != null ? reward.reward_image : _materialRewardIcon, reward.reward_description, reward, "\u8bb0\u5fc6\u788e\u7247");
|
||||
return true;
|
||||
case mail_so.reward_type.expBottles_allies:
|
||||
if (TryResolveExpBottle(reward, rewardKey, rewardName, amount, out resolved))
|
||||
@@ -1644,6 +1735,44 @@ public static class MailRewardGrantService
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ShouldOverrideRewardDisplayName(mail_so.rewardItem reward, ResolvedReward resolved)
|
||||
{
|
||||
if (reward == null || resolved == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (resolved.Kind)
|
||||
{
|
||||
case ResolvedKind.ExpBottle:
|
||||
case ResolvedKind.GrowthMaterial:
|
||||
case ResolvedKind.EquipmentConsumable:
|
||||
case ResolvedKind.StoreItem:
|
||||
return !string.IsNullOrWhiteSpace(resolved.DisplayName);
|
||||
default:
|
||||
return string.IsNullOrWhiteSpace(reward.rewardName) && !string.IsNullOrWhiteSpace(resolved.DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldOverrideRewardDescription(mail_so.rewardItem reward, ResolvedReward resolved)
|
||||
{
|
||||
if (reward == null || resolved == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (resolved.Kind)
|
||||
{
|
||||
case ResolvedKind.ExpBottle:
|
||||
case ResolvedKind.GrowthMaterial:
|
||||
case ResolvedKind.EquipmentConsumable:
|
||||
case ResolvedKind.StoreItem:
|
||||
return !string.IsNullOrWhiteSpace(resolved.Description);
|
||||
default:
|
||||
return string.IsNullOrWhiteSpace(reward.reward_description) && !string.IsNullOrWhiteSpace(resolved.Description);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolveByRewardKey(mail_so.rewardItem reward, string rewardKey, string rewardName, int amount, out ResolvedReward resolved)
|
||||
{
|
||||
if (TryResolveExpBottle(reward, rewardKey, rewardName, amount, out resolved))
|
||||
|
||||
@@ -1502,6 +1502,7 @@ RectTransform:
|
||||
- {fileID: 4507029859229390628}
|
||||
- {fileID: 3166511394318740470}
|
||||
- {fileID: 4201371652593379150}
|
||||
- {fileID: 7304390476110563193}
|
||||
m_Father: {fileID: 4008714089197239054}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
@@ -3837,6 +3838,41 @@ MonoBehaviour:
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &3000813276228866063
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7304390476110563193}
|
||||
m_Layer: 5
|
||||
m_Name: putSettingsPrefabHere
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7304390476110563193
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3000813276228866063}
|
||||
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: 5634981615641515618}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -504.3711}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &3004460849469971855
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -4101,7 +4137,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -28, y: 1.3}
|
||||
m_SizeDelta: {x: 53, y: 47}
|
||||
m_SizeDelta: {x: 90, y: 90}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5184938500494465621
|
||||
CanvasRenderer:
|
||||
@@ -4125,13 +4161,13 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 8139719423079072813, guid: 16f0765cc15fce04f828ecd44d1dbf91, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: 16f0765cc15fce04f828ecd44d1dbf91, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -5862,7 +5898,9 @@ MonoBehaviour:
|
||||
playerCoins_legacy: {fileID: 7376147910533281946}
|
||||
player_mmrFragment: {fileID: 1718535194614905540}
|
||||
player_rks: {fileID: 3469340257331446344}
|
||||
rksProgressImage: {fileID: 4240146692151177402}
|
||||
putPrefabsHere: {fileID: 6773918330215714056}
|
||||
putSettingsPrefabHere: {fileID: 3000813276228866063}
|
||||
settings_launch: {fileID: 865676738076373517}
|
||||
userInfo_launch: {fileID: 1502937460034745443}
|
||||
store_launch: {fileID: 5472029888669800656}
|
||||
@@ -6106,7 +6144,7 @@ MonoBehaviour:
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 0
|
||||
m_FillAmount: 0.457
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
@@ -8854,7 +8892,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -0.000041962, y: -0.000047684}
|
||||
m_SizeDelta: {x: 52, y: 49}
|
||||
m_SizeDelta: {x: 90, y: 90}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5175879897170093321
|
||||
CanvasRenderer:
|
||||
@@ -8878,13 +8916,13 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 6309817061271938796, guid: 6fd4dd33d986d824d95f7b01579acc4d, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: 6fd4dd33d986d824d95f7b01579acc4d, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
|
||||
@@ -23,8 +23,10 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
public Text playerCoins_legacy;
|
||||
public Text player_mmrFragment;
|
||||
public Text player_rks;
|
||||
public Image rksProgressImage;
|
||||
[Header("put prefabs here")]
|
||||
public GameObject putPrefabsHere;
|
||||
public GameObject putSettingsPrefabHere;
|
||||
[Header("son buttons")]
|
||||
public Button settings_launch;
|
||||
public Button userInfo_launch;
|
||||
@@ -154,7 +156,9 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
PlayerRksService.OnRksChanged += HandleRksChanged;
|
||||
UpdatePlayerCoinsLegacyText(PlayerEconomyLedger.EnsureInstance().GetCoins());
|
||||
UpdatePlayerMmrFragmentText(PlayerEconomyLedger.EnsureInstance().GetMaterial());
|
||||
UpdatePlayerRksText(PlayerRksService.GetBestOverallRks(player_SO));
|
||||
float currentRks = PlayerRksService.GetBestOverallRks(player_SO);
|
||||
UpdatePlayerRksText(currentRks);
|
||||
UpdateRksProgressImage(currentRks);
|
||||
|
||||
if (button_Music == null)
|
||||
{
|
||||
@@ -223,7 +227,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
}
|
||||
|
||||
// Pre-instantiate settings prefab and keep it inactive, ensuring only one exists
|
||||
if (settings_prefab != null && putPrefabsHere != null)
|
||||
if (settings_prefab != null && GetSettingsParentTransform() != null)
|
||||
{
|
||||
// Try to find existing one in scene first
|
||||
if (settingsInstance == null)
|
||||
@@ -237,7 +241,8 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
|
||||
if (settingsInstance == null)
|
||||
{
|
||||
settingsInstance = Instantiate(settings_prefab, putPrefabsHere.transform);
|
||||
settingsInstance = Instantiate(settings_prefab, GetSettingsParentTransform());
|
||||
AttachManagedPanelVisibilityRelay(settingsInstance, true);
|
||||
EnsureSettingsEnterAnimator(settingsInstance);
|
||||
EnsureSettingsLastSibling();
|
||||
settingsInstance.SetActive(false);
|
||||
@@ -261,8 +266,9 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
// ensure it's parented correctly under putPrefabsHere
|
||||
settingsInstance.transform.SetParent(putPrefabsHere.transform, false);
|
||||
// ensure it's parented correctly beside putPrefabsHere
|
||||
settingsInstance.transform.SetParent(GetSettingsParentTransform(), false);
|
||||
AttachManagedPanelVisibilityRelay(settingsInstance, true);
|
||||
EnsureSettingsLastSibling();
|
||||
EnsureSettingsEnterAnimator(settingsInstance);
|
||||
settingsInstance.SetActive(false);
|
||||
@@ -283,60 +289,106 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
if (userInfoInstance == null)
|
||||
{
|
||||
userInfoInstance = Instantiate(userInfo_prefab, putPrefabsHere.transform);
|
||||
AttachManagedPanelVisibilityRelay(userInfoInstance, false);
|
||||
userInfoInstance.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
userInfoInstance.transform.SetParent(putPrefabsHere.transform, false);
|
||||
AttachManagedPanelVisibilityRelay(userInfoInstance, false);
|
||||
userInfoInstance.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
lastSettingsVisibilityState = IsSettingsPanelVisible;
|
||||
lastOverlayPanelsVisibilityState = AreOverlayPanelsVisible();
|
||||
CurrentOverlayPanelsVisible = lastOverlayPanelsVisibilityState;
|
||||
SyncManagedPanelVisibilityState(true);
|
||||
}
|
||||
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if(Input.GetKeyDown(KeyCode.Escape))
|
||||
{
|
||||
//ToggleSettingsPrefab();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
private void OnTransformChildrenChanged()
|
||||
{
|
||||
if (!IsUiUiSceneActive())
|
||||
{
|
||||
if (CurrentSettingsVisible)
|
||||
{
|
||||
BroadcastSettingsVisibility(false);
|
||||
}
|
||||
if (CurrentOverlayPanelsVisible)
|
||||
{
|
||||
lastOverlayPanelsVisibilityState = false;
|
||||
CurrentOverlayPanelsVisible = false;
|
||||
GlobalOverlayPanelVisibilityChanged?.Invoke(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool visible = IsSettingsPanelVisible;
|
||||
if (visible != lastSettingsVisibilityState)
|
||||
topNavigationGeometryDirty = true;
|
||||
EnsureTopNavigationFront();
|
||||
}
|
||||
|
||||
internal void NotifyManagedPanelVisibilityChanged(bool includesSettingsPanel)
|
||||
{
|
||||
if (!ReferenceEquals(activeInstance, this))
|
||||
{
|
||||
BroadcastSettingsVisibility(visible);
|
||||
return;
|
||||
}
|
||||
|
||||
bool overlayVisible = AreOverlayPanelsVisible();
|
||||
if (overlayVisible != lastOverlayPanelsVisibilityState)
|
||||
if (!IsUiUiSceneActive())
|
||||
{
|
||||
lastOverlayPanelsVisibilityState = overlayVisible;
|
||||
CurrentOverlayPanelsVisible = overlayVisible;
|
||||
GlobalOverlayPanelVisibilityChanged?.Invoke(overlayVisible);
|
||||
SetSettingsVisibilityState(false);
|
||||
SetOverlayPanelsVisibilityState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (includesSettingsPanel)
|
||||
{
|
||||
SetSettingsVisibilityState(IsSettingsPanelVisible);
|
||||
}
|
||||
|
||||
SetOverlayPanelsVisibilityState(AreOverlayPanelsVisible());
|
||||
}
|
||||
|
||||
private void SyncManagedPanelVisibilityState(bool forceNotify)
|
||||
{
|
||||
if (!IsUiUiSceneActive())
|
||||
{
|
||||
SetSettingsVisibilityState(false, forceNotify);
|
||||
SetOverlayPanelsVisibilityState(false, forceNotify);
|
||||
return;
|
||||
}
|
||||
|
||||
SetSettingsVisibilityState(IsSettingsPanelVisible, forceNotify);
|
||||
SetOverlayPanelsVisibilityState(AreOverlayPanelsVisible(), forceNotify);
|
||||
}
|
||||
|
||||
private void AttachManagedPanelVisibilityRelay(GameObject panel, bool includesSettingsPanel)
|
||||
{
|
||||
if (panel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
btmandtopPanelVisibilityRelay relay = panel.GetComponent<btmandtopPanelVisibilityRelay>();
|
||||
if (relay == null)
|
||||
{
|
||||
relay = panel.AddComponent<btmandtopPanelVisibilityRelay>();
|
||||
}
|
||||
|
||||
relay.Initialize(this, includesSettingsPanel);
|
||||
}
|
||||
|
||||
private void SetSettingsVisibilityState(bool visible, bool forceNotify = false)
|
||||
{
|
||||
lastSettingsVisibilityState = visible;
|
||||
if (!forceNotify && CurrentSettingsVisible == visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentSettingsVisible = visible;
|
||||
SettingsVisibilityChanged?.Invoke(visible);
|
||||
GlobalSettingsVisibilityChanged?.Invoke(visible);
|
||||
}
|
||||
|
||||
private void SetOverlayPanelsVisibilityState(bool visible, bool forceNotify = false)
|
||||
{
|
||||
lastOverlayPanelsVisibilityState = visible;
|
||||
if (!forceNotify && CurrentOverlayPanelsVisible == visible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentOverlayPanelsVisible = visible;
|
||||
GlobalOverlayPanelVisibilityChanged?.Invoke(visible);
|
||||
topNavigationGeometryDirty = true;
|
||||
EnsureTopNavigationFront();
|
||||
}
|
||||
|
||||
@@ -554,6 +606,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
private void HandleRksChanged(float rksAmount)
|
||||
{
|
||||
UpdatePlayerRksText(rksAmount);
|
||||
UpdateRksProgressImage(rksAmount);
|
||||
}
|
||||
|
||||
private void InitializeMailRedPot()
|
||||
@@ -644,6 +697,16 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRksProgressImage(float rksAmount)
|
||||
{
|
||||
if (rksProgressImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rksProgressImage.fillAmount = Mathf.Clamp01(Mathf.InverseLerp(0f, 100f, rksAmount));
|
||||
}
|
||||
|
||||
private Text FindLegacyTextByLiteral(string literal)
|
||||
{
|
||||
if (string.IsNullOrEmpty(literal))
|
||||
@@ -802,12 +865,13 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
|
||||
private void ToggleSettingsPrefab()
|
||||
{
|
||||
if (settings_prefab == null || putPrefabsHere == null) return;
|
||||
if (settings_prefab == null || GetSettingsParentTransform() == null) return;
|
||||
|
||||
// If we don't have an instance (it may have been destroyed), instantiate one
|
||||
if (settingsInstance == null)
|
||||
{
|
||||
settingsInstance = Instantiate(settings_prefab, putPrefabsHere.transform);
|
||||
settingsInstance = Instantiate(settings_prefab, GetSettingsParentTransform());
|
||||
AttachManagedPanelVisibilityRelay(settingsInstance, true);
|
||||
EnsureSettingsEnterAnimator(settingsInstance);
|
||||
EnsureSettingsLastSibling();
|
||||
|
||||
@@ -830,22 +894,20 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
|
||||
settingsInstance.SetActive(true);
|
||||
EnsureSettingsLastSibling();
|
||||
BroadcastSettingsVisibility(true);
|
||||
BroadcastOverlayPanelsVisibility();
|
||||
RegisterManagedPanel(settingsInstance, () =>
|
||||
{
|
||||
if (settingsInstance != null)
|
||||
{
|
||||
settingsInstance.SetActive(false);
|
||||
BroadcastSettingsVisibility(false);
|
||||
BroadcastOverlayPanelsVisibility();
|
||||
}
|
||||
});
|
||||
NotifyManagedPanelVisibilityChanged(true);
|
||||
return;
|
||||
}
|
||||
|
||||
AttachManagedPanelVisibilityRelay(settingsInstance, true);
|
||||
EnsureSettingsEnterAnimator(settingsInstance);
|
||||
settingsInstance.transform.SetParent(putPrefabsHere.transform, false);
|
||||
settingsInstance.transform.SetParent(GetSettingsParentTransform(), false);
|
||||
EnsureSettingsLastSibling();
|
||||
|
||||
// Toggle active state
|
||||
@@ -859,13 +921,10 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
if (settingsInstance != null)
|
||||
{
|
||||
settingsInstance.SetActive(false);
|
||||
BroadcastSettingsVisibility(false);
|
||||
BroadcastOverlayPanelsVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
BroadcastSettingsVisibility(settingsInstance.activeSelf);
|
||||
BroadcastOverlayPanelsVisibility();
|
||||
NotifyManagedPanelVisibilityChanged(true);
|
||||
}
|
||||
|
||||
public bool IsSettingsPanelVisible
|
||||
@@ -889,16 +948,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
|
||||
private void BroadcastSettingsVisibility(bool visible)
|
||||
{
|
||||
lastSettingsVisibilityState = visible;
|
||||
if (CurrentSettingsVisible == visible)
|
||||
{
|
||||
SettingsVisibilityChanged?.Invoke(visible);
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentSettingsVisible = visible;
|
||||
SettingsVisibilityChanged?.Invoke(visible);
|
||||
GlobalSettingsVisibilityChanged?.Invoke(visible);
|
||||
SetSettingsVisibilityState(visible);
|
||||
}
|
||||
|
||||
private bool IsUiUiSceneActive()
|
||||
@@ -921,12 +971,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
|
||||
private void BroadcastOverlayPanelsVisibility()
|
||||
{
|
||||
bool visible = AreOverlayPanelsVisible();
|
||||
lastOverlayPanelsVisibilityState = visible;
|
||||
CurrentOverlayPanelsVisible = visible;
|
||||
GlobalOverlayPanelVisibilityChanged?.Invoke(visible);
|
||||
topNavigationGeometryDirty = true;
|
||||
EnsureTopNavigationFront();
|
||||
SetOverlayPanelsVisibilityState(AreOverlayPanelsVisible());
|
||||
}
|
||||
|
||||
private void RegisterManagedPanel(GameObject panel, System.Action closeAction)
|
||||
@@ -1071,6 +1116,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
instance.SetActive(true);
|
||||
}
|
||||
|
||||
AttachManagedPanelVisibilityRelay(instance, false);
|
||||
TryAssignCanvasCamera(instance);
|
||||
BroadcastOverlayPanelsVisibility();
|
||||
EnsureTopNavigationFront();
|
||||
@@ -1079,15 +1125,31 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
|
||||
private void EnsureSettingsLastSibling()
|
||||
{
|
||||
if (settingsInstance == null || putPrefabsHere == null)
|
||||
if (settingsInstance == null || GetSettingsParentTransform() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
settingsInstance.transform.SetParent(putPrefabsHere.transform, false);
|
||||
settingsInstance.transform.SetParent(GetSettingsParentTransform(), false);
|
||||
settingsInstance.transform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
private Transform GetSettingsParentTransform()
|
||||
{
|
||||
if (putSettingsPrefabHere != null)
|
||||
{
|
||||
return putSettingsPrefabHere.transform;
|
||||
}
|
||||
|
||||
if (putPrefabsHere == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Transform siblingParent = putPrefabsHere.transform.parent;
|
||||
return siblingParent != null ? siblingParent : putPrefabsHere.transform;
|
||||
}
|
||||
|
||||
private void CloseInfoPanels(GameObject keep)
|
||||
{
|
||||
CloseInstance(ref userInfoInstance, keep);
|
||||
@@ -1147,7 +1209,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
{
|
||||
if (activeInstance != null)
|
||||
{
|
||||
activeInstance.BroadcastOverlayPanelsVisibility();
|
||||
activeInstance.SyncManagedPanelVisibilityState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1387,6 +1449,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
{
|
||||
activeInstance.topNavigationGeometryDirty = true;
|
||||
activeInstance.EnsureTopNavigationFront();
|
||||
activeInstance.SyncManagedPanelVisibilityState(true);
|
||||
activeInstance.ScheduleDeferredUiRefresh();
|
||||
}
|
||||
}
|
||||
@@ -1587,3 +1650,31 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class btmandtopPanelVisibilityRelay : MonoBehaviour
|
||||
{
|
||||
private btmandtopController owner;
|
||||
private bool includesSettingsPanel;
|
||||
|
||||
public void Initialize(btmandtopController controller, bool isSettingsPanel)
|
||||
{
|
||||
owner = controller;
|
||||
includesSettingsPanel = isSettingsPanel;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (owner != null)
|
||||
{
|
||||
owner.NotifyManagedPanelVisibilityChanged(includesSettingsPanel);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (owner != null)
|
||||
{
|
||||
owner.NotifyManagedPanelVisibilityChanged(includesSettingsPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class Explosion3DTestController : MonoBehaviour
|
||||
{
|
||||
[Header("Trigger")]
|
||||
[SerializeField] private bool enableDebugKey = true;
|
||||
[SerializeField] private KeyCode triggerKey = KeyCode.Alpha0;
|
||||
|
||||
[Header("Target")]
|
||||
[SerializeField] private GameObject intactObject;
|
||||
[SerializeField] private GameObject fracturedPrefab;
|
||||
[SerializeField] private Transform spawnRoot;
|
||||
[SerializeField] private bool disableIntactObjectOnExplode = true;
|
||||
[SerializeField] private bool destroyPreviousFractureInstance = true;
|
||||
|
||||
[Header("Explosion Force")]
|
||||
[SerializeField] private float explosionForce = 2.5f;
|
||||
[SerializeField] private float explosionRadius = 1.25f;
|
||||
[SerializeField] private float upwardModifier = 0.15f;
|
||||
[SerializeField] private float randomForceMultiplier = 0.2f;
|
||||
[SerializeField] private float randomTorque = 8f;
|
||||
|
||||
[Header("Containment")]
|
||||
[SerializeField] private bool keepFragmentsClose = true;
|
||||
[SerializeField] private float maxFragmentSpeed = 1.2f;
|
||||
[SerializeField] private float fragmentDrag = 4f;
|
||||
[SerializeField] private float fragmentAngularDrag = 6f;
|
||||
[SerializeField] private bool disableFragmentGravity = false;
|
||||
|
||||
[Header("Lifecycle")]
|
||||
[SerializeField] private bool autoCleanupFragments = false;
|
||||
[SerializeField] private float cleanupDelay = 5f;
|
||||
|
||||
private GameObject spawnedFractureRoot;
|
||||
private bool exploded;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!enableDebugKey)
|
||||
return;
|
||||
|
||||
if (Input.GetKeyDown(triggerKey))
|
||||
{
|
||||
TriggerExplosion();
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Trigger Explosion")]
|
||||
public void TriggerExplosion()
|
||||
{
|
||||
if (exploded)
|
||||
return;
|
||||
|
||||
if (fracturedPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[Explosion3DTest] Missing fracturedPrefab.");
|
||||
return;
|
||||
}
|
||||
|
||||
Transform sourceTransform = intactObject != null ? intactObject.transform : transform;
|
||||
Transform parent = spawnRoot != null ? spawnRoot : sourceTransform.parent;
|
||||
|
||||
if (destroyPreviousFractureInstance && spawnedFractureRoot != null)
|
||||
{
|
||||
Destroy(spawnedFractureRoot);
|
||||
spawnedFractureRoot = null;
|
||||
}
|
||||
|
||||
spawnedFractureRoot = Instantiate(
|
||||
fracturedPrefab,
|
||||
sourceTransform.position,
|
||||
sourceTransform.rotation,
|
||||
parent);
|
||||
spawnedFractureRoot.name = fracturedPrefab.name + "_Exploded";
|
||||
|
||||
if (disableIntactObjectOnExplode && intactObject != null)
|
||||
{
|
||||
intactObject.SetActive(false);
|
||||
}
|
||||
|
||||
ApplyExplosionToFragments(spawnedFractureRoot, sourceTransform.position);
|
||||
|
||||
exploded = true;
|
||||
|
||||
if (autoCleanupFragments && cleanupDelay > 0f)
|
||||
{
|
||||
StartCoroutine(CleanupAfterDelay(cleanupDelay));
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Reset Explosion")]
|
||||
public void ResetExplosion()
|
||||
{
|
||||
exploded = false;
|
||||
|
||||
if (spawnedFractureRoot != null)
|
||||
{
|
||||
Destroy(spawnedFractureRoot);
|
||||
spawnedFractureRoot = null;
|
||||
}
|
||||
|
||||
if (intactObject != null)
|
||||
{
|
||||
intactObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyExplosionToFragments(GameObject fractureRoot, Vector3 explosionCenter)
|
||||
{
|
||||
if (fractureRoot == null)
|
||||
return;
|
||||
|
||||
Rigidbody[] rigidbodies = fractureRoot.GetComponentsInChildren<Rigidbody>(true);
|
||||
for (int i = 0; i < rigidbodies.Length; i++)
|
||||
{
|
||||
Rigidbody rb = rigidbodies[i];
|
||||
if (rb == null)
|
||||
continue;
|
||||
|
||||
rb.isKinematic = false;
|
||||
rb.useGravity = !disableFragmentGravity;
|
||||
rb.linearDamping = fragmentDrag;
|
||||
rb.angularDamping = fragmentAngularDrag;
|
||||
|
||||
Vector3 fragmentCenter = rb.worldCenterOfMass;
|
||||
Vector3 direction = fragmentCenter - explosionCenter;
|
||||
if (direction.sqrMagnitude < 0.0001f)
|
||||
{
|
||||
direction = Random.onUnitSphere;
|
||||
}
|
||||
|
||||
float randomScale = 1f + Random.Range(-randomForceMultiplier, randomForceMultiplier);
|
||||
Vector3 force = direction.normalized * Mathf.Max(0f, explosionForce * randomScale);
|
||||
force += Vector3.up * upwardModifier;
|
||||
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
rb.AddForce(force, ForceMode.Impulse);
|
||||
|
||||
if (randomTorque > 0f)
|
||||
{
|
||||
Vector3 torqueAxis = Random.onUnitSphere * randomTorque;
|
||||
rb.AddTorque(torqueAxis, ForceMode.Impulse);
|
||||
}
|
||||
|
||||
if (keepFragmentsClose)
|
||||
{
|
||||
LimitFragmentVelocity(rb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LimitFragmentVelocity(Rigidbody rb)
|
||||
{
|
||||
if (rb == null)
|
||||
return;
|
||||
|
||||
float maxSpeed = Mathf.Max(0.01f, maxFragmentSpeed);
|
||||
if (rb.linearVelocity.sqrMagnitude > maxSpeed * maxSpeed)
|
||||
{
|
||||
rb.linearVelocity = rb.linearVelocity.normalized * maxSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator CleanupAfterDelay(float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
if (spawnedFractureRoot != null)
|
||||
{
|
||||
Destroy(spawnedFractureRoot);
|
||||
spawnedFractureRoot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4049e381a6fd1314abb021f18344b744
|
||||
@@ -0,0 +1,269 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class RuntimeVoxelExplosion3DTest : MonoBehaviour
|
||||
{
|
||||
[Header("Trigger")]
|
||||
[SerializeField] private bool enableDebugKey = true;
|
||||
[SerializeField] private KeyCode triggerKey = KeyCode.Alpha0;
|
||||
[SerializeField] private bool supportKeypad0 = true;
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
|
||||
[Header("Target")]
|
||||
[SerializeField] private GameObject targetObject;
|
||||
[SerializeField] private Transform chunkParent;
|
||||
[SerializeField] private bool disableTargetOnExplode = true;
|
||||
[SerializeField] private bool destroyPreviousChunks = true;
|
||||
|
||||
[Header("Voxel Cut")]
|
||||
[SerializeField] private int chunksX = 4;
|
||||
[SerializeField] private int chunksY = 4;
|
||||
[SerializeField] private int chunksZ = 4;
|
||||
[SerializeField] private float chunkScaleMultiplier = 0.92f;
|
||||
[SerializeField] private float occupancyPadding = 0.02f;
|
||||
[SerializeField] private bool requireColliderOverlap = true;
|
||||
[SerializeField] private int maxChunkCount = 128;
|
||||
|
||||
[Header("Explosion")]
|
||||
[SerializeField] private float explosionForce = 1.2f;
|
||||
[SerializeField] private float upwardForce = 0.08f;
|
||||
[SerializeField] private float randomForceJitter = 0.12f;
|
||||
[SerializeField] private float randomTorque = 5f;
|
||||
[SerializeField] private float maxChunkSpeed = 0.9f;
|
||||
[SerializeField] private float linearDamping = 5f;
|
||||
[SerializeField] private float angularDamping = 7f;
|
||||
[SerializeField] private bool disableGravity = false;
|
||||
|
||||
[Header("Cleanup")]
|
||||
[SerializeField] private bool autoDestroyChunks = false;
|
||||
[SerializeField] private float destroyDelay = 5f;
|
||||
|
||||
private readonly List<GameObject> spawnedChunks = new List<GameObject>();
|
||||
private bool exploded;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!enableDebugKey)
|
||||
return;
|
||||
|
||||
bool pressed = Input.GetKeyDown(triggerKey);
|
||||
if (!pressed && supportKeypad0)
|
||||
{
|
||||
pressed = Input.GetKeyDown(KeyCode.Keypad0);
|
||||
}
|
||||
|
||||
if (pressed)
|
||||
{
|
||||
if (debugLogs)
|
||||
{
|
||||
Debug.Log("[RuntimeVoxelExplosion3DTest] Explosion key pressed.");
|
||||
}
|
||||
TriggerExplosion();
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Trigger Runtime Voxel Explosion")]
|
||||
public void TriggerExplosion()
|
||||
{
|
||||
if (exploded)
|
||||
{
|
||||
if (debugLogs)
|
||||
{
|
||||
Debug.Log("[RuntimeVoxelExplosion3DTest] Already exploded, ignoring trigger.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject target = targetObject != null ? targetObject : gameObject;
|
||||
if (targetObject == null && debugLogs)
|
||||
{
|
||||
Debug.Log("[RuntimeVoxelExplosion3DTest] targetObject not assigned, using current GameObject.");
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
Debug.LogWarning("[RuntimeVoxelExplosion3DTest] Missing targetObject.");
|
||||
return;
|
||||
}
|
||||
|
||||
Renderer[] renderers = target.GetComponentsInChildren<Renderer>(true);
|
||||
if (renderers == null || renderers.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[RuntimeVoxelExplosion3DTest] Target has no renderer.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (destroyPreviousChunks)
|
||||
{
|
||||
ClearSpawnedChunks();
|
||||
}
|
||||
|
||||
Bounds bounds = CalculateCombinedBounds(renderers);
|
||||
Collider[] colliders = target.GetComponentsInChildren<Collider>(true);
|
||||
Material chunkMaterial = ResolveChunkMaterial(renderers);
|
||||
|
||||
int safeX = Mathf.Max(1, chunksX);
|
||||
int safeY = Mathf.Max(1, chunksY);
|
||||
int safeZ = Mathf.Max(1, chunksZ);
|
||||
Vector3 cellSize = new Vector3(
|
||||
bounds.size.x / safeX,
|
||||
bounds.size.y / safeY,
|
||||
bounds.size.z / safeZ);
|
||||
|
||||
int spawnedCount = 0;
|
||||
for (int x = 0; x < safeX; x++)
|
||||
{
|
||||
for (int y = 0; y < safeY; y++)
|
||||
{
|
||||
for (int z = 0; z < safeZ; z++)
|
||||
{
|
||||
if (spawnedCount >= Mathf.Max(1, maxChunkCount))
|
||||
break;
|
||||
|
||||
Vector3 center = new Vector3(
|
||||
bounds.min.x + cellSize.x * (x + 0.5f),
|
||||
bounds.min.y + cellSize.y * (y + 0.5f),
|
||||
bounds.min.z + cellSize.z * (z + 0.5f));
|
||||
|
||||
if (requireColliderOverlap && colliders.Length > 0 && !CellOverlapsTarget(center, cellSize, colliders))
|
||||
continue;
|
||||
|
||||
GameObject chunk = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
chunk.name = $"RuntimeChunk_{x}_{y}_{z}";
|
||||
chunk.transform.SetParent(chunkParent != null ? chunkParent : null, true);
|
||||
chunk.transform.position = center;
|
||||
chunk.transform.rotation = target.transform.rotation;
|
||||
chunk.transform.localScale = Vector3.Scale(cellSize, Vector3.one * Mathf.Clamp(chunkScaleMultiplier, 0.01f, 1f));
|
||||
|
||||
Renderer chunkRenderer = chunk.GetComponent<Renderer>();
|
||||
if (chunkRenderer != null && chunkMaterial != null)
|
||||
{
|
||||
chunkRenderer.sharedMaterial = chunkMaterial;
|
||||
}
|
||||
|
||||
Rigidbody rb = chunk.AddComponent<Rigidbody>();
|
||||
rb.mass = 0.08f;
|
||||
rb.linearDamping = linearDamping;
|
||||
rb.angularDamping = angularDamping;
|
||||
rb.useGravity = !disableGravity;
|
||||
|
||||
ApplyChunkImpulse(rb, bounds.center);
|
||||
|
||||
spawnedChunks.Add(chunk);
|
||||
spawnedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disableTargetOnExplode)
|
||||
{
|
||||
target.SetActive(false);
|
||||
}
|
||||
|
||||
if (debugLogs)
|
||||
{
|
||||
Debug.Log($"[RuntimeVoxelExplosion3DTest] Spawned {spawnedCount} runtime chunks.");
|
||||
}
|
||||
|
||||
exploded = true;
|
||||
|
||||
if (autoDestroyChunks && destroyDelay > 0f)
|
||||
{
|
||||
Invoke(nameof(ClearSpawnedChunks), destroyDelay);
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Reset Runtime Voxel Explosion")]
|
||||
public void ResetExplosion()
|
||||
{
|
||||
CancelInvoke(nameof(ClearSpawnedChunks));
|
||||
ClearSpawnedChunks();
|
||||
exploded = false;
|
||||
|
||||
if (targetObject != null)
|
||||
{
|
||||
targetObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private Bounds CalculateCombinedBounds(Renderer[] renderers)
|
||||
{
|
||||
Bounds bounds = renderers[0].bounds;
|
||||
for (int i = 1; i < renderers.Length; i++)
|
||||
{
|
||||
bounds.Encapsulate(renderers[i].bounds);
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
private Material ResolveChunkMaterial(Renderer[] renderers)
|
||||
{
|
||||
for (int i = 0; i < renderers.Length; i++)
|
||||
{
|
||||
if (renderers[i] != null && renderers[i].sharedMaterial != null)
|
||||
return renderers[i].sharedMaterial;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool CellOverlapsTarget(Vector3 center, Vector3 cellSize, Collider[] colliders)
|
||||
{
|
||||
Bounds cellBounds = new Bounds(center, cellSize + Vector3.one * occupancyPadding);
|
||||
for (int i = 0; i < colliders.Length; i++)
|
||||
{
|
||||
Collider col = colliders[i];
|
||||
if (col == null || !col.enabled)
|
||||
continue;
|
||||
|
||||
if (col.bounds.Intersects(cellBounds))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyChunkImpulse(Rigidbody rb, Vector3 explosionCenter)
|
||||
{
|
||||
if (rb == null)
|
||||
return;
|
||||
|
||||
Vector3 direction = rb.worldCenterOfMass - explosionCenter;
|
||||
if (direction.sqrMagnitude < 0.0001f)
|
||||
{
|
||||
direction = Random.onUnitSphere;
|
||||
}
|
||||
|
||||
float jitter = 1f + Random.Range(-randomForceJitter, randomForceJitter);
|
||||
Vector3 impulse = direction.normalized * Mathf.Max(0f, explosionForce * jitter);
|
||||
impulse += Vector3.up * upwardForce;
|
||||
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
rb.AddForce(impulse, ForceMode.Impulse);
|
||||
|
||||
if (randomTorque > 0f)
|
||||
{
|
||||
rb.AddTorque(Random.onUnitSphere * randomTorque, ForceMode.Impulse);
|
||||
}
|
||||
|
||||
if (rb.linearVelocity.sqrMagnitude > maxChunkSpeed * maxChunkSpeed)
|
||||
{
|
||||
rb.linearVelocity = rb.linearVelocity.normalized * maxChunkSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSpawnedChunks()
|
||||
{
|
||||
for (int i = 0; i < spawnedChunks.Count; i++)
|
||||
{
|
||||
if (spawnedChunks[i] != null)
|
||||
{
|
||||
Destroy(spawnedChunks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
spawnedChunks.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 619dd149d97efea45b1068f55dd7ed99
|
||||
@@ -1,4 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
// NOTE: Windows-only. Launches an external Flutter .exe via Process.Start and
|
||||
// controls its native window. Wrapped in UNITY_STANDALONE_WIN so the Windows
|
||||
// build/logic is unchanged while Android compiles this class out.
|
||||
#if UNITY_STANDALONE_WIN
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
@@ -713,3 +717,4 @@ public class WebViewLauncher : MonoBehaviour
|
||||
WebViewReady?.Invoke();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using UnityEngine;
|
||||
// NOTE: Windows-only. Depends on WebViewWin32 (user32.dll) and WebViewLauncher,
|
||||
// which are Windows-only. Wrapped in UNITY_STANDALONE_WIN so the Windows
|
||||
// build/logic is unchanged while Android compiles this class out.
|
||||
#if UNITY_STANDALONE_WIN
|
||||
using UnityEngine;
|
||||
using Bansonic;
|
||||
|
||||
public class WebViewPanelFollower : MonoBehaviour
|
||||
@@ -154,3 +158,4 @@ public class WebViewPanelFollower : MonoBehaviour
|
||||
return a.Left == b.Left && a.Top == b.Top && a.Right == b.Right && a.Bottom == b.Bottom;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System;
|
||||
// NOTE: Windows-only. Uses user32.dll P/Invoke, which does not compile on Android.
|
||||
// Wrapped in UNITY_STANDALONE_WIN so the Windows build/logic is unchanged while
|
||||
// Android simply compiles this class out.
|
||||
#if UNITY_STANDALONE_WIN
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
@@ -268,3 +272,4 @@ public static class WebViewWin32
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// NOTE: Windows-only. Depends on WebViewLauncher (external Flutter .exe launcher),
|
||||
// which is Windows-only. Wrapped in UNITY_STANDALONE_WIN so the Windows
|
||||
// build/logic is unchanged while Android compiles this class out.
|
||||
#if UNITY_STANDALONE_WIN
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
@@ -193,3 +197,4 @@ public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// NOTE: Windows-only. Depends on WebViewLauncher (external Flutter .exe / native
|
||||
// window control), which is Windows-only. Wrapped in UNITY_STANDALONE_WIN so the
|
||||
// Windows build/logic is unchanged while Android compiles this class out.
|
||||
#if UNITY_STANDALONE_WIN
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.IO;
|
||||
@@ -287,3 +291,4 @@ public class loadLittleGamesPrefab : MonoBehaviour
|
||||
return raw.Replace("-", "\n").Replace("/", "\n").Replace("_", "\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -12,26 +12,50 @@ public static class AllyHeroDeployLedgerStorage
|
||||
private const string BackupFileName = ".ahd.bak";
|
||||
private const string TempFileName = ".ahd.tmp";
|
||||
|
||||
private static string VaultDirectoryPath => Path.Combine(Application.persistentDataPath, VaultDirectoryName);
|
||||
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName);
|
||||
private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName);
|
||||
private static string BackupFilePath => Path.Combine(VaultDirectoryPath, BackupFileName);
|
||||
private static string TempFilePath => Path.Combine(VaultDirectoryPath, TempFileName);
|
||||
|
||||
public static bool HasExistingSaveFile()
|
||||
{
|
||||
return File.Exists(MainFilePath) || File.Exists(BackupFilePath);
|
||||
var mainCandidates = SaveIdentityUtility.GetVaultFilePathVariants(MainFileName);
|
||||
for (int i = 0; i < mainCandidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(mainCandidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var backupCandidates = SaveIdentityUtility.GetVaultFilePathVariants(BackupFileName);
|
||||
for (int i = 0; i < backupCandidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(backupCandidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryLoad(out AllyHeroDeployLedgerPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
|
||||
if (TryReadPayload(MainFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(MainFileName, out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryReadPayload(BackupFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(BackupFileName, out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PlayerProgressBackupService.TryRestoreAllyHeroDeploy(out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
@@ -60,6 +84,7 @@ public static class AllyHeroDeployLedgerStorage
|
||||
File.Copy(TempFilePath, MainFilePath, true);
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveAllyHeroDeploy(payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -96,17 +121,32 @@ public static class AllyHeroDeployLedgerStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
string expectedSignature = ComputeSignature(envelope.payload);
|
||||
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
|
||||
if (!TryValidateSignature(envelope.payload, envelope.signature))
|
||||
{
|
||||
Debug.LogWarning("[AllyHeroDeployLedgerStorage] Save signature mismatch. Possible tampering detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
|
||||
string payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
AllyHeroDeployLedgerPayload loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
|
||||
AllyHeroDeployLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
string payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedPayload == null)
|
||||
{
|
||||
return false;
|
||||
@@ -122,6 +162,21 @@ public static class AllyHeroDeployLedgerStorage
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadPayloadFromVariants(string fileName, out AllyHeroDeployLedgerPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (TryReadPayload(candidates[i], out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(AllyHeroDeployLedgerPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
@@ -141,7 +196,7 @@ public static class AllyHeroDeployLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
string signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(signText);
|
||||
@@ -151,14 +206,45 @@ public static class AllyHeroDeployLedgerStorage
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
{
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
string seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(signText);
|
||||
byte[] hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] XorTransform(byte[] source, byte[] key)
|
||||
{
|
||||
byte[] result = new byte[source.Length];
|
||||
|
||||
@@ -43,9 +43,13 @@ public static class DlcManifestService
|
||||
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName, ManifestFolderName));
|
||||
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName));
|
||||
|
||||
#if !UNITY_ANDROID
|
||||
// 项目外(可执行文件同级目录)的 DLC 扫描仅在 PC 端有意义;
|
||||
// 安卓无此目录概念,Application.dataPath 指向 APK,跳过以避免无效/异常路径。
|
||||
string playerRoot = GetPlayerRootDirectory();
|
||||
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName, ManifestFolderName));
|
||||
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName));
|
||||
#endif
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
@@ -91,9 +91,13 @@ public static class DlcPackageArchiveService
|
||||
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName, PackageFolderName));
|
||||
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName));
|
||||
|
||||
#if !UNITY_ANDROID
|
||||
// 项目外(可执行文件同级目录)的 DLC 扫描仅在 PC 端有意义;
|
||||
// 安卓无此目录概念,Application.dataPath 指向 APK,跳过以避免无效/异常路径。
|
||||
string playerRoot = GetPlayerRootDirectory();
|
||||
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName, PackageFolderName));
|
||||
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName));
|
||||
#endif
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class DushMaterialLedgerStorage
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
|
||||
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
|
||||
}
|
||||
|
||||
private static string MainFilePath
|
||||
@@ -36,12 +36,18 @@ public static class DushMaterialLedgerStorage
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
|
||||
if (TryReadPayload(MainFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(MainFileName, out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryReadPayload(BackupFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(BackupFileName, out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PlayerProgressBackupService.TryRestoreDushMaterial(out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
@@ -70,6 +76,7 @@ public static class DushMaterialLedgerStorage
|
||||
File.Copy(TempFilePath, MainFilePath, true);
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveDushMaterial(payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -106,17 +113,32 @@ public static class DushMaterialLedgerStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedSignature = ComputeSignature(envelope.payload);
|
||||
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
|
||||
if (!TryValidateSignature(envelope.payload, envelope.signature))
|
||||
{
|
||||
Debug.LogWarning("[DushMaterialLedgerStorage] Save signature mismatch. Possible tampering detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
var loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
|
||||
DushMaterialLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedPayload == null)
|
||||
{
|
||||
return false;
|
||||
@@ -132,6 +154,21 @@ public static class DushMaterialLedgerStorage
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadPayloadFromVariants(string fileName, out DushMaterialLedgerPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (TryReadPayload(candidates[i], out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(DushMaterialLedgerPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
@@ -151,7 +188,7 @@ public static class DushMaterialLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
@@ -161,14 +198,45 @@ public static class DushMaterialLedgerStorage
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] XorTransform(byte[] source, byte[] key)
|
||||
{
|
||||
var result = new byte[source.Length];
|
||||
|
||||
@@ -12,7 +12,7 @@ public static class EquipmentConsumableLedgerStorage
|
||||
private const string BackupFileName = ".eqc.bak";
|
||||
private const string TempFileName = ".eqc.tmp";
|
||||
|
||||
private static string VaultDirectoryPath => Path.Combine(Application.persistentDataPath, VaultDirectoryName);
|
||||
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName);
|
||||
private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName);
|
||||
private static string BackupFilePath => Path.Combine(VaultDirectoryPath, BackupFileName);
|
||||
private static string TempFilePath => Path.Combine(VaultDirectoryPath, TempFileName);
|
||||
@@ -21,12 +21,18 @@ public static class EquipmentConsumableLedgerStorage
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
|
||||
if (TryReadPayload(MainFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(MainFileName, out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryReadPayload(BackupFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(BackupFileName, out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PlayerProgressBackupService.TryRestoreEquipmentConsumable(out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
@@ -55,6 +61,7 @@ public static class EquipmentConsumableLedgerStorage
|
||||
File.Copy(TempFilePath, MainFilePath, true);
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveEquipmentConsumable(payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -91,17 +98,32 @@ public static class EquipmentConsumableLedgerStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedSignature = ComputeSignature(envelope.payload);
|
||||
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
|
||||
if (!TryValidateSignature(envelope.payload, envelope.signature))
|
||||
{
|
||||
Debug.LogWarning("[EquipmentConsumableLedgerStorage] Save signature mismatch. Possible tampering detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
var loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
|
||||
EquipmentConsumableLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedPayload == null)
|
||||
{
|
||||
return false;
|
||||
@@ -117,6 +139,21 @@ public static class EquipmentConsumableLedgerStorage
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadPayloadFromVariants(string fileName, out EquipmentConsumableLedgerPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (TryReadPayload(candidates[i], out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(EquipmentConsumableLedgerPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
@@ -136,7 +173,7 @@ public static class EquipmentConsumableLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
@@ -146,14 +183,45 @@ public static class EquipmentConsumableLedgerStorage
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] XorTransform(byte[] source, byte[] key)
|
||||
{
|
||||
var result = new byte[source.Length];
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class ExpBottleLedgerStorage
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
|
||||
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
|
||||
}
|
||||
|
||||
private static string MainFilePath
|
||||
@@ -36,12 +36,18 @@ public static class ExpBottleLedgerStorage
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
|
||||
if (TryReadPayload(MainFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(MainFileName, out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryReadPayload(BackupFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(BackupFileName, out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PlayerProgressBackupService.TryRestoreExpBottle(out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
@@ -70,6 +76,7 @@ public static class ExpBottleLedgerStorage
|
||||
File.Copy(TempFilePath, MainFilePath, true);
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveExpBottle(payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -106,17 +113,32 @@ public static class ExpBottleLedgerStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedSignature = ComputeSignature(envelope.payload);
|
||||
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
|
||||
if (!TryValidateSignature(envelope.payload, envelope.signature))
|
||||
{
|
||||
Debug.LogWarning("[ExpBottleLedgerStorage] Save signature mismatch. Possible tampering detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
var loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
|
||||
ExpBottleLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedPayload == null)
|
||||
{
|
||||
return false;
|
||||
@@ -132,6 +154,21 @@ public static class ExpBottleLedgerStorage
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadPayloadFromVariants(string fileName, out ExpBottleLedgerPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (TryReadPayload(candidates[i], out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(ExpBottleLedgerPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
@@ -151,7 +188,7 @@ public static class ExpBottleLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
@@ -161,14 +198,45 @@ public static class ExpBottleLedgerStorage
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] XorTransform(byte[] source, byte[] key)
|
||||
{
|
||||
var result = new byte[source.Length];
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class PlayerEconomyStorage
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
|
||||
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
|
||||
}
|
||||
|
||||
private static string MainFilePath
|
||||
@@ -36,12 +36,18 @@ public static class PlayerEconomyStorage
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
|
||||
if (TryReadPayload(MainFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(MainFileName, out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryReadPayload(BackupFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(BackupFileName, out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PlayerProgressBackupService.TryRestoreEconomy(out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
@@ -70,6 +76,7 @@ public static class PlayerEconomyStorage
|
||||
File.Copy(TempFilePath, MainFilePath, true);
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveEconomy(payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -107,17 +114,32 @@ public static class PlayerEconomyStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedSignature = ComputeSignature(envelope.payload);
|
||||
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
|
||||
if (!TryValidateSignature(envelope.payload, envelope.signature))
|
||||
{
|
||||
Debug.LogWarning("[PlayerEconomyStorage] Save signature mismatch. Possible tampering detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
var loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
PlayerEconomyPayload loadedPayload = null;
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedPayload == null)
|
||||
{
|
||||
return false;
|
||||
@@ -133,6 +155,21 @@ public static class PlayerEconomyStorage
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadPayloadFromVariants(string fileName, out PlayerEconomyPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (TryReadPayload(candidates[i], out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(PlayerEconomyPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
@@ -152,7 +189,7 @@ public static class PlayerEconomyStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
@@ -162,14 +199,45 @@ public static class PlayerEconomyStorage
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] XorTransform(byte[] source, byte[] key)
|
||||
{
|
||||
var result = new byte[source.Length];
|
||||
|
||||
@@ -78,7 +78,17 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
loadedFromSave = SecureSaveVault.TryLoadJson("player_experience", "runtime", out payload, GetLegacySavePath());
|
||||
if (payload == null)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
int backupExperience;
|
||||
if (PlayerProgressBackupService.TryRestorePlayerExperience(out backupExperience))
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
payload.playerExp = Mathf.Max(0, backupExperience);
|
||||
loadedFromSave = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
@@ -166,6 +176,7 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
SecureSaveVault.SaveJson("player_experience", "runtime", payload, GetLegacySavePath());
|
||||
PlayerProgressBackupService.SavePlayerExperience(payload.playerExp);
|
||||
SyncToPlayerData();
|
||||
NotifyExperienceChanged();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class PlayerProgressBackupBundle
|
||||
{
|
||||
public int version = 1;
|
||||
public long savedUtcTicks;
|
||||
public PlayerEconomyPayload economy;
|
||||
public int playerExperience;
|
||||
public float bestOverallRks;
|
||||
public PlayerSkillSaveData playerSkill;
|
||||
public AllyHeroDeployLedgerPayload allyHeroDeploy;
|
||||
public ExpBottleLedgerPayload expBottle;
|
||||
public DushMaterialLedgerPayload dushMaterial;
|
||||
public EquipmentConsumableLedgerPayload equipmentConsumable;
|
||||
public StoreOwnershipPayload storeOwnership;
|
||||
}
|
||||
|
||||
public static class PlayerProgressBackupService
|
||||
{
|
||||
private const string BackupFileName = "player_progress.bbackup";
|
||||
private static PlayerProgressBackupBundle s_cachedBundle;
|
||||
private static bool s_cacheLoaded;
|
||||
private static bool s_isWriting;
|
||||
|
||||
public static bool TryRestoreEconomy(out PlayerEconomyPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.economy == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
payload = CloneEconomy(bundle.economy);
|
||||
return payload != null;
|
||||
}
|
||||
|
||||
public static void SaveEconomy(PlayerEconomyPayload payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBundle(bundle => bundle.economy = CloneEconomy(payload));
|
||||
}
|
||||
|
||||
public static bool TryRestorePlayerExperience(out int experience)
|
||||
{
|
||||
experience = 0;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bundle.playerExperience <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
experience = Mathf.Max(0, bundle.playerExperience);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SavePlayerExperience(int experience)
|
||||
{
|
||||
UpdateBundle(bundle => bundle.playerExperience = Mathf.Max(0, experience));
|
||||
}
|
||||
|
||||
public static bool TryRestoreRks(out float rks)
|
||||
{
|
||||
rks = 0f;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.bestOverallRks <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rks = Mathf.Max(0f, bundle.bestOverallRks);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SaveRks(float rks)
|
||||
{
|
||||
UpdateBundle(bundle => bundle.bestOverallRks = Mathf.Max(0f, rks));
|
||||
}
|
||||
|
||||
public static bool TryRestorePlayerSkill(out PlayerSkillSaveData saveData)
|
||||
{
|
||||
saveData = null;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.playerSkill == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
saveData = ClonePlayerSkill(bundle.playerSkill);
|
||||
return saveData != null;
|
||||
}
|
||||
|
||||
public static void SavePlayerSkill(PlayerSkillSaveData saveData)
|
||||
{
|
||||
if (saveData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBundle(bundle => bundle.playerSkill = ClonePlayerSkill(saveData));
|
||||
}
|
||||
|
||||
public static bool TryRestoreAllyHeroDeploy(out AllyHeroDeployLedgerPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.allyHeroDeploy == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
payload = CloneAllyHeroDeploy(bundle.allyHeroDeploy);
|
||||
return payload != null;
|
||||
}
|
||||
|
||||
public static void SaveAllyHeroDeploy(AllyHeroDeployLedgerPayload payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBundle(bundle => bundle.allyHeroDeploy = CloneAllyHeroDeploy(payload));
|
||||
}
|
||||
|
||||
public static bool TryRestoreExpBottle(out ExpBottleLedgerPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.expBottle == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
payload = CloneExpBottle(bundle.expBottle);
|
||||
return payload != null;
|
||||
}
|
||||
|
||||
public static void SaveExpBottle(ExpBottleLedgerPayload payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBundle(bundle => bundle.expBottle = CloneExpBottle(payload));
|
||||
}
|
||||
|
||||
public static bool TryRestoreDushMaterial(out DushMaterialLedgerPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.dushMaterial == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
payload = CloneDushMaterial(bundle.dushMaterial);
|
||||
return payload != null;
|
||||
}
|
||||
|
||||
public static void SaveDushMaterial(DushMaterialLedgerPayload payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBundle(bundle => bundle.dushMaterial = CloneDushMaterial(payload));
|
||||
}
|
||||
|
||||
public static bool TryRestoreEquipmentConsumable(out EquipmentConsumableLedgerPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.equipmentConsumable == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
payload = CloneEquipmentConsumable(bundle.equipmentConsumable);
|
||||
return payload != null;
|
||||
}
|
||||
|
||||
public static void SaveEquipmentConsumable(EquipmentConsumableLedgerPayload payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBundle(bundle => bundle.equipmentConsumable = CloneEquipmentConsumable(payload));
|
||||
}
|
||||
|
||||
public static bool TryRestoreStoreOwnership(out StoreOwnershipPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null || bundle.storeOwnership == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
payload = CloneStoreOwnership(bundle.storeOwnership);
|
||||
return payload != null;
|
||||
}
|
||||
|
||||
public static void SaveStoreOwnership(StoreOwnershipPayload payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBundle(bundle => bundle.storeOwnership = CloneStoreOwnership(payload));
|
||||
}
|
||||
|
||||
private static void UpdateBundle(Action<PlayerProgressBackupBundle> mutator)
|
||||
{
|
||||
if (mutator == null || s_isWriting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerProgressBackupBundle bundle;
|
||||
if (!TryLoadBundle(out bundle) || bundle == null)
|
||||
{
|
||||
bundle = CreateDefaultBundle();
|
||||
}
|
||||
|
||||
mutator(bundle);
|
||||
bundle.savedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
SaveBundle(bundle);
|
||||
}
|
||||
|
||||
private static bool TryLoadBundle(out PlayerProgressBackupBundle bundle)
|
||||
{
|
||||
if (s_cacheLoaded)
|
||||
{
|
||||
bundle = s_cachedBundle;
|
||||
return bundle != null;
|
||||
}
|
||||
|
||||
s_cacheLoaded = true;
|
||||
s_cachedBundle = null;
|
||||
|
||||
IReadOnlyList<string> candidates = SaveIdentityUtility.GetPersistentRootVariants();
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
string root = candidates[i];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string path = Path.Combine(root, BackupFileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerProgressBackupBundle loaded = JsonUtility.FromJson<PlayerProgressBackupBundle>(json);
|
||||
if (loaded == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
s_cachedBundle = loaded;
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[PlayerProgressBackup] Failed to read backup '" + path + "': " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
bundle = s_cachedBundle;
|
||||
return bundle != null;
|
||||
}
|
||||
|
||||
private static void SaveBundle(PlayerProgressBackupBundle bundle)
|
||||
{
|
||||
if (bundle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string root = SaveIdentityUtility.GetCanonicalPersistentRoot();
|
||||
string path = Path.Combine(root, BackupFileName);
|
||||
try
|
||||
{
|
||||
s_isWriting = true;
|
||||
Directory.CreateDirectory(root);
|
||||
string json = JsonUtility.ToJson(bundle, false);
|
||||
File.WriteAllText(path, json, Encoding.UTF8);
|
||||
s_cachedBundle = bundle;
|
||||
s_cacheLoaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[PlayerProgressBackup] Failed to save backup '" + path + "': " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
s_isWriting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static PlayerProgressBackupBundle CreateDefaultBundle()
|
||||
{
|
||||
return new PlayerProgressBackupBundle
|
||||
{
|
||||
version = 1,
|
||||
savedUtcTicks = DateTime.UtcNow.Ticks
|
||||
};
|
||||
}
|
||||
|
||||
private static PlayerEconomyPayload CloneEconomy(PlayerEconomyPayload source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PlayerEconomyPayload
|
||||
{
|
||||
version = source.version,
|
||||
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
|
||||
coins = source.coins,
|
||||
material = source.material
|
||||
};
|
||||
}
|
||||
|
||||
private static PlayerSkillSaveData ClonePlayerSkill(PlayerSkillSaveData source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PlayerSkillSaveData
|
||||
{
|
||||
selectedSkillIndex = source.selectedSkillIndex,
|
||||
postMatchRewardCounter = source.postMatchRewardCounter,
|
||||
skillSwitchCooldownRemainingMatches = source.skillSwitchCooldownRemainingMatches
|
||||
};
|
||||
}
|
||||
|
||||
private static AllyHeroDeployLedgerPayload CloneAllyHeroDeploy(AllyHeroDeployLedgerPayload source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var clone = new AllyHeroDeployLedgerPayload
|
||||
{
|
||||
version = source.version,
|
||||
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
|
||||
entries = new List<AllyHeroDeployEntry>()
|
||||
};
|
||||
|
||||
if (source.entries != null)
|
||||
{
|
||||
for (int i = 0; i < source.entries.Count; i++)
|
||||
{
|
||||
AllyHeroDeployEntry entry = source.entries[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
clone.entries.Add(new AllyHeroDeployEntry
|
||||
{
|
||||
heroId = entry.heroId,
|
||||
currentExp = entry.currentExp,
|
||||
unlockedTierIndex = entry.unlockedTierIndex,
|
||||
levelLock = entry.levelLock,
|
||||
autoBreakthroughEnabled = entry.autoBreakthroughEnabled,
|
||||
deployCount = entry.deployCount,
|
||||
finishCount = entry.finishCount,
|
||||
mvpCount = entry.mvpCount,
|
||||
joinDateUtcTicks = entry.joinDateUtcTicks
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static ExpBottleLedgerPayload CloneExpBottle(ExpBottleLedgerPayload source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var clone = new ExpBottleLedgerPayload
|
||||
{
|
||||
version = source.version,
|
||||
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
|
||||
entries = new List<ExpBottleEntry>()
|
||||
};
|
||||
|
||||
if (source.entries != null)
|
||||
{
|
||||
for (int i = 0; i < source.entries.Count; i++)
|
||||
{
|
||||
ExpBottleEntry entry = source.entries[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
clone.entries.Add(new ExpBottleEntry
|
||||
{
|
||||
key = entry.key,
|
||||
count = entry.count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static DushMaterialLedgerPayload CloneDushMaterial(DushMaterialLedgerPayload source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var clone = new DushMaterialLedgerPayload
|
||||
{
|
||||
version = source.version,
|
||||
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
|
||||
entries = new List<DushMaterialEntry>()
|
||||
};
|
||||
|
||||
if (source.entries != null)
|
||||
{
|
||||
for (int i = 0; i < source.entries.Count; i++)
|
||||
{
|
||||
DushMaterialEntry entry = source.entries[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
clone.entries.Add(new DushMaterialEntry
|
||||
{
|
||||
key = entry.key,
|
||||
count = entry.count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static EquipmentConsumableLedgerPayload CloneEquipmentConsumable(EquipmentConsumableLedgerPayload source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var clone = new EquipmentConsumableLedgerPayload
|
||||
{
|
||||
version = source.version,
|
||||
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
|
||||
entries = new List<EquipmentConsumableEntry>()
|
||||
};
|
||||
|
||||
if (source.entries != null)
|
||||
{
|
||||
for (int i = 0; i < source.entries.Count; i++)
|
||||
{
|
||||
EquipmentConsumableEntry entry = source.entries[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
clone.entries.Add(new EquipmentConsumableEntry
|
||||
{
|
||||
key = entry.key,
|
||||
count = entry.count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static StoreOwnershipPayload CloneStoreOwnership(StoreOwnershipPayload source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var clone = new StoreOwnershipPayload
|
||||
{
|
||||
version = source.version,
|
||||
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
|
||||
entries = new List<StoreOwnershipEntry>()
|
||||
};
|
||||
|
||||
if (source.entries != null)
|
||||
{
|
||||
for (int i = 0; i < source.entries.Count; i++)
|
||||
{
|
||||
StoreOwnershipEntry entry = source.entries[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
clone.entries.Add(new StoreOwnershipEntry
|
||||
{
|
||||
storeItemId = entry.storeItemId,
|
||||
owned = entry.owned,
|
||||
unlockedStorySonIds = entry.unlockedStorySonIds != null
|
||||
? new List<int>(entry.unlockedStorySonIds)
|
||||
: new List<int>()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c620f726faf81ae4aa12a68cd611cb81
|
||||
@@ -36,6 +36,10 @@ public static class PlayerRksService
|
||||
{
|
||||
bestOverallRks = Mathf.Max(0f, saveData.bestOverallRks);
|
||||
}
|
||||
else if (PlayerProgressBackupService.TryRestoreRks(out float backupRks))
|
||||
{
|
||||
bestOverallRks = Mathf.Max(0f, backupRks);
|
||||
}
|
||||
else if (player != null)
|
||||
{
|
||||
bestOverallRks = Mathf.Max(0f, player.URankingScore);
|
||||
@@ -66,6 +70,7 @@ public static class PlayerRksService
|
||||
{
|
||||
bestOverallRks = calculated;
|
||||
SecureSaveVault.SaveJson(SaveCategory, SaveKey, new PlayerRksSaveData { bestOverallRks = bestOverallRks });
|
||||
PlayerProgressBackupService.SaveRks(bestOverallRks);
|
||||
SyncPlayerSo(player);
|
||||
OnRksChanged?.Invoke(bestOverallRks);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,11 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
|
||||
public static void NotifySettlementCompleted(int idolScore)
|
||||
{
|
||||
if (GameConfig.autoPlayEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureInstance().HandleSettlementCompletedInternal(idolScore);
|
||||
}
|
||||
|
||||
@@ -185,7 +190,10 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
initialized = true;
|
||||
if (!SecureSaveVault.TryLoadJson(SaveCategory, SaveKey, out saveData) || saveData == null)
|
||||
{
|
||||
saveData = new PlayerSkillSaveData();
|
||||
if (!PlayerProgressBackupService.TryRestorePlayerSkill(out saveData) || saveData == null)
|
||||
{
|
||||
saveData = new PlayerSkillSaveData();
|
||||
}
|
||||
}
|
||||
|
||||
ResolveSkillAssetIfNeeded();
|
||||
@@ -692,5 +700,6 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
}
|
||||
|
||||
SecureSaveVault.SaveJson(SaveCategory, SaveKey, saveData);
|
||||
PlayerProgressBackupService.SavePlayerSkill(saveData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
public static class SaveIdentityUtility
|
||||
{
|
||||
public const string StableCompanyName = "FloatGamingStudio";
|
||||
public const string StableProductName = "Bansonic";
|
||||
public const string StableApplicationIdentifier = "com.FloatGamingStudio.Bansonic";
|
||||
|
||||
private static readonly string[] LegacyProductAliases =
|
||||
{
|
||||
"Bansonic",
|
||||
"ban_total",
|
||||
"EaseOut_Main_FreshNew"
|
||||
};
|
||||
|
||||
private static readonly string[] LegacyCompanyAliases =
|
||||
{
|
||||
"FloatGamingStudio",
|
||||
"DefaultCompany"
|
||||
};
|
||||
|
||||
private static readonly string[] LegacyIdentifierAliases =
|
||||
{
|
||||
"com.DefaultCompany.Bansonic",
|
||||
"com.FloatGamingStudio.ban_total",
|
||||
"com.DefaultCompany.ban_total",
|
||||
"com.FloatGamingStudio.EaseOut_Main_FreshNew",
|
||||
"com.DefaultCompany.EaseOut_Main_FreshNew"
|
||||
};
|
||||
|
||||
public static string GetPrimaryApplicationIdentifier()
|
||||
{
|
||||
return StableApplicationIdentifier;
|
||||
}
|
||||
|
||||
public static string GetCanonicalPersistentRoot()
|
||||
{
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
string localLow = Path.GetFullPath(Path.Combine(localAppData, "..", "LocalLow"));
|
||||
return Path.Combine(localLow, StableCompanyName, StableProductName);
|
||||
}
|
||||
|
||||
return Application.persistentDataPath;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> GetApplicationIdentifierVariants()
|
||||
{
|
||||
var result = new List<string>();
|
||||
AddDistinct(result, StableApplicationIdentifier);
|
||||
AddDistinct(result, Application.identifier);
|
||||
|
||||
string currentCompany = SafeTrim(Application.companyName);
|
||||
string currentProduct = SafeTrim(Application.productName);
|
||||
if (!string.IsNullOrEmpty(currentCompany) && !string.IsNullOrEmpty(currentProduct))
|
||||
{
|
||||
AddDistinct(result, $"com.{currentCompany}.{currentProduct}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < LegacyIdentifierAliases.Length; i++)
|
||||
{
|
||||
AddDistinct(result, LegacyIdentifierAliases[i]);
|
||||
}
|
||||
|
||||
for (int companyIndex = 0; companyIndex < LegacyCompanyAliases.Length; companyIndex++)
|
||||
{
|
||||
for (int productIndex = 0; productIndex < LegacyProductAliases.Length; productIndex++)
|
||||
{
|
||||
AddDistinct(result, $"com.{LegacyCompanyAliases[companyIndex]}.{LegacyProductAliases[productIndex]}");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> GetPersistentRootVariants(string extraLegacyPath = null)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
AddDistinct(result, GetCanonicalPersistentRoot());
|
||||
AddDistinct(result, Application.persistentDataPath);
|
||||
|
||||
string extraDirectory = string.IsNullOrEmpty(extraLegacyPath) ? null : Path.GetDirectoryName(extraLegacyPath);
|
||||
AddDistinct(result, extraDirectory);
|
||||
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
string localLow = Path.GetFullPath(Path.Combine(localAppData, "..", "LocalLow"));
|
||||
|
||||
string currentCompany = SafeTrim(Application.companyName);
|
||||
string currentProduct = SafeTrim(Application.productName);
|
||||
if (!string.IsNullOrEmpty(currentCompany) && !string.IsNullOrEmpty(currentProduct))
|
||||
{
|
||||
AddDistinct(result, Path.Combine(localLow, currentCompany, currentProduct));
|
||||
}
|
||||
|
||||
for (int companyIndex = 0; companyIndex < LegacyCompanyAliases.Length; companyIndex++)
|
||||
{
|
||||
for (int productIndex = 0; productIndex < LegacyProductAliases.Length; productIndex++)
|
||||
{
|
||||
AddDistinct(result, Path.Combine(localLow, LegacyCompanyAliases[companyIndex], LegacyProductAliases[productIndex]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> GetVaultFilePathVariants(string fileName)
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> roots = GetPersistentRootVariants();
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
string root = roots[i];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddDistinct(result, Path.Combine(root, ".cache_bridge", fileName));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddDistinct(List<string> target, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < target.Count; i++)
|
||||
{
|
||||
if (string.Equals(target[i], value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
target.Add(value);
|
||||
}
|
||||
|
||||
private static string SafeTrim(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acd513b6738225b46ab31a779eb308fa
|
||||
@@ -27,7 +27,7 @@ public static class SecureSaveVault
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
|
||||
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
|
||||
}
|
||||
|
||||
public static bool SaveJson<T>(string category, string key, T data, string legacyPlainPath = null)
|
||||
@@ -121,20 +121,11 @@ public static class SecureSaveVault
|
||||
return false;
|
||||
}
|
||||
|
||||
string mainPath = GetFilePath(category, key, ".dat");
|
||||
string backupPath = GetFilePath(category, key, ".bak");
|
||||
|
||||
if (TryReadEncryptedFile(category, key, mainPath, out json))
|
||||
if (TryLoadFromCurrentOrLegacyEncryptedFiles(category, key, legacyPlainPath, out json))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryReadEncryptedFile(category, key, backupPath, out json))
|
||||
{
|
||||
SaveRawJson(category, key, json, legacyPlainPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(legacyPlainPath) && File.Exists(legacyPlainPath))
|
||||
{
|
||||
try
|
||||
@@ -262,8 +253,7 @@ public static class SecureSaveVault
|
||||
return false;
|
||||
}
|
||||
|
||||
string expectedSignature = ComputeSignature(category, envelope.payload);
|
||||
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
|
||||
if (!TryValidateSignature(category, envelope.payload, envelope.signature))
|
||||
{
|
||||
Debug.LogWarning($"[SecureSaveVault] Signature mismatch ({category}/{key}). Possible tampering detected.");
|
||||
return false;
|
||||
@@ -329,7 +319,7 @@ public static class SecureSaveVault
|
||||
|
||||
private static string ComputeSignature(string category, string payloadBase64)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + category + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
string signText = payloadBase64 + "|" + category + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
|
||||
@@ -350,7 +340,7 @@ public static class SecureSaveVault
|
||||
{
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key);
|
||||
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
aes.GenerateIV();
|
||||
using (var encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
@@ -374,28 +364,44 @@ public static class SecureSaveVault
|
||||
return plainBytes != null;
|
||||
}
|
||||
#endif
|
||||
using (var aes = Aes.Create())
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key);
|
||||
int ivLength = aes.BlockSize / 8;
|
||||
if (protectedBytes == null || protectedBytes.Length <= ivLength)
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key, identifierVariants[i]);
|
||||
int ivLength = aes.BlockSize / 8;
|
||||
if (protectedBytes == null || protectedBytes.Length <= ivLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] iv = new byte[ivLength];
|
||||
byte[] cipher = new byte[protectedBytes.Length - ivLength];
|
||||
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
|
||||
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
|
||||
aes.IV = iv;
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
{
|
||||
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
return plainBytes != null;
|
||||
byte[] iv = new byte[ivLength];
|
||||
byte[] cipher = new byte[protectedBytes.Length - ivLength];
|
||||
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
|
||||
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
|
||||
aes.IV = iv;
|
||||
try
|
||||
{
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
{
|
||||
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
if (plainBytes != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plainBytes = null;
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -416,7 +422,7 @@ public static class SecureSaveVault
|
||||
|
||||
try
|
||||
{
|
||||
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category), s_dpapiCurrentUserScope }) as byte[];
|
||||
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier()), s_dpapiCurrentUserScope }) as byte[];
|
||||
return protectedBytes != null && protectedBytes.Length > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -435,16 +441,24 @@ public static class SecureSaveVault
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category), s_dpapiCurrentUserScope }) as byte[];
|
||||
return plainBytes != null && plainBytes.Length > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
plainBytes = null;
|
||||
return false;
|
||||
try
|
||||
{
|
||||
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i]), s_dpapiCurrentUserScope }) as byte[];
|
||||
if (plainBytes != null && plainBytes.Length > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
plainBytes = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool EnsureDpapi()
|
||||
@@ -492,29 +506,116 @@ public static class SecureSaveVault
|
||||
}
|
||||
#endif
|
||||
|
||||
private static byte[] BuildEntropy(string category)
|
||||
private static byte[] BuildEntropy(string category, string applicationIdentifier)
|
||||
{
|
||||
string seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildAesKey(string category, string key)
|
||||
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier)
|
||||
{
|
||||
return BuildEntropy(category);
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category + "|" + key;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static string ShortHash(string value)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value + "|" + Application.identifier + "|" + SecretSeed));
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed));
|
||||
return BitConverter.ToString(hash, 0, 12).Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ShortHash(string value, string applicationIdentifier)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value + "|" + applicationIdentifier + "|" + SecretSeed));
|
||||
return BitConverter.ToString(hash, 0, 12).Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryLoadFromCurrentOrLegacyEncryptedFiles(string category, string key, string legacyPlainPath, out string json)
|
||||
{
|
||||
json = null;
|
||||
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants(legacyPlainPath);
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
string root = roots[i];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int variantIndex = 0; variantIndex < identifierVariants.Count; variantIndex++)
|
||||
{
|
||||
string applicationIdentifier = identifierVariants[variantIndex];
|
||||
|
||||
string mainPath = GetFilePathForRoot(root, category, key, ".dat", applicationIdentifier);
|
||||
if (TryReadEncryptedFile(category, key, mainPath, out json))
|
||||
{
|
||||
SaveRawJson(category, key, json, legacyPlainPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
string backupPath = GetFilePathForRoot(root, category, key, ".bak", applicationIdentifier);
|
||||
if (TryReadEncryptedFile(category, key, backupPath, out json))
|
||||
{
|
||||
SaveRawJson(category, key, json, legacyPlainPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryValidateSignature(string category, string payloadBase64, string signature)
|
||||
{
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCategoryDirectoryForRoot(string rootPath, string category, string applicationIdentifier)
|
||||
{
|
||||
string safeCategory = ShortHash("cat|" + category, applicationIdentifier);
|
||||
return Path.Combine(rootPath, VaultDirectoryName, "." + safeCategory);
|
||||
}
|
||||
|
||||
private static string GetFilePathForRoot(string rootPath, string category, string key, string extension, string applicationIdentifier)
|
||||
{
|
||||
string categoryDirectory = GetCategoryDirectoryForRoot(rootPath, category, applicationIdentifier);
|
||||
string safeKey = ShortHash("key|" + key, applicationIdentifier);
|
||||
return Path.Combine(categoryDirectory, "." + safeKey + extension);
|
||||
}
|
||||
|
||||
private static void DeleteLegacyPlainFile(string legacyPlainPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(legacyPlainPath) || !File.Exists(legacyPlainPath))
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class StoreOwnershipStorage
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
|
||||
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
|
||||
}
|
||||
|
||||
private static string MainFilePath
|
||||
@@ -36,12 +36,18 @@ public static class StoreOwnershipStorage
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
|
||||
if (TryReadPayload(MainFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(MainFileName, out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryReadPayload(BackupFilePath, out payload))
|
||||
if (TryReadPayloadFromVariants(BackupFileName, out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PlayerProgressBackupService.TryRestoreStoreOwnership(out payload))
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
@@ -70,6 +76,7 @@ public static class StoreOwnershipStorage
|
||||
File.Copy(TempFilePath, MainFilePath, true);
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveStoreOwnership(payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -106,17 +113,32 @@ public static class StoreOwnershipStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedSignature = ComputeSignature(envelope.payload);
|
||||
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
|
||||
if (!TryValidateSignature(envelope.payload, envelope.signature))
|
||||
{
|
||||
Debug.LogWarning("[StoreOwnershipStorage] Save signature mismatch. Possible tampering detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
var loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
|
||||
StoreOwnershipPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedPayload == null)
|
||||
{
|
||||
return false;
|
||||
@@ -132,6 +154,21 @@ public static class StoreOwnershipStorage
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadPayloadFromVariants(string fileName, out StoreOwnershipPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (TryReadPayload(candidates[i], out payload))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(StoreOwnershipPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
@@ -151,7 +188,7 @@ public static class StoreOwnershipStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
@@ -161,14 +198,45 @@ public static class StoreOwnershipStorage
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] XorTransform(byte[] source, byte[] key)
|
||||
{
|
||||
var result = new byte[source.Length];
|
||||
|
||||
@@ -994,8 +994,8 @@ MonoBehaviour:
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4283845183
|
||||
m_fontColor: {r: 0.24705882, g: 0.2901961, b: 0.3372549, a: 1}
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8645c982f08afaa4f855b29f838448ed
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,123 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime driver for materials made from the Rainbow-Cats dissolve / HDR-outline shaders.
|
||||
/// Attach to any Renderer and it will animate the shader's exposed properties so you get the
|
||||
/// dissolve fade or the pulsing HDR outline without touching material properties by hand.
|
||||
///
|
||||
/// Works on an instanced copy of the material via MaterialPropertyBlock, so multiple objects
|
||||
/// sharing one material can dissolve independently.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Renderer))]
|
||||
public class DissolveEffectController : MonoBehaviour
|
||||
{
|
||||
public enum Effect
|
||||
{
|
||||
/// <summary>Drives "_Fade": 0 = fully dissolved/hidden, 1 = fully visible (Dissolve shader).</summary>
|
||||
Dissolve,
|
||||
/// <summary>Drives "_Emission": pulses the HDR outline glow (HDR Outline shader).</summary>
|
||||
OutlinePulse
|
||||
}
|
||||
|
||||
[Header("Which effect")]
|
||||
public Effect effect = Effect.Dissolve;
|
||||
|
||||
[Header("Dissolve")]
|
||||
[Tooltip("If true, the object animates from dissolved -> visible on enable. If false, visible -> dissolved.")]
|
||||
public bool appear = true;
|
||||
|
||||
[Tooltip("Seconds the dissolve animation takes.")]
|
||||
public float dissolveDuration = 1.5f;
|
||||
|
||||
[Tooltip("If true the animation plays automatically on enable. Otherwise call Play().")]
|
||||
public bool playOnEnable = true;
|
||||
|
||||
[Header("Outline pulse")]
|
||||
[Tooltip("Min emission intensity for the outline pulse.")]
|
||||
public float pulseMin = 1f;
|
||||
|
||||
[Tooltip("Max emission intensity for the outline pulse.")]
|
||||
public float pulseMax = 6f;
|
||||
|
||||
[Tooltip("Pulses per second.")]
|
||||
public float pulseSpeed = 1.5f;
|
||||
|
||||
// Property IDs from the shader graphs' exposed reference names.
|
||||
private static readonly int FadeId = Shader.PropertyToID("_Fade");
|
||||
private static readonly int EmissionId = Shader.PropertyToID("_Emission");
|
||||
|
||||
private Renderer targetRenderer;
|
||||
private MaterialPropertyBlock block;
|
||||
private float timer;
|
||||
private bool playing;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
targetRenderer = GetComponent<Renderer>();
|
||||
block = new MaterialPropertyBlock();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (playOnEnable)
|
||||
{
|
||||
Play();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Restart the effect from the beginning.</summary>
|
||||
public void Play()
|
||||
{
|
||||
timer = 0f;
|
||||
playing = true;
|
||||
}
|
||||
|
||||
/// <summary>Set the dissolve amount directly (0 = hidden, 1 = visible) and stop animating.</summary>
|
||||
public void SetFade(float value)
|
||||
{
|
||||
playing = false;
|
||||
targetRenderer.GetPropertyBlock(block);
|
||||
block.SetFloat(FadeId, Mathf.Clamp01(value));
|
||||
targetRenderer.SetPropertyBlock(block);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (effect == Effect.OutlinePulse)
|
||||
{
|
||||
UpdatePulse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (playing)
|
||||
{
|
||||
UpdateDissolve();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDissolve()
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
float t = dissolveDuration > 0f ? Mathf.Clamp01(timer / dissolveDuration) : 1f;
|
||||
float fade = appear ? t : 1f - t;
|
||||
|
||||
targetRenderer.GetPropertyBlock(block);
|
||||
block.SetFloat(FadeId, fade);
|
||||
targetRenderer.SetPropertyBlock(block);
|
||||
|
||||
if (t >= 1f)
|
||||
{
|
||||
playing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePulse()
|
||||
{
|
||||
float wave = (Mathf.Sin(Time.time * pulseSpeed * Mathf.PI * 2f) + 1f) * 0.5f;
|
||||
float emission = Mathf.Lerp(pulseMin, pulseMax, wave);
|
||||
|
||||
targetRenderer.GetPropertyBlock(block);
|
||||
block.SetFloat(EmissionId, emission);
|
||||
targetRenderer.SetPropertyBlock(block);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08240dd979e13634782be1a0c42dc369
|
||||
@@ -0,0 +1,978 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime-fractures the attached mesh object using OpenFracture's <see cref="Fragmenter"/>,
|
||||
/// then pushes every produced fragment toward a configurable direction so the pieces
|
||||
/// "drift"/fly off. Call <see cref="Shatter()"/> (or <see cref="Shatter(Vector3)"/> to
|
||||
/// override the direction) from your own gameplay code.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(MeshFilter))]
|
||||
[RequireComponent(typeof(MeshRenderer))]
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class FractureAndDrift : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// How the cut-plane normal is chosen at each subdivision step.
|
||||
/// </summary>
|
||||
public enum SliceMode
|
||||
{
|
||||
/// <summary>Original OpenFracture behaviour: random normal over the enabled axes.</summary>
|
||||
Random,
|
||||
/// <summary>Always cut perpendicular to the fragment's current longest axis. Best for avoiding stretched
|
||||
/// pieces on long/thin objects, because long dimensions get subdivided first.</summary>
|
||||
LongestAxis,
|
||||
/// <summary>Random normal biased by <see cref="axisWeights"/>. Higher weight on an axis produces more cuts
|
||||
/// perpendicular to it, i.e. that dimension is subdivided more finely.</summary>
|
||||
AxisWeighted
|
||||
}
|
||||
|
||||
[Header("Fracture")]
|
||||
[Tooltip("Fragment count / axes / inside material. Same options used by OpenFracture's Fracture component.")]
|
||||
public FractureOptions fractureOptions = new FractureOptions();
|
||||
|
||||
[Tooltip("Random = stock OpenFracture. LongestAxis = always cut the longest side first (best anti-stretch). " +
|
||||
"AxisWeighted = bias cuts toward the axes with higher weight.")]
|
||||
public SliceMode sliceMode = SliceMode.Random;
|
||||
|
||||
[Tooltip("Only used in AxisWeighted mode. Higher weight on an axis = that dimension is subdivided more (more cuts perpendicular to it). " +
|
||||
"e.g. (3,1,1) cuts a long-X object roughly 3x more along X.")]
|
||||
public Vector3 axisWeights = Vector3.one;
|
||||
|
||||
[Tooltip("Only used in LongestAxis mode. Random tilt (0..1) added to the cut plane so repeated cuts aren't perfectly parallel. 0 = perfectly axis-aligned cuts.")]
|
||||
[Range(0f, 1f)]
|
||||
public float longestAxisJitter = 0.15f;
|
||||
|
||||
[Tooltip("If true, logs what the controlled fracture actually did (source mesh stats, how many slices " +
|
||||
"succeeded vs came back empty, final fragment count). Turn this on once to diagnose 'still big blocks'.")]
|
||||
public bool fractureDebugLogs = false;
|
||||
|
||||
[Header("Drift")]
|
||||
[Tooltip("Direction the fragments fly toward. Interpreted in the space set by 'Direction Is Local'.")]
|
||||
public Vector3 driftDirection = Vector3.up;
|
||||
|
||||
[Tooltip("If true, driftDirection is relative to this object's rotation; if false it is world space.")]
|
||||
public bool directionIsLocal = false;
|
||||
|
||||
[Tooltip("Base speed (m/s) applied to each fragment along the drift direction.")]
|
||||
public float driftSpeed = 5f;
|
||||
|
||||
[Tooltip("Extra random speed [0..value] added on top of driftSpeed for a natural burst.")]
|
||||
public float driftSpeedRandomness = 1.5f;
|
||||
|
||||
[Tooltip("Random sideways scatter (cone half-angle in degrees) around the drift direction. 0 = perfectly straight.")]
|
||||
[Range(0f, 90f)]
|
||||
public float scatterAngle = 15f;
|
||||
|
||||
[Tooltip("Extra outward push (m/s) from the fracture center, gives an explosion feel. 0 = disabled.")]
|
||||
public float explosionSpeed = 0f;
|
||||
|
||||
[Tooltip("Random angular velocity (rad/s) applied so fragments tumble.")]
|
||||
public float spinSpeed = 4f;
|
||||
|
||||
[Header("Source Physics")]
|
||||
[Tooltip("If true, the source object's Rigidbody is frozen (kinematic, no gravity) until Shatter() is called. " +
|
||||
"This stops the object from falling/jittering under physics before it breaks. Highly recommended: the " +
|
||||
"source body is only used to compute fragment mass, it does not need to simulate before shattering.")]
|
||||
public bool freezeSourceUntilShatter = true;
|
||||
|
||||
[Header("Fragment Physics")]
|
||||
[Tooltip("If true, fragments are affected by Unity's global gravity (always straight down) after being launched. " +
|
||||
"Ignored when 'Use Custom Gravity' is enabled.")]
|
||||
public bool fragmentsUseGravity = true;
|
||||
|
||||
[Tooltip("Caps how fast PhysX may push a fragment when it resolves an overlap (depenetration). This is the REAL " +
|
||||
"fix for fragments 'accelerating'/exploding: freshly cut convex hulls overlap each other and any nearby " +
|
||||
"geometry, and by default PhysX separates them with an almost unlimited velocity, injecting huge energy. " +
|
||||
"A small value (1-2) makes overlaps resolve gently. Applies regardless of collision layers/ignores.")]
|
||||
public float maxDepenetrationVelocity = 1f;
|
||||
|
||||
[Tooltip("If true, the freshly spawned fragments collide with each other. Leave OFF for a clean drift: at birth " +
|
||||
"the convex fragment hulls overlap, and Unity resolves that overlap with large depenetration impulses " +
|
||||
"that blast the pieces apart. Ignoring sibling collisions removes that explosion.")]
|
||||
public bool fragmentsCollideWithEachOther = false;
|
||||
|
||||
[Tooltip("If true, fragments will NOT collide with other, still-unshattered FractureAndDrift objects in the scene. " +
|
||||
"Keep ON: otherwise fragments smash into the solid neighbouring objects and fly off chaotically, which is " +
|
||||
"why earlier-triggered objects looked wrong while the last-triggered one (no solid neighbours left) drifted cleanly.")]
|
||||
public bool ignoreOtherSourceObjects = true;
|
||||
|
||||
[Tooltip("If true, fragments ignore Unity's global gravity and are instead continuously pulled toward " +
|
||||
"'Custom Gravity' every physics step. Use this to make pieces drift toward an arbitrary direction.")]
|
||||
public bool useCustomGravity = false;
|
||||
|
||||
[Tooltip("Acceleration vector (m/s^2) applied to every fragment each physics step when 'Use Custom Gravity' is on. " +
|
||||
"e.g. (0,-9.81,0) = normal down; (5,0,0) = pulled toward +X; interpreted in the space set by 'Custom Gravity Is Local'.")]
|
||||
public Vector3 customGravity = new Vector3(0f, -9.81f, 0f);
|
||||
|
||||
[Tooltip("If true, 'Custom Gravity' is relative to this object's rotation at shatter time; if false it is world space.")]
|
||||
public bool customGravityIsLocal = false;
|
||||
|
||||
[Header("Cleanup")]
|
||||
[Tooltip("Fallback: if nothing calls ReclaimFragments() first, the fragments are auto-reclaimed this many " +
|
||||
"seconds after shattering. <= 0 keeps them forever (until something calls ReclaimFragments()).")]
|
||||
public float fragmentLifetime = 5f;
|
||||
|
||||
[Tooltip("When reclaiming, fragments are destroyed one-by-one spread across this many seconds instead of all " +
|
||||
"at once. This is the 'async collect' behaviour. 0 = destroy the whole batch together.")]
|
||||
public float reclaimStagger = 1f;
|
||||
|
||||
[Tooltip("If true, each fragment smoothly shrinks to nothing before being destroyed, for a graceful collect " +
|
||||
"instead of a hard pop.")]
|
||||
public bool reclaimShrink = true;
|
||||
|
||||
[Tooltip("If true, this GameObject is deactivated after shattering (mirrors OpenFracture behaviour).")]
|
||||
public bool deactivateSourceAfterShatter = true;
|
||||
|
||||
[Header("Prewarm")]
|
||||
[Tooltip("If true, the expensive mesh slicing runs asynchronously in Start(): the fragments are built up front " +
|
||||
"and kept hidden & frozen. Shatter() then becomes a cheap, instant reveal + launch, so there is no " +
|
||||
"slicing hitch at the moment the track breaks. Leave off to slice on-demand when Shatter() is called.")]
|
||||
public bool prewarmFragmentsOnStart = false;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("If true, pressing 'Debug Key' triggers the shatter at runtime. For testing only.")]
|
||||
public bool enableDebugKey = false;
|
||||
|
||||
[Tooltip("Key that triggers the shatter when 'Enable Debug Key' is on.")]
|
||||
public KeyCode debugKey = KeyCode.F;
|
||||
|
||||
private GameObject fragmentRoot;
|
||||
private FragmentReclaimer fragmentReclaimer;
|
||||
private bool hasShattered;
|
||||
|
||||
// Prewarm state: when prewarmFragmentsOnStart is on, the fragments are built ahead of time and parked
|
||||
// here (hidden + kinematic). Shatter() then just reveals & launches them instead of slicing on the spot.
|
||||
private bool prewarmComplete;
|
||||
private bool prewarmInProgress;
|
||||
private Vector3 pendingLaunchDirection;
|
||||
private bool launchPending;
|
||||
|
||||
// Name of the dedicated physics layer every fragment is placed on. The layer-collision matrix is
|
||||
// configured so this layer ignores itself, giving global cross-batch fragment isolation for free.
|
||||
private const string FragmentLayerName = "FractureFragment";
|
||||
private static int cachedFragmentLayer = -1;
|
||||
private static bool fragmentLayerResolved;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (freezeSourceUntilShatter)
|
||||
{
|
||||
// The source body is only needed to read mass for fragment computation. Freeze it so the
|
||||
// object doesn't fall or jitter under physics before Shatter() is called.
|
||||
var body = GetComponent<Rigidbody>();
|
||||
if (body != null)
|
||||
{
|
||||
body.isKinematic = true;
|
||||
body.useGravity = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (prewarmFragmentsOnStart)
|
||||
{
|
||||
StartCoroutine(PrewarmRoutine());
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (enableDebugKey && !hasShattered && Input.GetKeyDown(debugKey))
|
||||
{
|
||||
Shatter();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fractures the object and launches the fragments along the configured drift direction.
|
||||
/// </summary>
|
||||
public void Shatter()
|
||||
{
|
||||
Shatter(ResolveDriftDirection());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fractures the object and launches the fragments along <paramref name="worldDirection"/>.
|
||||
/// </summary>
|
||||
/// <param name="worldDirection">Direction in world space the fragments should fly toward.</param>
|
||||
public void Shatter(Vector3 worldDirection)
|
||||
{
|
||||
if (hasShattered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 direction = worldDirection.sqrMagnitude > Mathf.Epsilon
|
||||
? worldDirection.normalized
|
||||
: transform.up;
|
||||
|
||||
// Prewarm path: the expensive slicing already ran (or is running) in Start().
|
||||
if (prewarmFragmentsOnStart)
|
||||
{
|
||||
if (prewarmComplete)
|
||||
{
|
||||
// Fragments are pre-built and parked. This is now a cheap, instant reveal + launch.
|
||||
hasShattered = true;
|
||||
RevealAndLaunch(direction);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prewarmInProgress)
|
||||
{
|
||||
// Slicing hasn't finished yet; remember the request and launch the moment it completes.
|
||||
pendingLaunchDirection = direction;
|
||||
launchPending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Prewarm was requested but Start() hasn't run yet (or failed) — fall through to slice on-demand.
|
||||
}
|
||||
|
||||
MeshFilter meshFilter = GetComponent<MeshFilter>();
|
||||
if (meshFilter == null || meshFilter.sharedMesh == null)
|
||||
{
|
||||
Debug.LogWarning("[FractureAndDrift] No mesh to fracture.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
hasShattered = true;
|
||||
|
||||
CreateFragmentRoot();
|
||||
|
||||
GameObject fragmentTemplate = CreateFragmentTemplate();
|
||||
|
||||
if (sliceMode != SliceMode.Random)
|
||||
{
|
||||
// Custom subdivision loop that controls cut orientation to avoid stretched fragments.
|
||||
FractureControlled(fragmentTemplate);
|
||||
|
||||
Destroy(fragmentTemplate);
|
||||
LaunchFragments(direction);
|
||||
FinishShatter();
|
||||
}
|
||||
else if (fractureOptions.asynchronous)
|
||||
{
|
||||
StartCoroutine(Fragmenter.FractureAsync(
|
||||
gameObject,
|
||||
fractureOptions,
|
||||
fragmentTemplate,
|
||||
fragmentRoot.transform,
|
||||
() =>
|
||||
{
|
||||
Destroy(fragmentTemplate);
|
||||
LaunchFragments(direction);
|
||||
FinishShatter();
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
Fragmenter.Fracture(
|
||||
gameObject,
|
||||
fractureOptions,
|
||||
fragmentTemplate,
|
||||
fragmentRoot.transform);
|
||||
|
||||
Destroy(fragmentTemplate);
|
||||
LaunchFragments(direction);
|
||||
FinishShatter();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds all fragments ahead of time in Start() and parks them hidden & inactive, so that the
|
||||
/// actual Shatter() is a cheap reveal + launch with no slicing hitch. Runs the slicing across frames
|
||||
/// (the async slicer yields; the controlled/sync slicers run in one frame but still off the break moment).
|
||||
/// </summary>
|
||||
private IEnumerator PrewarmRoutine()
|
||||
{
|
||||
MeshFilter meshFilter = GetComponent<MeshFilter>();
|
||||
if (meshFilter == null || meshFilter.sharedMesh == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
prewarmInProgress = true;
|
||||
|
||||
CreateFragmentRoot();
|
||||
|
||||
// Park the container inactive BEFORE slicing so every fragment spawns inactive and never simulates
|
||||
// (falls/drifts) during the multi-frame prewarm. Shatter() reveals it later via SetActive(true).
|
||||
fragmentRoot.SetActive(false);
|
||||
|
||||
GameObject fragmentTemplate = CreateFragmentTemplate();
|
||||
|
||||
if (sliceMode != SliceMode.Random)
|
||||
{
|
||||
FractureControlled(fragmentTemplate);
|
||||
Destroy(fragmentTemplate);
|
||||
}
|
||||
else if (fractureOptions.asynchronous)
|
||||
{
|
||||
yield return StartCoroutine(Fragmenter.FractureAsync(
|
||||
gameObject,
|
||||
fractureOptions,
|
||||
fragmentTemplate,
|
||||
fragmentRoot.transform,
|
||||
() => { }));
|
||||
Destroy(fragmentTemplate);
|
||||
}
|
||||
else
|
||||
{
|
||||
Fragmenter.Fracture(
|
||||
gameObject,
|
||||
fractureOptions,
|
||||
fragmentTemplate,
|
||||
fragmentRoot.transform);
|
||||
Destroy(fragmentTemplate);
|
||||
}
|
||||
|
||||
// Container is already inactive (parked before slicing), so nothing simulated during prewarm.
|
||||
prewarmInProgress = false;
|
||||
prewarmComplete = true;
|
||||
|
||||
// If Shatter() was called while we were still slicing, honour it now.
|
||||
if (launchPending)
|
||||
{
|
||||
launchPending = false;
|
||||
hasShattered = true;
|
||||
RevealAndLaunch(pendingLaunchDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reveals the pre-built fragment container and launches its fragments. Used by the prewarm path.
|
||||
/// </summary>
|
||||
private void RevealAndLaunch(Vector3 direction)
|
||||
{
|
||||
if (fragmentRoot != null)
|
||||
{
|
||||
fragmentRoot.SetActive(true);
|
||||
}
|
||||
|
||||
LaunchFragments(direction);
|
||||
FinishShatter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the collector object that holds the produced fragments (matches OpenFracture's convention).
|
||||
/// </summary>
|
||||
private void CreateFragmentRoot()
|
||||
{
|
||||
fragmentRoot = new GameObject($"{name}Fragments");
|
||||
fragmentRoot.transform.SetParent(transform.parent);
|
||||
fragmentRoot.transform.position = transform.position;
|
||||
fragmentRoot.transform.rotation = transform.rotation;
|
||||
fragmentRoot.transform.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
private void FinishShatter()
|
||||
{
|
||||
// Attach the reclaimer to the fragment container (which stays active). It waits either for an
|
||||
// explicit ReclaimFragments() call or, as a fallback, for fragmentLifetime to elapse, then
|
||||
// collects the fragments asynchronously (staggered) rather than destroying them all at once.
|
||||
if (fragmentRoot != null)
|
||||
{
|
||||
fragmentReclaimer = fragmentRoot.AddComponent<FragmentReclaimer>();
|
||||
fragmentReclaimer.Configure(fragmentLifetime, reclaimStagger, reclaimShrink);
|
||||
}
|
||||
|
||||
if (deactivateSourceAfterShatter)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins the asynchronous, staggered collection of this track's fragments. Safe to call once the
|
||||
/// object has shattered; a no-op otherwise. This overrides the fragmentLifetime fallback timer.
|
||||
/// </summary>
|
||||
public void ReclaimFragments()
|
||||
{
|
||||
if (fragmentReclaimer != null)
|
||||
{
|
||||
fragmentReclaimer.BeginReclaim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the freshly created fragments and applies the launch velocity + spin.
|
||||
/// </summary>
|
||||
private void LaunchFragments(Vector3 direction)
|
||||
{
|
||||
if (fragmentRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 center = transform.position;
|
||||
var bodies = fragmentRoot.GetComponentsInChildren<Rigidbody>();
|
||||
for (int i = 0; i < bodies.Length; i++)
|
||||
{
|
||||
Rigidbody body = bodies[i];
|
||||
if (body == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// When custom gravity is on, disable Unity's global (down-only) gravity so the
|
||||
// per-step custom acceleration is the only gravity acting on the fragment.
|
||||
body.useGravity = !useCustomGravity && fragmentsUseGravity;
|
||||
|
||||
// Safety net: guarantee the depenetration clamp is applied to every produced body, even ones
|
||||
// that might not have inherited it from the template (e.g. extra pieces from FindDisconnectedMeshes).
|
||||
if (maxDepenetrationVelocity > 0f)
|
||||
{
|
||||
body.maxDepenetrationVelocity = maxDepenetrationVelocity;
|
||||
}
|
||||
|
||||
Vector3 launchDir = ApplyScatter(direction);
|
||||
float speed = driftSpeed + Random.value * Mathf.Max(0f, driftSpeedRandomness);
|
||||
Vector3 velocity = launchDir * speed;
|
||||
|
||||
if (explosionSpeed > 0f)
|
||||
{
|
||||
Vector3 fromCenter = body.worldCenterOfMass - center;
|
||||
if (fromCenter.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
velocity += fromCenter.normalized * explosionSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
body.linearVelocity = velocity;
|
||||
|
||||
if (spinSpeed > 0f)
|
||||
{
|
||||
body.angularVelocity = Random.insideUnitSphere * spinSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureFragmentCollisions();
|
||||
|
||||
if (useCustomGravity)
|
||||
{
|
||||
// The source object gets deactivated after shattering, so the custom-gravity driver
|
||||
// must live on the fragment container (which stays active) to keep applying force.
|
||||
Vector3 gravity = customGravityIsLocal ? transform.TransformDirection(customGravity) : customGravity;
|
||||
var driver = fragmentRoot.AddComponent<FragmentGravityField>();
|
||||
driver.gravity = gravity;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures fragment collisions via the physics layer-collision matrix instead of per-pair
|
||||
/// Physics.IgnoreCollision. All fragments live on <see cref="FragmentLayerName"/>; disabling that
|
||||
/// layer's collision with itself makes EVERY fragment ignore EVERY other fragment - same batch or a
|
||||
/// different track's batch, at any time, with no per-collider bookkeeping. This is why the earlier
|
||||
/// pairwise approach kept leaking cross-batch collisions: newly spawned colliders were never paired
|
||||
/// against batches that shattered later. The matrix rule has no such ordering dependency.
|
||||
/// </summary>
|
||||
private void ConfigureFragmentCollisions()
|
||||
{
|
||||
int fragmentLayer = ResolveFragmentLayer();
|
||||
if (fragmentLayer < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Fragments never collide with each other (unless explicitly opted in). This is a global matrix
|
||||
// rule, so it covers same-batch AND cross-batch (another track shattering at the same time)
|
||||
// with no per-collider bookkeeping and no ordering dependency.
|
||||
Physics.IgnoreLayerCollision(fragmentLayer, fragmentLayer, !fragmentsCollideWithEachOther);
|
||||
|
||||
if (ignoreOtherSourceObjects)
|
||||
{
|
||||
// Fragments ignore the still-solid source objects. Done per-collider (NOT via the layer matrix)
|
||||
// because sources sit on shared layers like Default that the ground/platforms also use - a
|
||||
// whole-layer ignore would let fragments fall through the floor. Per-collider keeps it precise.
|
||||
var fragmentColliders = fragmentRoot.GetComponentsInChildren<Collider>();
|
||||
var sources = FindObjectsByType<FractureAndDrift>(FindObjectsSortMode.None);
|
||||
for (int s = 0; s < sources.Length; s++)
|
||||
{
|
||||
FractureAndDrift source = sources[s];
|
||||
// Skip sources that already shattered - their solid collider is gone. Keep 'this' one:
|
||||
// its collider is still active this frame and the fragments spawn right on top of it.
|
||||
if (source == null || (source.hasShattered && source != this))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceCollider = source.GetComponent<Collider>();
|
||||
if (sourceCollider == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int f = 0; f < fragmentColliders.Length; f++)
|
||||
{
|
||||
if (fragmentColliders[f] != null)
|
||||
{
|
||||
Physics.IgnoreCollision(fragmentColliders[f], sourceCollider, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the dedicated fragment layer index, caching the lookup. Returns -1 if the project has no
|
||||
/// layer named <see cref="FragmentLayerName"/> (falls back to no layer assignment).
|
||||
/// </summary>
|
||||
private static int ResolveFragmentLayer()
|
||||
{
|
||||
if (!fragmentLayerResolved)
|
||||
{
|
||||
cachedFragmentLayer = LayerMask.NameToLayer(FragmentLayerName);
|
||||
fragmentLayerResolved = true;
|
||||
if (cachedFragmentLayer < 0)
|
||||
{
|
||||
Debug.LogWarning($"[FractureAndDrift] Layer '{FragmentLayerName}' not found. Add it in " +
|
||||
"Project Settings > Tags and Layers so fragments can be isolated from collisions.");
|
||||
}
|
||||
}
|
||||
|
||||
return cachedFragmentLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates <paramref name="direction"/> by a random angle within the scatter cone.
|
||||
/// </summary>
|
||||
private Vector3 ApplyScatter(Vector3 direction)
|
||||
{
|
||||
if (scatterAngle <= 0f)
|
||||
{
|
||||
return direction;
|
||||
}
|
||||
|
||||
// Random rotation within a cone of half-angle 'scatterAngle' around 'direction'.
|
||||
float angle = Random.Range(0f, scatterAngle);
|
||||
float roll = Random.Range(0f, 360f);
|
||||
Quaternion cone = Quaternion.AngleAxis(angle, Vector3.right) * Quaternion.identity;
|
||||
Quaternion spinAround = Quaternion.AngleAxis(roll, Vector3.forward);
|
||||
Quaternion align = Quaternion.FromToRotation(Vector3.forward, direction);
|
||||
return align * spinAround * cone * Vector3.forward;
|
||||
}
|
||||
|
||||
private Vector3 ResolveDriftDirection()
|
||||
{
|
||||
return directionIsLocal ? transform.TransformDirection(driftDirection) : driftDirection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a template GameObject each fragment clones. Mirrors OpenFracture's Fracture.CreateFragmentTemplate.
|
||||
/// </summary>
|
||||
private GameObject CreateFragmentTemplate()
|
||||
{
|
||||
GameObject obj = new GameObject("Fragment") { tag = tag };
|
||||
|
||||
// Put every fragment on the dedicated fragment layer. The physics layer-collision matrix is
|
||||
// configured (once) so this layer never collides with itself, which cleanly stops ANY fragment
|
||||
// from colliding with ANY other fragment - same batch or a different track's batch, at any time.
|
||||
int fragmentLayer = ResolveFragmentLayer();
|
||||
if (fragmentLayer >= 0)
|
||||
{
|
||||
obj.layer = fragmentLayer;
|
||||
}
|
||||
|
||||
obj.AddComponent<MeshFilter>();
|
||||
|
||||
// Normal material in slot 0, cut-face material in slot 1.
|
||||
var meshRenderer = obj.AddComponent<MeshRenderer>();
|
||||
meshRenderer.sharedMaterials = new Material[2]
|
||||
{
|
||||
GetComponent<MeshRenderer>().sharedMaterial,
|
||||
fractureOptions.insideMaterial
|
||||
};
|
||||
|
||||
var thisCollider = GetComponent<Collider>();
|
||||
var fragmentCollider = obj.AddComponent<MeshCollider>();
|
||||
fragmentCollider.convex = true;
|
||||
if (thisCollider != null)
|
||||
{
|
||||
fragmentCollider.sharedMaterial = thisCollider.sharedMaterial;
|
||||
fragmentCollider.isTrigger = thisCollider.isTrigger;
|
||||
}
|
||||
|
||||
var thisRigidBody = GetComponent<Rigidbody>();
|
||||
var fragmentRigidBody = obj.AddComponent<Rigidbody>();
|
||||
fragmentRigidBody.linearDamping = thisRigidBody.linearDamping;
|
||||
fragmentRigidBody.angularDamping = thisRigidBody.angularDamping;
|
||||
fragmentRigidBody.useGravity = fragmentsUseGravity;
|
||||
|
||||
// Root cause of the "fragments accelerate" blast: overlapping convex hulls are separated by PhysX
|
||||
// with a near-unlimited velocity by default. Clamp it so overlaps resolve gently instead of
|
||||
// launching pieces. Every fragment (both fracture paths) clones this template, so setting it here
|
||||
// covers all of them, independent of any collision-layer/ignore configuration.
|
||||
if (maxDepenetrationVelocity > 0f)
|
||||
{
|
||||
fragmentRigidBody.maxDepenetrationVelocity = maxDepenetrationVelocity;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subdivision loop that mirrors <see cref="Fragmenter.Fracture"/>, but chooses the cut-plane
|
||||
/// normal per <see cref="sliceMode"/> instead of a fully random one. This is what lets a given
|
||||
/// axis be subdivided more finely so long/thin fragments don't come out stretched.
|
||||
/// </summary>
|
||||
private void FractureControlled(GameObject fragmentTemplate)
|
||||
{
|
||||
Mesh srcMesh = GetComponent<MeshFilter>().sharedMesh;
|
||||
var sourceMesh = new FragmentData(srcMesh);
|
||||
|
||||
if (fractureDebugLogs)
|
||||
{
|
||||
Debug.Log($"[FractureAndDrift] '{name}' FractureControlled start. mode={sliceMode} " +
|
||||
$"targetCount={fractureOptions.fragmentCount} sourceVerts={srcMesh.vertexCount} " +
|
||||
$"sourceTris={srcMesh.triangles.Length / 3} subMeshes={srcMesh.subMeshCount} " +
|
||||
$"srcBoundsSize={srcMesh.bounds.size} readable={srcMesh.isReadable}", this);
|
||||
if (srcMesh.subMeshCount > 1)
|
||||
{
|
||||
Debug.LogWarning($"[FractureAndDrift] '{name}' source mesh has {srcMesh.subMeshCount} submeshes. " +
|
||||
"OpenFracture only slices submesh 0 — geometry in other submeshes is dropped. " +
|
||||
"Combine the model into a single submesh/material if pieces look missing or too coarse.", this);
|
||||
}
|
||||
}
|
||||
|
||||
var fragments = new Queue<FragmentData>();
|
||||
fragments.Enqueue(sourceMesh);
|
||||
|
||||
// Subdivide the largest-remaining fragment each step until we hit the target count.
|
||||
// Processing the largest one first (rather than stock FIFO) keeps fragment sizes even.
|
||||
// Guard against degenerate slices: if a piece refuses to split (one side comes back empty),
|
||||
// re-enqueuing it unchanged would spin forever AND leave that big block intact. We instead
|
||||
// drop it into a "done" set so it stops being reconsidered, and bail out if nothing splits.
|
||||
var done = new List<FragmentData>();
|
||||
int guardIterations = fractureOptions.fragmentCount * 8 + 16;
|
||||
int producedSplits = 0;
|
||||
int degenerateSlices = 0;
|
||||
|
||||
while (fragments.Count + done.Count < fractureOptions.fragmentCount && fragments.Count > 0)
|
||||
{
|
||||
if (guardIterations-- <= 0)
|
||||
{
|
||||
if (fractureDebugLogs)
|
||||
{
|
||||
Debug.LogWarning($"[FractureAndDrift] '{name}' subdivision hit iteration guard; " +
|
||||
"stopping early. The mesh likely can't be split further with the current settings.", this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
FragmentData meshData = DequeueLargest(fragments);
|
||||
meshData.CalculateBounds();
|
||||
|
||||
Vector3 normal = ChooseCutNormal(meshData.Bounds);
|
||||
|
||||
MeshSlicer.Slice(meshData,
|
||||
normal,
|
||||
meshData.Bounds.center,
|
||||
fractureOptions.textureScale,
|
||||
fractureOptions.textureOffset,
|
||||
out FragmentData topSlice,
|
||||
out FragmentData bottomSlice);
|
||||
|
||||
bool topEmpty = topSlice.triangleCount == 0;
|
||||
bool bottomEmpty = bottomSlice.triangleCount == 0;
|
||||
|
||||
// A real split yields geometry on BOTH sides. If one side is empty the plane didn't actually
|
||||
// divide this piece, so keep it aside as finished instead of looping on it forever.
|
||||
if (topEmpty || bottomEmpty)
|
||||
{
|
||||
degenerateSlices++;
|
||||
done.Add(meshData);
|
||||
continue;
|
||||
}
|
||||
|
||||
producedSplits++;
|
||||
fragments.Enqueue(topSlice);
|
||||
fragments.Enqueue(bottomSlice);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
var parentSize = srcMesh.bounds.size;
|
||||
var parentMass = GetComponent<Rigidbody>().mass;
|
||||
float density = (parentSize.x * parentSize.y * parentSize.z) / Mathf.Max(parentMass, Mathf.Epsilon);
|
||||
|
||||
foreach (FragmentData meshData in fragments)
|
||||
{
|
||||
CreateControlledFragment(meshData, fragmentTemplate, density, ref i);
|
||||
}
|
||||
foreach (FragmentData meshData in done)
|
||||
{
|
||||
CreateControlledFragment(meshData, fragmentTemplate, density, ref i);
|
||||
}
|
||||
|
||||
if (fractureDebugLogs)
|
||||
{
|
||||
Debug.Log($"[FractureAndDrift] '{name}' FractureControlled done. successfulSplits={producedSplits} " +
|
||||
$"degenerateSlices={degenerateSlices} fragmentsCreated={i}. " +
|
||||
(degenerateSlices > producedSplits && producedSplits < 4
|
||||
? "Most slices failed to divide the mesh -> that is why you still see big blocks. "
|
||||
: ""), this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks the slice-plane normal for the next cut based on the current fragment bounds and slice mode.
|
||||
/// </summary>
|
||||
private Vector3 ChooseCutNormal(Bounds bounds)
|
||||
{
|
||||
if (sliceMode == SliceMode.LongestAxis)
|
||||
{
|
||||
Vector3 size = bounds.size;
|
||||
// Normal points along the longest dimension, so the cut plane is perpendicular to it
|
||||
// and splits that long dimension in half.
|
||||
Vector3 normal = Vector3.right;
|
||||
if (size.y >= size.x && size.y >= size.z) normal = Vector3.up;
|
||||
else if (size.z >= size.x && size.z >= size.y) normal = Vector3.forward;
|
||||
|
||||
if (longestAxisJitter > 0f)
|
||||
{
|
||||
normal += Random.insideUnitSphere * longestAxisJitter;
|
||||
}
|
||||
return normal.sqrMagnitude > Mathf.Epsilon ? normal.normalized : Vector3.up;
|
||||
}
|
||||
|
||||
// AxisWeighted: bias each component by its weight so heavier axes get more perpendicular cuts.
|
||||
Vector3 w = axisWeights;
|
||||
Vector3 weighted = new Vector3(
|
||||
(fractureOptions.xAxis ? 1f : 0f) * Mathf.Max(0f, w.x) * Random.Range(-1f, 1f),
|
||||
(fractureOptions.yAxis ? 1f : 0f) * Mathf.Max(0f, w.y) * Random.Range(-1f, 1f),
|
||||
(fractureOptions.zAxis ? 1f : 0f) * Mathf.Max(0f, w.z) * Random.Range(-1f, 1f));
|
||||
|
||||
return weighted.sqrMagnitude > Mathf.Epsilon ? weighted.normalized : Vector3.up;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns the fragment with the largest bounding-box volume from the queue,
|
||||
/// preserving the order of the remaining items.
|
||||
/// </summary>
|
||||
private static FragmentData DequeueLargest(Queue<FragmentData> fragments)
|
||||
{
|
||||
int count = fragments.Count;
|
||||
FragmentData largest = null;
|
||||
float largestVolume = float.MinValue;
|
||||
|
||||
// Rotate the queue once, tracking the largest, then rotate again dropping that one.
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
FragmentData candidate = fragments.Dequeue();
|
||||
candidate.CalculateBounds();
|
||||
Vector3 s = candidate.Bounds.size;
|
||||
float volume = s.x * s.y * s.z;
|
||||
if (volume > largestVolume)
|
||||
{
|
||||
largestVolume = volume;
|
||||
largest = candidate;
|
||||
}
|
||||
fragments.Enqueue(candidate);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
FragmentData candidate = fragments.Dequeue();
|
||||
if (!ReferenceEquals(candidate, largest))
|
||||
{
|
||||
fragments.Enqueue(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return largest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a fragment GameObject from mesh data. Mirrors the private Fragmenter.CreateFragment.
|
||||
/// </summary>
|
||||
private void CreateControlledFragment(FragmentData meshData, GameObject fragmentTemplate, float density, ref int i)
|
||||
{
|
||||
if (meshData.triangleCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mesh[] meshes;
|
||||
Mesh fragmentMesh = meshData.ToMesh();
|
||||
if (fractureOptions.detectFloatingFragments)
|
||||
{
|
||||
meshes = MeshUtils.FindDisconnectedMeshes(fragmentMesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
meshes = new Mesh[] { fragmentMesh };
|
||||
}
|
||||
|
||||
for (int k = 0; k < meshes.Length; k++)
|
||||
{
|
||||
GameObject fragment = Instantiate(fragmentTemplate, fragmentRoot.transform);
|
||||
fragment.name = $"Fragment{i}";
|
||||
fragment.transform.localPosition = Vector3.zero;
|
||||
fragment.transform.localRotation = Quaternion.identity;
|
||||
fragment.transform.localScale = transform.localScale;
|
||||
|
||||
meshes[k].name = System.Guid.NewGuid().ToString();
|
||||
|
||||
fragment.GetComponent<MeshFilter>().sharedMesh = meshes[k];
|
||||
|
||||
var collider = fragment.GetComponent<MeshCollider>();
|
||||
collider.sharedMesh = meshes[k];
|
||||
collider.convex = true;
|
||||
|
||||
var size = meshes[k].bounds.size;
|
||||
var rigidBody = fragment.GetComponent<Rigidbody>();
|
||||
rigidBody.mass = (size.x * size.y * size.z) / Mathf.Max(density, Mathf.Epsilon);
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuously pulls a set of rigidbodies toward a fixed world-space acceleration every physics step.
|
||||
/// Attached to the fragment container by <see cref="FractureAndDrift"/> when "Use Custom Gravity" is on,
|
||||
/// so fragments drift toward an arbitrary direction instead of Unity's straight-down global gravity.
|
||||
/// It lives on the surviving fragment root (not the source object, which is deactivated after shattering).
|
||||
/// </summary>
|
||||
public class FragmentGravityField : MonoBehaviour
|
||||
{
|
||||
/// <summary>World-space acceleration (m/s^2) applied to every fragment each FixedUpdate.</summary>
|
||||
public Vector3 gravity;
|
||||
|
||||
private Rigidbody[] bodies;
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
// Refresh the body list lazily: FindDisconnectedMeshes can add fragments a frame late,
|
||||
// and null entries appear as the container is torn down at end of life.
|
||||
if (bodies == null)
|
||||
{
|
||||
bodies = GetComponentsInChildren<Rigidbody>();
|
||||
}
|
||||
|
||||
for (int i = 0; i < bodies.Length; i++)
|
||||
{
|
||||
Rigidbody body = bodies[i];
|
||||
if (body == null || body.isKinematic)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// ForceMode.Acceleration ignores mass, so every fragment falls at the same rate (like real gravity).
|
||||
body.AddForce(gravity, ForceMode.Acceleration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects (destroys) a batch of fragments asynchronously instead of all at once. Attached to the
|
||||
/// surviving fragment container by <see cref="FractureAndDrift"/>. It either waits for an explicit
|
||||
/// <see cref="BeginReclaim"/> call (driven by the track controller's fade timing) or, as a fallback,
|
||||
/// auto-starts after <c>fallbackDelay</c> seconds. Fragments are destroyed one-by-one spread across
|
||||
/// <c>stagger</c> seconds, optionally shrinking each one to nothing first for a graceful collect.
|
||||
/// </summary>
|
||||
public class FragmentReclaimer : MonoBehaviour
|
||||
{
|
||||
private float fallbackDelay;
|
||||
private float stagger;
|
||||
private bool shrink;
|
||||
private bool reclaiming;
|
||||
|
||||
public void Configure(float fallbackDelay, float stagger, bool shrink)
|
||||
{
|
||||
this.fallbackDelay = fallbackDelay;
|
||||
this.stagger = Mathf.Max(0f, stagger);
|
||||
this.shrink = shrink;
|
||||
|
||||
// Fallback timer: if nothing calls BeginReclaim() first, start on our own after the delay.
|
||||
if (fallbackDelay > 0f)
|
||||
{
|
||||
Invoke(nameof(BeginReclaim), fallbackDelay);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the staggered collection. Safe to call multiple times; only the first call takes effect.
|
||||
/// </summary>
|
||||
public void BeginReclaim()
|
||||
{
|
||||
if (reclaiming)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
reclaiming = true;
|
||||
CancelInvoke(nameof(BeginReclaim));
|
||||
StartCoroutine(ReclaimRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator ReclaimRoutine()
|
||||
{
|
||||
// Snapshot the current fragments (skip the top-level container transform itself).
|
||||
var fragments = new List<Transform>();
|
||||
foreach (Transform child in transform)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
fragments.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
int count = fragments.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Time budget between consecutive fragment removals.
|
||||
float interval = count > 1 ? stagger / (count - 1) : 0f;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Transform fragment = fragments[i];
|
||||
if (fragment != null)
|
||||
{
|
||||
if (shrink)
|
||||
{
|
||||
StartCoroutine(ShrinkAndDestroy(fragment.gameObject, Mathf.Max(0.05f, interval)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(fragment.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (interval > 0f)
|
||||
{
|
||||
yield return new WaitForSeconds(interval);
|
||||
}
|
||||
}
|
||||
|
||||
// Once the last fragment is gone (allow shrink time to finish), remove the container.
|
||||
yield return new WaitForSeconds(shrink ? Mathf.Max(0.05f, interval) : 0f);
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private IEnumerator ShrinkAndDestroy(GameObject fragment, float duration)
|
||||
{
|
||||
Transform t = fragment.transform;
|
||||
Vector3 startScale = t.localScale;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration && fragment != null)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float k = Mathf.Clamp01(elapsed / duration);
|
||||
t.localScale = Vector3.Lerp(startScale, Vector3.zero, k);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (fragment != null)
|
||||
{
|
||||
Destroy(fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d79617e4a494b34e80c479dd978a949
|
||||
@@ -9,6 +9,9 @@ public class eula_and_warnings : MonoBehaviour
|
||||
{
|
||||
private const string MainSceneName = "Main_main";
|
||||
private const string RankingConsentPrefKey = "before_everything_agree_ranking";
|
||||
private const string FirstLaunchCompletedPrefKey = "before_everything_first_launch_completed";
|
||||
private const string LightWarningAcceptedPrefKey = "before_everything_light_warning_accepted";
|
||||
private const string UserInfoAcceptedPrefKey = "before_everything_user_info_accepted";
|
||||
private const float LightWarningMinimumSeconds = 5f;
|
||||
private const float FadeDuration = 0.3f;
|
||||
private const float RequiredScrollReadThreshold = 0.1f;
|
||||
@@ -44,6 +47,9 @@ public class eula_and_warnings : MonoBehaviour
|
||||
private bool _isTransitioning;
|
||||
private bool _mainScenePreloadRequested;
|
||||
private bool _mainScenePreloadReady;
|
||||
private bool _hasCompletedFirstLaunch;
|
||||
private bool _hasAcceptedLightWarning;
|
||||
private bool _hasAcceptedUserInfoEula;
|
||||
private readonly List<GameObject> _hiddenMainSceneRoots = new List<GameObject>();
|
||||
|
||||
private void Awake()
|
||||
@@ -55,6 +61,14 @@ public class eula_and_warnings : MonoBehaviour
|
||||
|
||||
private void Start()
|
||||
{
|
||||
LoadPersistedConsentState();
|
||||
|
||||
if (ShouldSkipConfirmationFlow())
|
||||
{
|
||||
ActivateMainScene();
|
||||
return;
|
||||
}
|
||||
|
||||
PlayLightWarningIntro();
|
||||
}
|
||||
|
||||
@@ -96,6 +110,18 @@ public class eula_and_warnings : MonoBehaviour
|
||||
_isTransitioning = false;
|
||||
}
|
||||
|
||||
private void LoadPersistedConsentState()
|
||||
{
|
||||
_hasCompletedFirstLaunch = PlayerPrefs.GetInt(FirstLaunchCompletedPrefKey, 0) == 1;
|
||||
_hasAcceptedLightWarning = PlayerPrefs.GetInt(LightWarningAcceptedPrefKey, 0) == 1;
|
||||
_hasAcceptedUserInfoEula = PlayerPrefs.GetInt(UserInfoAcceptedPrefKey, 0) == 1;
|
||||
}
|
||||
|
||||
private bool ShouldSkipConfirmationFlow()
|
||||
{
|
||||
return _hasCompletedFirstLaunch && _hasAcceptedLightWarning && _hasAcceptedUserInfoEula;
|
||||
}
|
||||
|
||||
private void PlayLightWarningIntro()
|
||||
{
|
||||
if (lightWarningObj == null || lightWarningCG == null)
|
||||
@@ -203,6 +229,10 @@ public class eula_and_warnings : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
_hasAcceptedLightWarning = true;
|
||||
PlayerPrefs.SetInt(LightWarningAcceptedPrefKey, 1);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
TransitionCanvasGroup(lightWarningObj, lightWarningCG, false, () =>
|
||||
{
|
||||
BeginPreloadMainScene();
|
||||
@@ -235,6 +265,10 @@ public class eula_and_warnings : MonoBehaviour
|
||||
}
|
||||
|
||||
bool agreeRanking = uie_agreeRanking != null && uie_agreeRanking.isOn;
|
||||
_hasAcceptedUserInfoEula = true;
|
||||
_hasCompletedFirstLaunch = true;
|
||||
PlayerPrefs.SetInt(UserInfoAcceptedPrefKey, 1);
|
||||
PlayerPrefs.SetInt(FirstLaunchCompletedPrefKey, 1);
|
||||
PlayerPrefs.SetInt(RankingConsentPrefKey, agreeRanking ? 1 : 0);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
@@ -248,9 +282,6 @@ public class eula_and_warnings : MonoBehaviour
|
||||
|
||||
private static void MarkPolicyRejectedAndQuit()
|
||||
{
|
||||
PlayerPrefs.SetInt(RankingConsentPrefKey, 0);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.isPlaying = false;
|
||||
#else
|
||||
|
||||
@@ -223,6 +223,13 @@ public class friendCardPrefab : MonoBehaviour
|
||||
ReleaseRuntimeAvatarResources();
|
||||
|
||||
string resolvedAvatarUrl = NetworkManager.ResolveAvatarUrl(data.AvatarUrl, data.SteamId);
|
||||
if (playerMessagePrefab.TryGetCachedAvatarSprite(data.SteamId, resolvedAvatarUrl, out Sprite cachedSprite) && cachedSprite != null)
|
||||
{
|
||||
friendProfile.sprite = cachedSprite;
|
||||
_avatarLoadRoutine = null;
|
||||
yield break;
|
||||
}
|
||||
|
||||
bool loaded = false;
|
||||
|
||||
if (NetworkManager.IsUsableAvatarUrl(resolvedAvatarUrl)
|
||||
|
||||
@@ -132,7 +132,14 @@ public class friendRequestPrefab : MonoBehaviour
|
||||
}
|
||||
|
||||
bool loaded = false;
|
||||
if (NetworkManager.IsUsableAvatarUrl(avatarUrl)
|
||||
if (playerMessagePrefab.TryGetCachedAvatarSprite(steamId, avatarUrl, out Sprite cachedSprite) && cachedSprite != null)
|
||||
{
|
||||
profileImage.sprite = cachedSprite;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
if (!loaded
|
||||
&& NetworkManager.IsUsableAvatarUrl(avatarUrl)
|
||||
&& !avatarUrl.StartsWith(NetworkManager.SteamAvatarUrlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(avatarUrl))
|
||||
|
||||
@@ -124,7 +124,14 @@ public class friendSearchPrefab : MonoBehaviour
|
||||
}
|
||||
|
||||
bool loaded = false;
|
||||
if (NetworkManager.IsUsableAvatarUrl(avatarUrl)
|
||||
if (playerMessagePrefab.TryGetCachedAvatarSprite(steamId, avatarUrl, out Sprite cachedSprite) && cachedSprite != null)
|
||||
{
|
||||
profileImage.sprite = cachedSprite;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
if (!loaded
|
||||
&& NetworkManager.IsUsableAvatarUrl(avatarUrl)
|
||||
&& !avatarUrl.StartsWith(NetworkManager.SteamAvatarUrlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(avatarUrl))
|
||||
|
||||
@@ -6,6 +6,7 @@ public class gTransBlack : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Canvas b_Canvas;
|
||||
[SerializeField] private CanvasGroup b_CG;
|
||||
[SerializeField] private Text nowLoadingText;
|
||||
|
||||
public Canvas Canvas => b_Canvas;
|
||||
public CanvasGroup CanvasGroup => b_CG;
|
||||
@@ -38,6 +39,14 @@ public class gTransBlack : MonoBehaviour
|
||||
ApplySceneCamera();
|
||||
}
|
||||
|
||||
public void SetNowLoadingText(string text)
|
||||
{
|
||||
if (nowLoadingText != null)
|
||||
{
|
||||
nowLoadingText.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureReferences()
|
||||
{
|
||||
if (b_Canvas == null)
|
||||
|
||||
@@ -45,6 +45,195 @@ public class AnimationController : MonoBehaviour
|
||||
// Track active particle instances for immediate cleanup
|
||||
private List<GameObject> activeParticles = new List<GameObject>();
|
||||
|
||||
// Per-hit FX pooling. PlayDestroyAnimation used to Instantiate + Destroy three
|
||||
// objects on every judge (hit particle, hit ring, judge text prefab). Under dense
|
||||
// note bursts that is the dominant GC + instantiation spike. We now reuse instances
|
||||
// from a pool keyed by template, and cache each instance's ParticleSystem[] +
|
||||
// computed lifetime so the hot path never calls GetComponentsInChildren per hit.
|
||||
// Business behavior is unchanged: same templates, same tint, same lifetime, same
|
||||
// parenting; instances are recycled instead of created and destroyed.
|
||||
private sealed class FxCacheEntry
|
||||
{
|
||||
public ParticleSystem[] Systems;
|
||||
public float Lifetime;
|
||||
}
|
||||
|
||||
private readonly Dictionary<GameObject, Queue<GameObject>> fxPools =
|
||||
new Dictionary<GameObject, Queue<GameObject>>();
|
||||
private readonly Dictionary<GameObject, FxCacheEntry> fxCache =
|
||||
new Dictionary<GameObject, FxCacheEntry>();
|
||||
private readonly Dictionary<GameObject, GameObject> fxInstanceTemplate =
|
||||
new Dictionary<GameObject, GameObject>();
|
||||
// Instances currently rented out. Return is guarded by this set so the timed
|
||||
// RecycleFxRoutine and an explicit StopHoldParticles cleanup can never return the
|
||||
// same instance twice (which would double-enqueue it and hand it out concurrently).
|
||||
private readonly HashSet<GameObject> rentedFx = new HashSet<GameObject>();
|
||||
// Per-rent token. Each rent bumps a counter and records it for the instance. The
|
||||
// timed RecycleFxRoutine captures its token and only returns the instance if the
|
||||
// token still matches, so a stale timer from a previous rent cannot recycle an
|
||||
// instance that was already returned early (StopHoldParticles) and re-rented.
|
||||
private int fxRentCounter;
|
||||
private readonly Dictionary<GameObject, int> fxRentId = new Dictionary<GameObject, int>();
|
||||
private Transform fxPoolRoot;
|
||||
|
||||
private void EnsureFxPoolRoot()
|
||||
{
|
||||
if (fxPoolRoot == null)
|
||||
{
|
||||
var root = new GameObject("AnimationControllerFxPool");
|
||||
root.transform.SetParent(transform, false);
|
||||
root.SetActive(false);
|
||||
fxPoolRoot = root.transform;
|
||||
}
|
||||
}
|
||||
|
||||
// Creates one instance of the template, caches its particle systems + lifetime,
|
||||
// warms the play path once, and leaves it stopped/cleared ready for reuse.
|
||||
private GameObject CreateFxInstance(GameObject template)
|
||||
{
|
||||
if (template == null)
|
||||
return null;
|
||||
|
||||
GameObject instance = Instantiate(template);
|
||||
var systems = instance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (systems == null)
|
||||
systems = new ParticleSystem[0];
|
||||
|
||||
var entry = new FxCacheEntry
|
||||
{
|
||||
Systems = systems,
|
||||
Lifetime = ComputeFxLifetime(systems)
|
||||
};
|
||||
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
try { ps.Play(true); } catch { }
|
||||
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
|
||||
try { ps.Clear(true); } catch { }
|
||||
}
|
||||
|
||||
fxCache[instance] = entry;
|
||||
fxInstanceTemplate[instance] = template;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private GameObject RentFxInstance(GameObject template, out FxCacheEntry entry, out int rentToken)
|
||||
{
|
||||
entry = null;
|
||||
rentToken = -1;
|
||||
if (template == null)
|
||||
return null;
|
||||
|
||||
if (fxPools.TryGetValue(template, out var pool))
|
||||
{
|
||||
while (pool.Count > 0)
|
||||
{
|
||||
GameObject pooled = pool.Dequeue();
|
||||
if (pooled != null && fxCache.TryGetValue(pooled, out entry))
|
||||
{
|
||||
rentToken = MarkRented(pooled);
|
||||
return pooled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GameObject created = CreateFxInstance(template);
|
||||
if (created != null)
|
||||
{
|
||||
fxCache.TryGetValue(created, out entry);
|
||||
rentToken = MarkRented(created);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
private int CurrentRentToken(GameObject instance)
|
||||
{
|
||||
return fxRentId.TryGetValue(instance, out int token) ? token : -1;
|
||||
}
|
||||
|
||||
private int MarkRented(GameObject instance)
|
||||
{
|
||||
rentedFx.Add(instance);
|
||||
int token = ++fxRentCounter;
|
||||
fxRentId[instance] = token;
|
||||
return token;
|
||||
}
|
||||
|
||||
private void ReturnFxInstance(GameObject instance)
|
||||
{
|
||||
if (instance == null)
|
||||
return;
|
||||
|
||||
// Idempotent: if this instance was already returned, ignore the second call.
|
||||
if (!rentedFx.Remove(instance))
|
||||
return;
|
||||
|
||||
fxRentId.Remove(instance);
|
||||
activeParticles.Remove(instance);
|
||||
|
||||
if (fxCache.TryGetValue(instance, out var entry) && entry.Systems != null)
|
||||
{
|
||||
foreach (var ps in entry.Systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
|
||||
try { ps.Clear(true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
instance.SetActive(false);
|
||||
EnsureFxPoolRoot();
|
||||
instance.transform.SetParent(fxPoolRoot, false);
|
||||
|
||||
if (!fxInstanceTemplate.TryGetValue(instance, out var template) || template == null)
|
||||
{
|
||||
// Unknown origin (should not happen for pooled instances): drop it.
|
||||
Destroy(instance);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fxPools.TryGetValue(template, out var pool))
|
||||
{
|
||||
pool = new Queue<GameObject>();
|
||||
fxPools[template] = pool;
|
||||
}
|
||||
pool.Enqueue(instance);
|
||||
}
|
||||
|
||||
private IEnumerator RecycleFxRoutine(GameObject instance, int rentToken, float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
if (instance == null)
|
||||
yield break;
|
||||
|
||||
// Only recycle if this instance is still on the same rent. If it was
|
||||
// returned early (StopHoldParticles) and re-rented in the meantime, its
|
||||
// token changed and this stale timer must not touch it.
|
||||
if (CurrentRentToken(instance) != rentToken)
|
||||
yield break;
|
||||
|
||||
ReturnFxInstance(instance);
|
||||
}
|
||||
|
||||
private static float ComputeFxLifetime(ParticleSystem[] systems)
|
||||
{
|
||||
float maxLifetime = 0f;
|
||||
if (systems == null)
|
||||
return maxLifetime;
|
||||
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
var main = ps.main;
|
||||
var lifetime = main.startLifetime;
|
||||
float life = lifetime.constantMax;
|
||||
if (life <= 0f) life = lifetime.constant;
|
||||
if (life > maxLifetime) maxLifetime = life;
|
||||
}
|
||||
return maxLifetime;
|
||||
}
|
||||
|
||||
[Header("Inspector")]
|
||||
public GameObject perfect_judge_prefab;
|
||||
public GameObject great_judge_prefab;
|
||||
@@ -107,12 +296,16 @@ public class AnimationController : MonoBehaviour
|
||||
holdParticleCoroutine = null;
|
||||
}
|
||||
|
||||
// Immediately destroy all active particle instances
|
||||
foreach (var p in activeParticles)
|
||||
// Immediately return all active pooled instances (was Destroy). Returning is
|
||||
// idempotent and token-guarded, so a later timed RecycleFxRoutine for the same
|
||||
// instance becomes a no-op. Iterate a snapshot because ReturnFxInstance removes
|
||||
// the instance from activeParticles (mutating the list mid-enumeration would throw).
|
||||
var snapshot = activeParticles.ToArray();
|
||||
foreach (var p in snapshot)
|
||||
{
|
||||
if (p != null)
|
||||
{
|
||||
Destroy(p);
|
||||
ReturnFxInstance(p);
|
||||
}
|
||||
}
|
||||
activeParticles.Clear();
|
||||
@@ -235,8 +428,8 @@ public class AnimationController : MonoBehaviour
|
||||
}
|
||||
|
||||
SpawnJudgePrefabByTrackText(color);
|
||||
// Instantiate a copy of the scene object
|
||||
GameObject particleInstance = Instantiate(source, spawnPos, Quaternion.identity);
|
||||
// Rent a pooled copy of the scene particle source (was Instantiate + Destroy per hit).
|
||||
GameObject particleInstance = RentFxInstance(source, out var particleEntry, out int particleToken);
|
||||
if (particleInstance == null)
|
||||
{
|
||||
Debug.LogError("Failed to instantiate hit_particular_object");
|
||||
@@ -244,104 +437,67 @@ public class AnimationController : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// Place at spawn position, then optionally parent under the effect object so it
|
||||
// moves with UI/slot (worldPositionStays=true keeps world pos at spawnPos).
|
||||
particleInstance.transform.SetParent(parentTransform, false);
|
||||
particleInstance.transform.position = spawnPos;
|
||||
particleInstance.transform.rotation = Quaternion.identity;
|
||||
particleInstance.SetActive(true);
|
||||
|
||||
// Find all ParticleSystems on the instance (root + children) and set their startColor
|
||||
var systems = particleInstance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
var systems = particleEntry != null ? particleEntry.Systems : null;
|
||||
if (systems != null && systems.Length > 0)
|
||||
{
|
||||
float maxLifetime = 0f;
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
var main = ps.main;
|
||||
// Set start color (works for most setups)
|
||||
main.startColor = trackColor;
|
||||
// Play the system
|
||||
ps.Play();
|
||||
// determine lifetime (use constantMax for safety)
|
||||
var lifetime = main.startLifetime;
|
||||
float life = lifetime.constantMax;
|
||||
if (life <= 0f) life = lifetime.constant; // fallback
|
||||
if (life > maxLifetime) maxLifetime = life;
|
||||
}
|
||||
|
||||
// Optionally parent the instance under the effect object so it moves with UI/slot
|
||||
if (parentTransform != null)
|
||||
{
|
||||
particleInstance.transform.SetParent(parentTransform, true);
|
||||
}
|
||||
// Recycle after cached lifetime + small buffer (token-guarded).
|
||||
StartCoroutine(RecycleFxRoutine(particleInstance, particleToken, Mathf.Max(0.5f, particleEntry.Lifetime + 0.1f)));
|
||||
|
||||
// Destroy after max lifetime + small buffer
|
||||
Destroy(particleInstance, Mathf.Max(0.5f, maxLifetime + 0.1f));
|
||||
|
||||
// Track for immediate cleanup
|
||||
// Track for immediate cleanup on hold stop.
|
||||
activeParticles.Add(particleInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("No ParticleSystem components found on hit_particular_object instance");
|
||||
Destroy(particleInstance);
|
||||
ReturnFxInstance(particleInstance);
|
||||
TriggerAnimatorEffect(color);
|
||||
}
|
||||
|
||||
// NEW: instantiate and play ring effect (overlay) if assigned
|
||||
// Ring effect (overlay) if assigned.
|
||||
if (hit_ring_object != null)
|
||||
{
|
||||
GameObject ringInstance = Instantiate(hit_ring_object, spawnPos, Quaternion.identity);
|
||||
GameObject ringInstance = RentFxInstance(hit_ring_object, out var ringEntry, out int ringToken);
|
||||
if (ringInstance != null)
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"AnimationController: instantiated hit_ring_object '{ringInstance.name}' at {spawnPos}. parentTransform={(parentTransform != null ? parentTransform.name : "null")} ");
|
||||
|
||||
// Parent before activation to avoid transform surprises when Simulation Space = Local
|
||||
if (parentTransform != null)
|
||||
{
|
||||
ringInstance.transform.SetParent(parentTransform, false);
|
||||
// keep world position at spawnPos
|
||||
ringInstance.transform.position = spawnPos;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name);
|
||||
}
|
||||
|
||||
// Activate after parenting
|
||||
// Parent before activation to avoid transform surprises when Simulation Space = Local.
|
||||
ringInstance.transform.SetParent(parentTransform, false);
|
||||
ringInstance.transform.position = spawnPos;
|
||||
ringInstance.transform.rotation = Quaternion.identity;
|
||||
ringInstance.SetActive(true);
|
||||
|
||||
var ringSystems = ringInstance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0));
|
||||
var ringSystems = ringEntry != null ? ringEntry.Systems : null;
|
||||
if (ringSystems != null && ringSystems.Length > 0)
|
||||
{
|
||||
float ringMaxLife = 0f;
|
||||
foreach (var rps in ringSystems)
|
||||
{
|
||||
if (rps == null) continue;
|
||||
var rmain = rps.main;
|
||||
// log important runtime properties for debugging
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"ring PS: {rps.gameObject.name} simulationSpace={rmain.simulationSpace} startLifetime={rmain.startLifetime.constant} startColor={rmain.startColor.color}");
|
||||
|
||||
// reuse same track color
|
||||
rmain.startColor = trackColor;
|
||||
rps.Play();
|
||||
var rlifetime = rmain.startLifetime;
|
||||
float rlife = rlifetime.constantMax;
|
||||
if (rlife <= 0f) rlife = rlifetime.constant;
|
||||
if (rlife > ringMaxLife) ringMaxLife = rlife;
|
||||
|
||||
// Also log emission rate if available
|
||||
try
|
||||
{
|
||||
var emission = rps.emission;
|
||||
var rate = emission.rateOverTime;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"ring PS emission rateOverTime.constant (approx) = {rate.constant}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
Destroy(ringInstance, Mathf.Max(0.5f, ringMaxLife + 0.1f));
|
||||
|
||||
// Track for immediate cleanup
|
||||
StartCoroutine(RecycleFxRoutine(ringInstance, ringToken, Mathf.Max(0.5f, ringEntry.Lifetime + 0.1f)));
|
||||
activeParticles.Add(ringInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("hit_ring_object has no ParticleSystem components");
|
||||
Destroy(ringInstance);
|
||||
ReturnFxInstance(ringInstance);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -452,11 +608,18 @@ public class AnimationController : MonoBehaviour
|
||||
|
||||
if (prefabToUse != null)
|
||||
{
|
||||
// Documentation text normalized.
|
||||
GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity);
|
||||
// Rent a pooled copy of the judge-text prefab (was Instantiate + Destroy per hit).
|
||||
GameObject instance = RentFxInstance(prefabToUse, out _, out int judgeToken);
|
||||
if (instance == null)
|
||||
return;
|
||||
|
||||
// Documentation text normalized.
|
||||
Destroy(instance, 1.0f);
|
||||
instance.transform.SetParent(null, false);
|
||||
instance.transform.position = spawnTransform.position;
|
||||
instance.transform.rotation = Quaternion.identity;
|
||||
instance.SetActive(true);
|
||||
|
||||
// Same 1.0s visible lifetime as before, token-guarded recycle instead of Destroy.
|
||||
StartCoroutine(RecycleFxRoutine(instance, judgeToken, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,50 +636,45 @@ public class AnimationController : MonoBehaviour
|
||||
|
||||
private IEnumerator PrewarmCoroutine(int perTemplate)
|
||||
{
|
||||
// List of templates to warm
|
||||
// Templates that flow through PlayDestroyAnimation on every judge. Seeding the
|
||||
// FX pool here (instead of instantiate-then-destroy) means the first real hits
|
||||
// rent a ready instance rather than paying Instantiate + first-Play on the hot
|
||||
// path. CreateFxInstance already warms each instance's play path once.
|
||||
var templates = new List<GameObject>();
|
||||
if (hit_particular_object != null) templates.Add(hit_particular_object);
|
||||
if (hit_ring_object != null) templates.Add(hit_ring_object);
|
||||
if (perfect_judge_prefab != null) templates.Add(perfect_judge_prefab);
|
||||
if (great_judge_prefab != null) templates.Add(great_judge_prefab);
|
||||
if (good_judge_prefab != null) templates.Add(good_judge_prefab);
|
||||
if (miss_judge_prefab != null) templates.Add(miss_judge_prefab);
|
||||
|
||||
EnsureFxPoolRoot();
|
||||
|
||||
for (int i = 0; i < templates.Count; i++)
|
||||
{
|
||||
var prefab = templates[i];
|
||||
var template = templates[i];
|
||||
for (int j = 0; j < perTemplate; j++)
|
||||
{
|
||||
GameObject inst = null;
|
||||
try
|
||||
{
|
||||
inst = Instantiate(prefab, this.transform);
|
||||
inst.SetActive(true);
|
||||
var systems = inst.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (systems != null)
|
||||
inst = CreateFxInstance(template);
|
||||
if (inst != null)
|
||||
{
|
||||
foreach (var ps in systems)
|
||||
inst.SetActive(false);
|
||||
inst.transform.SetParent(fxPoolRoot, false);
|
||||
|
||||
if (!fxPools.TryGetValue(template, out var pool))
|
||||
{
|
||||
try { ps.Play(); } catch { }
|
||||
pool = new Queue<GameObject>();
|
||||
fxPools[template] = pool;
|
||||
}
|
||||
pool.Enqueue(inst);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// wait a frame to allow any internal initialization to run
|
||||
yield return null;
|
||||
|
||||
// stop and destroy the instance to free memory - the warmup work is done
|
||||
if (inst != null)
|
||||
{
|
||||
var systems = inst.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (systems != null)
|
||||
{
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
|
||||
}
|
||||
}
|
||||
Destroy(inst);
|
||||
}
|
||||
|
||||
// small yield to spread work across frames
|
||||
// spread work across frames to avoid a long load frame
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +504,11 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
var judgeFx = Animation_GenerateJudgementSituationPrefab.Instance
|
||||
?? SceneObjectLookupCache.FindAny<Animation_GenerateJudgementSituationPrefab>();
|
||||
judgeFx?.PrewarmJudgePrefabs(1);
|
||||
judgeFx?.PrewarmJudgePrefabs(5);
|
||||
|
||||
var trackHitFx = TrackJudgeHitEffectController.Instance
|
||||
?? SceneObjectLookupCache.FindAny<TrackJudgeHitEffectController>();
|
||||
trackHitFx?.PrewarmTrackParticles(10);
|
||||
|
||||
var fxEvent = effectEventController.Instance ?? SceneObjectLookupCache.FindAny<effectEventController>();
|
||||
if (fxEvent != null && fxEvent.isActiveAndEnabled)
|
||||
|
||||
@@ -43,6 +43,20 @@ public class HoldNote : BaseNote
|
||||
// whether this middle segment was held from the start (used for logic checks)
|
||||
private bool hasBeenHeldFromStart = false;
|
||||
|
||||
// Previous-frame held state for this track, used to derive key-down / key-up edges
|
||||
// from the platform-agnostic InputManager.IsTrackHeld table instead of polling
|
||||
// Input.GetKeyDown/Up directly (which touch cannot drive on Android).
|
||||
private bool prevTrackHeld = false;
|
||||
|
||||
// Query the shared held-state table (keyboard OR touch). Falls back to legacy
|
||||
// Input.GetKey if the InputManager is somehow absent so editor/standalone still works.
|
||||
private bool IsHeld()
|
||||
{
|
||||
var im = InputManager.Instance;
|
||||
if (im != null) return im.IsTrackHeld(trackIndex);
|
||||
return Input.GetKey(keyToPress);
|
||||
}
|
||||
|
||||
[Header("Judge configuration")]
|
||||
public NoteJudgeConfig judgeConfig; // judge windows configuration
|
||||
private NoteData noteData;
|
||||
@@ -56,9 +70,9 @@ public class HoldNote : BaseNote
|
||||
private List<UnityEngine.UI.Graphic> cachedUIComponents = new List<UnityEngine.UI.Graphic>();
|
||||
|
||||
[Header("Hold judgement adjustments")]
|
||||
[Tooltip("Multiplier applied to judgement windows for hold notes. >1 makes hold judgement more lenient (wider windows).")]
|
||||
[Range(1f, 2f)]
|
||||
public float holdWindowMultiplier = 1.3f;
|
||||
[Tooltip("Multiplier applied to judgement windows for hold notes. Lower values make hold judgement stricter.")]
|
||||
[Range(0.1f, 2f)]
|
||||
public float holdWindowMultiplier = 0.8f;
|
||||
|
||||
public float visualSpeedMultiplier = 1f; // injected from NoteSpawner to adapt windows when visual speed changes
|
||||
|
||||
@@ -434,6 +448,16 @@ public class HoldNote : BaseNote
|
||||
bool debugEnabled = JudgeManager.IsDebugEnabled;
|
||||
float now = Time.time;
|
||||
|
||||
// Sample held state and derive key-down/up edges at the very top, BEFORE any
|
||||
// early return. Unity's Input.GetKeyDown/Up are global per-frame edges that
|
||||
// don't depend on whether we polled last frame; updating prevTrackHeld every
|
||||
// frame reproduces that. If we only sampled after the early exits below, a key
|
||||
// already held when the note enters range would produce a false keyDown.
|
||||
bool keyHeld = IsHeld();
|
||||
bool keyDown = keyHeld && !prevTrackHeld;
|
||||
bool keyUp = !keyHeld && prevTrackHeld;
|
||||
prevTrackHeld = keyHeld;
|
||||
|
||||
// Optimization: Early exit if the note is far from judgment line and hasn't entered yet
|
||||
if (!hasEnteredLine)
|
||||
{
|
||||
@@ -448,10 +472,6 @@ public class HoldNote : BaseNote
|
||||
return;
|
||||
}
|
||||
|
||||
bool keyHeld = Input.GetKey(keyToPress);
|
||||
bool keyDown = Input.GetKeyDown(keyToPress);
|
||||
bool keyUp = Input.GetKeyUp(keyToPress);
|
||||
|
||||
// If start was judged and player is holding key, start hold effects
|
||||
if (!isHoldActive && jm != null && jm.IsStartJudged(noteID) && !jm.HasNoteReleased(noteID) && keyHeld)
|
||||
{
|
||||
@@ -694,8 +714,8 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
else if (segment == NoteSegment.Middle)
|
||||
{
|
||||
hasBeenHeldFromStart = Input.GetKey(keyToPress);
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] Middle enter: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
|
||||
hasBeenHeldFromStart = IsHeld();
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] Middle enter: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={IsHeld()}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
|
||||
|
||||
if (autoReturnCoroutine != null)
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
@@ -716,7 +736,7 @@ public class HoldNote : BaseNote
|
||||
|
||||
// If the key is still being held down when end segment enters judge zone, immediately judge.
|
||||
// Autoplay must ignore physical input to guarantee Perfect at scheduledEndTime.
|
||||
if (!GameConfig.autoPlayEnabled && Input.GetKey(keyToPress) && JudgeManager.Instance.IsStartJudged(noteID) && !isJudged)
|
||||
if (!GameConfig.autoPlayEnabled && IsHeld() && JudgeManager.Instance.IsStartJudged(noteID) && !isJudged)
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] End entered while key still held: {noteColor}, forcing Perfect");
|
||||
// Record release time as current time (player is still holding)
|
||||
@@ -980,6 +1000,11 @@ public class HoldNote : BaseNote
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound(result);
|
||||
}
|
||||
|
||||
if (result != "Miss")
|
||||
{
|
||||
TrackJudgeHitEffectController.PlayTrackHitFx(trackIndex);
|
||||
}
|
||||
|
||||
// Do not spawn the judgement animation prefab for Start (head) presses to avoid duplicated prefabs
|
||||
if (segment != NoteSegment.Start)
|
||||
{
|
||||
@@ -1152,6 +1177,11 @@ public class HoldNote : BaseNote
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
}
|
||||
|
||||
if (result != "Miss")
|
||||
{
|
||||
TrackJudgeHitEffectController.PlayTrackHitFx(trackIndex);
|
||||
}
|
||||
|
||||
// Ensure END always spawns judgement prefab (including Miss)
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
|
||||
|
||||
@@ -1295,7 +1325,7 @@ public class HoldNote : BaseNote
|
||||
// record release time and register release in JudgeManager
|
||||
if (!hasReleased)
|
||||
{
|
||||
if (Input.GetKey(keyToPress))
|
||||
if (IsHeld())
|
||||
{
|
||||
releaseTime = scheduledEndTime;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ using TMPro;
|
||||
using System;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Runs before default-order scripts so the per-track held-state table (trackHeld) is
|
||||
// updated from keyboard/touch this frame *before* hold notes read it. This preserves the
|
||||
// frame-accurate timing the old direct Input.GetKey polling had in HoldNote.
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public class InputManager : MonoBehaviour
|
||||
{
|
||||
public static InputManager Instance { get; private set; }
|
||||
@@ -42,6 +46,21 @@ public class InputManager : MonoBehaviour
|
||||
private KeyCode[] cachedKeys = new KeyCode[5];
|
||||
private bool pauseBlockedLastFrame = false;
|
||||
|
||||
// Per-track held state. Written by both keyboard (Update) and touch (PressTrack/
|
||||
// ReleaseTrack) so hold-note logic can query one platform-agnostic source instead
|
||||
// of polling Input.GetKey directly (which cannot be driven by touch on Android).
|
||||
private bool[] trackHeld = new bool[5];
|
||||
|
||||
/// <summary>
|
||||
/// True while the given track (0-4) is currently held, regardless of whether the
|
||||
/// source is keyboard or touch. Replaces direct Input.GetKey polling in hold notes.
|
||||
/// </summary>
|
||||
public bool IsTrackHeld(int trackIndex)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= trackHeld.Length) return false;
|
||||
return trackHeld[trackIndex];
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
@@ -111,45 +130,61 @@ public class InputManager : MonoBehaviour
|
||||
KeyCode key = cachedKeys[i];
|
||||
if (key == KeyCode.None) continue;
|
||||
|
||||
int index = i; // TrackColors array index matches track index logic here
|
||||
|
||||
if (Input.GetKeyDown(key))
|
||||
{
|
||||
OnKeyPressed?.Invoke(key);
|
||||
// Documentation text normalized.
|
||||
|
||||
// Update lane sprite alpha
|
||||
if (laneSprites != null && index >= 0 && index < laneSprites.Length)
|
||||
{
|
||||
SetSpriteAlpha(laneSprites[index], pressedAlpha);
|
||||
}
|
||||
|
||||
// Documentation text normalized.
|
||||
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
|
||||
{
|
||||
var txt = trackKeyTexts[index];
|
||||
if (txt != null)
|
||||
txt.color = keyActiveColor;
|
||||
}
|
||||
}
|
||||
PressTrack(i);
|
||||
if (Input.GetKeyUp(key))
|
||||
{
|
||||
OnKeyReleased?.Invoke(key);
|
||||
ReleaseTrack(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Update lane sprite alpha
|
||||
if (laneSprites != null && index >= 0 && index < laneSprites.Length)
|
||||
{
|
||||
SetSpriteAlpha(laneSprites[index], 0f);
|
||||
}
|
||||
/// <summary>
|
||||
/// Begin holding a track (0-4). Called by keyboard Update on key-down and by touch
|
||||
/// regions on pointer-down. Sets the held-state table, raises OnKeyPressed with the
|
||||
/// track's bound key so event-driven tap notes keep working, and updates lane visuals.
|
||||
/// </summary>
|
||||
public void PressTrack(int index)
|
||||
{
|
||||
if (index < 0 || index >= TrackColors.Length) return;
|
||||
if (trackHeld[index]) return; // already held; avoid duplicate press events
|
||||
|
||||
// Documentation text normalized.
|
||||
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
|
||||
{
|
||||
var txt = trackKeyTexts[index];
|
||||
if (txt != null)
|
||||
txt.color = keyInactiveColor;
|
||||
}
|
||||
}
|
||||
trackHeld[index] = true;
|
||||
|
||||
KeyCode key = cachedKeys[index];
|
||||
if (key != KeyCode.None)
|
||||
OnKeyPressed?.Invoke(key);
|
||||
|
||||
if (laneSprites != null && index < laneSprites.Length)
|
||||
SetSpriteAlpha(laneSprites[index], pressedAlpha);
|
||||
|
||||
if (trackKeyTexts != null && index < trackKeyTexts.Length)
|
||||
{
|
||||
var txt = trackKeyTexts[index];
|
||||
if (txt != null) txt.color = keyActiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Release a track (0-4). Called by keyboard Update on key-up and by touch regions
|
||||
/// on pointer-up. Clears the held-state table, raises OnKeyReleased, and resets visuals.
|
||||
/// </summary>
|
||||
public void ReleaseTrack(int index)
|
||||
{
|
||||
if (index < 0 || index >= TrackColors.Length) return;
|
||||
if (!trackHeld[index]) return; // not held; nothing to release
|
||||
|
||||
trackHeld[index] = false;
|
||||
|
||||
KeyCode key = cachedKeys[index];
|
||||
if (key != KeyCode.None)
|
||||
OnKeyReleased?.Invoke(key);
|
||||
|
||||
if (laneSprites != null && index < laneSprites.Length)
|
||||
SetSpriteAlpha(laneSprites[index], 0f);
|
||||
|
||||
if (trackKeyTexts != null && index < trackKeyTexts.Length)
|
||||
{
|
||||
var txt = trackKeyTexts[index];
|
||||
if (txt != null) txt.color = keyInactiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +204,7 @@ public class InputManager : MonoBehaviour
|
||||
for (int i = 0; i < TrackColors.Length; i++)
|
||||
{
|
||||
string color = TrackColors[i];
|
||||
trackHeld[i] = false; // clear held state so hold notes don't see a stale press after pause/clear
|
||||
KeyCode key = KeyBindingManager.GetKeyForColor(color);
|
||||
if (key == KeyCode.None) continue;
|
||||
try { OnKeyReleased?.Invoke(key); } catch { }
|
||||
|
||||
@@ -15,4 +15,4 @@ MonoBehaviour:
|
||||
perfectRange: 0.1
|
||||
greatRange: 0.15
|
||||
goodRange: 0.2
|
||||
missRange: 0.25
|
||||
missRange: 0.2
|
||||
|
||||
@@ -159,7 +159,11 @@ public class Note : BaseNote
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
anim = GetComponent<AnimationController>();
|
||||
anim = AnimationController.Global;
|
||||
if (anim == null)
|
||||
{
|
||||
anim = GetComponent<AnimationController>();
|
||||
}
|
||||
controller = GetComponent<NoteController>();
|
||||
}
|
||||
|
||||
@@ -298,6 +302,11 @@ public class Note : BaseNote
|
||||
effectEventController.TryTriggerMultiNoteShake();
|
||||
}
|
||||
|
||||
if (judgeResult != "Miss")
|
||||
{
|
||||
TrackJudgeHitEffectController.PlayTrackHitFx(TrackIndex);
|
||||
}
|
||||
|
||||
globalNoteEffect.TryTrigger(noteData != null ? noteData.noteFunction : null);
|
||||
|
||||
Judge();
|
||||
@@ -464,6 +473,11 @@ public class Note : BaseNote
|
||||
effectEventController.TryTriggerMultiNoteShake();
|
||||
}
|
||||
|
||||
if (judgeResult != "Miss")
|
||||
{
|
||||
TrackJudgeHitEffectController.PlayTrackHitFx(TrackIndex);
|
||||
}
|
||||
|
||||
globalNoteEffect.TryTrigger(noteData != null ? noteData.noteFunction : null);
|
||||
}
|
||||
|
||||
@@ -498,12 +512,14 @@ public class Note : BaseNote
|
||||
hasLock = false;
|
||||
}
|
||||
|
||||
ReturnToPool();
|
||||
if (anim != null)
|
||||
var animationController = AnimationController.Global ?? anim;
|
||||
if (animationController != null)
|
||||
{
|
||||
anim.PlayDestroyAnimation(noteColor);
|
||||
animationController.PlayDestroyAnimation(noteColor);
|
||||
}
|
||||
|
||||
ReturnToPool();
|
||||
|
||||
// notify global judge manager that this short note has been finally judged (hit)
|
||||
JudgeManager.Instance?.NotifyNoteJudged();
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@ public class NoteJudgeConfig : ScriptableObject
|
||||
[Tooltip("Good")]
|
||||
public float goodRange = 0.3f;
|
||||
[Tooltip("Miss")]
|
||||
public float missRange = 0.5f;
|
||||
public float missRange = 0.2f;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public class NotePool : MonoBehaviour
|
||||
var anim = AnimationController.Global ?? SceneObjectLookupCache.FindAny<AnimationController>();
|
||||
if (anim != null)
|
||||
{
|
||||
anim.PrewarmParticles(2);
|
||||
anim.PrewarmParticles(6);
|
||||
if (verboseLogging) Debug.Log("NotePool: requested AnimationController prewarm");
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,7 @@ public class NotePool : MonoBehaviour
|
||||
if (pool.Count > 0)
|
||||
{
|
||||
GameObject obj = pool.Pop();
|
||||
GamePlay.PoolItem pi = null;
|
||||
if (obj == null)
|
||||
{
|
||||
// Documentation text normalized.
|
||||
@@ -199,26 +200,26 @@ public class NotePool : MonoBehaviour
|
||||
else
|
||||
{
|
||||
// Documentation text normalized.
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi == null || prefab == null || pi.prefabName != prefab.name)
|
||||
{
|
||||
// Documentation text normalized.
|
||||
if (pi == null && verboseLogging) Debug.LogWarning("PoolItem missing on pooled object, replacing.");
|
||||
Destroy(obj);
|
||||
obj = InstantiateAndPrepare(prefab);
|
||||
pi = obj != null ? obj.GetComponent<GamePlay.PoolItem>() : null;
|
||||
}
|
||||
}
|
||||
obj.transform.SetParent(null);
|
||||
obj.SetActive(true);
|
||||
|
||||
// mark as taken from pool
|
||||
GamePlay.PoolItem takenPi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (takenPi != null)
|
||||
// mark as taken from pool (reuse the PoolItem already resolved above)
|
||||
if (pi != null)
|
||||
{
|
||||
takenPi.inPool = false;
|
||||
if (takenPi.initialLocalScaleCaptured)
|
||||
pi.inPool = false;
|
||||
if (pi.initialLocalScaleCaptured)
|
||||
{
|
||||
obj.transform.localScale = takenPi.initialLocalScale;
|
||||
obj.transform.localScale = pi.initialLocalScale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
private Dictionary<int, int> noteIndexToHoldId = new Dictionary<int, int>();
|
||||
|
||||
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
|
||||
private const float NoteSpeedDefault = 1f;
|
||||
private const float BaseTravelDistance = 10.75f;
|
||||
|
||||
private Coroutine spawnCoroutine;
|
||||
@@ -118,13 +119,25 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
EnsureDefaultNoteSpeedPreference();
|
||||
// Load saved visual speed multiplier before any spawning logic uses it
|
||||
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, speedMultiplier);
|
||||
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
|
||||
saved = Mathf.Clamp(saved, SpeedMultiplierMin, SpeedMultiplierMax);
|
||||
speedMultiplier = saved;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[NoteSpawner] Loaded speedMultiplier={speedMultiplier} from PlayerPrefs");
|
||||
}
|
||||
|
||||
private static void EnsureDefaultNoteSpeedPreference()
|
||||
{
|
||||
if (PlayerPrefs.HasKey(NoteSpeedPrefKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerPrefs.SetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Documentation text normalized.
|
||||
@@ -429,7 +442,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
|
||||
int segmentCount = Mathf.CeilToInt(noteData.length / segmentInterval);
|
||||
if (segmentCount < 1) segmentCount = 1; // Documentation text normalized.
|
||||
if (segmentCount < 2) segmentCount = 2; // Force at least one middle segment between start and end.
|
||||
float actualSegmentInterval = segmentCount > 0 ? noteData.length / segmentCount : segmentInterval;
|
||||
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
|
||||
@@ -0,0 +1,996 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using KD.Destro2D;
|
||||
using UnityEngine;
|
||||
|
||||
public class TrackCrashController : MonoBehaviour
|
||||
{
|
||||
public static TrackCrashController Instance { get; private set; }
|
||||
|
||||
public enum TrackCrashShape
|
||||
{
|
||||
Auto,
|
||||
Radial,
|
||||
RectCut
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class TrackEntry
|
||||
{
|
||||
public int trackIndex;
|
||||
public GameObject trackObject;
|
||||
public bool hideOriginalWhenCrashed = true;
|
||||
public float restoreDelay = 0f;
|
||||
}
|
||||
|
||||
[Header("Track Crash")]
|
||||
[SerializeField] private List<TrackEntry> tracks = new List<TrackEntry>(5);
|
||||
[SerializeField] private bool useDestro2D = true;
|
||||
[SerializeField] private bool autoRestoreOnTrackCrash = false;
|
||||
[SerializeField] private float defaultRestoreDelay = 0f;
|
||||
[SerializeField] private bool enableDebugKeyTrigger = true;
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
[SerializeField] private bool useAutoFractureSize = true;
|
||||
[SerializeField] private float fractureCoreRadius = 0.05f;
|
||||
[SerializeField] private float fractureOuterRadius = 0.45f;
|
||||
[SerializeField] private float fractureOuterRadiusScale = 1.1f;
|
||||
[SerializeField] private float fractureNoiseScale = 10f;
|
||||
[SerializeField] private float fractureThickness = 0.12f;
|
||||
[SerializeField] private int fractureLines = 8;
|
||||
[SerializeField] private TrackCrashShape trackCrashShape = TrackCrashShape.Auto;
|
||||
[SerializeField] private float rectCutOversize = 1.15f;
|
||||
[SerializeField] private float chunkDetectDelay = 0.08f;
|
||||
[SerializeField] private int rectCutCount = 3;
|
||||
[SerializeField] private float rectCutBandScale = 0.05f;
|
||||
[SerializeField] private float rectCutPositionSpan = 0.65f;
|
||||
[SerializeField] private int rectCrossCutCount = 1;
|
||||
[SerializeField] private float rectCrossCutBandScale = 0.22f;
|
||||
[SerializeField] private float chunkExplodeForce = 2.4f;
|
||||
[SerializeField] private float chunkExplodeTorque = 30f;
|
||||
[SerializeField] private Vector2 chunkWindDirection = Vector2.zero;
|
||||
[SerializeField] private float chunkWindForce = 0f;
|
||||
[SerializeField] private Vector2 chunkGravityDirection = Vector2.down;
|
||||
[SerializeField] private bool keepDestro2DBaseTrackVisible = true;
|
||||
[SerializeField] private bool keepChunksInPlace = true;
|
||||
[SerializeField] private bool disableChunkGravity = true;
|
||||
[SerializeField] private bool autoTuneSplitHandler = true;
|
||||
[SerializeField] private int splitHandlerMaxChunkCount = 12;
|
||||
[SerializeField] private int splitHandlerMinCount = 3;
|
||||
[SerializeField] private bool manualChunkMotion = true;
|
||||
[SerializeField] private float chunkGravityStrength = 6f;
|
||||
[SerializeField] private float chunkLinearDamping = 4f;
|
||||
[SerializeField] private float chunkAngularDamping = 5f;
|
||||
[SerializeField] private float chunkMotionDuration = 0.45f;
|
||||
[SerializeField] private bool useRuntimeSpriteFallback = true;
|
||||
[SerializeField] private int runtimeFallbackColumns = 3;
|
||||
[SerializeField] private int runtimeFallbackRows = 12;
|
||||
[SerializeField] private int runtimeFallbackMinVisiblePieces = 6;
|
||||
[SerializeField] private float runtimeFallbackRandomOffset = 0.015f;
|
||||
|
||||
private readonly Dictionary<int, TrackEntry> trackMap = new Dictionary<int, TrackEntry>();
|
||||
private readonly HashSet<int> crashedTracks = new HashSet<int>();
|
||||
private readonly HashSet<int> processingTracks = new HashSet<int>();
|
||||
private readonly Dictionary<int, Coroutine> restoreRoutines = new Dictionary<int, Coroutine>();
|
||||
private readonly Dictionary<int, GameObject> fractureProxies = new Dictionary<int, GameObject>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else if (Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
RebuildCache();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
return;
|
||||
|
||||
RebuildCache();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!enableDebugKeyTrigger)
|
||||
return;
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[TrackCrash] Debug key 1 pressed.");
|
||||
TriggerTrackCrash(0);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[TrackCrash] Debug key 2 pressed.");
|
||||
TriggerTrackCrash(1);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[TrackCrash] Debug key 3 pressed.");
|
||||
TriggerTrackCrash(2);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[TrackCrash] Debug key 4 pressed.");
|
||||
TriggerTrackCrash(3);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[TrackCrash] Debug key 5 pressed.");
|
||||
TriggerTrackCrash(4);
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildCache()
|
||||
{
|
||||
trackMap.Clear();
|
||||
for (int i = 0; i < tracks.Count; i++)
|
||||
{
|
||||
TrackEntry entry = tracks[i];
|
||||
if (entry == null || entry.trackObject == null)
|
||||
continue;
|
||||
trackMap[entry.trackIndex] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
public static void TrackCrash(int trackIndex)
|
||||
{
|
||||
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackCrashController>();
|
||||
if (controller == null)
|
||||
return;
|
||||
|
||||
controller.TriggerTrackCrash(trackIndex);
|
||||
}
|
||||
|
||||
public static void RestoreTrack(int trackIndex)
|
||||
{
|
||||
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackCrashController>();
|
||||
if (controller == null)
|
||||
return;
|
||||
|
||||
controller.RestoreTrackInternal(trackIndex);
|
||||
}
|
||||
|
||||
public void TriggerTrackCrash(int trackIndex)
|
||||
{
|
||||
if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null)
|
||||
{
|
||||
if (debugLogs) Debug.LogWarning($"[TrackCrash] Missing track entry or track object for index {trackIndex}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (crashedTracks.Contains(trackIndex))
|
||||
{
|
||||
if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already crashed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (processingTracks.Contains(trackIndex))
|
||||
{
|
||||
if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already processing.");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(TriggerTrackCrashRoutine(trackIndex, entry));
|
||||
}
|
||||
|
||||
private IEnumerator TriggerTrackCrashRoutine(int trackIndex, TrackEntry entry)
|
||||
{
|
||||
processingTracks.Add(trackIndex);
|
||||
try
|
||||
{
|
||||
DestroyFractureProxy(trackIndex);
|
||||
|
||||
bool fractureSucceeded = !useDestro2D;
|
||||
SplitHandler splitHandler = null;
|
||||
int chunkCountBefore = -1;
|
||||
int chunkCountAfter = -1;
|
||||
float outerRadius = fractureOuterRadius;
|
||||
bool waitForChunkDetection = false;
|
||||
bool usedDestro2D = false;
|
||||
bool usedRuntimeSpriteFallback = false;
|
||||
Vector2 fractureCenter = entry.trackObject.transform.position;
|
||||
Destro2DMain resolvedDestro = null;
|
||||
SpriteRenderer resolvedSpriteRenderer = null;
|
||||
TrackCrashShape resolvedShape = TrackCrashShape.Radial;
|
||||
GameObject fractureTarget = entry.trackObject;
|
||||
|
||||
if (useDestro2D)
|
||||
{
|
||||
fractureTarget = CreateFractureProxy(trackIndex, entry);
|
||||
var destro = fractureTarget != null ? fractureTarget.GetComponent<KD.Destro2D.Destro2DMain>() : null;
|
||||
if (destro == null)
|
||||
{
|
||||
destro = fractureTarget != null ? fractureTarget.GetComponentInChildren<KD.Destro2D.Destro2DMain>(true) : null;
|
||||
}
|
||||
|
||||
if (destro != null)
|
||||
{
|
||||
usedDestro2D = true;
|
||||
resolvedDestro = destro;
|
||||
if (debugLogs) Debug.Log($"[TrackCrash] Fracturing track {trackIndex} on {fractureTarget.name}.");
|
||||
try
|
||||
{
|
||||
splitHandler = destro.GetComponent<SplitHandler>();
|
||||
TuneSplitHandler(splitHandler);
|
||||
chunkCountBefore = splitHandler != null ? splitHandler.splitChunks.Count : -1;
|
||||
var spriteRenderer = ResolveDestroSpriteRenderer(destro, fractureTarget);
|
||||
resolvedSpriteRenderer = spriteRenderer;
|
||||
resolvedShape = ResolveCrashShape(spriteRenderer);
|
||||
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
fractureCenter = spriteRenderer.bounds.center;
|
||||
if (useAutoFractureSize)
|
||||
{
|
||||
float extent = Mathf.Max(spriteRenderer.bounds.extents.x, spriteRenderer.bounds.extents.y);
|
||||
outerRadius = Mathf.Max(fractureOuterRadius, extent * fractureOuterRadiusScale);
|
||||
}
|
||||
}
|
||||
|
||||
TriggerDestro2D(destro, spriteRenderer, fractureCenter, outerRadius);
|
||||
waitForChunkDetection = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
fractureSucceeded = false;
|
||||
if (debugLogs) Debug.LogError($"[TrackCrash] Fracture failed for track {trackIndex}: {ex}");
|
||||
}
|
||||
}
|
||||
else if (debugLogs)
|
||||
{
|
||||
Debug.LogWarning($"[TrackCrash] No Destro2DMain found on track {trackIndex} object {(fractureTarget != null ? fractureTarget.name : "NULL")} or its children.");
|
||||
}
|
||||
}
|
||||
|
||||
if (waitForChunkDetection)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay));
|
||||
chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1;
|
||||
fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore;
|
||||
|
||||
if (!fractureSucceeded && resolvedDestro != null && resolvedSpriteRenderer != null && resolvedShape == TrackCrashShape.RectCut)
|
||||
{
|
||||
if (debugLogs)
|
||||
{
|
||||
Debug.Log($"[TrackCrash] Track {trackIndex} primary cut did not split. Applying aggressive fallback cut.");
|
||||
}
|
||||
|
||||
ApplyAggressiveRectCut(resolvedDestro, resolvedSpriteRenderer, fractureCenter);
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay));
|
||||
chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1;
|
||||
fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore;
|
||||
}
|
||||
|
||||
if (fractureSucceeded && splitHandler != null)
|
||||
{
|
||||
int visibleChunkCount = CountLiveChunks(splitHandler);
|
||||
if (visibleChunkCount < 2)
|
||||
{
|
||||
fractureSucceeded = false;
|
||||
if (debugLogs)
|
||||
{
|
||||
Debug.Log($"[TrackCrash] Track {trackIndex} Destro2D only produced {visibleChunkCount} visible chunk(s); switching to runtime sprite fallback.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fractureSucceeded && splitHandler != null)
|
||||
{
|
||||
PrepareChunksForFinalState(splitHandler, fractureCenter);
|
||||
}
|
||||
if (debugLogs)
|
||||
{
|
||||
Debug.Log($"[TrackCrash] Track {trackIndex} chunk count before={chunkCountBefore}, after={chunkCountAfter}, success={fractureSucceeded}, outerRadius={outerRadius}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!fractureSucceeded && useRuntimeSpriteFallback)
|
||||
{
|
||||
DestroyFractureProxy(trackIndex);
|
||||
fractureTarget = CreateRuntimeSpriteFallbackProxy(trackIndex, entry, out int pieceCount);
|
||||
fractureSucceeded = fractureTarget != null && pieceCount >= runtimeFallbackMinVisiblePieces;
|
||||
usedRuntimeSpriteFallback = fractureSucceeded;
|
||||
if (debugLogs)
|
||||
{
|
||||
Debug.Log($"[TrackCrash] Track {trackIndex} runtime sprite fallback pieces={pieceCount}, success={fractureSucceeded}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!fractureSucceeded)
|
||||
{
|
||||
DestroyFractureProxy(trackIndex);
|
||||
yield break;
|
||||
}
|
||||
|
||||
crashedTracks.Add(trackIndex);
|
||||
|
||||
bool shouldHideOriginalObject = entry.hideOriginalWhenCrashed;
|
||||
if (usedDestro2D)
|
||||
{
|
||||
shouldHideOriginalObject = true;
|
||||
}
|
||||
|
||||
if (shouldHideOriginalObject)
|
||||
{
|
||||
entry.trackObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (usedDestro2D && !usedRuntimeSpriteFallback && fractureTarget != null)
|
||||
{
|
||||
if (keepDestro2DBaseTrackVisible)
|
||||
{
|
||||
fractureTarget.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
fractureTarget.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (usedRuntimeSpriteFallback && fractureTarget != null)
|
||||
{
|
||||
fractureTarget.SetActive(true);
|
||||
}
|
||||
|
||||
if (autoRestoreOnTrackCrash)
|
||||
{
|
||||
float delay = entry.restoreDelay > 0f ? entry.restoreDelay : defaultRestoreDelay;
|
||||
if (delay > 0f)
|
||||
{
|
||||
if (restoreRoutines.TryGetValue(trackIndex, out var routine) && routine != null)
|
||||
{
|
||||
StopCoroutine(routine);
|
||||
}
|
||||
restoreRoutines[trackIndex] = StartCoroutine(RestoreAfterDelay(trackIndex, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
processingTracks.Remove(trackIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerDestro2D(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, float outerRadius)
|
||||
{
|
||||
if (destro == null)
|
||||
return;
|
||||
|
||||
TrackCrashShape shape = ResolveCrashShape(spriteRenderer);
|
||||
if (shape == TrackCrashShape.RectCut)
|
||||
{
|
||||
ApplyRectCut(destro, spriteRenderer, fractureCenter);
|
||||
return;
|
||||
}
|
||||
|
||||
destro.DynamicFracture(fractureCenter, fractureCoreRadius, outerRadius, fractureNoiseScale, fractureThickness, fractureLines);
|
||||
}
|
||||
|
||||
private TrackCrashShape ResolveCrashShape(SpriteRenderer spriteRenderer)
|
||||
{
|
||||
if (trackCrashShape != TrackCrashShape.Auto)
|
||||
return trackCrashShape;
|
||||
|
||||
if (spriteRenderer == null)
|
||||
return TrackCrashShape.Radial;
|
||||
|
||||
Vector3 size = spriteRenderer.bounds.size;
|
||||
return size.y > size.x * 2f || size.x > size.y * 2f
|
||||
? TrackCrashShape.RectCut
|
||||
: TrackCrashShape.Radial;
|
||||
}
|
||||
|
||||
private void ApplyRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
|
||||
{
|
||||
ApplyRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(1, rectCutCount), Mathf.Clamp01(rectCutPositionSpan), Mathf.Max(0.005f, rectCutBandScale));
|
||||
ApplyCrossRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(0, rectCrossCutCount), Mathf.Max(0.01f, rectCrossCutBandScale));
|
||||
}
|
||||
|
||||
private void ApplyAggressiveRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
|
||||
{
|
||||
ApplyRectCutInternal(
|
||||
destro,
|
||||
spriteRenderer,
|
||||
fractureCenter,
|
||||
Mathf.Max(3, rectCutCount + 2),
|
||||
Mathf.Max(0.75f, Mathf.Clamp01(rectCutPositionSpan)),
|
||||
Mathf.Max(0.08f, rectCutBandScale));
|
||||
|
||||
ApplyCrossRectCutInternal(
|
||||
destro,
|
||||
spriteRenderer,
|
||||
fractureCenter,
|
||||
Mathf.Max(1, rectCrossCutCount + 1),
|
||||
Mathf.Max(0.12f, rectCrossCutBandScale));
|
||||
}
|
||||
|
||||
private void ApplyRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float span, float bandScale)
|
||||
{
|
||||
if (destro == null || spriteRenderer == null)
|
||||
return;
|
||||
|
||||
Vector3 extents = spriteRenderer.bounds.extents;
|
||||
bool verticalTrack = extents.y >= extents.x;
|
||||
|
||||
for (int i = 0; i < cutCount; i++)
|
||||
{
|
||||
float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1);
|
||||
float offsetNormalized = Mathf.Lerp(-span, span, t);
|
||||
Vector2 cutPosition = fractureCenter;
|
||||
RectDestruction rect = new RectDestruction();
|
||||
|
||||
if (verticalTrack)
|
||||
{
|
||||
cutPosition.y += extents.y * offsetNormalized;
|
||||
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
|
||||
rect.b = Mathf.Max(fractureThickness, extents.y * bandScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
cutPosition.x += extents.x * offsetNormalized;
|
||||
rect.l = Mathf.Max(fractureThickness, extents.x * bandScale);
|
||||
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
|
||||
}
|
||||
|
||||
rect.Setup(destro.gameObject);
|
||||
destro.DynamicDestroyWorld(cutPosition, new List<Destruction> { rect });
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLongitudinalCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
|
||||
{
|
||||
if (destro == null || spriteRenderer == null)
|
||||
return;
|
||||
|
||||
Vector3 extents = spriteRenderer.bounds.extents;
|
||||
bool verticalTrack = extents.y >= extents.x;
|
||||
RectDestruction rect = new RectDestruction();
|
||||
|
||||
if (verticalTrack)
|
||||
{
|
||||
rect.l = Mathf.Max(fractureThickness * 0.6f, extents.x * 0.1f);
|
||||
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
|
||||
}
|
||||
else
|
||||
{
|
||||
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
|
||||
rect.b = Mathf.Max(fractureThickness * 0.6f, extents.y * 0.1f);
|
||||
}
|
||||
|
||||
rect.Setup(destro.gameObject);
|
||||
destro.DynamicDestroyWorld(fractureCenter, new List<Destruction> { rect });
|
||||
}
|
||||
|
||||
private void ApplyCrossRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float bandScale)
|
||||
{
|
||||
if (destro == null || spriteRenderer == null || cutCount <= 0)
|
||||
return;
|
||||
|
||||
Vector3 extents = spriteRenderer.bounds.extents;
|
||||
bool verticalTrack = extents.y >= extents.x;
|
||||
float usableSpan = 0.6f;
|
||||
|
||||
for (int i = 0; i < cutCount; i++)
|
||||
{
|
||||
float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1);
|
||||
float offsetNormalized = Mathf.Lerp(-usableSpan, usableSpan, t);
|
||||
Vector2 cutPosition = fractureCenter;
|
||||
RectDestruction rect = new RectDestruction();
|
||||
|
||||
if (verticalTrack)
|
||||
{
|
||||
cutPosition.x += extents.x * offsetNormalized;
|
||||
rect.l = Mathf.Max(fractureThickness, extents.x * bandScale);
|
||||
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
|
||||
}
|
||||
else
|
||||
{
|
||||
cutPosition.y += extents.y * offsetNormalized;
|
||||
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
|
||||
rect.b = Mathf.Max(fractureThickness, extents.y * bandScale);
|
||||
}
|
||||
|
||||
rect.Setup(destro.gameObject);
|
||||
destro.DynamicDestroyWorld(cutPosition, new List<Destruction> { rect });
|
||||
}
|
||||
}
|
||||
|
||||
private void TuneSplitHandler(SplitHandler splitHandler)
|
||||
{
|
||||
if (!autoTuneSplitHandler || splitHandler == null)
|
||||
return;
|
||||
|
||||
splitHandler.MaxChunkCount = Mathf.Max(splitHandler.MaxChunkCount, splitHandlerMaxChunkCount);
|
||||
splitHandler.minCount = Mathf.Max(1, Mathf.Min(splitHandler.minCount, splitHandlerMinCount));
|
||||
}
|
||||
|
||||
private SpriteRenderer ResolveDestroSpriteRenderer(Destro2DMain destro, GameObject fallbackObject)
|
||||
{
|
||||
if (destro != null)
|
||||
{
|
||||
SpriteRenderer directRenderer = destro.GetComponent<SpriteRenderer>();
|
||||
if (directRenderer != null)
|
||||
return directRenderer;
|
||||
|
||||
SpriteRenderer childRenderer = destro.GetComponentInChildren<SpriteRenderer>(true);
|
||||
if (childRenderer != null)
|
||||
return childRenderer;
|
||||
}
|
||||
|
||||
if (fallbackObject != null)
|
||||
{
|
||||
SpriteRenderer entryRenderer = fallbackObject.GetComponent<SpriteRenderer>();
|
||||
if (entryRenderer != null)
|
||||
return entryRenderer;
|
||||
|
||||
return fallbackObject.GetComponentInChildren<SpriteRenderer>(true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void PushChunksAway(SplitHandler splitHandler, Vector2 fractureCenter)
|
||||
{
|
||||
if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0)
|
||||
return;
|
||||
|
||||
Vector2 normalizedWindDirection = chunkWindDirection.sqrMagnitude > 0.0001f
|
||||
? chunkWindDirection.normalized
|
||||
: Vector2.zero;
|
||||
|
||||
foreach (GameObject chunk in splitHandler.splitChunks)
|
||||
{
|
||||
if (chunk == null)
|
||||
continue;
|
||||
|
||||
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
|
||||
SplitHandler chunkSplitHandler = chunk.GetComponent<SplitHandler>();
|
||||
if (rb == null)
|
||||
continue;
|
||||
|
||||
Vector2 worldChunkCenter = chunk.transform.position;
|
||||
if (chunkSplitHandler != null)
|
||||
{
|
||||
worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre);
|
||||
}
|
||||
|
||||
Vector2 direction = worldChunkCenter - fractureCenter;
|
||||
if (direction.sqrMagnitude < 0.0001f)
|
||||
{
|
||||
direction = UnityEngine.Random.insideUnitCircle.normalized;
|
||||
}
|
||||
|
||||
Vector2 forceVector = direction.normalized * chunkExplodeForce;
|
||||
if (normalizedWindDirection != Vector2.zero && chunkWindForce > 0f)
|
||||
{
|
||||
forceVector += normalizedWindDirection * chunkWindForce;
|
||||
}
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
rb.AddForce(forceVector, ForceMode2D.Impulse);
|
||||
|
||||
if (chunkExplodeTorque > 0f)
|
||||
{
|
||||
float torqueSign = UnityEngine.Random.value > 0.5f ? 1f : -1f;
|
||||
rb.AddTorque(chunkExplodeTorque * torqueSign, ForceMode2D.Impulse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareChunksForFinalState(SplitHandler splitHandler, Vector2 fractureCenter)
|
||||
{
|
||||
if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0)
|
||||
return;
|
||||
|
||||
if (manualChunkMotion)
|
||||
{
|
||||
StartCoroutine(DriveChunksManually(splitHandler, fractureCenter));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keepChunksInPlace)
|
||||
{
|
||||
PushChunksAway(splitHandler, fractureCenter);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (GameObject chunk in splitHandler.splitChunks)
|
||||
{
|
||||
if (chunk == null)
|
||||
continue;
|
||||
|
||||
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
continue;
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
|
||||
if (disableChunkGravity)
|
||||
{
|
||||
rb.gravityScale = 0f;
|
||||
}
|
||||
|
||||
rb.constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation;
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator RestoreAfterDelay(int trackIndex, float delay)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
RestoreTrackInternal(trackIndex);
|
||||
restoreRoutines.Remove(trackIndex);
|
||||
processingTracks.Remove(trackIndex);
|
||||
}
|
||||
|
||||
private void RestoreTrackInternal(int trackIndex)
|
||||
{
|
||||
if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null)
|
||||
return;
|
||||
|
||||
DestroyFractureProxy(trackIndex);
|
||||
|
||||
if (entry.trackObject != null)
|
||||
{
|
||||
entry.trackObject.SetActive(true);
|
||||
}
|
||||
|
||||
crashedTracks.Remove(trackIndex);
|
||||
}
|
||||
|
||||
public void RebuildFromSceneLookup()
|
||||
{
|
||||
RebuildCache();
|
||||
}
|
||||
|
||||
private IEnumerator DriveChunksManually(SplitHandler splitHandler, Vector2 fractureCenter)
|
||||
{
|
||||
List<Rigidbody2D> rigidbodies = new List<Rigidbody2D>();
|
||||
List<Vector2> velocities = new List<Vector2>();
|
||||
List<float> angularVelocities = new List<float>();
|
||||
Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero;
|
||||
Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f
|
||||
? Vector2.zero
|
||||
: chunkGravityDirection.normalized;
|
||||
|
||||
foreach (GameObject chunk in splitHandler.splitChunks)
|
||||
{
|
||||
if (chunk == null)
|
||||
continue;
|
||||
|
||||
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
continue;
|
||||
|
||||
SplitHandler chunkSplitHandler = chunk.GetComponent<SplitHandler>();
|
||||
Vector2 worldChunkCenter = chunk.transform.position;
|
||||
if (chunkSplitHandler != null)
|
||||
{
|
||||
worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre);
|
||||
}
|
||||
|
||||
Vector2 radialDirection = worldChunkCenter - fractureCenter;
|
||||
if (radialDirection.sqrMagnitude < 0.0001f)
|
||||
{
|
||||
radialDirection = UnityEngine.Random.insideUnitCircle.normalized;
|
||||
}
|
||||
|
||||
Vector2 velocity = Vector2.zero;
|
||||
if (!keepChunksInPlace)
|
||||
{
|
||||
velocity += radialDirection.normalized * chunkExplodeForce;
|
||||
velocity += normalizedWind * chunkWindForce;
|
||||
}
|
||||
|
||||
rigidbodies.Add(rb);
|
||||
velocities.Add(velocity);
|
||||
angularVelocities.Add(UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque));
|
||||
|
||||
rb.gravityScale = 0f;
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
}
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < chunkMotionDuration)
|
||||
{
|
||||
float dt = Time.deltaTime;
|
||||
for (int i = 0; i < rigidbodies.Count; i++)
|
||||
{
|
||||
Rigidbody2D rb = rigidbodies[i];
|
||||
if (rb == null)
|
||||
continue;
|
||||
|
||||
Vector2 velocity = velocities[i];
|
||||
float angularVelocity = angularVelocities[i];
|
||||
|
||||
velocity += normalizedGravity * chunkGravityStrength * dt;
|
||||
velocity += normalizedWind * chunkWindForce * dt;
|
||||
|
||||
velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt));
|
||||
angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt));
|
||||
|
||||
rb.position += velocity * dt;
|
||||
rb.rotation += angularVelocity * dt;
|
||||
|
||||
velocities[i] = velocity;
|
||||
angularVelocities[i] = angularVelocity;
|
||||
}
|
||||
|
||||
elapsed += dt;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rigidbodies.Count; i++)
|
||||
{
|
||||
if (rigidbodies[i] == null)
|
||||
continue;
|
||||
|
||||
rigidbodies[i].linearVelocity = Vector2.zero;
|
||||
rigidbodies[i].angularVelocity = 0f;
|
||||
if (keepChunksInPlace)
|
||||
{
|
||||
rigidbodies[i].constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject CreateFractureProxy(int trackIndex, TrackEntry entry)
|
||||
{
|
||||
if (entry == null || entry.trackObject == null)
|
||||
return null;
|
||||
|
||||
GameObject proxy = Instantiate(entry.trackObject, entry.trackObject.transform.parent);
|
||||
proxy.name = entry.trackObject.name + "_FractureProxy";
|
||||
proxy.transform.position = entry.trackObject.transform.position;
|
||||
proxy.transform.rotation = entry.trackObject.transform.rotation;
|
||||
proxy.transform.localScale = entry.trackObject.transform.localScale;
|
||||
fractureProxies[trackIndex] = proxy;
|
||||
return proxy;
|
||||
}
|
||||
|
||||
private void DestroyFractureProxy(int trackIndex)
|
||||
{
|
||||
if (!fractureProxies.TryGetValue(trackIndex, out GameObject proxy))
|
||||
return;
|
||||
|
||||
fractureProxies.Remove(trackIndex);
|
||||
if (proxy != null)
|
||||
{
|
||||
Destroy(proxy);
|
||||
}
|
||||
}
|
||||
|
||||
private int CountLiveChunks(SplitHandler splitHandler)
|
||||
{
|
||||
if (splitHandler == null || splitHandler.splitChunks == null)
|
||||
return 0;
|
||||
|
||||
int count = 0;
|
||||
foreach (GameObject chunk in splitHandler.splitChunks)
|
||||
{
|
||||
if (chunk == null || !chunk.activeInHierarchy)
|
||||
continue;
|
||||
|
||||
SpriteRenderer spriteRenderer = chunk.GetComponent<SpriteRenderer>();
|
||||
if (spriteRenderer == null || spriteRenderer.sprite == null)
|
||||
continue;
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private GameObject CreateRuntimeSpriteFallbackProxy(int trackIndex, TrackEntry entry, out int pieceCount)
|
||||
{
|
||||
pieceCount = 0;
|
||||
if (entry == null || entry.trackObject == null)
|
||||
return null;
|
||||
|
||||
SpriteRenderer sourceRenderer = ResolveDestroSpriteRenderer(null, entry.trackObject);
|
||||
if (sourceRenderer == null || sourceRenderer.sprite == null || sourceRenderer.sprite.texture == null)
|
||||
return null;
|
||||
|
||||
Sprite sourceSprite = sourceRenderer.sprite;
|
||||
Rect sourceRect = sourceSprite.rect;
|
||||
if (sourceRect.width < 2f || sourceRect.height < 2f)
|
||||
return null;
|
||||
|
||||
int columns = Mathf.Max(1, runtimeFallbackColumns);
|
||||
int rows = Mathf.Max(1, runtimeFallbackRows);
|
||||
|
||||
float aspect = sourceRect.height / Mathf.Max(1f, sourceRect.width);
|
||||
if (aspect > 2f)
|
||||
{
|
||||
rows = Mathf.Max(rows, Mathf.CeilToInt(aspect * columns * 1.75f));
|
||||
}
|
||||
|
||||
GameObject proxy = new GameObject(entry.trackObject.name + "_RuntimeFractureProxy");
|
||||
proxy.transform.SetParent(entry.trackObject.transform.parent, false);
|
||||
proxy.transform.position = sourceRenderer.transform.position;
|
||||
proxy.transform.rotation = sourceRenderer.transform.rotation;
|
||||
proxy.transform.localScale = sourceRenderer.transform.lossyScale;
|
||||
fractureProxies[trackIndex] = proxy;
|
||||
|
||||
TrackCrashRuntimeSpriteCleanup cleanup = proxy.AddComponent<TrackCrashRuntimeSpriteCleanup>();
|
||||
List<Transform> pieceTransforms = new List<Transform>();
|
||||
List<Vector2> pieceVelocities = new List<Vector2>();
|
||||
List<float> pieceAngularVelocities = new List<float>();
|
||||
|
||||
float pixelsPerUnit = Mathf.Max(1f, sourceSprite.pixelsPerUnit);
|
||||
float minLocalX = -sourceSprite.pivot.x / pixelsPerUnit;
|
||||
float minLocalY = -sourceSprite.pivot.y / pixelsPerUnit;
|
||||
|
||||
for (int row = 0; row < rows; row++)
|
||||
{
|
||||
float normalizedY0 = (float)row / rows;
|
||||
float normalizedY1 = (float)(row + 1) / rows;
|
||||
float yMin = sourceRect.y + (sourceRect.height * normalizedY0);
|
||||
float yMax = sourceRect.y + (sourceRect.height * normalizedY1);
|
||||
yMin = Mathf.Clamp(yMin, sourceRect.y, sourceRect.yMax - 1f);
|
||||
yMax = Mathf.Clamp(yMax, yMin + 1f, sourceRect.yMax);
|
||||
|
||||
for (int col = 0; col < columns; col++)
|
||||
{
|
||||
float normalizedX0 = (float)col / columns;
|
||||
float normalizedX1 = (float)(col + 1) / columns;
|
||||
float xMin = sourceRect.x + (sourceRect.width * normalizedX0);
|
||||
float xMax = sourceRect.x + (sourceRect.width * normalizedX1);
|
||||
xMin = Mathf.Clamp(xMin, sourceRect.x, sourceRect.xMax - 1f);
|
||||
xMax = Mathf.Clamp(xMax, xMin + 1f, sourceRect.xMax);
|
||||
|
||||
Rect pieceRect = Rect.MinMaxRect(xMin, yMin, xMax, yMax);
|
||||
if (pieceRect.width < 1f || pieceRect.height < 1f)
|
||||
continue;
|
||||
|
||||
GameObject piece = new GameObject($"Piece_{row}_{col}");
|
||||
piece.transform.SetParent(proxy.transform, false);
|
||||
|
||||
SpriteRenderer pieceRenderer = piece.AddComponent<SpriteRenderer>();
|
||||
pieceRenderer.sprite = Sprite.Create(
|
||||
sourceSprite.texture,
|
||||
pieceRect,
|
||||
new Vector2(0.5f, 0.5f),
|
||||
pixelsPerUnit,
|
||||
0,
|
||||
SpriteMeshType.FullRect);
|
||||
cleanup.Register(pieceRenderer.sprite);
|
||||
pieceRenderer.sharedMaterial = sourceRenderer.sharedMaterial;
|
||||
pieceRenderer.color = sourceRenderer.color;
|
||||
pieceRenderer.flipX = sourceRenderer.flipX;
|
||||
pieceRenderer.flipY = sourceRenderer.flipY;
|
||||
pieceRenderer.maskInteraction = sourceRenderer.maskInteraction;
|
||||
pieceRenderer.sortingLayerID = sourceRenderer.sortingLayerID;
|
||||
pieceRenderer.sortingOrder = sourceRenderer.sortingOrder;
|
||||
pieceRenderer.renderingLayerMask = sourceRenderer.renderingLayerMask;
|
||||
piece.layer = sourceRenderer.gameObject.layer;
|
||||
|
||||
float localPieceX = (pieceRect.x - sourceRect.x);
|
||||
float localPieceY = (pieceRect.y - sourceRect.y);
|
||||
float localCenterX = minLocalX + (localPieceX + pieceRect.width * 0.5f) / pixelsPerUnit;
|
||||
float localCenterY = minLocalY + (localPieceY + pieceRect.height * 0.5f) / pixelsPerUnit;
|
||||
|
||||
Vector2 randomOffset = runtimeFallbackRandomOffset > 0f
|
||||
? UnityEngine.Random.insideUnitCircle * runtimeFallbackRandomOffset
|
||||
: Vector2.zero;
|
||||
|
||||
piece.transform.localPosition = new Vector3(localCenterX + randomOffset.x, localCenterY + randomOffset.y, 0f);
|
||||
piece.transform.localRotation = Quaternion.identity;
|
||||
piece.transform.localScale = Vector3.one;
|
||||
|
||||
pieceTransforms.Add(piece.transform);
|
||||
|
||||
Vector2 radialDirection = ((Vector2)piece.transform.position - (Vector2)sourceRenderer.bounds.center);
|
||||
if (radialDirection.sqrMagnitude < 0.0001f)
|
||||
{
|
||||
radialDirection = UnityEngine.Random.insideUnitCircle.normalized;
|
||||
}
|
||||
|
||||
Vector2 initialVelocity = Vector2.zero;
|
||||
if (!keepChunksInPlace)
|
||||
{
|
||||
initialVelocity += radialDirection.normalized * chunkExplodeForce;
|
||||
if (chunkWindDirection.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
initialVelocity += chunkWindDirection.normalized * chunkWindForce;
|
||||
}
|
||||
}
|
||||
|
||||
pieceVelocities.Add(initialVelocity);
|
||||
pieceAngularVelocities.Add(keepChunksInPlace ? 0f : UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque));
|
||||
pieceCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (pieceCount <= 0)
|
||||
{
|
||||
Destroy(proxy);
|
||||
fractureProxies.Remove(trackIndex);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (manualChunkMotion && !keepChunksInPlace)
|
||||
{
|
||||
StartCoroutine(DriveRuntimeSpritePieces(pieceTransforms, pieceVelocities, pieceAngularVelocities));
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
private IEnumerator DriveRuntimeSpritePieces(
|
||||
List<Transform> pieceTransforms,
|
||||
List<Vector2> pieceVelocities,
|
||||
List<float> pieceAngularVelocities)
|
||||
{
|
||||
Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero;
|
||||
Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f
|
||||
? Vector2.zero
|
||||
: chunkGravityDirection.normalized;
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < chunkMotionDuration)
|
||||
{
|
||||
float dt = Time.deltaTime;
|
||||
for (int i = 0; i < pieceTransforms.Count; i++)
|
||||
{
|
||||
Transform pieceTransform = pieceTransforms[i];
|
||||
if (pieceTransform == null)
|
||||
continue;
|
||||
|
||||
Vector2 velocity = pieceVelocities[i];
|
||||
float angularVelocity = pieceAngularVelocities[i];
|
||||
|
||||
velocity += normalizedGravity * chunkGravityStrength * dt;
|
||||
velocity += normalizedWind * chunkWindForce * dt;
|
||||
velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt));
|
||||
angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt));
|
||||
|
||||
pieceTransform.position += (Vector3)(velocity * dt);
|
||||
pieceTransform.Rotate(0f, 0f, angularVelocity * dt, Space.Self);
|
||||
|
||||
pieceVelocities[i] = velocity;
|
||||
pieceAngularVelocities[i] = angularVelocity;
|
||||
}
|
||||
|
||||
elapsed += dt;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TrackCrashRuntimeSpriteCleanup : MonoBehaviour
|
||||
{
|
||||
private readonly List<Sprite> runtimeSprites = new List<Sprite>();
|
||||
|
||||
public void Register(Sprite sprite)
|
||||
{
|
||||
if (sprite != null)
|
||||
{
|
||||
runtimeSprites.Add(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
for (int i = 0; i < runtimeSprites.Count; i++)
|
||||
{
|
||||
if (runtimeSprites[i] != null)
|
||||
{
|
||||
Destroy(runtimeSprites[i]);
|
||||
}
|
||||
}
|
||||
runtimeSprites.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f3a5dd0d9a44f0e95d4a5fb83a0f001
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,443 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using SpriteGlow;
|
||||
using UnityEngine;
|
||||
|
||||
public class TrackJudgeHitEffectController : MonoBehaviour
|
||||
{
|
||||
public static TrackJudgeHitEffectController Instance { get; private set; }
|
||||
|
||||
[Header("Track Sprite FX")]
|
||||
[Tooltip("0-4 maps to track 1-5.")]
|
||||
[SerializeField] private GameObject[] trackSpriteFxObjects = new GameObject[5];
|
||||
|
||||
[Header("Track Glow FX")]
|
||||
[Tooltip("0-4 maps to track 1-5.")]
|
||||
[SerializeField] private SpriteGlowEffect[] trackGlowEffects = new SpriteGlowEffect[5];
|
||||
[SerializeField, Range(0f, 1f)] private float trackGlowSpriteAlpha = 0f;
|
||||
|
||||
[Header("Particle FX")]
|
||||
[SerializeField] private GameObject trackParticlePrefab;
|
||||
[SerializeField] private Vector3 trackParticleLocalPosition = new Vector3(0f, -5.29f, -0.13f);
|
||||
[SerializeField] private Vector3 trackParticleLocalEulerAngles = new Vector3(70f, 0f, 0f);
|
||||
[SerializeField] private Vector3 trackParticleLocalScale = Vector3.one;
|
||||
|
||||
[Header("Track Colors")]
|
||||
[SerializeField] private Color[] trackColors = new Color[5]
|
||||
{
|
||||
new Color(1f, 0.44f, 0.42f, 1f),
|
||||
new Color(0.22f, 0.96f, 0.79f, 1f),
|
||||
new Color(1f, 0.88f, 0.63f, 1f),
|
||||
new Color(0.95f, 0.51f, 0.89f, 1f),
|
||||
new Color(0.48f, 0.98f, 1f, 1f)
|
||||
};
|
||||
|
||||
[Header("Fade")]
|
||||
[SerializeField, Range(0f, 1f)] private float initialAlpha = 10f / 255f;
|
||||
[SerializeField] private float fadeDuration = 0.18f;
|
||||
[SerializeField] private float glowFadeDuration = 0.18f;
|
||||
[SerializeField] private bool useUnscaledTime = true;
|
||||
|
||||
private readonly Dictionary<int, Coroutine> activeFadeRoutines = new Dictionary<int, Coroutine>();
|
||||
private readonly Dictionary<int, Coroutine> activeGlowFadeRoutines = new Dictionary<int, Coroutine>();
|
||||
|
||||
private readonly Queue<GameObject> particlePool = new Queue<GameObject>();
|
||||
private Transform particlePoolRoot;
|
||||
|
||||
// Per-instance cache: the particle systems and computed lifetime are resolved once
|
||||
// when an instance is created, so the burst hot path never calls
|
||||
// GetComponentsInChildren (an allocating call) per hit. Business behavior is
|
||||
// unchanged: same systems, same tint, same lifetime.
|
||||
private sealed class ParticleCacheEntry
|
||||
{
|
||||
public ParticleSystem[] Systems;
|
||||
public float Lifetime;
|
||||
}
|
||||
|
||||
private readonly Dictionary<GameObject, ParticleCacheEntry> particleCache =
|
||||
new Dictionary<GameObject, ParticleCacheEntry>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
ResetAllGlowAlpha();
|
||||
ResetAllGlowSpriteAlpha();
|
||||
}
|
||||
else if (Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static void PlayTrackHitFx(int trackIndex)
|
||||
{
|
||||
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackJudgeHitEffectController>();
|
||||
if (controller == null)
|
||||
return;
|
||||
|
||||
controller.PlayTrackHitFxInternal(trackIndex);
|
||||
}
|
||||
|
||||
private void PlayTrackHitFxInternal(int trackIndex)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= 5)
|
||||
return;
|
||||
|
||||
GameObject trackFxObject = GetTrackFxObject(trackIndex);
|
||||
if (trackFxObject == null)
|
||||
return;
|
||||
|
||||
if (activeGlowFadeRoutines.TryGetValue(trackIndex, out Coroutine glowRoutine) && glowRoutine != null)
|
||||
{
|
||||
StopCoroutine(glowRoutine);
|
||||
activeGlowFadeRoutines.Remove(trackIndex);
|
||||
}
|
||||
|
||||
if (activeFadeRoutines.TryGetValue(trackIndex, out Coroutine fadeRoutine) && fadeRoutine != null)
|
||||
{
|
||||
StopCoroutine(fadeRoutine);
|
||||
activeFadeRoutines.Remove(trackIndex);
|
||||
}
|
||||
|
||||
trackFxObject.SetActive(true);
|
||||
PlayGlowFx(trackIndex);
|
||||
|
||||
SpriteRenderer spriteRenderer = trackFxObject.GetComponent<SpriteRenderer>();
|
||||
if (spriteRenderer == null)
|
||||
{
|
||||
spriteRenderer = trackFxObject.GetComponentInChildren<SpriteRenderer>(true);
|
||||
}
|
||||
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
Color c = spriteRenderer.color;
|
||||
c.a = initialAlpha;
|
||||
spriteRenderer.color = c;
|
||||
activeFadeRoutines[trackIndex] = StartCoroutine(FadeSpriteRoutine(trackIndex, spriteRenderer));
|
||||
}
|
||||
|
||||
SpawnTrackParticle(trackIndex, trackFxObject.transform);
|
||||
}
|
||||
|
||||
private void ResetAllGlowAlpha()
|
||||
{
|
||||
if (trackGlowEffects == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < trackGlowEffects.Length; i++)
|
||||
{
|
||||
SetGlowAlpha(trackGlowEffects[i], 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAllGlowSpriteAlpha()
|
||||
{
|
||||
if (trackGlowEffects == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < trackGlowEffects.Length; i++)
|
||||
{
|
||||
ResetGlowSpriteAlpha(trackGlowEffects[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject GetTrackFxObject(int trackIndex)
|
||||
{
|
||||
if (trackSpriteFxObjects == null || trackIndex < 0 || trackIndex >= trackSpriteFxObjects.Length)
|
||||
return null;
|
||||
|
||||
return trackSpriteFxObjects[trackIndex];
|
||||
}
|
||||
|
||||
private SpriteGlowEffect GetTrackGlowEffect(int trackIndex)
|
||||
{
|
||||
if (trackGlowEffects == null || trackIndex < 0 || trackIndex >= trackGlowEffects.Length)
|
||||
return null;
|
||||
|
||||
return trackGlowEffects[trackIndex];
|
||||
}
|
||||
|
||||
private void ResetGlowSpriteAlpha(SpriteGlowEffect glowEffect)
|
||||
{
|
||||
if (glowEffect == null || glowEffect.Renderer == null)
|
||||
return;
|
||||
|
||||
Color color = glowEffect.Renderer.color;
|
||||
color.a = trackGlowSpriteAlpha;
|
||||
glowEffect.Renderer.color = color;
|
||||
}
|
||||
|
||||
private void PlayGlowFx(int trackIndex)
|
||||
{
|
||||
SpriteGlowEffect glowEffect = GetTrackGlowEffect(trackIndex);
|
||||
if (glowEffect == null)
|
||||
return;
|
||||
|
||||
ResetGlowSpriteAlpha(glowEffect);
|
||||
SetGlowAlpha(glowEffect, 1f);
|
||||
|
||||
if (activeGlowFadeRoutines.TryGetValue(trackIndex, out Coroutine routine) && routine != null)
|
||||
{
|
||||
StopCoroutine(routine);
|
||||
activeGlowFadeRoutines.Remove(trackIndex);
|
||||
}
|
||||
|
||||
activeGlowFadeRoutines[trackIndex] = StartCoroutine(FadeGlowRoutine(trackIndex, glowEffect));
|
||||
}
|
||||
|
||||
private void SetGlowAlpha(SpriteGlowEffect glowEffect, float alpha)
|
||||
{
|
||||
if (glowEffect == null)
|
||||
return;
|
||||
|
||||
Color color = glowEffect.GlowColor;
|
||||
color.a = alpha;
|
||||
glowEffect.GlowColor = color;
|
||||
}
|
||||
|
||||
private IEnumerator FadeSpriteRoutine(int trackIndex, SpriteRenderer spriteRenderer)
|
||||
{
|
||||
float duration = Mathf.Max(0.01f, fadeDuration);
|
||||
float elapsed = 0f;
|
||||
|
||||
Color startColor = spriteRenderer != null ? spriteRenderer.color : Color.white;
|
||||
Color endColor = startColor;
|
||||
endColor.a = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (spriteRenderer == null)
|
||||
yield break;
|
||||
|
||||
elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
|
||||
Color c = Color.LerpUnclamped(startColor, endColor, t);
|
||||
spriteRenderer.color = c;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
Color c = spriteRenderer.color;
|
||||
c.a = 0f;
|
||||
spriteRenderer.color = c;
|
||||
}
|
||||
|
||||
activeFadeRoutines.Remove(trackIndex);
|
||||
}
|
||||
|
||||
private IEnumerator FadeGlowRoutine(int trackIndex, SpriteGlowEffect glowEffect)
|
||||
{
|
||||
float duration = Mathf.Max(0.01f, glowFadeDuration);
|
||||
float elapsed = 0f;
|
||||
|
||||
Color startColor = glowEffect != null ? glowEffect.GlowColor : Color.white;
|
||||
Color endColor = startColor;
|
||||
endColor.a = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (glowEffect == null)
|
||||
yield break;
|
||||
|
||||
elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
|
||||
Color c = Color.LerpUnclamped(startColor, endColor, t);
|
||||
glowEffect.GlowColor = c;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (glowEffect != null)
|
||||
{
|
||||
Color c = glowEffect.GlowColor;
|
||||
c.a = 0f;
|
||||
glowEffect.GlowColor = c;
|
||||
}
|
||||
|
||||
activeGlowFadeRoutines.Remove(trackIndex);
|
||||
}
|
||||
|
||||
private void SpawnTrackParticle(int trackIndex, Transform parent)
|
||||
{
|
||||
if (trackParticlePrefab == null || parent == null)
|
||||
return;
|
||||
|
||||
GameObject instance = RentParticleInstance(out ParticleCacheEntry entry);
|
||||
if (instance == null)
|
||||
return;
|
||||
|
||||
instance.transform.SetParent(parent, false);
|
||||
instance.transform.localPosition = trackParticleLocalPosition;
|
||||
instance.transform.localRotation = Quaternion.Euler(trackParticleLocalEulerAngles);
|
||||
instance.transform.localScale = trackParticleLocalScale;
|
||||
instance.SetActive(true);
|
||||
|
||||
Color tint = GetTrackColor(trackIndex);
|
||||
ApplyParticleColor(entry, tint);
|
||||
|
||||
StartCoroutine(RecycleParticleRoutine(instance, entry, Mathf.Max(0.05f, entry.Lifetime)));
|
||||
}
|
||||
|
||||
// Prewarm the track particle pool during load so the first judge does not pay
|
||||
// the Instantiate + first-Play() cost on the hot path. Business behavior is
|
||||
// unchanged: the same prefab is used, instances are just reused instead of
|
||||
// being created and destroyed per hit.
|
||||
public void PrewarmTrackParticles(int count)
|
||||
{
|
||||
if (trackParticlePrefab == null || count <= 0)
|
||||
return;
|
||||
|
||||
EnsurePoolRoot();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
GameObject instance = CreateParticleInstance();
|
||||
if (instance == null)
|
||||
continue;
|
||||
instance.transform.SetParent(particlePoolRoot, false);
|
||||
instance.SetActive(false);
|
||||
particlePool.Enqueue(instance);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsurePoolRoot()
|
||||
{
|
||||
if (particlePoolRoot == null)
|
||||
{
|
||||
var root = new GameObject("TrackParticlePool");
|
||||
root.transform.SetParent(transform, false);
|
||||
root.SetActive(false);
|
||||
particlePoolRoot = root.transform;
|
||||
}
|
||||
}
|
||||
|
||||
// Central instance factory: instantiates the prefab, resolves and caches its
|
||||
// particle systems + computed lifetime once, and leaves it stopped/cleared.
|
||||
private GameObject CreateParticleInstance()
|
||||
{
|
||||
if (trackParticlePrefab == null)
|
||||
return null;
|
||||
|
||||
GameObject instance = Instantiate(trackParticlePrefab);
|
||||
var systems = instance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (systems == null)
|
||||
systems = new ParticleSystem[0];
|
||||
|
||||
var entry = new ParticleCacheEntry
|
||||
{
|
||||
Systems = systems,
|
||||
Lifetime = ComputeLifetime(systems)
|
||||
};
|
||||
|
||||
// Warm the play path once (forces emission buffer allocation and first-Play
|
||||
// cost during load), then reset to a clean stopped state ready for reuse.
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
ps.Play(true);
|
||||
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||||
ps.Clear(true);
|
||||
}
|
||||
|
||||
particleCache[instance] = entry;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private GameObject RentParticleInstance(out ParticleCacheEntry entry)
|
||||
{
|
||||
while (particlePool.Count > 0)
|
||||
{
|
||||
GameObject pooled = particlePool.Dequeue();
|
||||
if (pooled != null && particleCache.TryGetValue(pooled, out entry))
|
||||
return pooled;
|
||||
}
|
||||
|
||||
GameObject created = CreateParticleInstance();
|
||||
if (created != null && particleCache.TryGetValue(created, out entry))
|
||||
return created;
|
||||
|
||||
entry = null;
|
||||
return created;
|
||||
}
|
||||
|
||||
private IEnumerator RecycleParticleRoutine(GameObject instance, ParticleCacheEntry entry, float delay)
|
||||
{
|
||||
if (useUnscaledTime)
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
else
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
if (instance == null)
|
||||
yield break;
|
||||
|
||||
if (entry != null && entry.Systems != null)
|
||||
{
|
||||
foreach (var ps in entry.Systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||||
ps.Clear(true);
|
||||
}
|
||||
}
|
||||
|
||||
instance.SetActive(false);
|
||||
EnsurePoolRoot();
|
||||
instance.transform.SetParent(particlePoolRoot, false);
|
||||
particlePool.Enqueue(instance);
|
||||
}
|
||||
|
||||
private Color GetTrackColor(int trackIndex)
|
||||
{
|
||||
if (trackColors != null && trackIndex >= 0 && trackIndex < trackColors.Length)
|
||||
{
|
||||
return trackColors[trackIndex];
|
||||
}
|
||||
return Color.white;
|
||||
}
|
||||
|
||||
private void ApplyParticleColor(ParticleCacheEntry entry, Color tint)
|
||||
{
|
||||
if (entry == null || entry.Systems == null)
|
||||
return;
|
||||
|
||||
foreach (var ps in entry.Systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
var main = ps.main;
|
||||
main.startColor = tint;
|
||||
if (!ps.isPlaying)
|
||||
{
|
||||
ps.Play(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float ComputeLifetime(ParticleSystem[] systems)
|
||||
{
|
||||
float maxLifetime = 0.5f;
|
||||
if (systems == null)
|
||||
return maxLifetime;
|
||||
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
if (ps == null) continue;
|
||||
var main = ps.main;
|
||||
float startLifetime = 0.5f;
|
||||
if (main.startLifetime.mode == ParticleSystemCurveMode.Constant)
|
||||
startLifetime = main.startLifetime.constant;
|
||||
else if (main.startLifetime.mode == ParticleSystemCurveMode.TwoConstants)
|
||||
startLifetime = main.startLifetime.constantMax;
|
||||
else
|
||||
startLifetime = main.startLifetime.constantMax;
|
||||
|
||||
maxLifetime = Mathf.Max(maxLifetime, main.duration + startLifetime + 0.25f);
|
||||
}
|
||||
|
||||
return maxLifetime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ccce61c2f0d4dd5b4f0b7d9f7c3a001
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,61 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
/// <summary>
|
||||
/// Touch/click region that drives one gameplay track (0-4) through InputManager,
|
||||
/// exactly as if the bound keyboard key were pressed/released. Attach to a UI
|
||||
/// element (Image with Raycast Target on) laid over a lane; set trackIndex in the
|
||||
/// inspector. Uses EventSystem pointer callbacks so it supports multi-touch (each
|
||||
/// finger routes its own down/up to the region it started on) and works on Android
|
||||
/// touch as well as desktop mouse.
|
||||
///
|
||||
/// Judgment/scoring is unchanged: this only injects the same press/release the
|
||||
/// keyboard path produces. On a track already held by keyboard, PressTrack/
|
||||
/// ReleaseTrack are idempotent so there is no double-trigger.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class TrackTouchRegion : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
|
||||
{
|
||||
[Tooltip("Track this region controls: 0=red 1=green 2=yellow 3=purple 4=blue")]
|
||||
[Range(0, 4)]
|
||||
public int trackIndex = 0;
|
||||
|
||||
// The pointerId that pressed this region, so we only release on the matching
|
||||
// pointer's up event (relevant for multi-touch where several fingers are down).
|
||||
private int activePointerId = int.MinValue;
|
||||
private bool pressed = false;
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
if (pressed) return; // already driven by another finger; ignore extras
|
||||
var im = InputManager.Instance;
|
||||
if (im == null) return;
|
||||
|
||||
activePointerId = eventData.pointerId;
|
||||
pressed = true;
|
||||
im.PressTrack(trackIndex);
|
||||
}
|
||||
|
||||
public void OnPointerUp(PointerEventData eventData)
|
||||
{
|
||||
if (!pressed || eventData.pointerId != activePointerId) return;
|
||||
var im = InputManager.Instance;
|
||||
|
||||
pressed = false;
|
||||
activePointerId = int.MinValue;
|
||||
if (im != null) im.ReleaseTrack(trackIndex);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// If the region is hidden/destroyed mid-hold, make sure the track is released
|
||||
// so a note is not left stuck in the held state.
|
||||
if (pressed)
|
||||
{
|
||||
pressed = false;
|
||||
activePointerId = int.MinValue;
|
||||
var im = InputManager.Instance;
|
||||
if (im != null) im.ReleaseTrack(trackIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b671f741e30e73840abdef6732a1145f
|
||||
@@ -25,6 +25,9 @@ public class effectEventController : MonoBehaviour
|
||||
public float playerSkillCameraOffsetAmount = 0.6f;
|
||||
public CameraOffsetAxis playerSkillCameraOffsetAxis = CameraOffsetAxis.Y;
|
||||
|
||||
[Header("Track Crash")]
|
||||
[SerializeField] private TrackCrashController trackCrashController;
|
||||
|
||||
private Transform cachedShakeTarget;
|
||||
private Transform cachedCameraDriftPivot;
|
||||
private Transform cachedCameraSkillPivot;
|
||||
@@ -54,6 +57,7 @@ public class effectEventController : MonoBehaviour
|
||||
{
|
||||
CacheShakeTarget();
|
||||
ResetCameraDriftIfNeeded();
|
||||
CacheTrackCrashController();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -73,6 +77,14 @@ public class effectEventController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheTrackCrashController()
|
||||
{
|
||||
if (trackCrashController == null)
|
||||
{
|
||||
trackCrashController = SceneObjectLookupCache.FindAny<TrackCrashController>();
|
||||
}
|
||||
}
|
||||
|
||||
private void PrewarmShakeComponent()
|
||||
{
|
||||
if (cachedShakeTarget == null)
|
||||
@@ -265,6 +277,15 @@ public class effectEventController : MonoBehaviour
|
||||
controller.TriggerMultiNoteShakeInternal();
|
||||
}
|
||||
|
||||
public static void TryTriggerTrackCrash(int trackIndex)
|
||||
{
|
||||
effectEventController controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<effectEventController>();
|
||||
if (controller == null)
|
||||
return;
|
||||
|
||||
controller.TriggerTrackCrashInternal(trackIndex);
|
||||
}
|
||||
|
||||
private void TriggerMultiNoteShakeInternal()
|
||||
{
|
||||
if (!enableMultiNoteScreenShake)
|
||||
@@ -311,4 +332,13 @@ public class effectEventController : MonoBehaviour
|
||||
shake.sum = new Vector3[2];
|
||||
shake.StartShake();
|
||||
}
|
||||
|
||||
private void TriggerTrackCrashInternal(int trackIndex)
|
||||
{
|
||||
CacheTrackCrashController();
|
||||
if (trackCrashController == null)
|
||||
return;
|
||||
|
||||
trackCrashController.TriggerTrackCrash(trackIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ public class GfxController : MonoBehaviour
|
||||
{
|
||||
public static GfxController Instance { get; private set; }
|
||||
|
||||
// 复用的属性块:命中特效透明度覆写用,避免每次命中访问 r.materials 克隆材质。
|
||||
private MaterialPropertyBlock _fxPropertyBlock;
|
||||
|
||||
[Header("Combatant Objects")]
|
||||
public GameObject enemyMoveObject;
|
||||
public GameObject ally01MoveObject;
|
||||
@@ -688,23 +691,28 @@ public class GfxController : MonoBehaviour
|
||||
// 设置排序层级
|
||||
r.sortingOrder = sortingOrder;
|
||||
|
||||
// 处理材质透明度
|
||||
// 处理材质透明度:用 MaterialPropertyBlock 覆写 alpha,避免访问 r.materials
|
||||
// 触发材质实例化(每次命中都会克隆并泄漏材质,产生 GC)。视觉结果等价。
|
||||
if (transparency < 1f)
|
||||
{
|
||||
foreach (var mat in r.materials)
|
||||
var sharedMat = r.sharedMaterial;
|
||||
if (sharedMat != null)
|
||||
{
|
||||
if (mat.HasProperty("_Color"))
|
||||
if (_fxPropertyBlock == null) _fxPropertyBlock = new MaterialPropertyBlock();
|
||||
r.GetPropertyBlock(_fxPropertyBlock);
|
||||
if (sharedMat.HasProperty("_Color"))
|
||||
{
|
||||
Color c = mat.color;
|
||||
Color c = sharedMat.color;
|
||||
c.a *= transparency;
|
||||
mat.color = c;
|
||||
_fxPropertyBlock.SetColor("_Color", c);
|
||||
}
|
||||
else if (mat.HasProperty("_BaseColor")) // URP 命名
|
||||
else if (sharedMat.HasProperty("_BaseColor")) // URP 命名
|
||||
{
|
||||
Color c = mat.GetColor("_BaseColor");
|
||||
Color c = sharedMat.GetColor("_BaseColor");
|
||||
c.a *= transparency;
|
||||
mat.SetColor("_BaseColor", c);
|
||||
_fxPropertyBlock.SetColor("_BaseColor", c);
|
||||
}
|
||||
r.SetPropertyBlock(_fxPropertyBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +445,10 @@ namespace GameServer.Client
|
||||
{
|
||||
song_id = CurrentRoom != null ? CurrentRoom.song_id : string.Empty,
|
||||
cycle_id = 0,
|
||||
server_name = "雅莉梦璃高新产业园",
|
||||
leaderboard_cycle_days = 3,
|
||||
rank_version_id = 20260101,
|
||||
next_settle_at = "2026-01-01 00:00:00",
|
||||
rankings = BuildRoomRankingEntries()
|
||||
};
|
||||
return result;
|
||||
@@ -628,10 +632,24 @@ namespace GameServer.Client
|
||||
}
|
||||
|
||||
await EnsureSocialConnected();
|
||||
await SendSocialAction("SEND_WORLD_MESSAGE", new
|
||||
JObject resp = await SendSocialAction("SEND_WORLD_MESSAGE", new
|
||||
{
|
||||
content = trimmed
|
||||
});
|
||||
|
||||
JObject messageObject = resp["message"] as JObject ?? resp;
|
||||
ArenaRoomChatMessage message = messageObject.ToObject<ArenaRoomChatMessage>();
|
||||
if (message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message.sender_id))
|
||||
{
|
||||
message.sender_id = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
|
||||
}
|
||||
|
||||
AppendWorldChatMessage(message);
|
||||
}
|
||||
|
||||
public async Task<List<ArenaRoomChatMessage>> LoadPrivateHistory(string partnerId, int limit, long? beforeId = null)
|
||||
|
||||
@@ -62,6 +62,13 @@ public class GameServerBridge : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LeaderboardConsentUtility.HasLeaderboardConsent())
|
||||
{
|
||||
Debug.Log("[Bridge] 玩家未同意加入排行榜,跳过世界排行榜提交");
|
||||
_hasSubmitted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_hasSubmitted = true;
|
||||
|
||||
try
|
||||
@@ -143,6 +150,10 @@ public class GameServerBridge : MonoBehaviour
|
||||
await Task.Delay(3000);
|
||||
SubmitCurrentSettlement();
|
||||
}
|
||||
else if (result == "OPT_OUT")
|
||||
{
|
||||
Debug.Log("[Bridge] 当前已关闭排行榜加入选项,未提交世界排行榜。");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"╔══════════════════════════════════════════════════════╗");
|
||||
|
||||
@@ -105,6 +105,36 @@ namespace GameServer.Client
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LeaderboardData> ForceRefresh(string songId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(songId))
|
||||
{
|
||||
throw new ArgumentException("songId is required", nameof(songId));
|
||||
}
|
||||
|
||||
NetworkManager nm = NetworkManager.Instance;
|
||||
if (nm == null)
|
||||
{
|
||||
throw new InvalidOperationException("NetworkManager is not available.");
|
||||
}
|
||||
|
||||
Invalidate(songId);
|
||||
|
||||
Task<LeaderboardData> task = FetchAndCache(songId, nm);
|
||||
_inflight[songId] = task;
|
||||
try
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_inflight.TryGetValue(songId, out Task<LeaderboardData> current) && current == task)
|
||||
{
|
||||
_inflight.Remove(songId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Invalidate(string songId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(songId))
|
||||
@@ -113,6 +143,7 @@ namespace GameServer.Client
|
||||
}
|
||||
|
||||
_cache.Remove(songId);
|
||||
_inflight.Remove(songId);
|
||||
}
|
||||
|
||||
private async void HandleSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameServer.Client
|
||||
{
|
||||
@@ -88,12 +90,35 @@ namespace GameServer.Client
|
||||
[JsonProperty("avatar_url")] public string avatar_url;
|
||||
[JsonProperty("player_level")] public int player_level;
|
||||
[JsonProperty("leaderboard_opt_out")] public bool leaderboard_opt_out;
|
||||
[JsonProperty("leaderboard_can_change_today")] public bool leaderboard_can_change_today;
|
||||
[JsonProperty("leaderboard_changed_today")] public bool leaderboard_changed_today;
|
||||
[JsonProperty("leaderboard_next_change_at")] public string leaderboard_next_change_at;
|
||||
[JsonProperty("leaderboard_cycle_id")] public int leaderboard_cycle_id;
|
||||
[JsonProperty("total_play_seconds")] public double total_play_seconds;
|
||||
[JsonProperty("total_plays")] public int total_plays;
|
||||
[JsonProperty("actime")] public string actime;
|
||||
[JsonProperty("u_reg_id")] public int u_reg_id;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LeaderboardMembershipResponse
|
||||
{
|
||||
[JsonProperty("success")] public bool success;
|
||||
[JsonProperty("error_code")] public string error_code;
|
||||
[JsonProperty("message")] public string message;
|
||||
[JsonProperty("steam_id")] public string steam_id;
|
||||
[JsonProperty("cycle_id")] public int cycle_id;
|
||||
[JsonProperty("is_joined")] public bool is_joined;
|
||||
[JsonProperty("leaderboard_opt_out")] public bool leaderboard_opt_out;
|
||||
[JsonProperty("changed_today")] public bool changed_today;
|
||||
[JsonProperty("can_change_today")] public bool can_change_today;
|
||||
[JsonProperty("next_change_at")] public string next_change_at;
|
||||
[JsonProperty("server_name")] public string server_name;
|
||||
[JsonProperty("leaderboard_cycle_days")] public int leaderboard_cycle_days;
|
||||
[JsonProperty("rank_version_id")] public int rank_version_id;
|
||||
[JsonProperty("next_settle_at")] public string next_settle_at;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LeaderboardEntry
|
||||
{
|
||||
@@ -118,6 +143,10 @@ namespace GameServer.Client
|
||||
[JsonProperty("song_id")] public string song_id;
|
||||
[JsonProperty("cycle_id")] public int cycle_id;
|
||||
[JsonProperty("is_locked")] public bool is_locked;
|
||||
[JsonProperty("server_name")] public string server_name;
|
||||
[JsonProperty("leaderboard_cycle_days")] public int leaderboard_cycle_days;
|
||||
[JsonProperty("rank_version_id")] public int rank_version_id;
|
||||
[JsonProperty("next_settle_at")] public string next_settle_at;
|
||||
[JsonProperty("rankings")] public List<LeaderboardEntry> rankings;
|
||||
}
|
||||
|
||||
@@ -499,4 +528,124 @@ namespace GameServer.Client
|
||||
// itemID 77001 = 角色 30206;77003 = 歌曲1;77004 = 歌曲2(见 Resources/so/storeSO/)
|
||||
}
|
||||
|
||||
public static class LeaderboardConsentUtility
|
||||
{
|
||||
public const string RankingConsentPrefKey = "before_everything_agree_ranking";
|
||||
private const string WarningPrefix = "下次排行榜更新时间:";
|
||||
|
||||
public static bool HasLeaderboardConsent()
|
||||
{
|
||||
return PlayerPrefs.GetInt(RankingConsentPrefKey, 0) == 1;
|
||||
}
|
||||
|
||||
public static void SetLeaderboardConsent(bool accepted)
|
||||
{
|
||||
PlayerPrefs.SetInt(RankingConsentPrefKey, accepted ? 1 : 0);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public static async Task<string> BuildNextSettleWarningTextAsync()
|
||||
{
|
||||
string nextSettleAt = await TryGetNextSettleAtAsync();
|
||||
return string.IsNullOrWhiteSpace(nextSettleAt)
|
||||
? WarningPrefix + "--"
|
||||
: WarningPrefix + nextSettleAt;
|
||||
}
|
||||
|
||||
public static async Task<string> TryGetNextSettleAtAsync()
|
||||
{
|
||||
LeaderboardData data = await TryGetLeaderboardMetaAsync();
|
||||
return data != null && !string.IsNullOrWhiteSpace(data.next_settle_at)
|
||||
? data.next_settle_at.Trim()
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
public static async Task<LeaderboardData> TryGetLeaderboardMetaAsync()
|
||||
{
|
||||
string songId = ResolveReferenceSongId();
|
||||
if (string.IsNullOrWhiteSpace(songId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
|
||||
if (cache != null
|
||||
&& cache.TryGetCached(songId, out LeaderboardData cached)
|
||||
&& cached != null
|
||||
&& !string.IsNullOrWhiteSpace(cached.next_settle_at))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
NetworkManager networkManager = NetworkManager.Instance;
|
||||
if (networkManager == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await networkManager.FetchLeaderboardFromServer(songId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[LeaderboardConsentUtility] Failed to fetch leaderboard meta: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveReferenceSongId()
|
||||
{
|
||||
SongData selectedSong = SongDataHolder.SelectedSongData;
|
||||
if (selectedSong != null && selectedSong.songID > 0)
|
||||
{
|
||||
return selectedSong.songID.ToString();
|
||||
}
|
||||
|
||||
if (BeatmapManager.pendingSongData != null && BeatmapManager.pendingSongData.songID > 0)
|
||||
{
|
||||
return BeatmapManager.pendingSongData.songID.ToString();
|
||||
}
|
||||
|
||||
BeatmapManager beatmapManager = BeatmapManager.Instance;
|
||||
if (beatmapManager != null
|
||||
&& beatmapManager.assignedSongData != null
|
||||
&& beatmapManager.assignedSongData.songID > 0)
|
||||
{
|
||||
return beatmapManager.assignedSongData.songID.ToString();
|
||||
}
|
||||
|
||||
SongDataLibrary library = SongDataLibrary.Instance;
|
||||
if (library != null && library.IsLoaded)
|
||||
{
|
||||
List<SongData> songs = library.GetAllSongs();
|
||||
if (songs != null)
|
||||
{
|
||||
for (int i = 0; i < songs.Count; i++)
|
||||
{
|
||||
SongData song = songs[i];
|
||||
if (song != null && song.songID > 0)
|
||||
{
|
||||
return song.songID.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SongData[] indexedSongs = RuntimeResourcesCache.LoadSongIndex();
|
||||
if (indexedSongs != null)
|
||||
{
|
||||
for (int i = 0; i < indexedSongs.Length; i++)
|
||||
{
|
||||
SongData song = indexedSongs[i];
|
||||
if (song != null && song.songID > 0)
|
||||
{
|
||||
return song.songID.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,57 @@ public class NetworkManager : MonoBehaviour
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task<LeaderboardMembershipResponse> GetLeaderboardMembership()
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
{
|
||||
return new LeaderboardMembershipResponse
|
||||
{
|
||||
success = true,
|
||||
steam_id = SteamId,
|
||||
is_joined = LeaderboardConsentUtility.HasLeaderboardConsent(),
|
||||
leaderboard_opt_out = !LeaderboardConsentUtility.HasLeaderboardConsent(),
|
||||
can_change_today = true,
|
||||
changed_today = false,
|
||||
next_change_at = string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
string steamIdQuery = Uri.EscapeDataString(SteamId ?? string.Empty);
|
||||
return await GetJson<LeaderboardMembershipResponse>(
|
||||
BuildApiUrl($"/api/profile/leaderboard-membership?steam_id={steamIdQuery}"),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<LeaderboardMembershipResponse> UpdateLeaderboardMembership(bool isJoined)
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
{
|
||||
LeaderboardConsentUtility.SetLeaderboardConsent(isJoined);
|
||||
return new LeaderboardMembershipResponse
|
||||
{
|
||||
success = true,
|
||||
steam_id = SteamId,
|
||||
is_joined = isJoined,
|
||||
leaderboard_opt_out = !isJoined,
|
||||
can_change_today = true,
|
||||
changed_today = false,
|
||||
next_change_at = string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
var req = new
|
||||
{
|
||||
steam_id = SteamId,
|
||||
is_joined = isJoined,
|
||||
leaderboard_opt_out = !isJoined
|
||||
};
|
||||
return await PutJson<LeaderboardMembershipResponse>(
|
||||
BuildApiUrl("/api/profile/leaderboard-membership"),
|
||||
req,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<ProfileData> GetPlayerByUid(int uid)
|
||||
{
|
||||
if (uid <= 0)
|
||||
@@ -525,6 +576,12 @@ public class NetworkManager : MonoBehaviour
|
||||
return "LOCAL_ONLY";
|
||||
}
|
||||
|
||||
if (!LeaderboardConsentUtility.HasLeaderboardConsent())
|
||||
{
|
||||
Debug.Log("[NetworkManager] Leaderboard submission skipped because the player has not consented.");
|
||||
return "OPT_OUT";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await PerformHandshakeAsync(CancellationToken.None);
|
||||
@@ -673,7 +730,7 @@ public class NetworkManager : MonoBehaviour
|
||||
if (VerboseLogs) Debug.Log($"[NetworkManager] Ping: {resp.message}");
|
||||
}
|
||||
|
||||
public async Task<LeaderboardData> GetLeaderboard(string songId)
|
||||
public async Task<LeaderboardData> GetLeaderboard(string songId, bool forceRefresh = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(songId))
|
||||
{
|
||||
@@ -692,9 +749,17 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
SetState(ConnectionState.LoadingLeaderboard);
|
||||
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
|
||||
LeaderboardData data = cache != null
|
||||
? await cache.GetOrFetch(songId)
|
||||
: await FetchLeaderboardFromServer(songId);
|
||||
LeaderboardData data;
|
||||
if (cache != null)
|
||||
{
|
||||
data = forceRefresh
|
||||
? await cache.ForceRefresh(songId)
|
||||
: await cache.GetOrFetch(songId);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = await FetchLeaderboardFromServer(songId);
|
||||
}
|
||||
OnLeaderboardLoaded?.Invoke(data);
|
||||
SetState(ConnectionState.Ready);
|
||||
return data;
|
||||
@@ -1503,6 +1568,42 @@ public class NetworkManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<T> PutJson<T>(string url, object payload, CancellationToken token)
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
{
|
||||
throw CreateLocalOnlyException($"HTTP PUT is disabled in local-only mode. url={url}");
|
||||
}
|
||||
|
||||
string json = JsonConvert.SerializeObject(payload);
|
||||
using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPUT))
|
||||
{
|
||||
byte[] body = Encoding.UTF8.GetBytes(json);
|
||||
request.uploadHandler = new UploadHandlerRaw(body);
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
request.timeout = 15;
|
||||
|
||||
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP PUT {url}");
|
||||
await SendRequestAsync(request.SendWebRequest(), token);
|
||||
|
||||
string responseText = request.downloadHandler != null ? request.downloadHandler.text : string.Empty;
|
||||
if (request.result != UnityWebRequest.Result.Success && string.IsNullOrWhiteSpace(responseText))
|
||||
{
|
||||
throw new Exception($"{request.result}: {request.error}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(responseText);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to parse response: {ex.Message}. Body={responseText}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<T> GetJson<T>(string url, CancellationToken token)
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
@@ -1612,6 +1713,10 @@ public class NetworkManager : MonoBehaviour
|
||||
song_id = songId,
|
||||
cycle_id = 0,
|
||||
is_locked = false,
|
||||
server_name = "雅莉梦璃高新产业园",
|
||||
leaderboard_cycle_days = 3,
|
||||
rank_version_id = 20260101,
|
||||
next_settle_at = "2026-01-01 00:00:00",
|
||||
rankings = new List<LeaderboardEntry>()
|
||||
};
|
||||
}
|
||||
|
||||
+66
-6
@@ -54,12 +54,12 @@ MonoBehaviour:
|
||||
iManaMinusSprite: {fileID: 21300000, guid: ab8b33f8f7d77e242b0da39cdebae043, type: 3}
|
||||
iIscorePlusSprite: {fileID: 21300000, guid: e49d4f09f0f1b0646bf8fccb5dc816b7, type: 3}
|
||||
iIscoreMinusSprite: {fileID: 21300000, guid: 629ef934809d32949a40ad47b1d13356, type: 3}
|
||||
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 0}
|
||||
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 0}
|
||||
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 0}
|
||||
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 0}
|
||||
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 0}
|
||||
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 0}
|
||||
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 1}
|
||||
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 1}
|
||||
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 1}
|
||||
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 1}
|
||||
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 1}
|
||||
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 1}
|
||||
this_prefab_gravity: -9.81
|
||||
x_ofst: 0
|
||||
y_ofst: 0
|
||||
@@ -70,6 +70,66 @@ MonoBehaviour:
|
||||
jump_force: 125
|
||||
fadeout_awaitTime: 0.25
|
||||
fadeout_time: 0.25
|
||||
verticalTravelDistance: 110
|
||||
horizontalDriftMin: 18
|
||||
horizontalDriftMax: 42
|
||||
verticalMotionCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 4.5
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 0.58
|
||||
value: 1
|
||||
inSlope: 0.12
|
||||
outSlope: 0.12
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0.42
|
||||
inSlope: -2.4
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
horizontalMotionCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 2.2
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0.2
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
--- !u!1 &5029723704172947119
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
+68
-8
@@ -54,12 +54,12 @@ MonoBehaviour:
|
||||
iManaMinusSprite: {fileID: 21300000, guid: ab8b33f8f7d77e242b0da39cdebae043, type: 3}
|
||||
iIscorePlusSprite: {fileID: 21300000, guid: e49d4f09f0f1b0646bf8fccb5dc816b7, type: 3}
|
||||
iIscoreMinusSprite: {fileID: 21300000, guid: 629ef934809d32949a40ad47b1d13356, type: 3}
|
||||
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 0}
|
||||
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 0}
|
||||
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 0}
|
||||
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 0}
|
||||
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 0}
|
||||
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 0}
|
||||
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 1}
|
||||
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 1}
|
||||
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 1}
|
||||
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 1}
|
||||
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 1}
|
||||
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 1}
|
||||
this_prefab_gravity: -9.81
|
||||
x_ofst: 0
|
||||
y_ofst: 0
|
||||
@@ -70,6 +70,66 @@ MonoBehaviour:
|
||||
jump_force: 125
|
||||
fadeout_awaitTime: 0.5
|
||||
fadeout_time: 0.25
|
||||
verticalTravelDistance: 110
|
||||
horizontalDriftMin: 18
|
||||
horizontalDriftMax: 42
|
||||
verticalMotionCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 4.5
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 0.58
|
||||
value: 1
|
||||
inSlope: 0.12
|
||||
outSlope: 0.12
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0.42
|
||||
inSlope: -2.4
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
horizontalMotionCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 2.2
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0.2
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
--- !u!1 &5029723704172947119
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -137,8 +197,8 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_text: 2147483647
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: 452fa97492cae71479cf143f29930751, type: 2}
|
||||
m_sharedMaterial: {fileID: -346136068272202111, guid: 452fa97492cae71479cf143f29930751, type: 2}
|
||||
m_fontAsset: {fileID: 11400000, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
|
||||
m_sharedMaterial: {fileID: -346136068272202111, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
|
||||
+57
-13
@@ -39,6 +39,22 @@ public class playerInstantNumbersPrefab : MonoBehaviour
|
||||
public float fadeout_awaitTime;
|
||||
public float fadeout_time = 0.25f;
|
||||
|
||||
[Header("motion feel")]
|
||||
[Tooltip("Total height reached by the popup before it starts dropping back down.")]
|
||||
public float verticalTravelDistance = 110f;
|
||||
[Tooltip("Random horizontal drift distance applied over the popup lifetime.")]
|
||||
public float horizontalDriftMin = 18f;
|
||||
public float horizontalDriftMax = 42f;
|
||||
[Tooltip("Controls the fast-rise, slow-peak, then drop feel. 0=start, 1=end.")]
|
||||
public AnimationCurve verticalMotionCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 4.5f),
|
||||
new Keyframe(0.58f, 1f, 0.12f, 0.12f),
|
||||
new Keyframe(1f, 0.42f, -2.4f, 0f));
|
||||
[Tooltip("Controls horizontal drift over lifetime. This is multiplied by a random left/right distance.")]
|
||||
public AnimationCurve horizontalMotionCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 2.2f),
|
||||
new Keyframe(1f, 1f, 0.2f, 0f));
|
||||
|
||||
private RectTransform _rectTransform;
|
||||
private CanvasGroup _canvasGroup;
|
||||
private Coroutine _animationCoroutine;
|
||||
@@ -151,10 +167,16 @@ public class playerInstantNumbersPrefab : MonoBehaviour
|
||||
|
||||
Vector2 baseOffset = new Vector2(x_ofst, y_ofst);
|
||||
Vector2 randomOffset = Random.insideUnitCircle * xy_rdm_spawnP;
|
||||
Vector2 finalOffset = baseOffset + randomOffset;
|
||||
Vector2 spawnOffset = baseOffset + randomOffset;
|
||||
|
||||
if (useAnchored) _rectTransform.anchoredPosition = startAnchoredPos + finalOffset;
|
||||
else transform.localPosition = startLocalPos + new Vector3(finalOffset.x, finalOffset.y, 0f);
|
||||
float horizontalDirection = Random.value < 0.5f ? -1f : 1f;
|
||||
float horizontalDistance = horizontalDirection * Random.Range(
|
||||
Mathf.Min(horizontalDriftMin, horizontalDriftMax),
|
||||
Mathf.Max(horizontalDriftMin, horizontalDriftMax));
|
||||
float verticalDistance = Mathf.Max(0f, verticalTravelDistance);
|
||||
|
||||
if (useAnchored) _rectTransform.anchoredPosition = startAnchoredPos + spawnOffset;
|
||||
else transform.localPosition = startLocalPos + new Vector3(spawnOffset.x, spawnOffset.y, 0f);
|
||||
|
||||
float zRot = Random.Range(-rdm_rotation_range, rdm_rotation_range);
|
||||
transform.localRotation = Quaternion.Euler(0f, 0f, zRot);
|
||||
@@ -162,28 +184,25 @@ public class playerInstantNumbersPrefab : MonoBehaviour
|
||||
float scale = Random.Range(rdm_scale_min, rdm_scale_max);
|
||||
transform.localScale = new Vector3(scale, scale, 1f);
|
||||
|
||||
float vy = jump_force;
|
||||
float gravity = this_prefab_gravity;
|
||||
float fadeDelay = Mathf.Max(0f, fadeout_awaitTime);
|
||||
float fadeDuration = Mathf.Max(0.0001f, fadeout_time);
|
||||
float totalDuration = Mathf.Max(0.01f, fadeDelay + fadeDuration);
|
||||
|
||||
float t = 0f;
|
||||
while (t < fadeDelay + fadeDuration)
|
||||
while (t < totalDuration)
|
||||
{
|
||||
float dt = Time.deltaTime;
|
||||
vy += gravity * dt;
|
||||
float normalized = Mathf.Clamp01(t / totalDuration);
|
||||
float x = EvaluateCurve(horizontalMotionCurve, normalized) * horizontalDistance;
|
||||
float y = EvaluateCurve(verticalMotionCurve, normalized) * verticalDistance;
|
||||
|
||||
if (useAnchored)
|
||||
{
|
||||
var p = _rectTransform.anchoredPosition;
|
||||
p.y += vy * dt;
|
||||
_rectTransform.anchoredPosition = p;
|
||||
_rectTransform.anchoredPosition = startAnchoredPos + spawnOffset + new Vector2(x, y);
|
||||
}
|
||||
else
|
||||
{
|
||||
var p = transform.localPosition;
|
||||
p.y += vy * dt;
|
||||
transform.localPosition = p;
|
||||
transform.localPosition = startLocalPos + new Vector3(spawnOffset.x + x, spawnOffset.y + y, 0f);
|
||||
}
|
||||
|
||||
if (t >= fadeDelay)
|
||||
@@ -197,10 +216,35 @@ public class playerInstantNumbersPrefab : MonoBehaviour
|
||||
yield return null;
|
||||
}
|
||||
|
||||
float finalX = EvaluateCurve(horizontalMotionCurve, 1f) * horizontalDistance;
|
||||
float finalY = EvaluateCurve(verticalMotionCurve, 1f) * verticalDistance;
|
||||
if (useAnchored)
|
||||
{
|
||||
_rectTransform.anchoredPosition = startAnchoredPos + spawnOffset + new Vector2(finalX, finalY);
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.localPosition = startLocalPos + new Vector3(spawnOffset.x + finalX, spawnOffset.y + finalY, 0f);
|
||||
}
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
}
|
||||
|
||||
_animationCoroutine = null;
|
||||
if (!iNumberPrefabController.ReturnInstance(this))
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private static float EvaluateCurve(AnimationCurve curve, float time)
|
||||
{
|
||||
if (curve == null || curve.length == 0)
|
||||
{
|
||||
return time;
|
||||
}
|
||||
|
||||
return curve.Evaluate(time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ using UnityEngine.UI;
|
||||
|
||||
public class rankingListPrefab : MonoBehaviour
|
||||
{
|
||||
private static string RoomRankingServerText => LocalizationService.Get("ranking.room_server", "根据房间成绩动态排名,所在服务端:雅莉梦璃高新产业园");
|
||||
private const string FallbackServerName = "雅莉梦璃高新产业园";
|
||||
private const int FallbackCycleDays = 3;
|
||||
|
||||
[Header("shutbutton")]
|
||||
public Button shutbutton;
|
||||
@@ -116,17 +117,8 @@ public class rankingListPrefab : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
LeaderboardData data = null;
|
||||
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
|
||||
if (cache != null && cache.TryGetCached(currentSongId, out LeaderboardData cached))
|
||||
{
|
||||
data = cached;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetEmptyState(false, LocalizationService.Get("ranking.loading", "正在拉取排行榜..."));
|
||||
data = await nm.GetLeaderboard(currentSongId);
|
||||
}
|
||||
SetEmptyState(false, LocalizationService.Get("ranking.loading", "正在拉取排行榜..."));
|
||||
LeaderboardData data = await nm.GetLeaderboard(currentSongId, true);
|
||||
|
||||
if (data == null || data.rankings == null || data.rankings.Count == 0)
|
||||
{
|
||||
@@ -246,7 +238,7 @@ public class rankingListPrefab : MonoBehaviour
|
||||
|
||||
if (versionandServer != null)
|
||||
{
|
||||
versionandServer.text = isRoomRanking ? RoomRankingServerText : string.Empty;
|
||||
versionandServer.text = BuildVersionAndServerText(data);
|
||||
}
|
||||
|
||||
if (yourPositionText == null)
|
||||
@@ -286,6 +278,50 @@ public class rankingListPrefab : MonoBehaviour
|
||||
yourPositionText.text = LocalizationService.GetFormat("ranking.your_position_value", localEntry.total_score, localEntry.rank);
|
||||
}
|
||||
|
||||
private string BuildVersionAndServerText(LeaderboardData data)
|
||||
{
|
||||
string serverName = data != null && !string.IsNullOrWhiteSpace(data.server_name)
|
||||
? data.server_name
|
||||
: FallbackServerName;
|
||||
int cycleDays = data != null && data.leaderboard_cycle_days > 0
|
||||
? data.leaderboard_cycle_days
|
||||
: FallbackCycleDays;
|
||||
int rankVersionId = data != null && data.rank_version_id > 0
|
||||
? data.rank_version_id
|
||||
: BuildFallbackRankVersionId(cycleDays);
|
||||
string nextSettleAt = data != null && !string.IsNullOrWhiteSpace(data.next_settle_at)
|
||||
? data.next_settle_at
|
||||
: BuildFallbackNextSettleAt(cycleDays);
|
||||
|
||||
return $"每{cycleDays}自然日进行一次排行榜更新。排行榜版本{rankVersionId},下次统计日期{nextSettleAt} 所在服务端:{serverName}";
|
||||
}
|
||||
|
||||
private static string BuildFallbackNextSettleAt(int cycleDays)
|
||||
{
|
||||
DateTime epoch = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Local);
|
||||
DateTime now = DateTime.Now;
|
||||
int elapsedDays = (now.Date - epoch.Date).Days;
|
||||
int nextSettleDays = ((elapsedDays / Mathf.Max(1, cycleDays)) + 1) * Mathf.Max(1, cycleDays);
|
||||
DateTime nextSettle = epoch.AddDays(nextSettleDays);
|
||||
if (nextSettle <= now)
|
||||
{
|
||||
nextSettle = nextSettle.AddDays(Mathf.Max(1, cycleDays));
|
||||
}
|
||||
|
||||
return nextSettle.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
private static int BuildFallbackRankVersionId(int cycleDays)
|
||||
{
|
||||
string nextSettleAt = BuildFallbackNextSettleAt(cycleDays);
|
||||
if (DateTime.TryParseExact(nextSettleAt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsed))
|
||||
{
|
||||
return int.Parse(parsed.ToString("yyyyMMdd"));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string FormatSubmitTime(LeaderboardEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
|
||||
@@ -58,7 +58,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -66,8 +66,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 18
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 20
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
@@ -137,7 +137,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -145,8 +145,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 18
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 20
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
@@ -196,7 +196,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -22.589}
|
||||
m_AnchoredPosition: {x: 0, y: -10}
|
||||
m_SizeDelta: {x: 1100, y: 539.869}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3236275801312876574
|
||||
@@ -332,7 +332,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 492b08f39f232d64e9ce8eb248b5daea, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: 3410813869a49f942b4e0198c93eb3ec, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -486,6 +486,81 @@ MonoBehaviour:
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: "\u62C9\u53D6\u6392\u884C\u699C\u5931\u8D25"
|
||||
--- !u!1 &3019233615837960289
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 455604901716592107}
|
||||
- component: {fileID: 2199194977054993423}
|
||||
- component: {fileID: 1477107587712217919}
|
||||
m_Layer: 0
|
||||
m_Name: btm
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &455604901716592107
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3019233615837960289}
|
||||
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: 3018378993363515480}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 10}
|
||||
m_SizeDelta: {x: 1050, y: 588.9733}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2199194977054993423
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3019233615837960289}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &1477107587712217919
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3019233615837960289}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 0.39215687}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7edca03691f782d46ac361a681ce5685, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &3483362754365948650
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -525,7 +600,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -53.89, y: 260.83}
|
||||
m_AnchoredPosition: {x: -53.89, y: 272.9}
|
||||
m_SizeDelta: {x: 888.204, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &3005028479092236020
|
||||
@@ -714,7 +789,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 292.2}
|
||||
m_AnchoredPosition: {x: 0, y: 332.1}
|
||||
m_SizeDelta: {x: 1400, y: 24}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6664561848449007207
|
||||
@@ -738,7 +813,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1144,7 +1219,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1152,8 +1227,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 18
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 20
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
@@ -1195,6 +1270,7 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 455604901716592107}
|
||||
- {fileID: 2611590964116081061}
|
||||
- {fileID: 4148558001495883808}
|
||||
- {fileID: 7280264963986874822}
|
||||
@@ -1237,7 +1313,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Sprite: {fileID: 2139583702370686162, guid: f034ddca36ad627428d85c127caac8da, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -1305,7 +1381,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1313,8 +1389,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 18
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 20
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
@@ -1360,8 +1436,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -324.4}
|
||||
m_SizeDelta: {x: 1400, y: 24}
|
||||
m_AnchoredPosition: {x: 0, y: -317}
|
||||
m_SizeDelta: {x: 1400, y: 28}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &341918732991438692
|
||||
CanvasRenderer:
|
||||
@@ -1384,7 +1460,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1393,7 +1469,7 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 22
|
||||
m_FontSize: 28
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
@@ -1463,7 +1539,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1471,8 +1547,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 16
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 18
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
@@ -1543,7 +1619,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1551,8 +1627,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 18
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 20
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
@@ -1998,7 +2074,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 312.4}
|
||||
m_AnchoredPosition: {x: 0, y: 374.2}
|
||||
m_SizeDelta: {x: 1400, y: 81.672}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4558535259015521777
|
||||
@@ -2022,7 +2098,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -2031,12 +2107,12 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 40
|
||||
m_FontSize: 32
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 230
|
||||
m_Alignment: 1
|
||||
m_Alignment: 4
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
m_HorizontalOverflow: 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
%YAML 1.1
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &118873712428923354
|
||||
GameObject:
|
||||
@@ -30,13 +30,13 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 4266895856353985475}
|
||||
- {fileID: 7346852462872466010}
|
||||
m_Father: {fileID: 5127964937674371758}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -60, y: 8.5}
|
||||
m_SizeDelta: {x: 15, y: 15}
|
||||
m_AnchoredPosition: {x: -60, y: 0}
|
||||
m_SizeDelta: {x: 20, y: 20}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &9219216993890693327
|
||||
CanvasRenderer:
|
||||
@@ -87,6 +87,7 @@ GameObject:
|
||||
- component: {fileID: 4266895856353985475}
|
||||
- component: {fileID: 3787018861283084079}
|
||||
- component: {fileID: 267584174794072845}
|
||||
- component: {fileID: 8475271739115695170}
|
||||
m_Layer: 0
|
||||
m_Name: rwdName
|
||||
m_TagString: Untagged
|
||||
@@ -101,18 +102,18 @@ RectTransform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1059233533614862680}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
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: 3546975410283370972}
|
||||
m_Father: {fileID: 7346852462872466010}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 64.438, y: 0}
|
||||
m_SizeDelta: {x: 105.799, y: 15}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -10}
|
||||
m_SizeDelta: {x: 91, y: 15}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &3787018861283084079
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -134,7 +135,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -142,7 +143,7 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 15
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
@@ -154,7 +155,115 @@ MonoBehaviour:
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: "\u54C1\u540D"
|
||||
m_Text: "\u54C1\u540D\u662F12123"
|
||||
--- !u!114 &8475271739115695170
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1059233533614862680}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_HorizontalFit: 2
|
||||
m_VerticalFit: 0
|
||||
--- !u!1 &2029636835483568584
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6759631360171910247}
|
||||
- component: {fileID: 4215255755343680456}
|
||||
- component: {fileID: 793626109444426662}
|
||||
- component: {fileID: 2852475067541627356}
|
||||
m_Layer: 0
|
||||
m_Name: space
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &6759631360171910247
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2029636835483568584}
|
||||
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: 7346852462872466010}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 91, y: -10}
|
||||
m_SizeDelta: {x: 8, y: 15}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &4215255755343680456
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2029636835483568584}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &793626109444426662
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2029636835483568584}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 15
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 40
|
||||
m_Alignment: 3
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: ' '
|
||||
--- !u!114 &2852475067541627356
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2029636835483568584}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_HorizontalFit: 2
|
||||
m_VerticalFit: 0
|
||||
--- !u!1 &5437409420464746555
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -185,13 +294,12 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 3546975410283370972}
|
||||
- {fileID: 682261594771255014}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -26.7554}
|
||||
m_SizeDelta: {x: 150, y: 35}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 150, y: 20}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &8491477728461931688
|
||||
MonoBehaviour:
|
||||
@@ -208,9 +316,9 @@ MonoBehaviour:
|
||||
reward_icon: {fileID: 6251169732777221245}
|
||||
reward_name: {fileID: 267584174794072845}
|
||||
reward_count: {fileID: 6866410370984929931}
|
||||
coin_sprite: {fileID: 21300000, guid: 16f0765cc15fce04f828ecd44d1dbf91, type: 3}
|
||||
jiyisuipian: {fileID: 21300000, guid: 6fd4dd33d986d824d95f7b01579acc4d, type: 3}
|
||||
player_exp: {fileID: 21300000, guid: af3ce18394be1b448a9e38602526eb80, type: 3}
|
||||
coin_sprite: {fileID: 21300000, guid: ddce9ea8759b6ca47b6d177982d1ed1e, type: 3}
|
||||
jiyisuipian: {fileID: 21300000, guid: 9753216db376a75409e917958811eb28, type: 3}
|
||||
player_exp: {fileID: 21300000, guid: de1739af68092d2478ed972daa5a9820, type: 3}
|
||||
--- !u!1 &7159171791523732843
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -222,6 +330,7 @@ GameObject:
|
||||
- component: {fileID: 682261594771255014}
|
||||
- component: {fileID: 8122003116666225205}
|
||||
- component: {fileID: 6866410370984929931}
|
||||
- component: {fileID: 2071136781458211337}
|
||||
m_Layer: 0
|
||||
m_Name: rwdACT
|
||||
m_TagString: Untagged
|
||||
@@ -241,13 +350,13 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 5127964937674371758}
|
||||
m_Father: {fileID: 7346852462872466010}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -5.157, y: -8.48}
|
||||
m_SizeDelta: {x: 124.989, y: 18}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 99, y: -10}
|
||||
m_SizeDelta: {x: 96, y: 18}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &8122003116666225205
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -269,7 +378,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -277,7 +386,7 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 16
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
@@ -289,4 +398,98 @@ MonoBehaviour:
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: +10086
|
||||
m_Text: +100861123
|
||||
--- !u!114 &2071136781458211337
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7159171791523732843}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_HorizontalFit: 2
|
||||
m_VerticalFit: 0
|
||||
--- !u!1 &9110073405764519532
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7346852462872466010}
|
||||
- component: {fileID: 1268090614278476671}
|
||||
- component: {fileID: 3900282080562181984}
|
||||
m_Layer: 0
|
||||
m_Name: hori
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7346852462872466010
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 9110073405764519532}
|
||||
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:
|
||||
- {fileID: 4266895856353985475}
|
||||
- {fileID: 6759631360171910247}
|
||||
- {fileID: 682261594771255014}
|
||||
m_Father: {fileID: 3546975410283370972}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 13.800049, y: 0.00002861023}
|
||||
m_SizeDelta: {x: 0, y: 20}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!114 &1268090614278476671
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 9110073405764519532}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Padding:
|
||||
m_Left: 0
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 3
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 1
|
||||
m_ChildForceExpandHeight: 1
|
||||
m_ChildControlWidth: 0
|
||||
m_ChildControlHeight: 0
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!114 &3900282080562181984
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 9110073405764519532}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_HorizontalFit: 2
|
||||
m_VerticalFit: 0
|
||||
|
||||
@@ -72,6 +72,7 @@ public class settlementController : MonoBehaviour
|
||||
public Text mvp_score;
|
||||
public Image mvp_heroIcon;
|
||||
public Text mvp_heroName;
|
||||
public Text legacy_text;
|
||||
|
||||
// Documentation text normalized.
|
||||
[Header("Inspector")]
|
||||
@@ -643,7 +644,7 @@ public class settlementController : MonoBehaviour
|
||||
goodHitPercent_Text.text = targetGoodPercent.ToString("F1") + "%";
|
||||
missHitPercent_Text.text = targetMissPercent.ToString("F1") + "%";
|
||||
|
||||
accuracy_Text.text = targetAccuracyPercent.ToString("F3") + "%";
|
||||
accuracy_Text.text = targetAccuracyPercent.ToString("F1") + "%";
|
||||
|
||||
// Timing Statistics
|
||||
if (earlyHitCount_Text != null) earlyHitCount_Text.text = sm.countEarly.ToString();
|
||||
@@ -769,6 +770,29 @@ public class settlementController : MonoBehaviour
|
||||
|
||||
settlementRewardsGranted = true;
|
||||
|
||||
if (GameConfig.autoPlayEnabled)
|
||||
{
|
||||
moneyToGive_thisLevel = 0;
|
||||
|
||||
if (reward_money_Text != null)
|
||||
{
|
||||
reward_money_Text.text = "+0";
|
||||
}
|
||||
|
||||
if (reward_idolEXP_bottle_Text != null)
|
||||
{
|
||||
reward_idolEXP_bottle_Text.text = "+0";
|
||||
}
|
||||
|
||||
if (reward_playerEXP_Text != null)
|
||||
{
|
||||
reward_playerEXP_Text.text = "+0";
|
||||
}
|
||||
|
||||
RebuildSettlementRewardVisuals(0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
int coinReward = Mathf.CeilToInt((targetTotalScore / 10000f) * 0.75f);
|
||||
int materialReward = Mathf.CeilToInt((targetTotalScore / 10000f) * 0.20f);
|
||||
int playerExpReward = Mathf.FloorToInt(targetTotalScore / 100000f);
|
||||
@@ -1142,7 +1166,7 @@ public class settlementController : MonoBehaviour
|
||||
|
||||
PrepareTextForCountUp(pmScoreSum_Text, false, 0);
|
||||
PrepareTextForCountUp(idolScoreSum_Text, false, 0);
|
||||
PrepareTextForCountUp(accuracy_Text, true, 0f, "F3");
|
||||
PrepareTextForCountUp(accuracy_Text, true, 0f, "F1");
|
||||
PrepareTextForCountUp(finalScore_Text, false, 0);
|
||||
PrepareTextForCountUp(thisLevel_currentPercentage_Text, true, 0f, "F2");
|
||||
PrepareTextForCountUp(perfectHitCount_Text, false, 0);
|
||||
@@ -1169,7 +1193,7 @@ public class settlementController : MonoBehaviour
|
||||
Coroutine scoreBottomIn = null;
|
||||
Coroutine shenstarsIn = null;
|
||||
if (scoreBottomRt != null)
|
||||
scoreBottomIn = StartCoroutine(AnimateAnchoredXAndFade(scoreBottomRt, -1646.9f, -432.4f, introMoveDuration, true));
|
||||
scoreBottomIn = StartCoroutine(AnimateAnchoredXAndFade(scoreBottomRt, -1646.9f, 0f, introMoveDuration, true));
|
||||
if (shenstarsRt != null)
|
||||
shenstarsIn = StartCoroutine(AnimateAnchoredXAndFade(shenstarsRt, -1602.1f, -437.5785f, introMoveDuration, true));
|
||||
if (scoreBottomIn != null || shenstarsIn != null)
|
||||
@@ -1182,7 +1206,7 @@ public class settlementController : MonoBehaviour
|
||||
// score numbers: pm -> idol -> accuracy
|
||||
yield return StartCoroutine(AnimateIntText(pmScoreSum_Text, targetPmScore, introNumberDuration));
|
||||
yield return StartCoroutine(AnimateIntText(idolScoreSum_Text, targetIdolScore, introNumberDuration));
|
||||
yield return StartCoroutine(AnimateFloatPercentText(accuracy_Text, targetAccuracyPercent, introNumberDuration, "F3"));
|
||||
yield return StartCoroutine(AnimateFloatPercentText(accuracy_Text, targetAccuracyPercent, introNumberDuration, "F1"));
|
||||
|
||||
// total + percent after above, and flash once completed
|
||||
Coroutine totalScoreIn = StartCoroutine(AnimateIntText(finalScore_Text, targetTotalScore, introNumberDuration));
|
||||
@@ -1287,7 +1311,7 @@ public class settlementController : MonoBehaviour
|
||||
|
||||
if (rightBottomRt != null) { SetAnchoredX(rightBottomRt, 1677.75f); SetCanvasAlpha(rightBottomRt, 1f); }
|
||||
if (quhuiImageTr != null) SetCanvasAlpha(quhuiImageTr, 1f);
|
||||
if (scoreBottomRt != null) { SetAnchoredX(scoreBottomRt, -432.4f); SetCanvasAlpha(scoreBottomRt, 1f); }
|
||||
if (scoreBottomRt != null) { SetAnchoredX(scoreBottomRt, 0f); SetCanvasAlpha(scoreBottomRt, 1f); }
|
||||
if (shenstarsRt != null) { SetAnchoredX(shenstarsRt, -437.5785f); SetCanvasAlpha(shenstarsRt, 1f); }
|
||||
if (rewardRt != null) { SetAnchoredY(rewardRt, 0f); SetCanvasAlpha(rewardRt, 1f); }
|
||||
if (lihuiTr != null) SetCanvasAlpha(lihuiTr, 1f);
|
||||
@@ -1311,7 +1335,7 @@ public class settlementController : MonoBehaviour
|
||||
|
||||
if (pmScoreSum_Text != null) { pmScoreSum_Text.text = targetPmScore.ToString(); SetCanvasAlpha(pmScoreSum_Text.transform, 1f); }
|
||||
if (idolScoreSum_Text != null) { idolScoreSum_Text.text = targetIdolScore.ToString(); SetCanvasAlpha(idolScoreSum_Text.transform, 1f); }
|
||||
if (accuracy_Text != null) { accuracy_Text.text = targetAccuracyPercent.ToString("F3") + "%"; SetCanvasAlpha(accuracy_Text.transform, 1f); }
|
||||
if (accuracy_Text != null) { accuracy_Text.text = targetAccuracyPercent.ToString("F1") + "%"; SetCanvasAlpha(accuracy_Text.transform, 1f); }
|
||||
if (finalScore_Text != null) { finalScore_Text.text = targetTotalScore.ToString(); SetCanvasAlpha(finalScore_Text.transform, 1f); }
|
||||
if (thisLevel_currentPercentage_Text != null) { thisLevel_currentPercentage_Text.text = targetTotalPercent.ToString("F2") + "%"; SetCanvasAlpha(thisLevel_currentPercentage_Text.transform, 1f); }
|
||||
|
||||
@@ -2123,6 +2147,8 @@ public class settlementController : MonoBehaviour
|
||||
mvp_hero_hd_image.sprite = null;
|
||||
mvp_hero_hd_image.color = new Color(mvp_hero_hd_image.color.r, mvp_hero_hd_image.color.g, mvp_hero_hd_image.color.b, 0f);
|
||||
}
|
||||
// Activate legacy_text when no MVP conditions are met
|
||||
if (legacy_text != null) legacy_text.gameObject.SetActive(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2186,6 +2212,9 @@ public class settlementController : MonoBehaviour
|
||||
if (mvp_heroIcon != null) mvp_heroIcon.sprite = heroSO.ally_hero_squareProfile;
|
||||
if (mvp_score != null) mvp_score.text = max.ToString();
|
||||
|
||||
// Deactivate legacy_text when MVP is successfully displayed
|
||||
if (legacy_text != null) legacy_text.gameObject.SetActive(false);
|
||||
|
||||
Debug.Log($"[SettlementController] MVP assigned from slot {topIndex+1} with score {max}: {heroSO.ally_heroName}");
|
||||
}
|
||||
else
|
||||
@@ -2197,6 +2226,10 @@ public class settlementController : MonoBehaviour
|
||||
mvp_hero_hd_image.sprite = null;
|
||||
mvp_hero_hd_image.color = new Color(mvp_hero_hd_image.color.r, mvp_hero_hd_image.color.g, mvp_hero_hd_image.color.b, 0f);
|
||||
}
|
||||
|
||||
// Activate legacy_text when hero SO is not found
|
||||
if (legacy_text != null) legacy_text.gameObject.SetActive(true);
|
||||
|
||||
Debug.LogWarning("[SettlementController] MVP SO or HD image not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class trackFractureController : MonoBehaviour
|
||||
{
|
||||
private const int TrackCount = 5;
|
||||
private static readonly int FadeId = Shader.PropertyToID("_Fade");
|
||||
|
||||
[System.Serializable]
|
||||
private class TrackSpriteFadeEntry
|
||||
{
|
||||
public SpriteRenderer spriteRendererA;
|
||||
public SpriteRenderer spriteRendererB;
|
||||
}
|
||||
|
||||
[Header("Tracks")]
|
||||
[SerializeField] private GameObject[] trackObjects = new GameObject[TrackCount];
|
||||
[SerializeField] private Material[] dissolveMaterials = new Material[TrackCount];
|
||||
[SerializeField] private TrackSpriteFadeEntry[] trackSpriteFadeEntries = new TrackSpriteFadeEntry[TrackCount];
|
||||
[SerializeField] private bool autoFetchFractureAndDrift = true;
|
||||
[SerializeField] private bool autoApplyDissolveMaterialOnStart = true;
|
||||
|
||||
[Tooltip("If true, each track gets its own runtime CLONE of its dissolve material. The clone REPLACES the " +
|
||||
"material in the renderer slot, so you can no longer drive _Fade through the original material asset " +
|
||||
"reference. Only needed when several tracks share one material asset. If false (default), the assigned " +
|
||||
"dissolve material is used directly on the renderer, so controlling that material's _Fade (via this " +
|
||||
"controller or externally) drives the dissolve.")]
|
||||
[SerializeField] private bool cloneDissolveMaterialAtRuntime = false;
|
||||
|
||||
[Header("Fracture Fade")]
|
||||
[Tooltip("float1: seconds to wait AFTER a track fractures before its dissolve _Fade starts changing.")]
|
||||
[SerializeField] private float fractureFadeDelay = 0.2f;
|
||||
|
||||
[Tooltip("Seconds over which the linked SpriteRenderer pair fades from alpha 1 to 0 after fracture.")]
|
||||
[SerializeField] private float spriteRendererFadeDuration = 0.2f;
|
||||
|
||||
[Tooltip("float2: seconds over which the dissolve _Fade is driven from 1 to 0 once the fade begins. " +
|
||||
"When this completes, the track's fragments are reclaimed asynchronously (collected one-by-one).")]
|
||||
[SerializeField] private float fractureFadeDuration = 1f;
|
||||
|
||||
[Tooltip("The _Fade value written when the fracture dissolve begins.")]
|
||||
[SerializeField] private float fractureFadeStartValue = 0f;
|
||||
|
||||
[Tooltip("The _Fade value written when the fracture dissolve completes.")]
|
||||
[SerializeField] private float fractureFadeEndValue = 1f;
|
||||
|
||||
[Header("Start Z Move")]
|
||||
[SerializeField] private bool playStartZMove = true;
|
||||
[SerializeField] private float startLocalZ = 0f;
|
||||
[SerializeField] private float endLocalZ = 0.286f;
|
||||
[SerializeField] private float moveDuration = 0.35f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool enableDebugKeyTrigger = true;
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
|
||||
private readonly FractureAndDrift[] fractureDrivers = new FractureAndDrift[TrackCount];
|
||||
private readonly Renderer[] trackRenderers = new Renderer[TrackCount];
|
||||
private readonly Material[] runtimeDissolveMaterials = new Material[TrackCount];
|
||||
// Original _Fade of each ASSIGNED material asset, captured before we touch it, so we can restore it
|
||||
// on teardown. Needed only when NOT cloning: we drive the asset directly, and Unity persists those
|
||||
// writes back to the .mat, so the value would otherwise stick after exiting play mode. NaN = not captured.
|
||||
private readonly float[] originalFadeValues = new float[TrackCount];
|
||||
private Coroutine startMoveRoutine;
|
||||
private readonly Coroutine[] fadeRoutines = new Coroutine[TrackCount];
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CacheTrackReferences();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
CacheTrackReferences();
|
||||
ApplyRuntimeDissolveMaterials();
|
||||
SetAllTrackLocalZ(startLocalZ);
|
||||
|
||||
if (playStartZMove)
|
||||
{
|
||||
startMoveRoutine = StartCoroutine(AnimateTracksToEndZ());
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!enableDebugKeyTrigger)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[trackFractureController] Debug key 1 pressed.", this);
|
||||
TriggerTrackFracture(0);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[trackFractureController] Debug key 2 pressed.", this);
|
||||
TriggerTrackFracture(1);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[trackFractureController] Debug key 3 pressed.", this);
|
||||
TriggerTrackFracture(2);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[trackFractureController] Debug key 4 pressed.", this);
|
||||
TriggerTrackFracture(3);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
|
||||
{
|
||||
if (debugLogs) Debug.Log("[trackFractureController] Debug key 5 pressed.", this);
|
||||
TriggerTrackFracture(4);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
EnsureArraySizes();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
CacheTrackReferences();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// When not cloning, we drove the assigned .mat asset directly and Unity persists those writes.
|
||||
// Restore each asset's original _Fade so the value doesn't stick after play mode ends.
|
||||
RestoreOriginalFadeValues();
|
||||
|
||||
for (int i = 0; i < runtimeDissolveMaterials.Length; i++)
|
||||
{
|
||||
if (runtimeDissolveMaterials[i] != null)
|
||||
{
|
||||
Destroy(runtimeDissolveMaterials[i]);
|
||||
runtimeDissolveMaterials[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreOriginalFadeValues()
|
||||
{
|
||||
for (int i = 0; i < TrackCount; i++)
|
||||
{
|
||||
// Only restore assets we drove directly (clones are thrown away, no restore needed).
|
||||
if (runtimeDissolveMaterials[i] != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Material sourceMaterial = dissolveMaterials[i];
|
||||
if (sourceMaterial == null || float.IsNaN(originalFadeValues[i]) || !sourceMaterial.HasProperty(FadeId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceMaterial.SetFloat(FadeId, originalFadeValues[i]);
|
||||
originalFadeValues[i] = float.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerTrackFractureByNumber(int trackNumber)
|
||||
{
|
||||
TriggerTrackFracture(trackNumber - 1);
|
||||
}
|
||||
|
||||
public void TriggerTrackFracture(int trackIndex)
|
||||
{
|
||||
if (!IsValidTrackIndex(trackIndex))
|
||||
{
|
||||
Debug.LogWarning($"[trackFractureController] Invalid track index: {trackIndex}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
FractureAndDrift fractureDriver = fractureDrivers[trackIndex];
|
||||
if (fractureDriver == null)
|
||||
{
|
||||
Debug.LogWarning($"[trackFractureController] Missing FractureAndDrift on track {trackIndex + 1}.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
fractureDriver.Shatter();
|
||||
|
||||
StartFractureFade(trackIndex);
|
||||
}
|
||||
|
||||
private void StartFractureFade(int trackIndex)
|
||||
{
|
||||
Material targetMaterial = GetFadeTargetMaterial(trackIndex);
|
||||
if (targetMaterial == null || !targetMaterial.HasProperty(FadeId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (fadeRoutines[trackIndex] != null)
|
||||
{
|
||||
StopCoroutine(fadeRoutines[trackIndex]);
|
||||
}
|
||||
|
||||
fadeRoutines[trackIndex] = StartCoroutine(FadeTrackOut(trackIndex, targetMaterial));
|
||||
}
|
||||
|
||||
private IEnumerator FadeTrackOut(int trackIndex, Material targetMaterial)
|
||||
{
|
||||
TrackSpriteFadeEntry spriteFadeEntry = GetSpriteFadeEntry(trackIndex);
|
||||
SetSpriteFadeEntryAlpha(spriteFadeEntry, 1f);
|
||||
|
||||
if (spriteRendererFadeDuration > 0f)
|
||||
{
|
||||
float spriteFadeElapsed = 0f;
|
||||
while (spriteFadeElapsed < spriteRendererFadeDuration)
|
||||
{
|
||||
spriteFadeElapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(spriteFadeElapsed / spriteRendererFadeDuration);
|
||||
SetSpriteFadeEntryAlpha(spriteFadeEntry, Mathf.Lerp(1f, 0f, t));
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSpriteFadeEntryAlpha(spriteFadeEntry, 0f);
|
||||
}
|
||||
|
||||
SetSpriteFadeEntryAlpha(spriteFadeEntry, 0f);
|
||||
|
||||
// float1: hold before the dissolve begins. Fragments have already started drifting during this time.
|
||||
if (fractureFadeDelay > 0f)
|
||||
{
|
||||
yield return new WaitForSeconds(fractureFadeDelay);
|
||||
}
|
||||
|
||||
// float2: drive _Fade from the configured start value to the configured end value over this duration.
|
||||
float duration = Mathf.Max(0.0001f, fractureFadeDuration);
|
||||
float startFade = Mathf.Clamp01(fractureFadeStartValue);
|
||||
float endFade = Mathf.Clamp01(fractureFadeEndValue);
|
||||
float elapsed = 0f;
|
||||
|
||||
targetMaterial.SetFloat(FadeId, startFade);
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
targetMaterial.SetFloat(FadeId, Mathf.Lerp(startFade, endFade, t));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
targetMaterial.SetFloat(FadeId, endFade);
|
||||
fadeRoutines[trackIndex] = null;
|
||||
|
||||
// Fade finished: asynchronously reclaim (collect) this track's fragments rather than destroying
|
||||
// them all at once.
|
||||
FractureAndDrift fractureDriver = fractureDrivers[trackIndex];
|
||||
if (fractureDriver != null)
|
||||
{
|
||||
fractureDriver.ReclaimFragments();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTrackFadeByNumber(int trackNumber, float fadeValue)
|
||||
{
|
||||
SetTrackFade(trackNumber - 1, fadeValue);
|
||||
}
|
||||
|
||||
public void SetTrackFade(int trackIndex, float fadeValue)
|
||||
{
|
||||
if (!IsValidTrackIndex(trackIndex))
|
||||
{
|
||||
Debug.LogWarning($"[trackFractureController] Invalid track index: {trackIndex}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
Material targetMaterial = GetFadeTargetMaterial(trackIndex);
|
||||
if (targetMaterial == null)
|
||||
{
|
||||
Debug.LogWarning($"[trackFractureController] Missing dissolve material on track {trackIndex + 1}.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!targetMaterial.HasProperty(FadeId))
|
||||
{
|
||||
Debug.LogWarning($"[trackFractureController] Material on track {trackIndex + 1} has no _Fade property.", targetMaterial);
|
||||
return;
|
||||
}
|
||||
|
||||
targetMaterial.SetFloat(FadeId, Mathf.Clamp01(fadeValue));
|
||||
}
|
||||
|
||||
public float GetTrackFade(int trackIndex)
|
||||
{
|
||||
if (!IsValidTrackIndex(trackIndex))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
Material targetMaterial = GetFadeTargetMaterial(trackIndex);
|
||||
if (targetMaterial == null || !targetMaterial.HasProperty(FadeId))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return targetMaterial.GetFloat(FadeId);
|
||||
}
|
||||
|
||||
public void SetAllTrackFade(float fadeValue)
|
||||
{
|
||||
for (int i = 0; i < TrackCount; i++)
|
||||
{
|
||||
SetTrackFade(i, fadeValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartStartZMove()
|
||||
{
|
||||
SetAllTrackLocalZ(startLocalZ);
|
||||
|
||||
if (startMoveRoutine != null)
|
||||
{
|
||||
StopCoroutine(startMoveRoutine);
|
||||
}
|
||||
|
||||
startMoveRoutine = StartCoroutine(AnimateTracksToEndZ());
|
||||
}
|
||||
|
||||
public void TriggerTrack1Fracture() => TriggerTrackFracture(0);
|
||||
public void TriggerTrack2Fracture() => TriggerTrackFracture(1);
|
||||
public void TriggerTrack3Fracture() => TriggerTrackFracture(2);
|
||||
public void TriggerTrack4Fracture() => TriggerTrackFracture(3);
|
||||
public void TriggerTrack5Fracture() => TriggerTrackFracture(4);
|
||||
|
||||
private void CacheTrackReferences()
|
||||
{
|
||||
EnsureArraySizes();
|
||||
|
||||
for (int i = 0; i < TrackCount; i++)
|
||||
{
|
||||
GameObject trackObject = trackObjects[i];
|
||||
fractureDrivers[i] = null;
|
||||
trackRenderers[i] = null;
|
||||
|
||||
if (trackObject == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (autoFetchFractureAndDrift)
|
||||
{
|
||||
fractureDrivers[i] = trackObject.GetComponent<FractureAndDrift>();
|
||||
if (fractureDrivers[i] == null)
|
||||
{
|
||||
fractureDrivers[i] = trackObject.GetComponentInChildren<FractureAndDrift>(true);
|
||||
}
|
||||
}
|
||||
|
||||
trackRenderers[i] = trackObject.GetComponent<Renderer>();
|
||||
if (trackRenderers[i] == null)
|
||||
{
|
||||
trackRenderers[i] = trackObject.GetComponentInChildren<Renderer>(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRuntimeDissolveMaterials()
|
||||
{
|
||||
for (int i = 0; i < TrackCount; i++)
|
||||
{
|
||||
if (runtimeDissolveMaterials[i] != null)
|
||||
{
|
||||
Destroy(runtimeDissolveMaterials[i]);
|
||||
runtimeDissolveMaterials[i] = null;
|
||||
}
|
||||
|
||||
originalFadeValues[i] = float.NaN;
|
||||
|
||||
Material sourceMaterial = dissolveMaterials[i];
|
||||
Renderer trackRenderer = trackRenderers[i];
|
||||
if (sourceMaterial == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The material actually used on the renderer AND driven for fade. When cloning is off this is the
|
||||
// assigned asset itself, so external control of that asset's _Fade drives the dissolve directly.
|
||||
Material appliedMaterial = sourceMaterial;
|
||||
if (cloneDissolveMaterialAtRuntime)
|
||||
{
|
||||
appliedMaterial = new Material(sourceMaterial) { name = sourceMaterial.name + "_Runtime" };
|
||||
runtimeDissolveMaterials[i] = appliedMaterial;
|
||||
}
|
||||
else if (sourceMaterial.HasProperty(FadeId))
|
||||
{
|
||||
// Not cloning: we mutate the asset directly, so remember its original _Fade to restore later.
|
||||
originalFadeValues[i] = sourceMaterial.GetFloat(FadeId);
|
||||
}
|
||||
|
||||
if (autoApplyDissolveMaterialOnStart && trackRenderer != null)
|
||||
{
|
||||
Material[] materials = trackRenderer.sharedMaterials;
|
||||
if (materials == null || materials.Length == 0)
|
||||
{
|
||||
trackRenderer.sharedMaterial = appliedMaterial;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool replaced = false;
|
||||
for (int j = 0; j < materials.Length; j++)
|
||||
{
|
||||
if (materials[j] == sourceMaterial)
|
||||
{
|
||||
materials[j] = appliedMaterial;
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!replaced)
|
||||
{
|
||||
materials[0] = appliedMaterial;
|
||||
}
|
||||
|
||||
trackRenderer.sharedMaterials = materials;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator AnimateTracksToEndZ()
|
||||
{
|
||||
float duration = Mathf.Max(0.0001f, moveDuration);
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
float z = Mathf.Lerp(startLocalZ, endLocalZ, t);
|
||||
SetAllTrackLocalZ(z);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetAllTrackLocalZ(endLocalZ);
|
||||
startMoveRoutine = null;
|
||||
}
|
||||
|
||||
private void SetAllTrackLocalZ(float zValue)
|
||||
{
|
||||
for (int i = 0; i < TrackCount; i++)
|
||||
{
|
||||
GameObject trackObject = trackObjects[i];
|
||||
if (trackObject == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Transform targetTransform = trackObject.transform;
|
||||
Vector3 localPosition = targetTransform.localPosition;
|
||||
localPosition.z = zValue;
|
||||
targetTransform.localPosition = localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private Material GetFadeTargetMaterial(int trackIndex)
|
||||
{
|
||||
if (!IsValidTrackIndex(trackIndex))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// When cloning is on, drive the runtime clone. Otherwise drive the assigned asset directly - which is
|
||||
// also the material now used on the renderer, so controlling that asset's _Fade drives the dissolve.
|
||||
return runtimeDissolveMaterials[trackIndex] != null
|
||||
? runtimeDissolveMaterials[trackIndex]
|
||||
: dissolveMaterials[trackIndex];
|
||||
}
|
||||
|
||||
private void EnsureArraySizes()
|
||||
{
|
||||
if (trackObjects == null || trackObjects.Length != TrackCount)
|
||||
{
|
||||
System.Array.Resize(ref trackObjects, TrackCount);
|
||||
}
|
||||
|
||||
if (dissolveMaterials == null || dissolveMaterials.Length != TrackCount)
|
||||
{
|
||||
System.Array.Resize(ref dissolveMaterials, TrackCount);
|
||||
}
|
||||
|
||||
if (trackSpriteFadeEntries == null || trackSpriteFadeEntries.Length != TrackCount)
|
||||
{
|
||||
System.Array.Resize(ref trackSpriteFadeEntries, TrackCount);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidTrackIndex(int trackIndex)
|
||||
{
|
||||
return trackIndex >= 0 && trackIndex < TrackCount;
|
||||
}
|
||||
|
||||
private TrackSpriteFadeEntry GetSpriteFadeEntry(int trackIndex)
|
||||
{
|
||||
if (!IsValidTrackIndex(trackIndex) || trackSpriteFadeEntries == null || trackIndex >= trackSpriteFadeEntries.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return trackSpriteFadeEntries[trackIndex];
|
||||
}
|
||||
|
||||
private static void SetSpriteFadeEntryAlpha(TrackSpriteFadeEntry entry, float alpha)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetSpriteRendererAlpha(entry.spriteRendererA, alpha);
|
||||
SetSpriteRendererAlpha(entry.spriteRendererB, alpha);
|
||||
}
|
||||
|
||||
private static void SetSpriteRendererAlpha(SpriteRenderer spriteRenderer, float alpha)
|
||||
{
|
||||
if (spriteRenderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color color = spriteRenderer.color;
|
||||
color.a = Mathf.Clamp01(alpha);
|
||||
spriteRenderer.color = color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f43164d05a229940a9b89f7c35236ae
|
||||
@@ -6,6 +6,7 @@ using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
using DG.Tweening;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
@@ -840,6 +841,12 @@ public class teamUIController : MonoBehaviour
|
||||
public TextMeshProUGUI allSum_pmScore;
|
||||
[Header("Inspector")]
|
||||
public TextMeshProUGUI allSum_idolScore;
|
||||
[Header("Inspector")]
|
||||
public Text recentPlusAmountText;
|
||||
[Header("Inspector")]
|
||||
public Image currentScoreRankImage;
|
||||
[Header("Inspector")]
|
||||
public RankConfig currentScoreRankConfig;
|
||||
|
||||
// new: runtime enemy instance and UI sync fields
|
||||
private EnemyCombatant enemyCombatantInstance;
|
||||
@@ -872,6 +879,7 @@ public class teamUIController : MonoBehaviour
|
||||
|
||||
private int combo = 0;
|
||||
public int CurrentCombo => combo;
|
||||
private Tween recentPlusAmountFadeTween;
|
||||
|
||||
public enum ComboJudgeType
|
||||
{
|
||||
@@ -1173,6 +1181,19 @@ public class teamUIController : MonoBehaviour
|
||||
{
|
||||
SpawnNextEnemy();
|
||||
}
|
||||
|
||||
if (recentPlusAmountText != null)
|
||||
{
|
||||
recentPlusAmountText.text = "+0";
|
||||
Color color = recentPlusAmountText.color;
|
||||
color.a = 0f;
|
||||
recentPlusAmountText.color = color;
|
||||
}
|
||||
|
||||
if (ScoreManager.Instance != null)
|
||||
{
|
||||
ScoreManager.Instance.RefreshAllScoreUi();
|
||||
}
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
@@ -1233,10 +1254,12 @@ public class teamUIController : MonoBehaviour
|
||||
if (IsCombo(result))
|
||||
{
|
||||
combo++;
|
||||
ResetRecentPlusAmountVisual();
|
||||
}
|
||||
else
|
||||
{
|
||||
combo = 0;
|
||||
FadeOutRecentPlusAmount();
|
||||
}
|
||||
if (comboCounter != null)
|
||||
{
|
||||
@@ -1260,6 +1283,42 @@ public class teamUIController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetRecentPlusAmountVisual()
|
||||
{
|
||||
if (recentPlusAmountText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (recentPlusAmountFadeTween != null)
|
||||
{
|
||||
recentPlusAmountFadeTween.Kill();
|
||||
recentPlusAmountFadeTween = null;
|
||||
}
|
||||
|
||||
Color color = recentPlusAmountText.color;
|
||||
color.a = 1f;
|
||||
recentPlusAmountText.color = color;
|
||||
}
|
||||
|
||||
public void FadeOutRecentPlusAmount()
|
||||
{
|
||||
if (recentPlusAmountText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (recentPlusAmountFadeTween != null)
|
||||
{
|
||||
recentPlusAmountFadeTween.Kill();
|
||||
}
|
||||
|
||||
recentPlusAmountFadeTween = recentPlusAmountText
|
||||
.DOFade(0f, 0.25f)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() => recentPlusAmountFadeTween = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Documentation text normalized.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
// Bakes a Unity Gradient into a 1D ramp texture (width x 1) that can be assigned
|
||||
// to the Dissolve URP material's "Gradient Ramp" (_GradientTex) slot. The dissolve
|
||||
// edge value samples this ramp along its U axis, so the left of the gradient maps to
|
||||
// the inner edge and the right maps to the outer edge of the dissolve.
|
||||
public class DissolveGradientRampBaker : EditorWindow
|
||||
{
|
||||
[SerializeField] private Gradient gradient = CreateDefaultGradient();
|
||||
[SerializeField] private int width = 256;
|
||||
[SerializeField] private bool hdr = true;
|
||||
[SerializeField] private string outputFolder = "Assets/Rainbow-Cats-Unity-Dissolve-HDR-Shaders-main/Materials";
|
||||
[SerializeField] private string fileName = "DissolveGradientRamp";
|
||||
|
||||
private SerializedObject serialized;
|
||||
|
||||
[MenuItem("Bansonic/Rendering/Dissolve Gradient Ramp Baker")]
|
||||
public static void Open()
|
||||
{
|
||||
GetWindow<DissolveGradientRampBaker>("Gradient Ramp Baker").minSize = new Vector2(360f, 260f);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
serialized = new SerializedObject(this);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
serialized.Update();
|
||||
|
||||
EditorGUILayout.LabelField("Bake a Gradient into a ramp texture", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Assign the baked texture to the material's 'Gradient Ramp' slot. " +
|
||||
"The dissolve edge samples it left-to-right.",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUILayout.PropertyField(serialized.FindProperty(nameof(gradient)));
|
||||
EditorGUILayout.PropertyField(serialized.FindProperty(nameof(width)));
|
||||
EditorGUILayout.PropertyField(serialized.FindProperty(nameof(hdr)),
|
||||
new GUIContent("HDR (float texture)"));
|
||||
EditorGUILayout.PropertyField(serialized.FindProperty(nameof(outputFolder)));
|
||||
EditorGUILayout.PropertyField(serialized.FindProperty(nameof(fileName)));
|
||||
|
||||
serialized.ApplyModifiedProperties();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
if (GUILayout.Button("Bake Ramp Texture", GUILayout.Height(32f)))
|
||||
Bake();
|
||||
}
|
||||
|
||||
private void Bake()
|
||||
{
|
||||
int w = Mathf.Clamp(width, 2, 4096);
|
||||
var format = hdr ? TextureFormat.RGBAFloat : TextureFormat.RGBA32;
|
||||
var tex = new Texture2D(w, 1, format, false, true)
|
||||
{
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear
|
||||
};
|
||||
|
||||
var pixels = new Color[w];
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
float t = w == 1 ? 0f : (float)x / (w - 1);
|
||||
pixels[x] = gradient.Evaluate(t);
|
||||
}
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply(false, false);
|
||||
|
||||
if (!AssetDatabase.IsValidFolder(outputFolder))
|
||||
{
|
||||
Debug.LogError($"[DissolveGradientRampBaker] Output folder does not exist: {outputFolder}");
|
||||
Object.DestroyImmediate(tex);
|
||||
return;
|
||||
}
|
||||
|
||||
string extension = hdr ? "exr" : "png";
|
||||
byte[] bytes = hdr
|
||||
? tex.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat)
|
||||
: tex.EncodeToPNG();
|
||||
string assetPath = $"{outputFolder}/{fileName}.{extension}";
|
||||
assetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath);
|
||||
File.WriteAllBytes(assetPath, bytes);
|
||||
Object.DestroyImmediate(tex);
|
||||
|
||||
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
|
||||
ConfigureImporter(assetPath);
|
||||
|
||||
var imported = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
|
||||
EditorGUIUtility.PingObject(imported);
|
||||
Selection.activeObject = imported;
|
||||
Debug.Log($"[DissolveGradientRampBaker] Baked ramp to {assetPath}");
|
||||
}
|
||||
|
||||
private void ConfigureImporter(string assetPath)
|
||||
{
|
||||
if (AssetImporter.GetAtPath(assetPath) is not TextureImporter importer)
|
||||
return;
|
||||
|
||||
importer.textureType = TextureImporterType.Default;
|
||||
importer.wrapMode = TextureWrapMode.Clamp;
|
||||
importer.filterMode = FilterMode.Bilinear;
|
||||
importer.mipmapEnabled = false;
|
||||
importer.sRGBTexture = !hdr;
|
||||
importer.alphaSource = TextureImporterAlphaSource.FromInput;
|
||||
importer.alphaIsTransparency = false;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
private static Gradient CreateDefaultGradient()
|
||||
{
|
||||
var g = new Gradient();
|
||||
g.SetKeys(
|
||||
new[]
|
||||
{
|
||||
new GradientColorKey(new Color(1f, 0.35f, 0.1f), 0f),
|
||||
new GradientColorKey(new Color(1f, 0.9f, 0.3f), 0.5f),
|
||||
new GradientColorKey(new Color(0.3f, 0.7f, 1f), 1f)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new GradientAlphaKey(1f, 0f),
|
||||
new GradientAlphaKey(1f, 1f)
|
||||
});
|
||||
return g;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9821637dad915c344b817ce763d891ae
|
||||
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
// Compiles the FX / sprite / particle shaders used by gameplay during the preload
|
||||
// screen so the first note (or first VFX / UI blur) does not pay the variant compile
|
||||
// cost mid-gameplay.
|
||||
//
|
||||
// Progress: WarmupRoutine drives compilation across frames and reports a localized
|
||||
// progress string ("正在编译着色器 xx%") through the onProgress callback so the caller
|
||||
// (preload) can surface it on the loading overlay. When nothing needs compiling
|
||||
// (cache valid) it reports the idle default ("加载中...").
|
||||
//
|
||||
// Persistence: after compiling, a validation manifest is written to a project-local
|
||||
// ShaderCache folder (never Application.persistentDataPath / the C: AppData path). On
|
||||
// the next load the manifest is compared against the current shader sources + GPU +
|
||||
// build. If everything matches, compilation is skipped; if any shader source changed
|
||||
// (or the GPU / build changed, invalidating previously compiled variants) the shaders
|
||||
// are recompiled and the manifest refreshed.
|
||||
public static class GlobalFxShaderWarmer
|
||||
{
|
||||
public const string IdleLoadingText = "加载中...";
|
||||
private const string CompilingTextFormat = "正在编译着色器 {0}%";
|
||||
|
||||
[Serializable]
|
||||
private class ShaderEntry
|
||||
{
|
||||
public string name;
|
||||
public string sourceHash;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class WarmManifest
|
||||
{
|
||||
public string unityVersion;
|
||||
public string appVersion;
|
||||
public string graphicsDevice;
|
||||
public string graphicsDeviceType;
|
||||
public ShaderEntry[] shaders;
|
||||
}
|
||||
|
||||
// A shader to warm plus its source path relative to Application.dataPath (the
|
||||
// Assets folder). sourcePath may be null for engine/built-in shaders that have no
|
||||
// file under Assets; those are still gated by the Unity/GPU/build fingerprint.
|
||||
private readonly struct WarmTarget
|
||||
{
|
||||
public readonly string ShaderName;
|
||||
public readonly string SourcePath;
|
||||
|
||||
public WarmTarget(string shaderName, string sourcePath)
|
||||
{
|
||||
ShaderName = shaderName;
|
||||
SourcePath = sourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
// Order is stable and used directly for manifest comparison.
|
||||
private static readonly WarmTarget[] Targets =
|
||||
{
|
||||
// Global full-screen FX blit shaders (noteFunction effects).
|
||||
new WarmTarget("Hidden/Bansonic/GlobalGlitch", "Shaders/GlobalGlitch.shader"),
|
||||
new WarmTarget("Hidden/Bansonic/GlobalMonochrome", "Shaders/GlobalMonochrome.shader"),
|
||||
new WarmTarget("Hidden/Bansonic/GlobalDistortion", "Shaders/GlobalDistortion.shader"),
|
||||
new WarmTarget("Hidden/Bansonic/GlobalAfterimage", "Shaders/GlobalAfterimage.shader"),
|
||||
|
||||
// Explicitly requested particle shaders.
|
||||
new WarmTarget("Cartoon FX/Remaster/Particle Ubershader",
|
||||
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Ubershader.cfxrshader"),
|
||||
new WarmTarget("Mobile/Particles/Additive", null),
|
||||
|
||||
// Other CFXR particle shaders used across gameplay VFX.
|
||||
new WarmTarget("Cartoon FX/Remaster/Particle Procedural Glow",
|
||||
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Glow.cfxrshader"),
|
||||
new WarmTarget("Cartoon FX/Remaster/Particle Screen Distortion",
|
||||
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Distortion.cfxrshader"),
|
||||
new WarmTarget("Cartoon FX/Remaster/Particle Procedural Ring",
|
||||
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Procedural Ring.cfxrshader"),
|
||||
|
||||
// Legacy Cartoon FX particle shaders still referenced by prefabs.
|
||||
new WarmTarget("Cartoon FX/Legacy/Particles Additive Alpha8",
|
||||
"JMO Assets/Cartoon FX (legacy)/Shaders/CFXM_MobileParticleAdd_Alpha8.shader"),
|
||||
new WarmTarget("Cartoon FX/Legacy/Particle Multiply Colored",
|
||||
"JMO Assets/Cartoon FX (legacy)/Shaders/CFX3 Multiply Color.shader"),
|
||||
|
||||
// Custom sprite/UI shaders used by gameplay materials.
|
||||
new WarmTarget("Bansonic/Sprite Glow", "Shaders/BansonicSpriteGlow.shader"),
|
||||
new WarmTarget("Bansonic/Sprite Light Curtain", "Shaders/BansonicSpriteLightCurtain.shader"),
|
||||
new WarmTarget("Bansonic/Sprite Vertical Glow Only", "Shaders/BansonicSpriteVerticalGlowOnly.shader"),
|
||||
new WarmTarget("Sprites/Outline", "SpriteGlow/Resources/SpriteGlow/Shaders/SpriteOutline.shader"),
|
||||
new WarmTarget("Custom/SphereGradientShader", "sphereShaders/SphereGradient.shader"),
|
||||
new WarmTarget("UI/Soft Shadow", "Shaders/UI/UIShadow.shader"),
|
||||
new WarmTarget("UI/UIGaussianBlur", "Shaders/UI/UIGaussianBlur.shader"),
|
||||
new WarmTarget("UI/Bansonic/Blur Behind", "Shaders/UI/BansonicUIBlurBehind.shader")
|
||||
};
|
||||
|
||||
private const string ManifestFolder = "ShaderCache";
|
||||
private const string ManifestFileName = "fx_shader_warm_manifest.json";
|
||||
|
||||
private static bool s_warmed;
|
||||
|
||||
// Coroutine entry point: warms shaders across frames and reports progress. Safe to
|
||||
// call once per process; subsequent calls are no-ops (guarded by s_warmed).
|
||||
public static IEnumerator WarmupRoutine(Action<string> onProgress)
|
||||
{
|
||||
if (s_warmed)
|
||||
{
|
||||
onProgress?.Invoke(IdleLoadingText);
|
||||
yield break;
|
||||
}
|
||||
s_warmed = true;
|
||||
|
||||
WarmManifest current = BuildCurrentManifest();
|
||||
WarmManifest cached = TryLoadManifest();
|
||||
|
||||
if (cached != null && ManifestsMatch(cached, current))
|
||||
{
|
||||
// Shaders (and GPU/build) unchanged: the driver's persistent pipeline cache
|
||||
// still holds valid binaries, so skip the redundant compile this process.
|
||||
Debug.Log("[GlobalFxShaderWarmer] Cached warm manifest valid; skipping shader recompile.");
|
||||
onProgress?.Invoke(IdleLoadingText);
|
||||
yield break;
|
||||
}
|
||||
|
||||
RenderTexture temp = null;
|
||||
RenderTexture previousActive = RenderTexture.active;
|
||||
int count = Targets.Length;
|
||||
try
|
||||
{
|
||||
temp = RenderTexture.GetTemporary(4, 4, 0);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int percent = Mathf.Clamp(Mathf.RoundToInt((float)i / count * 100f), 0, 100);
|
||||
onProgress?.Invoke(string.Format(CompilingTextFormat, percent));
|
||||
CompileOne(Targets[i].ShaderName, temp);
|
||||
|
||||
// Yield so the compile spikes are spread over frames and the progress
|
||||
// text can refresh instead of blocking on one long frame.
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (temp != null)
|
||||
RenderTexture.ReleaseTemporary(temp);
|
||||
RenderTexture.active = previousActive;
|
||||
}
|
||||
|
||||
SaveManifest(current);
|
||||
onProgress?.Invoke(IdleLoadingText);
|
||||
Debug.Log(cached == null
|
||||
? "[GlobalFxShaderWarmer] No cache found; compiled shaders and wrote manifest."
|
||||
: "[GlobalFxShaderWarmer] Shader source or device changed; recompiled shaders and refreshed manifest.");
|
||||
}
|
||||
|
||||
private static void CompileOne(string shaderName, RenderTexture temp)
|
||||
{
|
||||
Shader shader = Shader.Find(shaderName);
|
||||
if (shader == null)
|
||||
{
|
||||
Debug.LogWarning("[GlobalFxShaderWarmer] Shader not found: " + shaderName);
|
||||
return;
|
||||
}
|
||||
|
||||
Material material = CoreUtils.CreateEngineMaterial(shader);
|
||||
if (material == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Blit pass 0 to force the shader's variant to compile. Even for sprite /
|
||||
// particle shaders this triggers program compilation; the throwaway 4x4
|
||||
// render is discarded. Use a real source texture so URP's Blit does not
|
||||
// warn about a null source.
|
||||
Graphics.Blit(Texture2D.blackTexture, temp, material, 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GlobalFxShaderWarmer] Warmup blit failed for " + shaderName + ": " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CoreUtils.Destroy(material);
|
||||
}
|
||||
}
|
||||
|
||||
private static WarmManifest BuildCurrentManifest()
|
||||
{
|
||||
var entries = new ShaderEntry[Targets.Length];
|
||||
for (int i = 0; i < Targets.Length; i++)
|
||||
{
|
||||
entries[i] = new ShaderEntry
|
||||
{
|
||||
name = Targets[i].ShaderName,
|
||||
sourceHash = ComputeSourceHash(Targets[i].SourcePath)
|
||||
};
|
||||
}
|
||||
|
||||
return new WarmManifest
|
||||
{
|
||||
unityVersion = Application.unityVersion,
|
||||
appVersion = Application.version,
|
||||
graphicsDevice = SystemInfo.graphicsDeviceName,
|
||||
graphicsDeviceType = SystemInfo.graphicsDeviceType.ToString(),
|
||||
shaders = entries
|
||||
};
|
||||
}
|
||||
|
||||
// Hashes the .shader source so an edit invalidates the cache. Engine/built-in
|
||||
// shaders (sourcePath == null) or a stripped player where the file is absent hash
|
||||
// to empty; validity there is gated by the build version + GPU fingerprint.
|
||||
private static string ComputeSourceHash(string relativePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativePath))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
string full = Path.Combine(Application.dataPath, relativePath);
|
||||
if (!File.Exists(full))
|
||||
return string.Empty;
|
||||
|
||||
byte[] bytes = File.ReadAllBytes(full);
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(bytes);
|
||||
var sb = new StringBuilder(hash.Length * 2);
|
||||
foreach (byte b in hash)
|
||||
sb.Append(b.ToString("x2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GlobalFxShaderWarmer] Failed to hash shader source '" + relativePath + "': " + ex.Message);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ManifestsMatch(WarmManifest a, WarmManifest b)
|
||||
{
|
||||
if (a == null || b == null)
|
||||
return false;
|
||||
|
||||
if (a.unityVersion != b.unityVersion
|
||||
|| a.appVersion != b.appVersion
|
||||
|| a.graphicsDevice != b.graphicsDevice
|
||||
|| a.graphicsDeviceType != b.graphicsDeviceType)
|
||||
return false;
|
||||
|
||||
if (a.shaders == null || b.shaders == null || a.shaders.Length != b.shaders.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < a.shaders.Length; i++)
|
||||
{
|
||||
ShaderEntry ea = a.shaders[i];
|
||||
ShaderEntry eb = b.shaders[i];
|
||||
if (ea == null || eb == null)
|
||||
return false;
|
||||
if (ea.name != eb.name || ea.sourceHash != eb.sourceHash)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string GetManifestPath()
|
||||
{
|
||||
// Project-local folder only. Deliberately not Application.persistentDataPath
|
||||
// (that lives under C:/Users/.../AppData) per the requirement to stay off C:.
|
||||
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
string dir = Path.Combine(projectRoot, ManifestFolder);
|
||||
Directory.CreateDirectory(dir);
|
||||
return Path.Combine(dir, ManifestFileName);
|
||||
}
|
||||
|
||||
private static WarmManifest TryLoadManifest()
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = GetManifestPath();
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
string json = File.ReadAllText(path);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return null;
|
||||
|
||||
return JsonUtility.FromJson<WarmManifest>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GlobalFxShaderWarmer] Failed to read warm manifest: " + ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveManifest(WarmManifest manifest)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = GetManifestPath();
|
||||
string json = JsonUtility.ToJson(manifest, true);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GlobalFxShaderWarmer] Failed to write warm manifest: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6ba0a08c1b219f4e9eb2af1707a5510
|
||||
@@ -94,6 +94,38 @@ public class playerMessagePrefab : MonoBehaviour
|
||||
await TryGetAvatarSpriteAsync(steamId, normalizedAvatarUrl);
|
||||
}
|
||||
|
||||
public static bool TryGetCachedAvatarSprite(string steamId, string avatarUrl, out Sprite sprite)
|
||||
{
|
||||
sprite = null;
|
||||
|
||||
string normalizedAvatarUrl = GameServer.Client.NetworkManager.NormalizeAvatarUrlForClient(avatarUrl);
|
||||
if (!string.IsNullOrWhiteSpace(normalizedAvatarUrl)
|
||||
&& AvatarCache.TryGetValue(normalizedAvatarUrl, out Sprite cachedByUrl)
|
||||
&& cachedByUrl != null)
|
||||
{
|
||||
sprite = cachedByUrl;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(steamId)
|
||||
&& AvatarCache.TryGetValue(steamId, out Sprite cachedBySteamId)
|
||||
&& cachedBySteamId != null)
|
||||
{
|
||||
sprite = cachedBySteamId;
|
||||
return true;
|
||||
}
|
||||
|
||||
Sprite diskSprite = TryLoadAvatarSpriteFromDisk(steamId, normalizedAvatarUrl);
|
||||
if (diskSprite != null)
|
||||
{
|
||||
CacheAvatarSpriteInMemory(steamId, normalizedAvatarUrl, diskSprite);
|
||||
sprite = diskSprite;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task RefreshAvatarAsync(string steamId, string avatarUrl)
|
||||
{
|
||||
const int maxAttempts = 20;
|
||||
|
||||
@@ -667,7 +667,7 @@ RectTransform:
|
||||
m_GameObject: {fileID: 1226732915180594430}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1.0000305, y: 1.0000305, z: 1.0000305}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 2346837952888000687}
|
||||
@@ -675,7 +675,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -0.00024414062, y: -0.00024414062}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 3840, y: 2160}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2740858507354457569
|
||||
@@ -2004,8 +2004,8 @@ MonoBehaviour:
|
||||
m_TargetGraphic: {fileID: 6197548626279452246}
|
||||
m_HandleRect: {fileID: 3080131863592265227}
|
||||
m_Direction: 0
|
||||
m_Value: 1
|
||||
m_Size: 0.9999999
|
||||
m_Value: 0
|
||||
m_Size: 1
|
||||
m_NumberOfSteps: 0
|
||||
m_OnValueChanged:
|
||||
m_PersistentCalls:
|
||||
@@ -2246,8 +2246,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 435.98, y: -18.199}
|
||||
m_SizeDelta: {x: 333.95, y: 494.397}
|
||||
m_AnchoredPosition: {x: 479.1093, y: -18.199}
|
||||
m_SizeDelta: {x: 420.2086, y: 494.397}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6546604333839282624
|
||||
CanvasRenderer:
|
||||
@@ -2894,8 +2894,8 @@ MonoBehaviour:
|
||||
m_TargetGraphic: {fileID: 6407104958731660754}
|
||||
m_HandleRect: {fileID: 2464122768890705763}
|
||||
m_Direction: 0
|
||||
m_Value: 1
|
||||
m_Size: 1
|
||||
m_Value: 0
|
||||
m_Size: 0.99999976
|
||||
m_NumberOfSteps: 0
|
||||
m_OnValueChanged:
|
||||
m_PersistentCalls:
|
||||
@@ -3831,7 +3831,7 @@ MonoBehaviour:
|
||||
m_TargetGraphic: {fileID: 1040773132025281584}
|
||||
m_HandleRect: {fileID: 5614210503018957314}
|
||||
m_Direction: 2
|
||||
m_Value: 1
|
||||
m_Value: 0
|
||||
m_Size: 1
|
||||
m_NumberOfSteps: 0
|
||||
m_OnValueChanged:
|
||||
@@ -3954,7 +3954,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 1300, y: 800}
|
||||
m_SizeDelta: {x: 1500, y: 800}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7214062317850356192
|
||||
CanvasRenderer:
|
||||
@@ -4295,7 +4295,7 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 0
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
@@ -4303,7 +4303,7 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_AnchorMin.y
|
||||
value: 0
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
@@ -4311,7 +4311,7 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 30
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
@@ -4343,11 +4343,11 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: 0
|
||||
value: 169.875
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 0
|
||||
value: -100.5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6249172551117239467, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
@@ -4365,6 +4365,14 @@ PrefabInstance:
|
||||
propertyPath: m_SizeDelta.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6563396645772216654, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8888888888888888801, guid: 6ad36126be99a574285929280ea8fac7, type: 3}
|
||||
propertyPath: m_PreferredHeight
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
@@ -4527,7 +4535,7 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 752414209697581781, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: -53
|
||||
value: -62.5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3794995701616312333, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_Name
|
||||
@@ -4551,11 +4559,11 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4696069065802349809, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 53
|
||||
value: 62.500004
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4706209777741000001, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_PreferredHeight
|
||||
value: 53
|
||||
value: 62.500004
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5207749702406868266, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
@@ -4591,7 +4599,7 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5792101653971418633, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
value: 256
|
||||
value: 683
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5792101653971418633, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
@@ -4599,11 +4607,11 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5792101653971418633, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: 41
|
||||
value: 49
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5792101653971418633, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 20
|
||||
value: 15
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7157313819124811504, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_Pivot.x
|
||||
@@ -4635,7 +4643,7 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7157313819124811504, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 53
|
||||
value: 62.500004
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 7157313819124811504, guid: f3c648c5e98a3d64fb64514c03aaf7a3, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using GameServer.Client;
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class StartupSettingsApplier : MonoBehaviour
|
||||
{
|
||||
private const string BootstrapObjectName = "[StartupSettingsApplier]";
|
||||
private static StartupSettingsApplier _instance;
|
||||
private bool _started;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
if (_instance != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject go = new GameObject(BootstrapObjectName);
|
||||
_instance = go.AddComponent<StartupSettingsApplier>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
ApplyStartupSettings();
|
||||
StartCoroutine(SyncLeaderboardConsentRoutine());
|
||||
}
|
||||
|
||||
private void ApplyStartupSettings()
|
||||
{
|
||||
graphicSettings.ApplySavedDisplaySettingsAtStartup(this);
|
||||
ApplySavedAudioSettings();
|
||||
}
|
||||
|
||||
private void ApplySavedAudioSettings()
|
||||
{
|
||||
audioSettingsPreloader[] preloaders = FindObjectsByType<audioSettingsPreloader>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < preloaders.Length; i++)
|
||||
{
|
||||
audioSettingsPreloader preloader = preloaders[i];
|
||||
if (preloader == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
preloader.ApplyAudioSettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[StartupSettingsApplier] Failed to apply audio settings via scene preloader: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator SyncLeaderboardConsentRoutine()
|
||||
{
|
||||
float timeoutAt = Time.realtimeSinceStartup + 8f;
|
||||
while (NetworkManager.Instance == null && Time.realtimeSinceStartup < timeoutAt)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
NetworkManager networkManager = NetworkManager.Instance;
|
||||
if (networkManager == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
bool isJoined = LeaderboardConsentUtility.HasLeaderboardConsent();
|
||||
var task = networkManager.UpdateLeaderboardMembership(isJoined);
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
Exception ex = task.Exception;
|
||||
Debug.LogWarning($"[StartupSettingsApplier] Failed to sync leaderboard consent on startup: {ex?.GetBaseException().Message}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
LeaderboardMembershipResponse response = task.Result;
|
||||
if (response != null)
|
||||
{
|
||||
LeaderboardConsentUtility.SetLeaderboardConsent(response.is_joined);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e9ad3d519677da4bb6487b008a1eddb
|
||||
@@ -37,6 +37,7 @@ public class controllerSettings : MonoBehaviour
|
||||
private const float NoteSpeedMin = 0.5f;
|
||||
private const float NoteSpeedMax = 2f;
|
||||
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
|
||||
private const float NoteSpeedDefault = 1.0f;
|
||||
|
||||
// continuous change
|
||||
private Coroutine continuousChangeCoroutine = null;
|
||||
@@ -77,7 +78,8 @@ public class controllerSettings : MonoBehaviour
|
||||
{
|
||||
noteSpeedMultipler_slider.minValue = NoteSpeedMin;
|
||||
noteSpeedMultipler_slider.maxValue = NoteSpeedMax;
|
||||
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, 1.0f);
|
||||
EnsureDefaultNoteSpeedPreference();
|
||||
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
|
||||
saved = Mathf.Clamp(saved, NoteSpeedMin, NoteSpeedMax);
|
||||
noteSpeedMultipler_slider.value = saved;
|
||||
UpdateNoteSpeedText(saved);
|
||||
@@ -494,9 +496,20 @@ public class controllerSettings : MonoBehaviour
|
||||
private void ResetNoteSpeed()
|
||||
{
|
||||
if (noteSpeedMultipler_slider == null) return;
|
||||
noteSpeedMultipler_slider.value = 1.0f; // triggers change and save
|
||||
SaveNoteSpeedValue(1.0f);
|
||||
UpdateNoteSpeedText(1.0f);
|
||||
noteSpeedMultipler_slider.value = NoteSpeedDefault; // triggers change and save
|
||||
SaveNoteSpeedValue(NoteSpeedDefault);
|
||||
UpdateNoteSpeedText(NoteSpeedDefault);
|
||||
}
|
||||
|
||||
private static void EnsureDefaultNoteSpeedPreference()
|
||||
{
|
||||
if (PlayerPrefs.HasKey(NoteSpeedPrefKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerPrefs.SetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
private void AddButtonContinuousEvents(Button btn, float delta)
|
||||
|
||||
@@ -1,32 +1,205 @@
|
||||
using System;
|
||||
using Bansonic;
|
||||
using GameServer.Client;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
public class gameSettings : MonoBehaviour
|
||||
{
|
||||
[Header("multiNotesSigns")]
|
||||
public Toggle multiNoteNoticeToggle;
|
||||
public Text multiNoteNoticeText; // Using Legacy Text object
|
||||
public Text multiNoteNoticeText;
|
||||
|
||||
private const string PrefKey_EnableSyncNotePrefab = "EnableSyncNotePrefab";
|
||||
[Header("joinRankingList")]
|
||||
public Toggle user_accept_joinRankingList;
|
||||
public Text jrlWarningText;
|
||||
|
||||
void Start()
|
||||
private const string PrefKeyEnableSyncNotePrefab = "EnableSyncNotePrefab";
|
||||
private bool _isUpdatingJoinRankingToggle;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (multiNoteNoticeToggle != null)
|
||||
{
|
||||
// Initialize toggle state from PlayerPrefs, default to 1 (true)
|
||||
bool isEnabled = PlayerPrefs.GetInt(PrefKey_EnableSyncNotePrefab, 1) == 1;
|
||||
multiNoteNoticeToggle.isOn = isEnabled;
|
||||
multiNoteNoticeToggle.onValueChanged.RemoveListener(HandleMultiNoteNoticeToggleChanged);
|
||||
multiNoteNoticeToggle.onValueChanged.AddListener(HandleMultiNoteNoticeToggleChanged);
|
||||
}
|
||||
|
||||
// Initialize text based on current toggle value
|
||||
UpdateText(isEnabled);
|
||||
if (user_accept_joinRankingList != null)
|
||||
{
|
||||
user_accept_joinRankingList.onValueChanged.RemoveListener(HandleJoinRankingListToggleChanged);
|
||||
user_accept_joinRankingList.onValueChanged.AddListener(HandleJoinRankingListToggleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// Add listener to save value and update text when changed
|
||||
multiNoteNoticeToggle.onValueChanged.AddListener((isOn) =>
|
||||
private void OnEnable()
|
||||
{
|
||||
RefreshMultiNoteNoticeState();
|
||||
_ = RefreshJoinRankingStateFromServerAsync();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (multiNoteNoticeToggle != null)
|
||||
{
|
||||
multiNoteNoticeToggle.onValueChanged.RemoveListener(HandleMultiNoteNoticeToggleChanged);
|
||||
}
|
||||
|
||||
if (user_accept_joinRankingList != null)
|
||||
{
|
||||
user_accept_joinRankingList.onValueChanged.RemoveListener(HandleJoinRankingListToggleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshMultiNoteNoticeState()
|
||||
{
|
||||
if (multiNoteNoticeToggle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isEnabled = PlayerPrefs.GetInt(PrefKeyEnableSyncNotePrefab, 1) == 1;
|
||||
multiNoteNoticeToggle.SetIsOnWithoutNotify(isEnabled);
|
||||
UpdateText(isEnabled);
|
||||
}
|
||||
|
||||
private async Awaitable RefreshJoinRankingStateFromServerAsync()
|
||||
{
|
||||
if (user_accept_joinRankingList == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
NetworkManager nm = NetworkManager.Instance;
|
||||
if (nm == null)
|
||||
{
|
||||
PlayerPrefs.SetInt(PrefKey_EnableSyncNotePrefab, isOn ? 1 : 0);
|
||||
PlayerPrefs.Save();
|
||||
UpdateText(isOn);
|
||||
});
|
||||
RefreshJoinRankingListStateFromLocal();
|
||||
await RefreshJoinRankingWarningTextAsync(null);
|
||||
return;
|
||||
}
|
||||
|
||||
LeaderboardMembershipResponse resp = await nm.GetLeaderboardMembership();
|
||||
bool isJoined = resp != null ? resp.is_joined : LeaderboardConsentUtility.HasLeaderboardConsent();
|
||||
LeaderboardConsentUtility.SetLeaderboardConsent(isJoined);
|
||||
|
||||
_isUpdatingJoinRankingToggle = true;
|
||||
user_accept_joinRankingList.SetIsOnWithoutNotify(isJoined);
|
||||
_isUpdatingJoinRankingToggle = false;
|
||||
|
||||
await RefreshJoinRankingWarningTextAsync(resp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[gameSettings] Failed to refresh leaderboard membership state: {ex.Message}");
|
||||
RefreshJoinRankingListStateFromLocal();
|
||||
await RefreshJoinRankingWarningTextAsync(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshJoinRankingListStateFromLocal()
|
||||
{
|
||||
if (user_accept_joinRankingList == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isUpdatingJoinRankingToggle = true;
|
||||
user_accept_joinRankingList.SetIsOnWithoutNotify(LeaderboardConsentUtility.HasLeaderboardConsent());
|
||||
_isUpdatingJoinRankingToggle = false;
|
||||
}
|
||||
|
||||
private async Awaitable RefreshJoinRankingWarningTextAsync(LeaderboardMembershipResponse membership)
|
||||
{
|
||||
if (jrlWarningText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (membership != null && membership.changed_today && !string.IsNullOrWhiteSpace(membership.next_change_at))
|
||||
{
|
||||
jrlWarningText.text = BuildChangeCooldownText(membership.next_change_at);
|
||||
return;
|
||||
}
|
||||
|
||||
jrlWarningText.text = "下次排行榜更新时间:正在获取...";
|
||||
|
||||
try
|
||||
{
|
||||
jrlWarningText.text = membership != null && !string.IsNullOrWhiteSpace(membership.next_settle_at)
|
||||
? "下次排行榜更新时间:" + membership.next_settle_at
|
||||
: await LeaderboardConsentUtility.BuildNextSettleWarningTextAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[gameSettings] Failed to refresh ranking warning text: {ex.Message}");
|
||||
jrlWarningText.text = "下次排行榜更新时间:--";
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildChangeCooldownText(string nextChangeAt)
|
||||
{
|
||||
if (DateTime.TryParse(nextChangeAt, out DateTime parsed))
|
||||
{
|
||||
return $"{parsed.Hour}时{parsed.Minute:D2}分后可切换";
|
||||
}
|
||||
|
||||
return "今日已切换";
|
||||
}
|
||||
|
||||
private void HandleMultiNoteNoticeToggleChanged(bool isOn)
|
||||
{
|
||||
PlayerPrefs.SetInt(PrefKeyEnableSyncNotePrefab, isOn ? 1 : 0);
|
||||
PlayerPrefs.Save();
|
||||
UpdateText(isOn);
|
||||
}
|
||||
|
||||
private async void HandleJoinRankingListToggleChanged(bool isOn)
|
||||
{
|
||||
if (_isUpdatingJoinRankingToggle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
NetworkManager nm = NetworkManager.Instance;
|
||||
if (nm == null)
|
||||
{
|
||||
LeaderboardConsentUtility.SetLeaderboardConsent(isOn);
|
||||
await RefreshJoinRankingWarningTextAsync(null);
|
||||
return;
|
||||
}
|
||||
|
||||
LeaderboardMembershipResponse resp = await nm.UpdateLeaderboardMembership(isOn);
|
||||
bool finalJoined = resp != null ? resp.is_joined : isOn;
|
||||
LeaderboardConsentUtility.SetLeaderboardConsent(finalJoined);
|
||||
|
||||
_isUpdatingJoinRankingToggle = true;
|
||||
user_accept_joinRankingList.SetIsOnWithoutNotify(finalJoined);
|
||||
_isUpdatingJoinRankingToggle = false;
|
||||
|
||||
if (resp != null && !resp.success)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(resp.next_change_at))
|
||||
{
|
||||
gNotice.warning.display(BuildChangeCooldownText(resp.next_change_at));
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(resp.message))
|
||||
{
|
||||
gNotice.warning.display(resp.message);
|
||||
}
|
||||
}
|
||||
|
||||
await RefreshJoinRankingWarningTextAsync(resp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[gameSettings] Failed to update leaderboard membership: {ex.Message}");
|
||||
gNotice.error.display("排行榜加入状态更新失败");
|
||||
RefreshJoinRankingListStateFromLocal();
|
||||
await RefreshJoinRankingWarningTextAsync(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +207,18 @@ public class gameSettings : MonoBehaviour
|
||||
{
|
||||
if (multiNoteNoticeText != null)
|
||||
{
|
||||
multiNoteNoticeText.text = isOn ? "已启用多押指示器" : "已禁用多押指示器";
|
||||
multiNoteNoticeText.text = isOn ? "已开启多押提示音符同步显示" : "已关闭多押提示音符同步显示";
|
||||
}
|
||||
|
||||
if (multiNoteNoticeToggle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Also update the Toggle's own label text if it exists (usually a child object)
|
||||
Text toggleLabel = multiNoteNoticeToggle.GetComponentInChildren<Text>();
|
||||
if (toggleLabel != null)
|
||||
{
|
||||
toggleLabel.text = isOn ? "启用" : "禁用";
|
||||
toggleLabel.text = isOn ? "已开启" : "已关闭";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ using System.Runtime.InteropServices;
|
||||
|
||||
public class graphicSettings : MonoBehaviour
|
||||
{
|
||||
private const string PrefKeyScreenMode = "screenMode";
|
||||
private const string PrefKeyResolutionIndex = "resolutionIndex";
|
||||
private const string PrefKeyCustomResW = "customResW";
|
||||
private const string PrefKeyCustomResH = "customResH";
|
||||
|
||||
public Dropdown screenMode_Dropdown;
|
||||
public Dropdown resolution_Dropdown;
|
||||
public Dropdown frameRate_Dropdown;
|
||||
@@ -30,6 +35,22 @@ public class graphicSettings : MonoBehaviour
|
||||
lastScreenSize = new Vector2Int(Screen.width, Screen.height);
|
||||
}
|
||||
|
||||
public static void ApplySavedDisplaySettingsAtStartup(MonoBehaviour coroutineRunner)
|
||||
{
|
||||
int screenMode = PlayerPrefs.GetInt(PrefKeyScreenMode, 0);
|
||||
screenMode = Mathf.Clamp(screenMode, 0, 2);
|
||||
|
||||
try
|
||||
{
|
||||
ApplyScreenModeStatic(screenMode, coroutineRunner);
|
||||
ApplySavedResolutionStatic();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[graphicSettings] Failed to apply saved display settings at startup: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (screenMode_Dropdown != null)
|
||||
@@ -99,7 +120,7 @@ public class graphicSettings : MonoBehaviour
|
||||
int current = MapFullScreenModeToIndex(Screen.fullScreenMode);
|
||||
if (PlayerPrefs.HasKey("screenMode"))
|
||||
{
|
||||
int saved = PlayerPrefs.GetInt("screenMode", current);
|
||||
int saved = PlayerPrefs.GetInt(PrefKeyScreenMode, current);
|
||||
saved = Mathf.Clamp(saved, 0, options.Count - 1);
|
||||
current = saved;
|
||||
ApplyScreenMode(current);
|
||||
@@ -129,7 +150,7 @@ public class graphicSettings : MonoBehaviour
|
||||
private void OnScreenModeChanged(int index)
|
||||
{
|
||||
ApplyScreenMode(index);
|
||||
PlayerPrefs.SetInt("screenMode", index);
|
||||
PlayerPrefs.SetInt(PrefKeyScreenMode, index);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
bool isWindowed = (index == 0);
|
||||
@@ -141,96 +162,17 @@ public class graphicSettings : MonoBehaviour
|
||||
|
||||
private void ApplyScreenMode(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.Windowed;
|
||||
Screen.fullScreen = false;
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(ApplyWindowStyleNextFrame(true));
|
||||
Screen.SetResolution(Screen.width, Screen.height, FullScreenMode.Windowed, Screen.currentResolution.refreshRateRatio);
|
||||
}
|
||||
else if (index == 1)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
|
||||
Screen.fullScreen = true;
|
||||
Resolution res = Screen.currentResolution;
|
||||
Screen.SetResolution(res.width, res.height, FullScreenMode.ExclusiveFullScreen, res.refreshRateRatio);
|
||||
}
|
||||
else if (index == 2)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.FullScreenWindow;
|
||||
Screen.fullScreen = true;
|
||||
Resolution res = Screen.currentResolution;
|
||||
Screen.SetResolution(res.width, res.height, FullScreenMode.FullScreenWindow, res.refreshRateRatio);
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(ApplyWindowStyleNextFrame(false));
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"ApplyScreenMode failed: {ex}");
|
||||
}
|
||||
ApplyScreenModeStatic(index, this);
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator ApplyWindowStyleNextFrame(bool enable)
|
||||
{
|
||||
yield return null;
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
EnableWindowedModeResizable(enable);
|
||||
yield return ApplyWindowStyleNextFrameStatic(enable);
|
||||
}
|
||||
|
||||
private void EnableWindowedModeResizable(bool enable)
|
||||
{
|
||||
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
|
||||
try
|
||||
{
|
||||
IntPtr hWnd = GetForegroundWindow();
|
||||
if (hWnd == IntPtr.Zero) return;
|
||||
|
||||
const int GWL_STYLE = -16;
|
||||
const int WS_OVERLAPPEDWINDOW = unchecked((int)0x00CF0000);
|
||||
const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
if (IntPtr.Size == 8)
|
||||
{
|
||||
long style = GetWindowLongPtr64(hWnd, GWL_STYLE);
|
||||
if (enable)
|
||||
{
|
||||
style &= ~((long)WS_POPUP);
|
||||
style |= WS_OVERLAPPEDWINDOW;
|
||||
}
|
||||
else
|
||||
{
|
||||
style &= ~((long)WS_OVERLAPPEDWINDOW);
|
||||
style |= (long)WS_POPUP;
|
||||
}
|
||||
SetWindowLongPtr64(hWnd, GWL_STYLE, style);
|
||||
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
else
|
||||
{
|
||||
int style = GetWindowLong32(hWnd, GWL_STYLE);
|
||||
if (enable)
|
||||
{
|
||||
style &= ~WS_POPUP;
|
||||
style |= WS_OVERLAPPEDWINDOW;
|
||||
}
|
||||
else
|
||||
{
|
||||
style &= ~((int)WS_OVERLAPPEDWINDOW);
|
||||
style |= (int)WS_POPUP;
|
||||
}
|
||||
SetWindowLong32(hWnd, GWL_STYLE, style);
|
||||
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"EnableWindowedModeResizable failed: {e}");
|
||||
}
|
||||
#endif
|
||||
EnableWindowedModeResizableStatic(enable);
|
||||
}
|
||||
|
||||
private void InitResolutionDropdown()
|
||||
@@ -295,7 +237,7 @@ public class graphicSettings : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
int savedResIndex = PlayerPrefs.GetInt("resolutionIndex", -2);
|
||||
int savedResIndex = PlayerPrefs.GetInt(PrefKeyResolutionIndex, -2);
|
||||
if (savedResIndex >= 0 && savedResIndex < availableResolutions.Count)
|
||||
{
|
||||
resolution_Dropdown.SetValueWithoutNotify(savedResIndex);
|
||||
@@ -305,8 +247,8 @@ public class graphicSettings : MonoBehaviour
|
||||
}
|
||||
else if (savedResIndex == -1)
|
||||
{
|
||||
int cw = PlayerPrefs.GetInt("customResW", -1);
|
||||
int ch = PlayerPrefs.GetInt("customResH", -1);
|
||||
int cw = PlayerPrefs.GetInt(PrefKeyCustomResW, -1);
|
||||
int ch = PlayerPrefs.GetInt(PrefKeyCustomResH, -1);
|
||||
if (cw > 0 && ch > 0)
|
||||
{
|
||||
Screen.SetResolution(cw, ch, Screen.fullScreenMode, Screen.currentResolution.refreshRateRatio);
|
||||
@@ -325,7 +267,7 @@ public class graphicSettings : MonoBehaviour
|
||||
}
|
||||
resolution_Dropdown.onValueChanged.AddListener(OnResolutionChanged);
|
||||
|
||||
int screenMode = PlayerPrefs.GetInt("screenMode", MapFullScreenModeToIndex(Screen.fullScreenMode));
|
||||
int screenMode = PlayerPrefs.GetInt(PrefKeyScreenMode, MapFullScreenModeToIndex(Screen.fullScreenMode));
|
||||
bool isWindowed = (screenMode == 0) || (Screen.fullScreenMode == FullScreenMode.Windowed);
|
||||
if (resolutionLock_Image != null)
|
||||
resolutionLock_Image.gameObject.SetActive(!isWindowed);
|
||||
@@ -354,9 +296,9 @@ public class graphicSettings : MonoBehaviour
|
||||
if (index < 0) return;
|
||||
if (index >= availableResolutions.Count)
|
||||
{
|
||||
PlayerPrefs.SetInt("resolutionIndex", -1);
|
||||
PlayerPrefs.SetInt("customResW", Screen.width);
|
||||
PlayerPrefs.SetInt("customResH", Screen.height);
|
||||
PlayerPrefs.SetInt(PrefKeyResolutionIndex, -1);
|
||||
PlayerPrefs.SetInt(PrefKeyCustomResW, Screen.width);
|
||||
PlayerPrefs.SetInt(PrefKeyCustomResH, Screen.height);
|
||||
PlayerPrefs.Save();
|
||||
return;
|
||||
}
|
||||
@@ -364,7 +306,7 @@ public class graphicSettings : MonoBehaviour
|
||||
var r = availableResolutions[index];
|
||||
FullScreenMode mode = Screen.fullScreenMode;
|
||||
Screen.SetResolution(r.x, r.y, mode, Screen.currentResolution.refreshRateRatio);
|
||||
PlayerPrefs.SetInt("resolutionIndex", index);
|
||||
PlayerPrefs.SetInt(PrefKeyResolutionIndex, index);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
RemoveTempFreeOptionIfExists();
|
||||
@@ -484,6 +426,155 @@ public class graphicSettings : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyScreenModeStatic(int index, MonoBehaviour coroutineRunner)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.Windowed;
|
||||
Screen.fullScreen = false;
|
||||
Screen.SetResolution(Screen.width, Screen.height, FullScreenMode.Windowed, Screen.currentResolution.refreshRateRatio);
|
||||
if (coroutineRunner != null)
|
||||
{
|
||||
coroutineRunner.StartCoroutine(ApplyWindowStyleNextFrameStatic(true));
|
||||
}
|
||||
}
|
||||
else if (index == 1)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
|
||||
Screen.fullScreen = true;
|
||||
Resolution res = Screen.currentResolution;
|
||||
Screen.SetResolution(res.width, res.height, FullScreenMode.ExclusiveFullScreen, res.refreshRateRatio);
|
||||
}
|
||||
else if (index == 2)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.FullScreenWindow;
|
||||
Screen.fullScreen = true;
|
||||
Resolution res = Screen.currentResolution;
|
||||
Screen.SetResolution(res.width, res.height, FullScreenMode.FullScreenWindow, res.refreshRateRatio);
|
||||
if (coroutineRunner != null)
|
||||
{
|
||||
coroutineRunner.StartCoroutine(ApplyWindowStyleNextFrameStatic(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"ApplyScreenMode failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplySavedResolutionStatic()
|
||||
{
|
||||
int savedResIndex = PlayerPrefs.GetInt(PrefKeyResolutionIndex, -2);
|
||||
if (savedResIndex == -1)
|
||||
{
|
||||
int customWidth = PlayerPrefs.GetInt(PrefKeyCustomResW, -1);
|
||||
int customHeight = PlayerPrefs.GetInt(PrefKeyCustomResH, -1);
|
||||
if (customWidth > 0 && customHeight > 0)
|
||||
{
|
||||
Screen.SetResolution(customWidth, customHeight, Screen.fullScreenMode, Screen.currentResolution.refreshRateRatio);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (savedResIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2Int[] candidates =
|
||||
{
|
||||
new Vector2Int(1024, 576),
|
||||
new Vector2Int(1152, 648),
|
||||
new Vector2Int(1280, 720),
|
||||
new Vector2Int(1366, 768),
|
||||
new Vector2Int(1600, 900),
|
||||
new Vector2Int(1920, 1080),
|
||||
new Vector2Int(2560, 1440),
|
||||
new Vector2Int(3440, 1440),
|
||||
new Vector2Int(3200, 1800),
|
||||
new Vector2Int(3840, 2160),
|
||||
new Vector2Int(5120, 2880),
|
||||
new Vector2Int(7680, 4320),
|
||||
new Vector2Int(15360, 8640)
|
||||
};
|
||||
|
||||
if (savedResIndex >= candidates.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2Int resolution = candidates[savedResIndex];
|
||||
Resolution currentResolution = Screen.currentResolution;
|
||||
if (resolution.x > currentResolution.width || resolution.y > currentResolution.height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Screen.SetResolution(resolution.x, resolution.y, Screen.fullScreenMode, currentResolution.refreshRateRatio);
|
||||
}
|
||||
|
||||
private static System.Collections.IEnumerator ApplyWindowStyleNextFrameStatic(bool enable)
|
||||
{
|
||||
yield return null;
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
EnableWindowedModeResizableStatic(enable);
|
||||
}
|
||||
|
||||
private static void EnableWindowedModeResizableStatic(bool enable)
|
||||
{
|
||||
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
|
||||
try
|
||||
{
|
||||
IntPtr hWnd = GetForegroundWindow();
|
||||
if (hWnd == IntPtr.Zero) return;
|
||||
|
||||
const int GWL_STYLE = -16;
|
||||
const int WS_OVERLAPPEDWINDOW = unchecked((int)0x00CF0000);
|
||||
const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
if (IntPtr.Size == 8)
|
||||
{
|
||||
long style = GetWindowLongPtr64(hWnd, GWL_STYLE);
|
||||
if (enable)
|
||||
{
|
||||
style &= ~((long)WS_POPUP);
|
||||
style |= WS_OVERLAPPEDWINDOW;
|
||||
}
|
||||
else
|
||||
{
|
||||
style &= ~((long)WS_OVERLAPPEDWINDOW);
|
||||
style |= (long)WS_POPUP;
|
||||
}
|
||||
SetWindowLongPtr64(hWnd, GWL_STYLE, style);
|
||||
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
else
|
||||
{
|
||||
int style = GetWindowLong32(hWnd, GWL_STYLE);
|
||||
if (enable)
|
||||
{
|
||||
style &= ~WS_POPUP;
|
||||
style |= WS_OVERLAPPEDWINDOW;
|
||||
}
|
||||
else
|
||||
{
|
||||
style &= ~((int)WS_OVERLAPPEDWINDOW);
|
||||
style |= (int)WS_POPUP;
|
||||
}
|
||||
SetWindowLong32(hWnd, GWL_STYLE, style);
|
||||
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"EnableWindowedModeResizable failed: {e}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
@@ -3,11 +3,17 @@ using UnityEngine.UI;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public class readDeviceInfo : MonoBehaviour
|
||||
{
|
||||
private const string LabelColorHex = "#2457c6";
|
||||
private const string ValueColorHex = "#6b8acd";
|
||||
private const float ColumnPaddingPixels = 32f;
|
||||
private const int MinimumGapSpaces = 2;
|
||||
|
||||
public Button reread_button;
|
||||
public Button export_button;
|
||||
public Text deviceInfo_text;
|
||||
@@ -25,7 +31,9 @@ public class readDeviceInfo : MonoBehaviour
|
||||
}
|
||||
|
||||
if (deviceInfo_text != null)
|
||||
deviceInfo_text.supportRichText = true;
|
||||
{
|
||||
ConfigureDeviceInfoText();
|
||||
}
|
||||
|
||||
read_deviceInfo();
|
||||
}
|
||||
@@ -45,7 +53,8 @@ public class readDeviceInfo : MonoBehaviour
|
||||
|
||||
private void read_deviceInfo()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
ConfigureDeviceInfoText();
|
||||
var entries = new System.Collections.Generic.List<KeyValuePair<string, string>>();
|
||||
|
||||
void AddEntry(string title, object value)
|
||||
{
|
||||
@@ -54,9 +63,7 @@ public class readDeviceInfo : MonoBehaviour
|
||||
else if (value is bool b) str = b ? "是" : "否";
|
||||
else str = value.ToString();
|
||||
|
||||
sb.AppendLine($"<b>{title}</b>");
|
||||
sb.AppendLine(str);
|
||||
sb.AppendLine();
|
||||
entries.Add(new KeyValuePair<string, string>(title ?? string.Empty, str ?? string.Empty));
|
||||
}
|
||||
|
||||
AddEntry("平台", Application.platform.ToString());
|
||||
@@ -101,7 +108,7 @@ public class readDeviceInfo : MonoBehaviour
|
||||
AddEntry("启用后台运行", Application.runInBackground);
|
||||
AddEntry("启用多线程渲染", SystemInfo.graphicsMultiThreaded);
|
||||
|
||||
string output = sb.ToString();
|
||||
string output = BuildTwoColumnOutput(entries);
|
||||
|
||||
if (deviceInfo_text != null)
|
||||
{
|
||||
@@ -125,8 +132,7 @@ public class readDeviceInfo : MonoBehaviour
|
||||
try
|
||||
{
|
||||
string raw = deviceInfo_text.text;
|
||||
// Remove <b> tags and add colon after title
|
||||
string cleaned = Regex.Replace(raw, "<b>(.*?)</b>", "$1:");
|
||||
string cleaned = Regex.Replace(raw, "<.*?>", string.Empty);
|
||||
File.WriteAllText(path, cleaned);
|
||||
}
|
||||
catch (System.Exception)
|
||||
@@ -134,6 +140,246 @@ public class readDeviceInfo : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildTwoColumnOutput(System.Collections.Generic.IReadOnlyList<KeyValuePair<string, string>> entries)
|
||||
{
|
||||
if (entries == null || entries.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
float maxLabelWidth = 0f;
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
maxLabelWidth = Mathf.Max(maxLabelWidth, MeasurePlainTextWidth(entries[i].Key));
|
||||
}
|
||||
|
||||
float availableWidth = GetAvailableTextWidth();
|
||||
float targetLabelWidth = maxLabelWidth + ColumnPaddingPixels;
|
||||
if (availableWidth > 0f)
|
||||
{
|
||||
targetLabelWidth = Mathf.Min(targetLabelWidth, availableWidth * 0.38f);
|
||||
}
|
||||
|
||||
float spaceWidth = Mathf.Max(1f, MeasurePlainTextWidth(" "));
|
||||
int continuationIndentSpaces = Mathf.Max(MinimumGapSpaces, Mathf.CeilToInt(targetLabelWidth / spaceWidth) + MinimumGapSpaces);
|
||||
string continuationIndent = new string(' ', continuationIndentSpaces);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
string label = entries[i].Key ?? string.Empty;
|
||||
string value = entries[i].Value ?? string.Empty;
|
||||
|
||||
int gapSpaces = Mathf.Max(
|
||||
MinimumGapSpaces,
|
||||
Mathf.CeilToInt((targetLabelWidth - MeasurePlainTextWidth(label)) / spaceWidth) + MinimumGapSpaces);
|
||||
|
||||
string padding = new string(' ', gapSpaces);
|
||||
float firstLineValueWidth = availableWidth > 0f
|
||||
? Mathf.Max(32f, availableWidth - MeasurePlainTextWidth(label) - MeasurePlainTextWidth(padding))
|
||||
: 0f;
|
||||
float continuationValueWidth = availableWidth > 0f
|
||||
? Mathf.Max(32f, availableWidth - MeasurePlainTextWidth(continuationIndent))
|
||||
: 0f;
|
||||
|
||||
List<string> wrappedLines = WrapValueLines(value, firstLineValueWidth, continuationValueWidth);
|
||||
|
||||
sb.Append("<color=").Append(LabelColorHex).Append("><b>")
|
||||
.Append(EscapeRichText(label))
|
||||
.Append("</b></color>")
|
||||
.Append(padding)
|
||||
.Append("<color=").Append(ValueColorHex).Append(">")
|
||||
.Append(EscapeRichText(wrappedLines[0]))
|
||||
.Append("</color>");
|
||||
|
||||
for (int lineIndex = 1; lineIndex < wrappedLines.Count; lineIndex++)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.Append(continuationIndent)
|
||||
.Append("<color=").Append(ValueColorHex).Append(">")
|
||||
.Append(EscapeRichText(wrappedLines[lineIndex]))
|
||||
.Append("</color>");
|
||||
}
|
||||
|
||||
if (i < entries.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void ConfigureDeviceInfoText()
|
||||
{
|
||||
if (deviceInfo_text == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
deviceInfo_text.supportRichText = true;
|
||||
deviceInfo_text.alignment = TextAnchor.UpperLeft;
|
||||
deviceInfo_text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
deviceInfo_text.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
deviceInfo_text.resizeTextForBestFit = false;
|
||||
}
|
||||
|
||||
private float GetAvailableTextWidth()
|
||||
{
|
||||
if (deviceInfo_text == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
RectTransform rectTransform = deviceInfo_text.rectTransform;
|
||||
if (rectTransform == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float width = rectTransform.rect.width;
|
||||
return width > 0f ? width : 0f;
|
||||
}
|
||||
|
||||
private float MeasurePlainTextWidth(string text)
|
||||
{
|
||||
if (deviceInfo_text == null || string.IsNullOrEmpty(text))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
TextGenerationSettings settings = deviceInfo_text.GetGenerationSettings(Vector2.zero);
|
||||
settings.richText = false;
|
||||
settings.generateOutOfBounds = true;
|
||||
|
||||
TextGenerator generator = new TextGenerator();
|
||||
return generator.GetPreferredWidth(text, settings) / deviceInfo_text.pixelsPerUnit;
|
||||
}
|
||||
|
||||
private List<string> WrapValueLines(string value, float firstLineWidth, float continuationLineWidth)
|
||||
{
|
||||
string normalized = NormalizeLineBreaks(value);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
return new List<string> { string.Empty };
|
||||
}
|
||||
|
||||
string[] rawLines = normalized.Split('\n');
|
||||
List<string> wrapped = new List<string>();
|
||||
for (int i = 0; i < rawLines.Length; i++)
|
||||
{
|
||||
string rawLine = rawLines[i];
|
||||
List<string> lineParts = WrapSingleLine(rawLine, wrapped.Count == 0 ? firstLineWidth : continuationLineWidth);
|
||||
if (lineParts.Count == 0)
|
||||
{
|
||||
lineParts.Add(string.Empty);
|
||||
}
|
||||
|
||||
wrapped.Add(lineParts[0]);
|
||||
for (int j = 1; j < lineParts.Count; j++)
|
||||
{
|
||||
wrapped.Add(lineParts[j]);
|
||||
}
|
||||
}
|
||||
|
||||
return wrapped.Count > 0 ? wrapped : new List<string> { string.Empty };
|
||||
}
|
||||
|
||||
private List<string> WrapSingleLine(string text, float maxWidth)
|
||||
{
|
||||
List<string> lines = new List<string>();
|
||||
if (string.IsNullOrEmpty(text) || maxWidth <= 0f)
|
||||
{
|
||||
lines.Add(text ?? string.Empty);
|
||||
return lines;
|
||||
}
|
||||
|
||||
string remaining = text;
|
||||
while (!string.IsNullOrEmpty(remaining))
|
||||
{
|
||||
if (MeasurePlainTextWidth(remaining) <= maxWidth)
|
||||
{
|
||||
lines.Add(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
int splitIndex = FindSplitIndex(remaining, maxWidth);
|
||||
if (splitIndex <= 0 || splitIndex >= remaining.Length)
|
||||
{
|
||||
lines.Add(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
string current = remaining.Substring(0, splitIndex).TrimEnd();
|
||||
if (current.Length == 0)
|
||||
{
|
||||
current = remaining.Substring(0, Mathf.Min(1, remaining.Length));
|
||||
splitIndex = current.Length;
|
||||
}
|
||||
|
||||
lines.Add(current);
|
||||
remaining = remaining.Substring(splitIndex).TrimStart();
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
private int FindSplitIndex(string text, float maxWidth)
|
||||
{
|
||||
int lastBreakableIndex = -1;
|
||||
int fallbackIndex = -1;
|
||||
|
||||
for (int i = 1; i <= text.Length; i++)
|
||||
{
|
||||
string candidate = text.Substring(0, i);
|
||||
if (MeasurePlainTextWidth(candidate) <= maxWidth)
|
||||
{
|
||||
fallbackIndex = i;
|
||||
if (IsBreakableCharacter(text[i - 1]))
|
||||
{
|
||||
lastBreakableIndex = i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (lastBreakableIndex > 0)
|
||||
{
|
||||
return lastBreakableIndex;
|
||||
}
|
||||
|
||||
return fallbackIndex;
|
||||
}
|
||||
|
||||
private static bool IsBreakableCharacter(char c)
|
||||
{
|
||||
return char.IsWhiteSpace(c) || c == '/' || c == '\\' || c == '_' || c == '-' || c == '.' || c == ':' || c == ')' || c == ']';
|
||||
}
|
||||
|
||||
private static string NormalizeLineBreaks(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return input.Replace("\r\n", "\n").Replace('\r', '\n');
|
||||
}
|
||||
|
||||
private static string EscapeRichText(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return input.Replace("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">");
|
||||
}
|
||||
|
||||
string ShowSaveFileDialog()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -238,7 +238,15 @@ public class userSettings : MonoBehaviour
|
||||
index = 0;
|
||||
}
|
||||
|
||||
LocalizationService.SetLanguage(_availableLanguageCodes[index]);
|
||||
string targetLanguageCode = _availableLanguageCodes[index];
|
||||
if (IsTemporarilyUnsupportedLanguage(targetLanguageCode))
|
||||
{
|
||||
gNotice.error.display(LocalizationService.LocalizeLiteral("暂不支持该语言"));
|
||||
SyncLanguageDropdownValue();
|
||||
return;
|
||||
}
|
||||
|
||||
LocalizationService.SetLanguage(targetLanguageCode);
|
||||
}
|
||||
|
||||
private void HandleLanguageChanged(string languageCode)
|
||||
@@ -247,6 +255,12 @@ public class userSettings : MonoBehaviour
|
||||
UpdateUserInfoDisplay();
|
||||
}
|
||||
|
||||
private static bool IsTemporarilyUnsupportedLanguage(string languageCode)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(languageCode) &&
|
||||
languageCode.StartsWith("en", System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void OnClearupSaveDataClicked()
|
||||
{
|
||||
if (clearupClickCount > 0 && Time.unscaledTime > clearupExpireTime)
|
||||
|
||||
Reference in New Issue
Block a user