很多更新,服务端连接,新UI和系统

This commit is contained in:
FloatGaming
2026-04-17 20:09:41 +08:00
parent 85dfff28dd
commit e0bc0bbf08
910 changed files with 65191 additions and 1291 deletions
+5
View File
@@ -1676,6 +1676,10 @@ public class AllyCombatant : MonoBehaviour, ICombatant
int oldHP = currentHP;
float effective = amount * (1f - damageResistance);
int delta = Mathf.CeilToInt(effective);
if (SkillBuilder.Instance != null && SkillBuilder.Instance.TryPreventLethalDamage(this, delta))
{
return;
}
ModifyHP(-delta, true, true);
int actual = oldHP - currentHP;
@@ -1793,6 +1797,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
if (buff.scoreMultiplier != 1f) scoreEfficiency *= buff.scoreMultiplier;
if (buff.attackMultiplier != 1f) RecalculateAttackFromBuffs();
iBudeffPrefabController.Instance?.NotifyBuffApplied(this, buff);
SkillBuilder.Instance?.NotifyBuffApplied(slotIndex);
}
public void RemoveBuff(string buffId)
+812 -1
View File
@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@@ -77,6 +78,25 @@ public class SkillBuilder : MonoBehaviour
private readonly Dictionary<int, int> _equipManaFullSpendCountBySlot = new Dictionary<int, int>(8);
private readonly HashSet<int> _equipGhoulSchoolActiveSlots = new HashSet<int>();
private readonly Dictionary<int, HashSet<string>> _equipGhoulSchoolSeenSkillIdsBySlot = new Dictionary<int, HashSet<string>>(8);
private readonly HashSet<int> _equipThousandthActiveSlots = new HashSet<int>();
private readonly Dictionary<int, float> _equipThousandthAppliedBonusBySlot = new Dictionary<int, float>(8);
private readonly HashSet<int> _equipForgetfulRhythmActiveSlots = new HashSet<int>();
private readonly Dictionary<int, HashSet<string>> _equipForgetfulRhythmSeenSkillIdsBySlot = new Dictionary<int, HashSet<string>>(8);
private readonly Dictionary<int, int> _equipForgetfulRhythmStacksBySlot = new Dictionary<int, int>(8);
private readonly Dictionary<int, string> _equipTalentScoutLastBorrowedSkillBySlot = new Dictionary<int, string>(8);
private readonly Dictionary<int, Coroutine> _equipTalentScoutDisagreeCoroutinesBySlot = new Dictionary<int, Coroutine>(8);
private readonly Dictionary<int, float> _equipTalentScoutDisagreeRestoreScoreBySlot = new Dictionary<int, float>(8);
private readonly Dictionary<int, int> _equipOutOfLineKillCountsBySlot = new Dictionary<int, int>(8);
private readonly Dictionary<int, int> _equipStormStacksBySlot = new Dictionary<int, int>(8);
private readonly HashSet<int> _equipStormProcessingSlots = new HashSet<int>();
private readonly HashSet<int> _equipFateRelianceActiveSlots = new HashSet<int>();
private readonly Dictionary<int, int> _equipNewIdeaStacksBySlot = new Dictionary<int, int>(8);
private readonly Dictionary<int, List<Coroutine>> _equipNewIdeaExpireCoroutinesBySlot = new Dictionary<int, List<Coroutine>>(8);
private readonly HashSet<int> _equipSocialMaskActiveSlots = new HashSet<int>();
private readonly Dictionary<int, int> _equipSocialMaskOriginalMaxHpBySlot = new Dictionary<int, int>(8);
private readonly Dictionary<int, float> _equipSocialMaskOriginalScoreBySlot = new Dictionary<int, float>(8);
private readonly HashSet<int> _equipFutureActiveSlots = new HashSet<int>();
private bool _equipQuietTurnBroadcastInProgress;
private int _cachedAlliesFrame = -1;
private readonly List<GameObject> _cachedAllies = new List<GameObject>(8);
@@ -94,6 +114,32 @@ public class SkillBuilder : MonoBehaviour
private const string EquipSkillStartSetMana = "420010_start_set_mana";
private const string EquipSkillKillRefillMana = "420011_kill_refill_mana";
private const string EquipSkillManaSpendReduceCap = "420012_mana_spend_reduce_cap";
private const string EquipSkillVacantPerfectMana = "420013_vacant_perfect_mana";
private const string EquipSkillVacantMissHeal = "420014_vacant_miss_heal";
private const string EquipSkillThousandthTracker = "420015_thousandth_tracker";
private const string EquipSkillForgetfulRhythm = "420016_forgetful_rhythm";
private const string EquipSkillTalentScout = "420017_talent_scout";
private const string EquipSkillOutOfLineManaScore = "420018_out_of_line_mana_score";
private const string EquipSkillOutOfLineEnemyDead = "420019_out_of_line_enemy_dead";
private const string EquipSkillStormStart = "420020_storm_start";
private const string EquipSkillMissionStart = "420021_mission_start";
private const string EquipSkillMissionFullMana = "420022_mission_full_mana";
private const string EquipSkillAllForYouMana = "420023_all_for_you_mana";
private const string EquipSkillAllForYouKill = "420024_all_for_you_kill";
private const string EquipSkillFateRelianceStart = "420025_fate_reliance_start";
private const string EquipSkillNewIdeaKill = "420026_new_idea_kill";
private const string EquipSkillNewIdeaClear = "420027_new_idea_clear";
private const string EquipSkillNamelessLaneKill = "420028_nameless_lane_kill";
private const string EquipSkillSocialMaskStart = "420029_social_mask_start";
private const string EquipSkillSocialMaskKill = "420030_social_mask_kill";
private const string EquipSkillSocialMaskClear = "420031_social_mask_clear";
private const string EquipSkillQuietTurn = "420032_quiet_turn";
private const string EquipSkillBlock = "420033_block";
private const string EquipSkillFutureStart = "420034_future_start";
private const string EquipSkillFutureHeal = "420035_future_heal";
private const string EquipSkillFutureClear = "420036_future_clear";
private const string EquipSkillQuietTurnHealAdjacent = "420037_quiet_turn_heal_adjacent";
private const string EquipSkillBlockStart = "420038_block_start";
private const string IllusionSkillBole14StartMaxHp = "421001_bole14_start_maxhp";
private const string IllusionSkillBole6SpendFullMaxHp = "421002_bole6_spend_full_maxhp";
private const string IllusionSkillScarborough28StartAttack = "421003_scarborough28_start_attack";
@@ -1385,6 +1431,7 @@ ResolvedGroup:
}
NotifyGhoulSchoolSkillTriggered(def, slotIndex, caster);
NotifyForgetfulRhythmSkillTriggered(def, slotIndex);
if (TryHandleEquipmentSkill(def, slotIndex, caster, heroSoForName, specificTarget))
{
@@ -2012,6 +2059,58 @@ ResolvedGroup:
return HandleEquipSkillKillRefillMana(casterAlly);
case EquipSkillManaSpendReduceCap:
return HandleEquipSkillManaSpendReduceCap(casterAlly);
case EquipSkillVacantPerfectMana:
return HandleEquipSkillVacantPerfectMana(casterAlly);
case EquipSkillVacantMissHeal:
return HandleEquipSkillVacantMissHeal(casterAlly);
case EquipSkillThousandthTracker:
return HandleEquipSkillThousandthTracker(slotIndex);
case EquipSkillForgetfulRhythm:
return HandleEquipSkillForgetfulRhythm(slotIndex);
case EquipSkillTalentScout:
return HandleEquipSkillTalentScout(slotIndex);
case EquipSkillOutOfLineManaScore:
return HandleEquipSkillOutOfLineManaScore(slotIndex, casterAlly);
case EquipSkillOutOfLineEnemyDead:
return HandleEquipSkillOutOfLineEnemyDead(slotIndex);
case EquipSkillStormStart:
return HandleEquipSkillStormStart(slotIndex, casterAlly);
case EquipSkillMissionStart:
return HandleEquipSkillMissionStart(casterAlly);
case EquipSkillMissionFullMana:
return HandleEquipSkillMissionFullMana(casterAlly);
case EquipSkillAllForYouMana:
return HandleEquipSkillAllForYouMana(casterAlly);
case EquipSkillAllForYouKill:
return HandleEquipSkillAllForYouKill(casterAlly);
case EquipSkillFateRelianceStart:
return HandleEquipSkillFateRelianceStart(slotIndex);
case EquipSkillNewIdeaKill:
return HandleEquipSkillNewIdeaStack(slotIndex, 3f);
case EquipSkillNewIdeaClear:
return HandleEquipSkillNewIdeaStack(slotIndex, 8f);
case EquipSkillNamelessLaneKill:
return HandleEquipSkillNamelessLaneKill(slotIndex);
case EquipSkillSocialMaskStart:
return HandleEquipSkillSocialMaskStart(slotIndex, casterAlly);
case EquipSkillSocialMaskKill:
return HandleEquipSkillSocialMaskKill(slotIndex, casterAlly);
case EquipSkillSocialMaskClear:
return HandleEquipSkillSocialMaskClear(slotIndex, casterAlly);
case EquipSkillQuietTurn:
return HandleEquipSkillQuietTurn(casterAlly);
case EquipSkillBlock:
return HandleEquipSkillBlock(casterAlly);
case EquipSkillFutureStart:
return HandleEquipSkillFutureStart(slotIndex);
case EquipSkillFutureHeal:
return HandleEquipSkillFutureHeal(slotIndex, casterAlly);
case EquipSkillFutureClear:
return HandleEquipSkillFutureClear(slotIndex, casterAlly);
case EquipSkillQuietTurnHealAdjacent:
return HandleEquipSkillQuietTurnHealAdjacent(casterAlly);
case EquipSkillBlockStart:
return HandleEquipSkillBlockStart(casterAlly);
case IllusionSkillBole14StartMaxHp:
return HandleIllusionSkillBole14StartMaxHp(casterAlly);
case IllusionSkillBole6SpendFullMaxHp:
@@ -2066,6 +2165,185 @@ ResolvedGroup:
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_up, 0.01f, 0f);
}
private void NotifyForgetfulRhythmSkillTriggered(SkillDefinition def, int slotIndex)
{
if (def == null || slotIndex < 0) return;
if (def.skillId == EquipSkillForgetfulRhythm) return;
if (!_equipForgetfulRhythmActiveSlots.Contains(slotIndex)) return;
if (string.IsNullOrWhiteSpace(def.skillId)) return;
if (!_equipForgetfulRhythmSeenSkillIdsBySlot.TryGetValue(slotIndex, out HashSet<string> seen))
{
seen = new HashSet<string>();
_equipForgetfulRhythmSeenSkillIdsBySlot[slotIndex] = seen;
}
if (!seen.Add(def.skillId))
{
return;
}
int stacks = 0;
_equipForgetfulRhythmStacksBySlot.TryGetValue(slotIndex, out stacks);
_equipForgetfulRhythmStacksBySlot[slotIndex] = stacks + 1;
}
private void RefreshEquipThousandthBonus(int slotIndex)
{
if (!_equipThousandthActiveSlots.Contains(slotIndex)) return;
var allyGo = GetAllyObjectBySlot(slotIndex);
var ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (ally == null || ally.IsDead) return;
int currentCombo = teamUIController.Instance != null ? teamUIController.Instance.CurrentCombo : 0;
int perfectCount = ScoreManager.Instance != null ? Mathf.Max(0, ScoreManager.Instance.countPerfect) : 0;
float newBonus = 0f;
if (currentCombo >= 300)
{
newBonus += 0.02f;
}
newBonus += Mathf.FloorToInt(perfectCount / 100f) * 0.005f;
float previousBonus = 0f;
_equipThousandthAppliedBonusBySlot.TryGetValue(slotIndex, out previousBonus);
float delta = newBonus - previousBonus;
if (Mathf.Abs(delta) > 0.0001f)
{
ally.scoreEfficiency += delta;
_equipThousandthAppliedBonusBySlot[slotIndex] = newBonus;
}
}
private void ApplyForgetfulRhythmPerfectBonus(int slotIndex)
{
if (!_equipForgetfulRhythmActiveSlots.Contains(slotIndex)) return;
int stacks = 0;
_equipForgetfulRhythmStacksBySlot.TryGetValue(slotIndex, out stacks);
if (stacks <= 0) return;
ScoreManager scoreManager = ScoreManager.Instance;
if (scoreManager != null)
{
scoreManager.countPerfect += stacks;
if (slotIndex >= 0 && slotIndex < scoreManager.trackPerfectCounts.Length)
{
scoreManager.trackPerfectCounts[slotIndex] += stacks;
}
}
if (teamUIController.Instance != null)
{
for (int i = 0; i < stacks; i++)
{
teamUIController.Instance.OnJudgeResult("Perfect");
}
}
}
private void HandleEquipNoteJudgePassives(int slotIndex, string judgeResult)
{
if (slotIndex < 0 || string.IsNullOrWhiteSpace(judgeResult)) return;
if (judgeResult == "Perfect")
{
ApplyForgetfulRhythmPerfectBonus(slotIndex);
}
RefreshEquipThousandthBonus(slotIndex);
}
public void NotifyAllyAttackDealt(int slotIndex)
{
if (slotIndex < 0) return;
if (!_equipStormStacksBySlot.TryGetValue(slotIndex, out int stacks) || stacks <= 0) return;
if (_equipStormProcessingSlots.Contains(slotIndex)) return;
GameObject allyGo = GetAllyObjectBySlot(slotIndex);
AllyCombatant ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (ally == null || ally.IsDead || ally.attack <= 0) return;
if (EffectSystem.Instance == null) return;
float perHitDamage = Mathf.Max(1f, ally.attack / (float)stacks);
_equipStormProcessingSlots.Add(slotIndex);
try
{
for (int i = 0; i < stacks; i++)
{
EffectSystem.Instance.ApplyEffect(Selector.CurrentEnemies, EffectType.DamageSingleEnemy, perHitDamage, 0f, ally.gameObject, null);
}
}
finally
{
_equipStormProcessingSlots.Remove(slotIndex);
}
}
public void NotifyBuffApplied(int receiverSlotIndex)
{
if (receiverSlotIndex < 0) return;
GameObject receiverGo = GetAllyObjectBySlot(receiverSlotIndex);
AllyCombatant receiver = receiverGo != null ? receiverGo.GetComponent<AllyCombatant>() : null;
if (receiver == null || receiver.IsDead) return;
if (_equipFateRelianceActiveSlots.Contains(receiverSlotIndex))
{
int selfGain = Mathf.CeilToInt(receiver.maxMana * 0.10f);
if (selfGain > 0)
{
receiver.ModifyMana(selfGain, true, true);
}
}
if (teamUIController.Instance == null) return;
int[] adjacent = teamUIController.Instance.GetAdjacentAllyIndices(receiverSlotIndex);
if (adjacent == null) return;
for (int i = 0; i < adjacent.Length; i++)
{
int slot = adjacent[i];
if (!_equipFateRelianceActiveSlots.Contains(slot)) continue;
GameObject allyGo = GetAllyObjectBySlot(slot);
AllyCombatant ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (ally == null || ally.IsDead) continue;
int gain = Mathf.CeilToInt(ally.maxMana * 0.15f);
if (gain > 0)
{
ally.ModifyMana(gain, true, true);
}
}
}
public bool TryPreventLethalDamage(AllyCombatant ally, int incomingDamage)
{
if (ally == null || ally.IsDead) return false;
if (incomingDamage <= 0) return false;
if (ally.currentHP - incomingDamage > 0) return false;
int slotIndex = ally.slotIndex;
if (_equipSocialMaskActiveSlots.Contains(slotIndex))
{
DeactivateSocialMask(slotIndex, ally);
return true;
}
if (_equipFutureActiveSlots.Contains(slotIndex))
{
_equipFutureActiveSlots.Remove(slotIndex);
int restoreHp = Mathf.CeilToInt(ally.maxHP * 0.30f);
ally.SetCurrentHP(Mathf.Max(1, restoreHp), true);
return true;
}
return false;
}
private bool HandleEquipSkillLostMaster(AllyCombatant ally)
{
int overflow = Mathf.Max(0, ally.lastHealOverflowAmount);
@@ -2241,6 +2519,489 @@ ResolvedGroup:
return true;
}
private bool HandleEquipSkillVacantPerfectMana(AllyCombatant ally)
{
ally.ModifyMana(1, true, true);
return true;
}
private bool HandleEquipSkillVacantMissHeal(AllyCombatant ally)
{
int healAmount = Mathf.Max(0, ally.attack);
if (healAmount > 0)
{
ally.ModifyHP(healAmount, true);
}
return true;
}
private bool HandleEquipSkillThousandthTracker(int slotIndex)
{
_equipThousandthActiveSlots.Add(slotIndex);
_equipThousandthAppliedBonusBySlot[slotIndex] = 0f;
return true;
}
private bool HandleEquipSkillForgetfulRhythm(int slotIndex)
{
_equipForgetfulRhythmActiveSlots.Add(slotIndex);
_equipForgetfulRhythmStacksBySlot[slotIndex] = 0;
if (!_equipForgetfulRhythmSeenSkillIdsBySlot.ContainsKey(slotIndex))
{
_equipForgetfulRhythmSeenSkillIdsBySlot[slotIndex] = new HashSet<string>();
}
return true;
}
private bool HandleEquipSkillTalentScout(int slotIndex)
{
AllyHero_SO hero = GetAllyHeroSOBySlot(slotIndex);
if (hero == null) return true;
List<SkillDefinition> candidates = new List<SkillDefinition>();
HashSet<string> equippedIds = new HashSet<string>();
int[] equippedGroups = hero.GetEffectiveEquippedSkillGroupIDs();
if (equippedGroups != null)
{
for (int i = 0; i < equippedGroups.Length; i++)
{
SkillGroup equippedGroup = hero.GetSkillGroupByID(equippedGroups[i]);
if (equippedGroup == null || equippedGroup.skills == null) continue;
for (int j = 0; j < equippedGroup.skills.Length; j++)
{
SkillDefinition equippedDef = equippedGroup.skills[j];
if (equippedDef != null && !string.IsNullOrWhiteSpace(equippedDef.skillId))
{
equippedIds.Add(equippedDef.skillId);
}
}
}
}
if (hero.availableSkills != null)
{
for (int i = 0; i < hero.availableSkills.Length; i++)
{
SkillDefinition def = hero.availableSkills[i];
if (def == null || string.IsNullOrWhiteSpace(def.skillId)) continue;
if (equippedIds.Contains(def.skillId)) continue;
candidates.Add(def);
}
}
if (hero.skillGroups != null)
{
for (int i = 0; i < hero.skillGroups.Length; i++)
{
SkillGroup group = hero.skillGroups[i];
if (group == null || group.skills == null) continue;
bool groupEquipped = false;
if (equippedGroups != null)
{
for (int j = 0; j < equippedGroups.Length; j++)
{
if (equippedGroups[j] == group.skillGroupID)
{
groupEquipped = true;
break;
}
}
}
if (groupEquipped) continue;
for (int j = 0; j < group.skills.Length; j++)
{
SkillDefinition def = group.skills[j];
if (def == null || string.IsNullOrWhiteSpace(def.skillId)) continue;
if (equippedIds.Contains(def.skillId)) continue;
candidates.Add(def);
}
}
}
if (candidates.Count <= 0) return true;
SkillDefinition chosen = candidates[UnityEngine.Random.Range(0, candidates.Count)];
if (chosen == null) return true;
string heroObsession = hero != null ? (hero.obsessionTag ?? string.Empty).Trim() : string.Empty;
string chosenObsession = chosen != null ? (chosen.obsessionTag ?? string.Empty).Trim() : string.Empty;
bool obsessionMismatch =
!string.IsNullOrEmpty(heroObsession) &&
!string.IsNullOrEmpty(chosenObsession) &&
!string.Equals(heroObsession, chosenObsession, StringComparison.Ordinal);
if (obsessionMismatch)
{
ApplyTalentScoutDisagree(slotIndex);
}
_equipTalentScoutLastBorrowedSkillBySlot[slotIndex] = chosen.skillId;
UseSkillDefinition(chosen, slotIndex, -1f, null);
return true;
}
private void ApplyTalentScoutDisagree(int slotIndex)
{
GameObject allyGo = GetAllyObjectBySlot(slotIndex);
AllyCombatant ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (ally == null || ally.IsDead) return;
if (_equipTalentScoutDisagreeCoroutinesBySlot.TryGetValue(slotIndex, out Coroutine existing) && existing != null)
{
StopCoroutine(existing);
_equipTalentScoutDisagreeCoroutinesBySlot.Remove(slotIndex);
if (_equipTalentScoutDisagreeRestoreScoreBySlot.TryGetValue(slotIndex, out float oldScore))
{
ally.scoreEfficiency = Mathf.Max(0f, oldScore);
}
}
_equipTalentScoutDisagreeRestoreScoreBySlot[slotIndex] = ally.scoreEfficiency;
ally.scoreEfficiency = 0f;
_equipTalentScoutDisagreeCoroutinesBySlot[slotIndex] = StartCoroutine(TalentScoutDisagreeCoroutine(slotIndex, ally, 2f));
}
private IEnumerator TalentScoutDisagreeCoroutine(int slotIndex, AllyCombatant ally, float duration)
{
if (ally != null)
{
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_down, -ally.scoreEfficiency, duration);
}
yield return new WaitForSeconds(duration);
if (ally != null && !ally.IsDead && _equipTalentScoutDisagreeRestoreScoreBySlot.TryGetValue(slotIndex, out float restore))
{
ally.scoreEfficiency = Mathf.Max(0f, restore);
}
_equipTalentScoutDisagreeCoroutinesBySlot.Remove(slotIndex);
_equipTalentScoutDisagreeRestoreScoreBySlot.Remove(slotIndex);
}
private bool HandleEquipSkillOutOfLineManaScore(int slotIndex, AllyCombatant ally)
{
if (ally.lastManaSpentAmount <= 0) return true;
int killCount = 0;
_equipOutOfLineKillCountsBySlot.TryGetValue(slotIndex, out killCount);
ally.AddScoreDirect(75 + killCount * 55);
return true;
}
private bool HandleEquipSkillOutOfLineEnemyDead(int slotIndex)
{
int current = 0;
_equipOutOfLineKillCountsBySlot.TryGetValue(slotIndex, out current);
_equipOutOfLineKillCountsBySlot[slotIndex] = current + 1;
SwapEquippedMemoryWithLowerAdjacent(slotIndex);
return true;
}
private void SwapEquippedMemoryWithLowerAdjacent(int slotIndex)
{
int lowerSlot = (slotIndex + 1) % 5;
if (lowerSlot < 0 || lowerSlot >= 5 || lowerSlot == slotIndex) return;
AllyHero_SO self = GetAllyHeroSOBySlot(slotIndex);
AllyHero_SO lower = GetAllyHeroSOBySlot(lowerSlot);
if (self == null || lower == null) return;
equipmentSO selfEquip = self.GetEquippedEquipmentResolved();
equipmentSO lowerEquip = lower.GetEquippedEquipmentResolved();
self.SetEquippedEquipment(lowerEquip, false);
lower.SetEquippedEquipment(selfEquip, false);
}
private bool HandleEquipSkillStormStart(int slotIndex, AllyCombatant ally)
{
int reduce = Mathf.FloorToInt(ally.attack * 0.46f);
if (reduce > 0)
{
ally.ModifyAttack(-reduce);
}
_equipStormStacksBySlot[slotIndex] = 1;
return true;
}
private bool HandleEquipSkillMissionStart(AllyCombatant ally)
{
int bonusAttack = Mathf.FloorToInt(ally.damageResistance / 0.01f);
if (bonusAttack > 0)
{
ally.ModifyAttack(bonusAttack);
}
return true;
}
private bool HandleEquipSkillMissionFullMana(AllyCombatant ally)
{
if (!ally.lastManaSpendWasFullBar || ally.lastManaSpentAmount <= 0) return true;
ally.damageResistance = Mathf.Clamp01(ally.damageResistance + 0.04f);
return true;
}
private bool HandleEquipSkillAllForYouMana(AllyCombatant ally)
{
if (!ally.lastManaSpendWasFullBar || ally.lastManaSpentAmount <= 0) return true;
ally.ActivateSelfDamageRedirectToAdjacent(999f);
return true;
}
private bool HandleEquipSkillAllForYouKill(AllyCombatant ally)
{
ally.ActivateSelfDamageRedirectToAdjacent(999f);
return true;
}
private bool HandleEquipSkillFateRelianceStart(int slotIndex)
{
_equipFateRelianceActiveSlots.Add(slotIndex);
return true;
}
private bool HandleEquipSkillNewIdeaStack(int slotIndex, float duration)
{
int stacks = 0;
_equipNewIdeaStacksBySlot.TryGetValue(slotIndex, out stacks);
_equipNewIdeaStacksBySlot[slotIndex] = stacks + 1;
if (!_equipNewIdeaExpireCoroutinesBySlot.TryGetValue(slotIndex, out List<Coroutine> list) || list == null)
{
list = new List<Coroutine>();
_equipNewIdeaExpireCoroutinesBySlot[slotIndex] = list;
}
Coroutine expire = StartCoroutine(NewIdeaExpireCoroutine(slotIndex, duration));
list.Add(expire);
ApplyNewIdeaState(slotIndex);
return true;
}
private IEnumerator NewIdeaExpireCoroutine(int slotIndex, float duration)
{
yield return new WaitForSeconds(duration);
if (_equipNewIdeaStacksBySlot.TryGetValue(slotIndex, out int stacks))
{
_equipNewIdeaStacksBySlot[slotIndex] = Mathf.Max(0, stacks - 1);
}
ApplyNewIdeaState(slotIndex);
}
private void ApplyNewIdeaState(int slotIndex)
{
GameObject allyGo = GetAllyObjectBySlot(slotIndex);
AllyCombatant ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
AllyHero_SO hero = GetAllyHeroSOBySlot(slotIndex);
if (ally == null || hero == null || ally.IsDead) return;
int stacks = 0;
_equipNewIdeaStacksBySlot.TryGetValue(slotIndex, out stacks);
List<AllyHero_SO.AllyLevelInfo> sortedLevels = new List<AllyHero_SO.AllyLevelInfo>();
if (hero.levelStats != null)
{
for (int i = 0; i < hero.levelStats.Count; i++)
{
if (hero.levelStats[i] != null) sortedLevels.Add(hero.levelStats[i]);
}
}
if (sortedLevels.Count <= 0) return;
sortedLevels.Sort((left, right) => left.requiredEXP.CompareTo(right.requiredEXP));
int baseTierIndex = Mathf.Clamp((int)AllyHeroGrowthService.GetUnlockedTier(hero), 0, sortedLevels.Count - 1);
int targetTierIndex = Mathf.Clamp(baseTierIndex + stacks, 0, Mathf.Min(sortedLevels.Count - 1, 3));
AllyHero_SO.AllyLevelInfo targetInfo = hero.GetEffectiveLevelInfoWithEquipment(sortedLevels[targetTierIndex]);
if (targetInfo == null) return;
int currentHp = ally.currentHP;
int currentMana = ally.currentMana;
ally.SetMaxHP(Mathf.Max(1, targetInfo.maxHP), false);
ally.SetMaxMana(Mathf.Max(1, targetInfo.maxMana), false);
ally.SetAttack(Mathf.Max(0, targetInfo.attack));
ally.scoreEfficiency = Mathf.Max(0f, targetInfo.scoreEfficiency);
ally.damageResistance = Mathf.Clamp01(targetInfo.damageResistance);
ally.SetCurrentHP(Mathf.Clamp(currentHp, 0, ally.maxHP), false);
ally.SetCurrentMana(Mathf.Clamp(currentMana, 0, ally.maxMana), false, false);
}
private bool HandleEquipSkillNamelessLaneKill(int slotIndex)
{
GameObject allyGo = GetAllyObjectBySlot(slotIndex);
AllyCombatant ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
teamUIController.Instance?.SwapAllyWithLowerAdjacent(slotIndex);
if (ally != null && !ally.IsDead)
{
ally.scoreEfficiency += 0.012f;
}
return true;
}
private bool HandleEquipSkillSocialMaskStart(int slotIndex, AllyCombatant ally)
{
ActivateSocialMask(slotIndex, ally);
return true;
}
private void ActivateSocialMask(int slotIndex, AllyCombatant ally)
{
if (ally == null || ally.IsDead) return;
if (_equipSocialMaskActiveSlots.Contains(slotIndex)) return;
_equipSocialMaskActiveSlots.Add(slotIndex);
_equipSocialMaskOriginalMaxHpBySlot[slotIndex] = ally.maxHP;
_equipSocialMaskOriginalScoreBySlot[slotIndex] = ally.scoreEfficiency;
int newMaxHp = Mathf.Max(1, Mathf.FloorToInt(ally.maxHP * 0.20f));
ally.SetMaxHP(newMaxHp, false);
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency * 0.60f);
}
private bool HandleEquipSkillSocialMaskKill(int slotIndex, AllyCombatant ally)
{
if (ally == null || ally.IsDead) return true;
if (!_equipSocialMaskActiveSlots.Contains(slotIndex)) return true;
ally.ModifyAttack(2);
return true;
}
private bool HandleEquipSkillSocialMaskClear(int slotIndex, AllyCombatant ally)
{
DeactivateSocialMask(slotIndex, ally);
return true;
}
private void DeactivateSocialMask(int slotIndex, AllyCombatant ally)
{
if (ally == null) return;
if (!_equipSocialMaskActiveSlots.Remove(slotIndex)) return;
int currentAttack = ally.attack;
if (_equipSocialMaskOriginalMaxHpBySlot.TryGetValue(slotIndex, out int originalMaxHp))
{
ally.SetMaxHP(Mathf.Max(1, originalMaxHp), false);
ally.SetCurrentHP(ally.maxHP, true);
}
float baseScore = ally.scoreEfficiency;
if (_equipSocialMaskOriginalScoreBySlot.TryGetValue(slotIndex, out float originalScore))
{
baseScore = originalScore;
}
ally.SetAttack(0);
ally.scoreEfficiency = Mathf.Max(0f, baseScore + currentAttack * 0.01f);
_equipSocialMaskOriginalMaxHpBySlot.Remove(slotIndex);
_equipSocialMaskOriginalScoreBySlot.Remove(slotIndex);
}
private bool HandleEquipSkillQuietTurn(AllyCombatant ally)
{
if (!ally.lastManaSpendWasFullBar || ally.lastManaSpentAmount <= 0) return true;
int selfHeal = Mathf.CeilToInt(ally.maxHP * 0.01f);
if (selfHeal > 0)
{
ally.ModifyHP(selfHeal, true);
}
return true;
}
private bool HandleEquipSkillQuietTurnHealAdjacent(AllyCombatant ally)
{
if (ally == null || ally.IsDead) return true;
if (ally.lastHealActualAmount <= 0) return true;
if (teamUIController.Instance == null) return true;
if (_equipQuietTurnBroadcastInProgress) return true;
_equipQuietTurnBroadcastInProgress = true;
try
{
int[] adjacent = teamUIController.Instance.GetAdjacentAllyIndices(ally.slotIndex);
for (int i = 0; i < adjacent.Length; i++)
{
GameObject allyGo = GetAllyObjectBySlot(adjacent[i]);
AllyCombatant adj = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (adj == null || adj.IsDead) continue;
int manaGain = Mathf.CeilToInt(Mathf.Max(0, ally.attack) * 0.30f);
if (manaGain > 0)
{
adj.ModifyMana(manaGain, true, true);
}
}
}
finally
{
_equipQuietTurnBroadcastInProgress = false;
}
return true;
}
private bool HandleEquipSkillBlock(AllyCombatant ally)
{
int healAmount = Mathf.CeilToInt(ally.maxHP * 0.10f);
if (teamUIController.Instance != null)
{
int[] adjacent = teamUIController.Instance.GetAdjacentAllyIndices(ally.slotIndex);
for (int i = 0; i < adjacent.Length; i++)
{
GameObject allyGo = GetAllyObjectBySlot(adjacent[i]);
AllyCombatant adj = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (adj == null || adj.IsDead) continue;
if (healAmount > 0) adj.ModifyHP(healAmount, true);
}
}
return true;
}
private bool HandleEquipSkillBlockStart(AllyCombatant ally)
{
int increase = Mathf.CeilToInt(ally.maxHP * 0.05f);
if (increase > 0)
{
ally.SetMaxHP(ally.maxHP + increase, false);
}
return true;
}
private bool HandleEquipSkillFutureStart(int slotIndex)
{
_equipFutureActiveSlots.Add(slotIndex);
return true;
}
private bool HandleEquipSkillFutureHeal(int slotIndex, AllyCombatant ally)
{
if (ally == null || ally.IsDead) return true;
if (!_equipFutureActiveSlots.Contains(slotIndex)) return true;
if (teamUIController.Instance == null) return true;
int healedAmount = Mathf.Max(0, ally.lastHealActualAmount);
if (healedAmount <= 0) return true;
int bonusMaxHp = Mathf.CeilToInt(healedAmount * 0.10f);
if (bonusMaxHp <= 0) return true;
int[] adjacent = teamUIController.Instance.GetAdjacentAllyIndices(slotIndex);
for (int i = 0; i < adjacent.Length; i++)
{
GameObject allyGo = GetAllyObjectBySlot(adjacent[i]);
AllyCombatant adj = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (adj == null || adj.IsDead) continue;
adj.SetMaxHP(adj.maxHP + bonusMaxHp, false);
}
return true;
}
private bool HandleEquipSkillFutureClear(int slotIndex, AllyCombatant ally)
{
if (_equipFutureActiveSlots.Remove(slotIndex) && ally != null && !ally.IsDead)
{
ally.scoreEfficiency += 0.02f;
}
return true;
}
private bool HandleIllusionSkillBole14StartMaxHp(AllyCombatant ally)
{
int increase = Mathf.CeilToInt(ally.maxHP * 0.02f);
@@ -2386,6 +3147,51 @@ ResolvedGroup:
_equipManaFullSpendCountBySlot.Clear();
_equipGhoulSchoolActiveSlots.Clear();
_equipGhoulSchoolSeenSkillIdsBySlot.Clear();
foreach (var kv in _equipThousandthAppliedBonusBySlot)
{
var allyGo = GetAllyObjectBySlot(kv.Key);
var ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (ally != null)
{
ally.scoreEfficiency -= kv.Value;
}
}
_equipThousandthActiveSlots.Clear();
_equipThousandthAppliedBonusBySlot.Clear();
_equipForgetfulRhythmActiveSlots.Clear();
_equipForgetfulRhythmSeenSkillIdsBySlot.Clear();
_equipForgetfulRhythmStacksBySlot.Clear();
foreach (var kv in _equipTalentScoutDisagreeCoroutinesBySlot)
{
if (kv.Value != null) StopCoroutine(kv.Value);
}
foreach (var kv in _equipTalentScoutDisagreeRestoreScoreBySlot)
{
var allyGo = GetAllyObjectBySlot(kv.Key);
var ally = allyGo != null ? allyGo.GetComponent<AllyCombatant>() : null;
if (ally != null) ally.scoreEfficiency = Mathf.Max(0f, kv.Value);
}
_equipTalentScoutLastBorrowedSkillBySlot.Clear();
_equipTalentScoutDisagreeCoroutinesBySlot.Clear();
_equipTalentScoutDisagreeRestoreScoreBySlot.Clear();
_equipOutOfLineKillCountsBySlot.Clear();
_equipStormStacksBySlot.Clear();
_equipStormProcessingSlots.Clear();
_equipFateRelianceActiveSlots.Clear();
foreach (var kv in _equipNewIdeaExpireCoroutinesBySlot)
{
if (kv.Value == null) continue;
for (int i = 0; i < kv.Value.Count; i++)
{
if (kv.Value[i] != null) StopCoroutine(kv.Value[i]);
}
}
_equipNewIdeaExpireCoroutinesBySlot.Clear();
_equipNewIdeaStacksBySlot.Clear();
_equipSocialMaskActiveSlots.Clear();
_equipSocialMaskOriginalMaxHpBySlot.Clear();
_equipSocialMaskOriginalScoreBySlot.Clear();
_equipFutureActiveSlots.Clear();
int slots = teamUIController.Instance.allySlotIds.Count;
for (int i = 0; i < slots; i++)
{
@@ -2783,6 +3589,8 @@ ResolvedGroup:
}
}
HandleEquipNoteJudgePassives(trackIndex, judgeResult);
bool anyTriggered = false;
// If equipped groups exist, iterate equipped groups and check each skill for OnNoteHit
@@ -3281,3 +4089,6 @@ ResolvedGroup:
+2
View File
@@ -51,6 +51,8 @@ public class SkillDefinition : ScriptableObject
[Header("Inspector")]
[Tooltip("Documentation text normalized.")]
public string skillId;
[Tooltip("Optional obsession tag used by player memory skill mismatch checks. Leave empty to ignore.")]
public string obsessionTag;
[Tooltip("Documentation text normalized.")]
public string displayName;
+2
View File
@@ -28,6 +28,8 @@ public class AllyHero_SO : ScriptableObject
public int ally_heroID;
public bool isUnlocked;
public equipmentSO.EquipmentSkillType allyType;
[Tooltip("Optional obsession tag used by memory skills such as 30011012. Leave empty to ignore mismatch checks.")]
public string obsessionTag;
[Header("Inspector")]
public Sprite ally_heroImage;
@@ -5069,6 +5069,7 @@ MonoBehaviour:
player_SO: {fileID: 11400000, guid: a59c019c71199384eaac0703299047c8, type: 2}
playerCoins_legacy: {fileID: 7376147910533281946}
player_mmrFragment: {fileID: 1718535194614905540}
player_rks: {fileID: 0}
putPrefabsHere: {fileID: 6773918330215714056}
settings_launch: {fileID: 865676738076373517}
userInfo_launch: {fileID: 1502937460034745443}
+10 -1
View File
@@ -3,6 +3,7 @@ using System;
using System.Collections;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using GameServer.Client;
#if UNITY_EDITOR
using UnityEditor;
#endif
@@ -599,7 +600,15 @@ public class PauseManager : MonoBehaviour
Time.timeScale = 1f;
try { OnPauseStateChanged?.Invoke(false); } catch { }
yield return LoadSceneAsync(ExitSceneName);
string targetScene = ExitSceneName;
ArenaRoomService arenaRoomService = ArenaRoomService.Instance;
if (arenaRoomService != null && arenaRoomService.IsInRoom)
{
arenaRoomService.MarkLocalGameplayExitedEarly();
targetScene = arenaRoomService.RoomSceneName;
}
yield return LoadSceneAsync(targetScene);
}
private IEnumerator LoadSceneAsync(string sceneName)
@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
tasks:
- taskID: 10101
description: 登录游戏 1 次
description: "\u767B\u5F55\u6E38\u620F 1 \u6B21"
taskType: 0
refreshFrequency: 1
targetValue: 1
@@ -25,7 +25,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10102
description: 完成任意歌曲 3 首
description: "\u5B8C\u6210\u4EFB\u610F\u6B4C\u66F2 3 \u9996"
taskType: 1
refreshFrequency: 1
targetValue: 3
@@ -36,7 +36,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10103
description: 单局达到 99 连击
description: "\u5355\u5C40\u8FBE\u5230 99 \u8FDE\u51FB"
taskType: 9
refreshFrequency: 1
targetValue: 99
@@ -47,7 +47,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10104
description: 单局总分达到 1000000
description: "\u5355\u5C40\u603B\u5206\u8FBE\u5230 1000000"
taskType: 10
refreshFrequency: 1
targetValue: 1000000
@@ -58,7 +58,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10105
description: 今日累计分数达到 10000000
description: "\u4ECA\u65E5\u7D2F\u8BA1\u5206\u6570\u8FBE\u5230 10000000"
taskType: 3
refreshFrequency: 1
targetValue: 10000000
@@ -69,7 +69,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10106
description: 阅读剧情 1 次
description: "\u9605\u8BFB\u5267\u60C5 1 \u6B21"
taskType: 4
refreshFrequency: 1
targetValue: 1
@@ -80,7 +80,8 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10107
description: 使用任意经验提升或突破材料 1 次
description: "\u4F7F\u7528\u4EFB\u610F\u7ECF\u9A8C\u63D0\u5347\u6216\u7A81\u7834\u6750\u6599
1 \u6B21"
taskType: 5
refreshFrequency: 1
targetValue: 1
@@ -91,7 +92,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10108
description: 在线 15 分钟
description: "\u5728\u7EBF 15 \u5206\u949F"
taskType: 11
refreshFrequency: 1
targetValue: 900
@@ -102,7 +103,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10109
description: 消费 2000 coins
description: "\u6D88\u8D39 2000 coins"
taskType: 7
refreshFrequency: 1
targetValue: 2000
@@ -113,7 +114,7 @@ MonoBehaviour:
addToTaskPool: 1
selectionWeight: 1
- taskID: 10110
description: 游玩 3 首不同的歌曲
description: "\u6E38\u73A9 3 \u9996\u4E0D\u540C\u7684\u6B4C\u66F2"
taskType: 12
refreshFrequency: 1
targetValue: 3
@@ -6,6 +6,7 @@ using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngineInternal;
using Bansonic;
using GameServer.Client;
public class GameManager : MonoBehaviour
{
@@ -1563,14 +1564,22 @@ public class GameManager : MonoBehaviour
// restore time scale to normal so UI animations driven by unscaled or scaled time behave correctly
try { Time.timeScale = 1f; } catch { }
string targetScene = "selectYourSongFirst";
ArenaRoomService arenaRoomService = ArenaRoomService.Instance;
if (arenaRoomService != null && arenaRoomService.IsInRoom)
{
arenaRoomService.MarkLocalGameplayExitedEarly();
targetScene = arenaRoomService.RoomSceneName;
}
// start coroutine to fade mask then load
if (blackMaskImage == null)
{
StartCoroutine(LoadSceneAsync("selectYourSongFirst"));
StartCoroutine(LoadSceneAsync(targetScene));
return;
}
StartCoroutine(FadeToBlackAndLoad("selectYourSongFirst", 0.25f));
StartCoroutine(FadeToBlackAndLoad(targetScene, 0.25f));
}
private IEnumerator LoadSceneAsync(string sceneName)
@@ -64,6 +64,7 @@ public class GameplayTrackStartIntroAnimator : MonoBehaviour
private readonly List<Transform> gameObjectRects = new List<Transform>();
private readonly List<Transform> colorRects = new List<Transform>();
private readonly List<Transform> allies = new List<Transform>();
private readonly List<Transform> explicitKeyDownRects = new List<Transform>(5);
private readonly Dictionary<Graphic, float> baseGraphicAlpha = new Dictionary<Graphic, float>();
private readonly Dictionary<SpriteRenderer, float> baseSpriteAlpha = new Dictionary<SpriteRenderer, float>();
@@ -105,6 +106,24 @@ public class GameplayTrackStartIntroAnimator : MonoBehaviour
playRoutine = StartCoroutine(PlayRoutine());
}
public void SetExplicitKeyDownRects(IEnumerable<Transform> rects)
{
explicitKeyDownRects.Clear();
if (rects != null)
{
foreach (Transform rect in rects)
{
if (rect != null)
{
explicitKeyDownRects.Add(rect);
}
}
}
refsCached = false;
preStartPrepared = false;
}
private IEnumerator PreparePreStartStateNextFrame()
{
// Re-apply after one frame so late UI initializers don't bring these elements visible before start.
@@ -154,10 +173,17 @@ public class GameplayTrackStartIntroAnimator : MonoBehaviour
enemyStatics = FindSceneTransformByNameUnder("statics", enemyRoot);
keyDownRects.Clear();
if (keyDown != null)
keyDownRects.AddRange(CollectDescendantsByNameContains(keyDown, "sdw 3"));
keyDownRects.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x));
FilterLaneRects(keyDownRects, keyDownTargetY, 5);
if (explicitKeyDownRects.Count > 0)
{
keyDownRects.AddRange(explicitKeyDownRects);
}
else
{
if (keyDown != null)
keyDownRects.AddRange(CollectDescendantsByNameContains(keyDown, "sdw 3"));
keyDownRects.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x));
FilterLaneRects(keyDownRects, keyDownTargetY, 5);
}
gameObjectRects.Clear();
List<Transform> byNamedRoots = CollectSdw3UnderNamedRoots("GameObject", keyDown);
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6ee7cac27eaef744ab53dc64d7970f55
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5cf0e42b79b4a5aa40d1a98d52641c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
namespace GameServer.Client
{
public enum ConnectionState
{
Disconnected,
Connecting,
Handshaking,
LoadingProfile,
LoadingLeaderboard,
Ready,
ConnectionFailed
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 30018fafe3957e743a4781b802f01860
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: afebae6e2cf96f345856c473879a4916
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Compilation;
using GameServer.Client;
[InitializeOnLoad]
public static class ArenaRoomEditorExitGuard
{
static ArenaRoomEditorExitGuard()
{
EditorApplication.playModeStateChanged -= HandlePlayModeStateChanged;
EditorApplication.playModeStateChanged += HandlePlayModeStateChanged;
AssemblyReloadEvents.beforeAssemblyReload -= HandleBeforeAssemblyReload;
AssemblyReloadEvents.beforeAssemblyReload += HandleBeforeAssemblyReload;
}
private static void HandlePlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.ExitingPlayMode)
{
ArenaRoomService.Instance?.BestEffortLeaveRoomOnShutdown();
}
}
private static void HandleBeforeAssemblyReload()
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
{
ArenaRoomService.Instance?.BestEffortLeaveRoomOnShutdown();
}
}
}
#endif
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a1deac595959bca44b546dc4f794c8b3
@@ -0,0 +1,167 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using GameServer.Client;
public class GameServerBridge : MonoBehaviour
{
public static GameServerBridge Instance { get; private set; }
[Header("引用")]
[SerializeField] private settlementController settlement;
[SerializeField] private ScoreManager scoreManager;
[SerializeField] private BeatmapManager beatmapManager;
[SerializeField] private GameManager gameManager;
[Header("配置")]
[SerializeField] private bool autoSubmitOnSettlement = true;
private const string HMAC_SECRET = "your_secret_key_change_this_in_production";
private bool _hasSubmitted = false;
private float _gameStartTime;
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
private void OnEnable()
{
_gameStartTime = Time.realtimeSinceStartup;
_hasSubmitted = false;
if (autoSubmitOnSettlement)
settlementController.OnSettlementCompleted += OnSettlementTriggered;
}
private void OnDisable()
{
settlementController.OnSettlementCompleted -= OnSettlementTriggered;
}
private void OnSettlementTriggered()
{
if (_hasSubmitted) return;
SubmitCurrentSettlement();
}
public async void SubmitCurrentSettlement()
{
if (_hasSubmitted) { Debug.Log("[Bridge] 本次已提交过,跳过"); return; }
var nm = NetworkManager.Instance;
if (nm == null) { Debug.LogWarning("[Bridge] NetworkManager 不存在"); return; }
_hasSubmitted = true;
try
{
// ── 采集当前歌曲数据 ──
string songId = "0";
string difficulty = "unknown";
if (beatmapManager != null)
{
SongData songData = beatmapManager.assignedSongData;
songId = songData != null ? songData.songID.ToString() : "0";
difficulty = ConvertDifficulty(beatmapManager.assignedDifficulty);
}
int chartScore = 0, idolScore = 0;
long totalScore = 0;
if (scoreManager != null)
{
chartScore = scoreManager.allSum_pmScore;
idolScore = scoreManager.allSum_idolScore;
totalScore = chartScore + idolScore;
}
string grade = GetGrade(totalScore);
double runtime = Time.realtimeSinceStartup - _gameStartTime;
if (gameManager != null) { float t = gameManager.GetPendingSessionDurationSeconds(); if (t > 0) runtime = t; }
string playedAt = DateTime.Now.ToString("yyyyMMdd HHmmss");
// ── 本地预校验 ──
if (difficulty != "in")
{
Debug.Log($"[Bridge] 难度={difficulty} 不是 IN,不上传");
_hasSubmitted = false; return;
}
if (totalScore < 1000000)
{
Debug.Log($"[Bridge] 总分={totalScore} < 100万,不上传");
_hasSubmitted = false; return;
}
if (runtime < 60)
{
Debug.Log($"[Bridge] 游玩时间={runtime:F1}秒 < 60秒,不上传");
_hasSubmitted = false; return;
}
// ── HMAC 签名 ──
string payload = $"{songId}|{difficulty}|{totalScore}|{chartScore}|{idolScore}";
string hmac = ComputeHmacSha256(payload, HMAC_SECRET);
// ── 上传日志(清晰可见) ──
Debug.Log("╔══════════════════════════════════════════════════════╗");
Debug.Log("║ 开始上传结算数据到服务器 ║");
Debug.Log("╠══════════════════════════════════════════════════════╣");
Debug.Log($"║ 歌曲ID: {songId}");
Debug.Log($"║ 难度: {difficulty}");
Debug.Log($"║ 谱面分数: {chartScore}");
Debug.Log($"║ 偶像分数: {idolScore}");
Debug.Log($"║ 总分: {totalScore}");
Debug.Log($"║ 评级: {grade}");
Debug.Log($"║ 游玩时长: {runtime:F1}秒");
Debug.Log($"║ 游玩时间: {playedAt}");
Debug.Log($"║ HMAC签名: {hmac.Substring(0, 16)}...");
Debug.Log("╚══════════════════════════════════════════════════════╝");
string result = await nm.PushSettlement(songId, difficulty, chartScore, idolScore,
totalScore, grade, runtime, playedAt, hmac);
// ── 结果日志 ──
if (result == "OK")
{
Debug.Log("╔══════════════════════════════════════════════════════╗");
Debug.Log("║ ✅✅✅ 上传成功!数据已写入服务器数据库 ✅✅✅ ║");
Debug.Log("╚══════════════════════════════════════════════════════╝");
}
else if (result == "QUEUED")
{
Debug.LogWarning("[Bridge] ⏳ 服务器繁忙,3秒后重试...");
_hasSubmitted = false;
await Task.Delay(3000);
SubmitCurrentSettlement();
}
else
{
Debug.LogWarning($"╔══════════════════════════════════════════════════════╗");
Debug.LogWarning($"║ ❌ 上传失败!服务器返回: {result}");
Debug.LogWarning($"╚══════════════════════════════════════════════════════╝");
_hasSubmitted = false;
}
}
catch (Exception ex)
{
Debug.LogError($"[Bridge] 上传异常: {ex.Message}");
_hasSubmitted = false;
}
}
private static string ComputeHmacSha256(string payload, string secret)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var sb = new StringBuilder(hash.Length * 2);
foreach (byte b in hash) sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
private static string ConvertDifficulty(int i) => i switch { 0 => "ez", 1 => "hd", 2 => "in", 3 => "im", _ => "unknown" };
private static string GetGrade(long s) => s >= 960000 ? "SSS" : s >= 920000 ? "SS" : s >= 880000 ? "S" : s >= 820000 ? "A" : s >= 720000 ? "B" : s >= 600000 ? "C" : s >= 400000 ? "D" : "F";
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 40b7e7063942e3641b595de0b106fa40
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace GameServer.Client
{
public sealed class LeaderboardCacheService : MonoBehaviour
{
public static LeaderboardCacheService Instance { get; private set; }
private readonly Dictionary<string, LeaderboardData> _cache = new Dictionary<string, LeaderboardData>();
private readonly Dictionary<string, Task<LeaderboardData>> _inflight = new Dictionary<string, Task<LeaderboardData>>();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static LeaderboardCacheService EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
GameObject host = new GameObject("__runtime_leaderboard_cache");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<LeaderboardCacheService>();
return Instance;
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
private void OnEnable()
{
SceneManager.sceneLoaded -= HandleSceneLoaded;
SceneManager.sceneLoaded += HandleSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= HandleSceneLoaded;
}
public bool TryGetCached(string songId, out LeaderboardData data)
{
data = null;
if (string.IsNullOrWhiteSpace(songId))
{
return false;
}
return _cache.TryGetValue(songId, out data) && data != null;
}
public async Task<LeaderboardData> GetOrFetch(string songId)
{
if (string.IsNullOrWhiteSpace(songId))
{
throw new ArgumentException("songId is required", nameof(songId));
}
if (TryGetCached(songId, out LeaderboardData cached))
{
return cached;
}
if (_inflight.TryGetValue(songId, out Task<LeaderboardData> pending) && pending != null)
{
return await pending;
}
NetworkManager nm = NetworkManager.Instance;
if (nm == null)
{
throw new InvalidOperationException("NetworkManager is not available.");
}
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))
{
return;
}
_cache.Remove(songId);
}
private async void HandleSceneLoaded(Scene scene, LoadSceneMode mode)
{
await Task.Yield();
if (!TryResolveCurrentSongId(out string songId))
{
return;
}
if (TryGetCached(songId, out _))
{
return;
}
try
{
await GetOrFetch(songId);
Debug.Log($"[LeaderboardCacheService] Preloaded leaderboard for songId={songId} after scene '{scene.name}'.");
}
catch (Exception ex)
{
Debug.LogWarning($"[LeaderboardCacheService] Failed to preload leaderboard for songId={songId}: {ex.Message}");
}
}
private async Task<LeaderboardData> FetchAndCache(string songId, NetworkManager nm)
{
LeaderboardData data = await nm.FetchLeaderboardFromServer(songId);
if (data != null)
{
_cache[songId] = data;
}
return data;
}
private static bool TryResolveCurrentSongId(out string songId)
{
songId = null;
SongData selectedSong = SongDataHolder.SelectedSongData;
if (selectedSong != null)
{
songId = selectedSong.songID.ToString();
return true;
}
if (BeatmapManager.pendingSongData != null)
{
songId = BeatmapManager.pendingSongData.songID.ToString();
return true;
}
BeatmapManager beatmapManager = BeatmapManager.Instance;
if (beatmapManager != null && beatmapManager.assignedSongData != null)
{
songId = beatmapManager.assignedSongData.songID.ToString();
return true;
}
return false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22b74214e8fd41ed9415535de7dee096
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,144 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace GameServer.Client
{
/// <summary>
/// Manages serialization, deserialization, and dispatch of WebSocket messages.
/// Thread-safe; pending request callbacks are invoked on receive.
/// </summary>
public class MessageHandler
{
private readonly WebSocketClient _client;
// type → list of callbacks
private readonly Dictionary<string, List<Action<JObject, string>>> _handlers
= new Dictionary<string, List<Action<JObject, string>>>();
// requestId → TaskCompletionSource for request-response pattern
private readonly ConcurrentDictionary<string, TaskCompletionSource<JObject>> _pending
= new ConcurrentDictionary<string, TaskCompletionSource<JObject>>();
public MessageHandler(WebSocketClient client)
{
_client = client;
_client.OnMessageReceived += OnRawMessage;
}
// ── Send ─────────────────────────────────────────────────────────────
public async Task SendMessage(string type, object data,
string requestId = null)
{
requestId ??= Guid.NewGuid().ToString();
var envelope = new
{
type,
data,
requestId
};
string json = JsonConvert.SerializeObject(envelope);
await _client.Send(json);
}
// ── Request-Response ─────────────────────────────────────────────────
/// <summary>
/// Sends a typed request and awaits the matching response by requestId.
/// </summary>
public async Task<JObject> SendRequest(string type, object data,
TimeSpan? timeout = null)
{
string requestId = Guid.NewGuid().ToString();
var tcs = new TaskCompletionSource<JObject>(
TaskCreationOptions.RunContinuationsAsynchronously);
_pending[requestId] = tcs;
await SendMessage(type, data, requestId);
var timeoutTask = Task.Delay(timeout ?? TimeSpan.FromSeconds(10));
var completedTask = await Task.WhenAny(tcs.Task, timeoutTask);
_pending.TryRemove(requestId, out _);
if (completedTask == timeoutTask)
throw new TimeoutException($"Request '{type}' timed out");
return await tcs.Task;
}
// ── Handler Registration ─────────────────────────────────────────────
public void RegisterHandler(string type, Action<JObject, string> callback)
{
lock (_handlers)
{
if (!_handlers.TryGetValue(type, out var list))
{
list = new List<Action<JObject, string>>();
_handlers[type] = list;
}
list.Add(callback);
}
}
public void UnregisterHandler(string type, Action<JObject, string> callback)
{
lock (_handlers)
{
if (_handlers.TryGetValue(type, out var list))
list.Remove(callback);
}
}
// ── Internal ─────────────────────────────────────────────────────────
private void OnRawMessage(string raw)
{
JObject envelope;
try
{
envelope = JObject.Parse(raw);
}
catch (Exception ex)
{
Debug.LogWarning("[MessageHandler] Failed to parse JSON: " + ex.Message);
return;
}
string type = envelope["type"]?.ToString();
string requestId = envelope["requestId"]?.ToString();
JObject data = envelope["data"] as JObject ?? new JObject();
// Resolve pending request
if (requestId != null && _pending.TryRemove(requestId, out var tcs))
{
tcs.TrySetResult(data);
return;
}
// Dispatch to registered handlers
List<Action<JObject, string>> callbacks = null;
lock (_handlers)
{
if (type != null && _handlers.TryGetValue(type, out var list))
callbacks = new List<Action<JObject, string>>(list);
}
if (callbacks == null) return;
foreach (var cb in callbacks)
{
try { cb(data, requestId); }
catch (Exception ex)
{
Debug.LogError($"[MessageHandler] Handler for '{type}' threw: {ex}");
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4876980774d12f84181c818ea913ae53
@@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace GameServer.Client
{
// ── Generic message wrapper ──────────────────────────────────────────────
[Serializable]
public class WsMessage<T>
{
[JsonProperty("type")] public string type;
[JsonProperty("data")] public T data;
[JsonProperty("requestId")] public string requestId;
public string ToJson() => JsonConvert.SerializeObject(this);
public static WsMessage<T> FromJson(string json) =>
JsonConvert.DeserializeObject<WsMessage<T>>(json);
}
[Serializable]
public class WsMessage
{
[JsonProperty("type")] public string type;
[JsonProperty("data")] public object data;
[JsonProperty("requestId")] public string requestId;
public static WsMessage FromJson(string json) =>
JsonConvert.DeserializeObject<WsMessage>(json);
}
// ── Request / Response DTOs ──────────────────────────────────────────────
[Serializable]
public class HandshakeRequest
{
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("token")] public string token;
[JsonProperty("display_name")] public string display_name;
[JsonProperty("avatar_url")] public string avatar_url;
}
[Serializable]
public class HandshakeResponse
{
[JsonProperty("success")] public bool success;
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
}
[Serializable]
public class ProfileData
{
[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("display_name")] public string display_name;
[JsonProperty("avatar_url")] public string avatar_url;
[JsonProperty("player_level")] public int player_level;
[JsonProperty("leaderboard_opt_out")] public bool leaderboard_opt_out;
[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 LeaderboardEntry
{
[JsonProperty("rank")] public int rank;
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("display_name")] public string display_name;
[JsonProperty("avatar_url")] public string avatar_url;
[JsonProperty("total_score")] public long total_score;
[JsonProperty("chart_score")] public int chart_score;
[JsonProperty("idol_score")] public int idol_score;
[JsonProperty("grade")] public string grade;
[JsonProperty("achieved_at")] public string achieved_at;
[JsonProperty("achieved_at_unix")] public long achieved_at_unix;
[JsonProperty("play_count")] public int play_count;
[JsonProperty("keep_on_time")] public int keep_on_time;
[JsonProperty("room_status_text")] public string room_status_text;
}
[Serializable]
public class LeaderboardData
{
[JsonProperty("song_id")] public string song_id;
[JsonProperty("cycle_id")] public int cycle_id;
[JsonProperty("is_locked")] public bool is_locked;
[JsonProperty("rankings")] public List<LeaderboardEntry> rankings;
}
[Serializable]
public class ScorePushRequest
{
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("song_id")] public string song_id;
[JsonProperty("difficulty")] public string difficulty;
[JsonProperty("chart_score")] public int chart_score;
[JsonProperty("idol_score")] public int idol_score;
[JsonProperty("total_score")] public long total_score;
[JsonProperty("grade")] public string grade;
[JsonProperty("runtime_seconds")] public double runtime_seconds;
[JsonProperty("played_at")] public string played_at;
[JsonProperty("hmac")] public string hmac;
}
[Serializable]
public class ScoreAckResponse
{
[JsonProperty("status")] public string status;
[JsonProperty("reason")] public string reason;
[JsonProperty("message")] public string message;
}
[Serializable]
public class ArenaCreateRequest
{
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("song_id")] public string song_id;
[JsonProperty("difficulty")] public string difficulty;
[JsonProperty("password")] public string password;
}
[Serializable]
public class ArenaCreateResponse
{
[JsonProperty("success")] public bool success;
[JsonProperty("room_code")] public string room_code;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
}
[Serializable]
public class ArenaJoinRequest
{
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("room_code")] public string room_code;
[JsonProperty("password")] public string password;
}
[Serializable]
public class ArenaJoinResponse
{
[JsonProperty("success")] public bool success;
[JsonProperty("room_code")] public string room_code;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
[JsonProperty("participant_count")] public int participant_count;
}
[Serializable]
public class ArenaStartRequest
{
[JsonProperty("steam_id")] public string steam_id;
}
[Serializable]
public class ArenaStartResponse
{
[JsonProperty("success")] public bool success;
[JsonProperty("room_code")] public string room_code;
[JsonProperty("song_id")] public string song_id;
[JsonProperty("difficulty")] public string difficulty;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
}
[Serializable]
public class ArenaSubmitRequest
{
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("room_code")] public string room_code;
[JsonProperty("total_score")] public long total_score;
[JsonProperty("grade")] public string grade;
}
[Serializable]
public class ArenaResultEntry
{
[JsonProperty("rank")] public int rank;
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("total_score")] public long total_score;
[JsonProperty("grade")] public string grade;
}
[Serializable]
public class ArenaResult
{
[JsonProperty("success")] public bool success;
[JsonProperty("finished")] public bool finished;
[JsonProperty("room_code")] public string room_code;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
[JsonProperty("rankings")] public List<ArenaResultEntry> rankings;
}
[Serializable]
public class HeartbeatResponse
{
[JsonProperty("success")] public bool success;
[JsonProperty("message")] public string message;
}
[Serializable]
public class ErrorResponse
{
[JsonProperty("code")] public string code;
[JsonProperty("message")] public string message;
}
[Serializable]
public class ArenaRoomParticipant
{
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("display_name")] public string display_name;
[JsonProperty("is_host")] public bool is_host;
[JsonProperty("is_ready")] public bool is_ready;
[JsonProperty("join_order")] public int join_order;
[JsonProperty("total_score")] public long? total_score;
[JsonProperty("grade")] public string grade;
[JsonProperty("rank")] public int? rank;
}
[Serializable]
public class ArenaRoomSnapshot
{
[JsonProperty("success")] public bool success;
[JsonProperty("room_code")] public string room_code;
[JsonProperty("host_steam_id")] public string host_steam_id;
[JsonProperty("song_id")] public string song_id;
[JsonProperty("song_name")] public string song_name;
[JsonProperty("difficulty")] public string difficulty;
[JsonProperty("has_password")] public bool has_password;
[JsonProperty("status")] public string status;
[JsonProperty("player_count")] public int player_count;
[JsonProperty("created_at_unix")] public long created_at_unix;
[JsonProperty("expires_at_unix")] public long expires_at_unix;
[JsonProperty("expire_seconds")] public int expire_seconds;
[JsonProperty("participants")] public List<ArenaRoomParticipant> participants;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
}
[Serializable]
public class ArenaSubmittedScore
{
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("display_name")] public string display_name;
[JsonProperty("total_score")] public long total_score;
[JsonProperty("grade")] public string grade;
[JsonProperty("rank")] public int rank;
[JsonProperty("submitted_at_unix")] public long submitted_at_unix;
[JsonProperty("room_status_text")] public string room_status_text;
[JsonProperty("has_submitted")] public bool has_submitted;
[JsonProperty("join_order")] public int join_order;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ac89c509dffbbf94b8056f91b457552d
@@ -0,0 +1,30 @@
using UnityEngine;
namespace GameServer.Client
{
/// <summary>
/// 网络自动初始化器
/// 使用 [RuntimeInitializeOnLoadMethod] 在任何场景加载之前自动创建 NetworkManager
/// 这样无论从哪个场景启动,NetworkManager 都会存在且不会被场景切换销毁
/// </summary>
public static class NetworkBootstrap
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
// 如果已经存在就不重复创建
if (NetworkManager.Instance != null)
{
Debug.Log("[NetworkBootstrap] NetworkManager 已存在,跳过创建");
return;
}
// 创建物体并挂载 NetworkManager
GameObject go = new GameObject("[NetworkManager]");
go.AddComponent<NetworkManager>();
Object.DontDestroyOnLoad(go);
Debug.Log("[NetworkBootstrap] NetworkManager 已自动创建(DontDestroyOnLoad");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 50f3a4e57ae0e3d428fcf1c08c7f6fc1
@@ -0,0 +1,487 @@
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Networking;
using Bansonic;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define NETWORK_DISABLE_STEAMWORKS
#endif
#if !NETWORK_DISABLE_STEAMWORKS
using Steamworks;
#endif
namespace GameServer.Client
{
public class NetworkManager : MonoBehaviour
{
public static NetworkManager Instance { get; private set; }
[Header("Server")]
[SerializeField] private string serverUrl = "http://47.112.187.172:8080";
[Header("Auth")]
public string steamId = "";
[SerializeField] private string authToken = "test_token";
[Header("Steam")]
[SerializeField] private string steamDisplayName = "";
[SerializeField] private string steamAvatarUrl = "";
public event Action<ConnectionState> OnStateChanged;
public event Action<HandshakeResponse> OnHandshakeResult;
public event Action<ProfileData> OnProfileLoaded;
public event Action<LeaderboardData> OnLeaderboardLoaded;
public event Action<ArenaCreateResponse> OnArenaCreated;
public event Action<ArenaJoinResponse> OnArenaJoined;
public event Action<ArenaResult> OnArenaResult;
private ConnectionState _state = ConnectionState.Disconnected;
private CancellationTokenSource _requestCts;
private bool _isApplicationQuitting;
public ConnectionState State => _state;
public MessageHandler MessageHandler => null;
public bool IsReady => _state == ConnectionState.Ready;
public bool IsConnectedAndHandshaked => _state == ConnectionState.Ready;
public string ServerUrl => serverUrl;
public string SteamId
{
get
{
RefreshSteamIdentity(false);
return steamId;
}
}
public string SteamDisplayName
{
get
{
RefreshSteamIdentity(false);
return steamDisplayName;
}
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
RefreshSteamIdentity(false);
Debug.Log("[NetworkManager] HTTP lazy mode singleton initialized");
}
private void Start()
{
RefreshSteamIdentity(false);
Debug.Log($"[NetworkManager] Lazy HTTP mode enabled. Settlement submit will use {BuildApiUrl("/api/submit")}");
}
private void OnApplicationQuit()
{
_isApplicationQuitting = true;
}
private void OnDestroy()
{
_requestCts?.Cancel();
if (!_isApplicationQuitting && Instance == this)
{
Instance = null;
}
}
public void Connect()
{
_requestCts?.Cancel();
_requestCts = new CancellationTokenSource();
ConnectWithErrorHandling(_requestCts.Token);
}
public void Disconnect()
{
_requestCts?.Cancel();
SetState(ConnectionState.Disconnected);
}
public void SkipToReady()
{
SetState(ConnectionState.Ready);
}
public async Task SendHandshake()
{
await PerformHandshakeAsync(CancellationToken.None);
}
public async Task LoadProfile()
{
try
{
SetState(ConnectionState.LoadingProfile);
ProfileData data = await GetJson<ProfileData>(BuildApiUrl($"/api/profile/{SteamId}"), CancellationToken.None);
OnProfileLoaded?.Invoke(data);
SetState(ConnectionState.Ready);
}
catch (Exception ex)
{
Debug.LogError($"[NetworkManager] Failed to load profile: {ex.Message}");
NotifyNetworkError(ex.Message);
SetState(ConnectionState.ConnectionFailed);
throw;
}
}
public Task LoadLeaderboard(string songId)
{
return GetLeaderboard(songId);
}
public async Task<string> PushSettlement(string songId, string difficulty,
int chartScore, int idolScore, long totalScore, string grade,
double runtimeSeconds, string playedAt, string hmac = "")
{
try
{
await PerformHandshakeAsync(CancellationToken.None);
var req = new ScorePushRequest
{
steam_id = SteamId,
song_id = songId,
difficulty = difficulty,
chart_score = chartScore,
idol_score = idolScore,
total_score = totalScore,
grade = grade,
runtime_seconds = runtimeSeconds,
played_at = playedAt,
hmac = hmac
};
ScoreAckResponse resp = await PostJson<ScoreAckResponse>(BuildApiUrl("/api/submit"), req, CancellationToken.None);
string status = string.IsNullOrEmpty(resp?.status) ? "ERROR" : resp.status;
if (status == "OK")
{
SetState(ConnectionState.Ready);
}
else
{
Debug.LogWarning($"[NetworkManager] Submit rejected: status={resp?.status} reason={resp?.reason} msg={resp?.message}");
NotifyNetworkError(resp?.message ?? resp?.reason ?? "Submit rejected");
}
return status;
}
catch (Exception ex)
{
Debug.LogError($"[NetworkManager] HTTP submit failed: {ex.Message}");
NotifyNetworkError(ex.Message);
SetState(ConnectionState.ConnectionFailed);
return "ERROR";
}
}
public async Task PushScore(string songId, string difficulty, long totalScore, string grade)
{
await PushSettlement(songId, difficulty, 0, 0, totalScore, grade, 0, "", "");
}
public async Task<ArenaCreateResponse> CreateArenaRoom(string songId, string difficulty, string password = null)
{
var req = new ArenaCreateRequest
{
steam_id = SteamId,
song_id = songId,
difficulty = difficulty,
password = password
};
ArenaCreateResponse resp = await PostJson<ArenaCreateResponse>(BuildApiUrl("/api/arena/create"), req, CancellationToken.None);
OnArenaCreated?.Invoke(resp);
return resp;
}
public async Task<ArenaJoinResponse> JoinArenaRoom(string roomCode, string password = null)
{
var req = new ArenaJoinRequest
{
steam_id = SteamId,
room_code = roomCode,
password = password
};
ArenaJoinResponse resp = await PostJson<ArenaJoinResponse>(BuildApiUrl("/api/arena/join"), req, CancellationToken.None);
OnArenaJoined?.Invoke(resp);
return resp;
}
public async Task StartArenaGame()
{
var req = new ArenaStartRequest
{
steam_id = SteamId
};
ArenaStartResponse resp = await PostJson<ArenaStartResponse>(BuildApiUrl("/api/arena/start"), req, CancellationToken.None);
if (!resp.success)
{
throw new Exception($"Arena start failed: {resp.error_code ?? resp.message}");
}
}
public async Task SubmitArenaScore(string roomCode, long totalScore, string grade)
{
var req = new ArenaSubmitRequest
{
steam_id = SteamId,
room_code = roomCode,
total_score = totalScore,
grade = grade
};
ArenaResult resp = await PostJson<ArenaResult>(BuildApiUrl("/api/arena/submit"), req, CancellationToken.None);
if (!resp.success)
{
throw new Exception($"Arena submit failed: {resp.error_code ?? resp.message}");
}
if (resp.finished)
{
OnArenaResult?.Invoke(resp);
}
}
public async Task SendHeartbeat()
{
HeartbeatResponse resp = await GetJson<HeartbeatResponse>(BuildApiUrl("/api/ping"), CancellationToken.None);
Debug.Log($"[NetworkManager] Ping: {resp.message}");
}
public async Task<LeaderboardData> GetLeaderboard(string songId)
{
if (string.IsNullOrWhiteSpace(songId))
{
throw new ArgumentException("songId is required", nameof(songId));
}
try
{
SetState(ConnectionState.LoadingLeaderboard);
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
LeaderboardData data = cache != null
? await cache.GetOrFetch(songId)
: await FetchLeaderboardFromServer(songId);
OnLeaderboardLoaded?.Invoke(data);
SetState(ConnectionState.Ready);
return data;
}
catch (Exception ex)
{
Debug.LogError($"[NetworkManager] Failed to load leaderboard: {ex.Message}");
NotifyNetworkError(ex.Message);
SetState(ConnectionState.ConnectionFailed);
throw;
}
}
internal Task<LeaderboardData> FetchLeaderboardFromServer(string songId)
{
return GetJson<LeaderboardData>(BuildApiUrl($"/api/leaderboard/{songId}"), CancellationToken.None);
}
private async void ConnectWithErrorHandling(CancellationToken token)
{
try
{
await PerformHandshakeAsync(token);
}
catch (OperationCanceledException)
{
Debug.Log("[NetworkManager] HTTP connect cancelled");
}
catch (Exception ex)
{
Debug.LogError($"[NetworkManager] HTTP connect failed: {ex.Message}");
NotifyNetworkError(ex.Message);
SetState(ConnectionState.ConnectionFailed);
}
}
private async Task PerformHandshakeAsync(CancellationToken token)
{
RefreshSteamIdentity(true);
SetState(ConnectionState.Handshaking);
var req = new HandshakeRequest
{
steam_id = SteamId,
token = authToken,
display_name = steamDisplayName,
avatar_url = steamAvatarUrl
};
HandshakeResponse resp = await PostJson<HandshakeResponse>(BuildApiUrl("/api/handshake"), req, token);
OnHandshakeResult?.Invoke(resp);
if (resp != null && resp.success)
{
SetState(ConnectionState.Ready);
return;
}
string message = resp == null
? "empty handshake response"
: $"{resp.error_code} - {resp.message}";
throw new Exception("Handshake failed: " + message);
}
public bool RefreshSteamIdentity(bool logWarnings)
{
#if NETWORK_DISABLE_STEAMWORKS
if (logWarnings)
{
Debug.LogWarning("[NetworkManager] Steamworks is unavailable on this platform. Using current serialized network identity.");
}
return false;
#else
if (!SteamManager.Initialized)
{
if (logWarnings)
{
Debug.LogWarning("[NetworkManager] SteamManager is not initialized. Using current network identity.");
}
return false;
}
try
{
CSteamID currentSteamId = SteamUser.GetSteamID();
if (currentSteamId.m_SteamID != 0)
{
steamId = currentSteamId.m_SteamID.ToString();
}
string personaName = SteamFriends.GetPersonaName();
if (!string.IsNullOrWhiteSpace(personaName))
{
steamDisplayName = personaName;
}
return true;
}
catch (Exception ex)
{
if (logWarnings)
{
Debug.LogWarning($"[NetworkManager] Failed to read Steam identity: {ex.Message}");
}
return false;
}
#endif
}
private async Task<T> PostJson<T>(string url, object payload, CancellationToken token)
{
string json = JsonConvert.SerializeObject(payload);
using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
byte[] body = Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(body);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.timeout = 15;
Debug.Log($"[NetworkManager] HTTP POST {url}");
var operation = request.SendWebRequest();
while (!operation.isDone)
{
token.ThrowIfCancellationRequested();
await Task.Yield();
}
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)
{
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = 15;
Debug.Log($"[NetworkManager] HTTP GET {url}");
var operation = request.SendWebRequest();
while (!operation.isDone)
{
token.ThrowIfCancellationRequested();
await Task.Yield();
}
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 string BuildApiUrl(string apiPath)
{
Uri uri = new Uri(serverUrl);
string scheme = uri.Scheme;
if (string.Equals(scheme, "ws", StringComparison.OrdinalIgnoreCase))
{
scheme = "http";
}
else if (string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase))
{
scheme = "https";
}
string authority = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}";
return $"{scheme}://{authority}{apiPath}";
}
private void SetState(ConnectionState newState)
{
if (_state == newState)
{
return;
}
ConnectionState oldState = _state;
_state = newState;
Debug.Log($"[NetworkManager] State: {oldState} -> {newState}");
OnStateChanged?.Invoke(newState);
}
private static void NotifyNetworkError(string message)
{
if (!string.IsNullOrWhiteSpace(message))
{
gNotice.error.display(message);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7941755436422684d99a4f87847324e4
@@ -0,0 +1,350 @@
using System;
using UnityEngine;
using GameServer.Client;
/// <summary>
/// 网络功能测试面板
/// 用 OnGUI 画按钮,点击后触发各项功能
/// 测试完毕后可删除此脚本
/// </summary>
public class NetworkTestUI : MonoBehaviour
{
// ── 配置 ──
[Header("测试用参数")]
[SerializeField] private string testSongId = "song_001";
[SerializeField] private string testDifficulty = "master";
[SerializeField] private long testScore = 1500000;
[SerializeField] private string testGrade = "S";
[Header("演武场测试")]
[SerializeField] private string arenaPassword = "1234";
[SerializeField] private string joinRoomCode = "";
// ── 状态显示 ──
private string _statusText = "等待连接...";
private string _lastResponse = "";
private Vector2 _scrollPos;
// 把这个方法名从 OnEnable 改成 Start
private void Start()
{
// 订阅 NetworkManager 的事件
var nm = NetworkManager.Instance;
if (nm == null)
{
Debug.LogError("[NetworkTestUI] NetworkManager.Instance 为空!确认场景中有 NetworkManager 物体");
return;
}
nm.OnStateChanged += OnStateChanged;
nm.OnHandshakeResult += OnHandshake;
nm.OnProfileLoaded += OnProfile;
nm.OnLeaderboardLoaded += OnLeaderboard;
nm.OnArenaCreated += OnArenaCreated;
nm.OnArenaJoined += OnArenaJoined;
nm.OnArenaResult += OnArenaResult;
Debug.Log("[NetworkTestUI] 事件订阅完成");
}
private void OnDisable()
{
var nm = NetworkManager.Instance;
if (nm == null) return;
nm.OnStateChanged -= OnStateChanged;
nm.OnHandshakeResult -= OnHandshake;
nm.OnProfileLoaded -= OnProfile;
nm.OnLeaderboardLoaded -= OnLeaderboard;
nm.OnArenaCreated -= OnArenaCreated;
nm.OnArenaJoined -= OnArenaJoined;
nm.OnArenaResult -= OnArenaResult;
}
// ═══════════════════════════════════════════
// OnGUI —— 测试面板
// ═══════════════════════════════════════════
private void OnGUI()
{
GUILayout.BeginArea(new Rect(10, 10, 420, Screen.height - 20));
// ── 状态栏 ──
GUILayout.Label($"<b>连接状态:</b> {_statusText}", CreateRichStyle());
GUILayout.Space(5);
// ── 基础连接流程 ──
GUILayout.Label("<b>─── 基础连接流程 ───</b>", CreateRichStyle());
if (GUILayout.Button("1. 重新连接服务器", GUILayout.Height(35)))
{
_lastResponse = "正在连接...";
NetworkManager.Instance.Connect();
}
if (GUILayout.Button("2. 加载个人数据 (GET_PROFILE)", GUILayout.Height(35)))
{
_ = LoadProfileAsync();
}
if (GUILayout.Button("3. 加载排行榜 (GET_LEADERBOARD)", GUILayout.Height(35)))
{
_ = LoadLeaderboardAsync();
}
GUILayout.Space(10);
// ── 成绩推送 ──
GUILayout.Label("<b>─── 成绩推送 ───</b>", CreateRichStyle());
GUILayout.BeginHorizontal();
GUILayout.Label("歌曲ID:", GUILayout.Width(60));
testSongId = GUILayout.TextField(testSongId);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("难度:", GUILayout.Width(60));
testDifficulty = GUILayout.TextField(testDifficulty);
GUILayout.Label("分数:", GUILayout.Width(40));
string scoreStr = GUILayout.TextField(testScore.ToString());
if (long.TryParse(scoreStr, out long parsed)) testScore = parsed;
GUILayout.Label("评级:", GUILayout.Width(40));
testGrade = GUILayout.TextField(testGrade, GUILayout.Width(40));
GUILayout.EndHorizontal();
if (GUILayout.Button("推送成绩 (PUSH_SCORE)", GUILayout.Height(35)))
{
_ = PushScoreAsync();
}
GUILayout.Space(10);
// ── 演武场 ──
GUILayout.Label("<b>─── 演武场 ───</b>", CreateRichStyle());
if (GUILayout.Button("创建演武房间 (ARENA_CREATE)", GUILayout.Height(35)))
{
_ = CreateArenaAsync();
}
GUILayout.BeginHorizontal();
GUILayout.Label("房间号:", GUILayout.Width(60));
joinRoomCode = GUILayout.TextField(joinRoomCode);
if (GUILayout.Button("加入房间", GUILayout.Width(80), GUILayout.Height(25)))
{
_ = JoinArenaAsync();
}
GUILayout.EndHorizontal();
if (GUILayout.Button("开始比赛 (ARENA_START)", GUILayout.Height(30)))
{
_ = NetworkManager.Instance.StartArenaGame();
_lastResponse = "已发送开始比赛指令";
}
if (GUILayout.Button("提交演武成绩 (ARENA_SUBMIT)", GUILayout.Height(30)))
{
_ = SubmitArenaAsync();
}
GUILayout.Space(10);
// ── 工具 ──
GUILayout.Label("<b>─── 工具 ───</b>", CreateRichStyle());
if (GUILayout.Button("发送心跳 (HEARTBEAT)", GUILayout.Height(30)))
{
_ = NetworkManager.Instance.SendHeartbeat();
_lastResponse = "已发送心跳";
}
if (GUILayout.Button("断开连接", GUILayout.Height(30)))
{
NetworkManager.Instance.Disconnect();
_lastResponse = "已断开连接";
}
GUILayout.Space(10);
// ── 日志区 ──
GUILayout.Label("<b>─── 服务器响应 ───</b>", CreateRichStyle());
_scrollPos = GUILayout.BeginScrollView(_scrollPos, GUILayout.Height(200));
GUILayout.Label(_lastResponse);
GUILayout.EndScrollView();
GUILayout.EndArea();
}
// ═══════════════════════════════════════════
// 异步操作方法
// ═══════════════════════════════════════════
private async Awaitable LoadProfileAsync()
{
try
{
_lastResponse = "正在加载个人数据...";
await NetworkManager.Instance.LoadProfile();
}
catch (Exception ex)
{
_lastResponse = $"加载个人数据失败: {ex.Message}";
}
}
private async Awaitable LoadLeaderboardAsync()
{
try
{
_lastResponse = "正在加载排行榜...";
await NetworkManager.Instance.LoadLeaderboard(testSongId);
}
catch (Exception ex)
{
_lastResponse = $"加载排行榜失败: {ex.Message}";
}
}
private async Awaitable PushScoreAsync()
{
try
{
_lastResponse = "正在推送成绩...";
await NetworkManager.Instance.PushScore(testSongId, testDifficulty, testScore, testGrade);
_lastResponse = $"成绩推送成功! {testSongId} | {testDifficulty} | {testScore} | {testGrade}";
}
catch (Exception ex)
{
_lastResponse = $"推送成绩失败: {ex.Message}";
}
}
private async Awaitable CreateArenaAsync()
{
try
{
_lastResponse = "正在创建演武房间...";
var result = await NetworkManager.Instance.CreateArenaRoom(
testSongId, testDifficulty, arenaPassword);
if (result.success)
{
joinRoomCode = result.room_code;
_lastResponse = $"房间创建成功!房间号: {result.room_code}";
}
else
{
_lastResponse = $"创建房间失败: {result.error_code}";
}
}
catch (Exception ex)
{
_lastResponse = $"创建房间异常: {ex.Message}";
}
}
private async Awaitable JoinArenaAsync()
{
try
{
_lastResponse = $"正在加入房间 {joinRoomCode}...";
var result = await NetworkManager.Instance.JoinArenaRoom(joinRoomCode, arenaPassword);
_lastResponse = result.success
? $"加入成功!当前人数: {result.participant_count}"
: $"加入失败: {result.error_code}";
}
catch (Exception ex)
{
_lastResponse = $"加入房间异常: {ex.Message}";
}
}
private async Awaitable SubmitArenaAsync()
{
try
{
_lastResponse = "正在提交演武成绩...";
await NetworkManager.Instance.SubmitArenaScore(joinRoomCode, testScore, testGrade);
_lastResponse = "演武成绩已提交,等待结算...";
}
catch (Exception ex)
{
_lastResponse = $"提交演武成绩失败: {ex.Message}";
}
}
// ═══════════════════════════════════════════
// 事件回调
// ═══════════════════════════════════════════
private void OnStateChanged(ConnectionState state)
{
_statusText = state.ToString();
Debug.Log($"[NetworkTestUI] 状态变更: {state}");
}
private void OnHandshake(HandshakeResponse resp)
{
_lastResponse = resp.success
? $"✅ 握手成功!SteamID: {resp.steam_id}"
: $"❌ 握手失败: {resp.error_code} - {resp.message}";
Debug.Log($"[NetworkTestUI] 握手结果: {_lastResponse}");
}
private void OnProfile(ProfileData profile)
{
_lastResponse = $"✅ 个人数据已加载\n"
+ $" SteamID: {profile.steam_id}\n"
+ $" 昵称: {profile.display_name}\n"
+ $" 等级: {profile.player_level}\n"
+ $" 排行榜: {(profile.leaderboard_opt_out ? "" : "")}";
Debug.Log($"[NetworkTestUI] {_lastResponse}");
}
private void OnLeaderboard(LeaderboardData data)
{
_lastResponse = $"✅ 排行榜已加载 | 歌曲: {data.song_id} | 周期: {data.cycle_id}\n";
if (data.rankings == null || data.rankings.Count == 0)
{
_lastResponse += " (排行榜为空)";
}
else
{
foreach (var entry in data.rankings)
{
_lastResponse += $" #{entry.rank} {entry.display_name} - {entry.total_score} [{entry.grade}]\n";
}
}
Debug.Log($"[NetworkTestUI] {_lastResponse}");
}
private void OnArenaCreated(ArenaCreateResponse resp)
{
Debug.Log($"[NetworkTestUI] 演武房间创建: success={resp.success} code={resp.room_code}");
}
private void OnArenaJoined(ArenaJoinResponse resp)
{
Debug.Log($"[NetworkTestUI] 加入演武: success={resp.success}");
}
private void OnArenaResult(ArenaResult result)
{
_lastResponse = $"🏆 演武结果 | 房间: {result.room_code}\n";
if (result.rankings != null)
{
foreach (var entry in result.rankings)
{
_lastResponse += $" #{entry.rank} {entry.steam_id} - {entry.total_score} [{entry.grade}]\n";
}
}
Debug.Log($"[NetworkTestUI] {_lastResponse}");
}
// ── 工具 ──
private GUIStyle CreateRichStyle()
{
var style = new GUIStyle(GUI.skin.label);
style.richText = true;
style.fontSize = 14;
return style;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7c09f6b5355d38644b45e2721d8d59ac
@@ -0,0 +1,208 @@
using System;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
namespace GameServer.Client
{
public class WebSocketClient : IDisposable
{
public event Action OnConnected;
public event Action<string> OnMessageReceived;
public event Action<int, string> OnClosed;
public event Action<Exception> OnError;
private ClientWebSocket _ws;
private CancellationTokenSource _cts;
private readonly int _receiveBufferSize;
private readonly int _connectTimeoutMs;
private readonly object _closeLock = new object();
public bool IsConnected
{
get
{
lock (_closeLock)
return _ws != null && _ws.State == WebSocketState.Open;
}
}
public WebSocketClient(int receiveBufferSize = 65536, int connectTimeoutMs = 15000)
{
_receiveBufferSize = receiveBufferSize;
_connectTimeoutMs = connectTimeoutMs;
}
public async Task Connect(Uri uri)
{
_ws = new ClientWebSocket();
_cts = new CancellationTokenSource();
// 跳过系统代理
_ws.Options.Proxy = GlobalProxySelection.GetEmptyWebProxy();
_ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(20);
try
{
Debug.Log($"[WebSocketClient] 正在连接 {uri} (超时={_connectTimeoutMs}ms)...");
using (var connectCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token))
{
connectCts.CancelAfter(_connectTimeoutMs);
try
{
await _ws.ConnectAsync(uri, connectCts.Token);
}
catch (OperationCanceledException)
when (connectCts.IsCancellationRequested && !_cts.IsCancellationRequested)
{
throw new TimeoutException(
$"连接超时 ({_connectTimeoutMs}ms),服务器 {uri} 无响应");
}
}
if (_ws.State == WebSocketState.Open)
{
Debug.Log($"[WebSocketClient] ✅ 连接成功!状态={_ws.State}");
OnConnected?.Invoke();
_ = ReceiveLoop();
}
else
{
string msg = $"连接后状态异常: {_ws.State}";
Debug.LogWarning($"[WebSocketClient] {msg}");
OnError?.Invoke(new Exception(msg));
}
}
catch (Exception ex)
{
// ★ 关键:打印所有层级的 InnerException,找到真正原因 ★
Debug.LogError($"[WebSocketClient] ========== 连接失败详细信息 ==========");
Debug.LogError($"[WebSocketClient] 目标地址: {uri}");
Debug.LogError($"[WebSocketClient] 异常类型: {ex.GetType().FullName}");
Debug.LogError($"[WebSocketClient] 异常信息: {ex.Message}");
Exception inner = ex.InnerException;
int depth = 1;
while (inner != null)
{
Debug.LogError($"[WebSocketClient] InnerException[{depth}] 类型: {inner.GetType().FullName}");
Debug.LogError($"[WebSocketClient] InnerException[{depth}] 信息: {inner.Message}");
inner = inner.InnerException;
depth++;
}
Debug.LogError($"[WebSocketClient] ==========================================");
OnError?.Invoke(ex);
throw;
}
}
public async Task Send(string message)
{
ClientWebSocket ws;
lock (_closeLock) { ws = _ws; }
if (ws == null || ws.State != WebSocketState.Open)
{
Debug.LogWarning("[WebSocketClient] Send called while not connected.");
return;
}
byte[] bytes = Encoding.UTF8.GetBytes(message);
var segment = new ArraySegment<byte>(bytes);
try
{
await ws.SendAsync(segment, WebSocketMessageType.Text,
endOfMessage: true, cancellationToken: _cts.Token);
}
catch (Exception ex)
{
OnError?.Invoke(ex);
}
}
private async Task ReceiveLoop()
{
var buffer = new byte[_receiveBufferSize];
var sb = new StringBuilder();
try
{
while (_ws.State == WebSocketState.Open && !_cts.IsCancellationRequested)
{
sb.Clear();
WebSocketReceiveResult result;
do
{
var segment = new ArraySegment<byte>(buffer);
result = await _ws.ReceiveAsync(segment, _cts.Token);
if (result.MessageType == WebSocketMessageType.Close)
{
await _ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure,
string.Empty, CancellationToken.None);
int code = result.CloseStatus.HasValue
? (int)result.CloseStatus.Value : 1000;
OnClosed?.Invoke(code, result.CloseStatusDescription ?? string.Empty);
return;
}
sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
}
while (!result.EndOfMessage);
string text = sb.ToString();
if (!string.IsNullOrEmpty(text))
OnMessageReceived?.Invoke(text);
}
}
catch (OperationCanceledException) { }
catch (WebSocketException ex)
when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
Debug.LogWarning("[WebSocketClient] 服务器断开了连接");
OnClosed?.Invoke(1006, "Connection closed prematurely");
}
catch (Exception ex)
{
OnError?.Invoke(ex);
}
}
public async Task Close()
{
ClientWebSocket wsToClose;
lock (_closeLock)
{
wsToClose = _ws;
_ws = null;
_cts?.Cancel();
}
if (wsToClose == null) return;
try
{
if (wsToClose.State == WebSocketState.Open)
await wsToClose.CloseAsync(WebSocketCloseStatus.NormalClosure,
"Client closing", CancellationToken.None);
}
catch (Exception ex)
{
Debug.LogWarning("[WebSocketClient] Error during close: " + ex.Message);
}
finally
{
wsToClose.Dispose();
}
}
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
_ws?.Dispose();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 28ff8f6aee1e54c4ca44e10a7a4af0f3
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c19bb031396ae0b4a825cabcd9135849
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
using UnityEngine;
public class rankingList : MonoBehaviour
{
[Header("prefab")]
public GameObject rkl_prefab;
public Transform rkl_parent;
private GameObject currentInstance;
public void Open(string songId, string songName)
{
if (rkl_prefab == null || rkl_parent == null)
{
Debug.LogWarning("[rankingList] Missing prefab or parent reference.");
return;
}
if (currentInstance == null)
{
currentInstance = Instantiate(rkl_prefab, rkl_parent);
}
var listPrefab = currentInstance.GetComponent<rankingListPrefab>();
if (listPrefab == null)
{
Debug.LogWarning("[rankingList] rankingListPrefab component missing.");
return;
}
listPrefab.Bind(songId, songName, this);
}
public void NotifyClosed(rankingListPrefab prefab)
{
if (prefab == null)
{
return;
}
if (currentInstance == prefab.gameObject)
{
currentInstance = null;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 83b124015738c654aa6d26ea238726fd
@@ -0,0 +1,313 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using GameServer.Client;
using UnityEngine;
using UnityEngine.UI;
public class rankingListPrefab : MonoBehaviour
{
private const string RoomRankingServerText = "根据房间成绩动态排名,所在服务端:雅莉梦璃高新产业园";
[Header("shutbutton")]
public Button shutbutton;
[Header("texts")]
public Text titleText;
public Text yourPositionText;
public Text versionandServer;
public Text rankingListType;
[Header("objs")]
public GameObject sv;
public Text emptyText;
[Header("RK Prefabs")]
public GameObject rkPrefab;
public GameObject rkParent;
[Header("sprite List")]
public Sprite[] btmSprites;
private string currentSongId;
private string currentSongName;
private rankingList owner;
private bool isLoading;
private bool isRoomRanking;
private LeaderboardData roomRankingData;
private string roomRankingLocalSteamId;
private void Awake()
{
if (shutbutton != null)
{
shutbutton.onClick.AddListener(HandleCloseClicked);
}
}
private void OnDestroy()
{
if (shutbutton != null)
{
shutbutton.onClick.RemoveListener(HandleCloseClicked);
}
}
public void Bind(string songId, string songName, rankingList sourceOwner)
{
currentSongId = songId;
currentSongName = songName;
owner = sourceOwner;
isRoomRanking = false;
roomRankingData = null;
roomRankingLocalSteamId = null;
if (titleText != null)
{
titleText.text = string.IsNullOrWhiteSpace(currentSongName) ? currentSongId : currentSongName;
}
ApplyListMeta(null);
if (!isLoading)
{
_ = RefreshAsync();
}
}
public void BindRoomRanking(string songId, string songName, LeaderboardData data, string localSteamId)
{
currentSongId = songId;
currentSongName = songName;
owner = null;
isRoomRanking = true;
roomRankingData = data;
roomRankingLocalSteamId = localSteamId;
if (titleText != null)
{
titleText.text = string.IsNullOrWhiteSpace(currentSongName) ? currentSongId : currentSongName;
}
ApplyListMeta(roomRankingData);
ClearRows();
if (roomRankingData == null || roomRankingData.rankings == null || roomRankingData.rankings.Count == 0)
{
SetEmptyState(true, "房间排行榜暂无数据");
return;
}
RenderRows(roomRankingData);
}
private async Task RefreshAsync()
{
isLoading = true;
ClearRows();
try
{
ApplyListMeta(null);
NetworkManager nm = NetworkManager.Instance;
if (nm == null)
{
SetEmptyState(true, "排行榜服务不可用");
return;
}
LeaderboardData data = null;
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
if (cache != null && cache.TryGetCached(currentSongId, out LeaderboardData cached))
{
data = cached;
}
else
{
SetEmptyState(false, "正在拉取排行榜...");
data = await nm.GetLeaderboard(currentSongId);
}
if (data == null || data.rankings == null || data.rankings.Count == 0)
{
SetEmptyState(true, "排行榜暂无数据");
return;
}
RenderRows(data);
}
catch (Exception ex)
{
Debug.LogWarning($"[rankingListPrefab] Failed to load leaderboard: {ex.Message}");
SetEmptyState(true, "拉取排行榜失败");
}
finally
{
isLoading = false;
}
}
private void RenderRows(LeaderboardData data)
{
SetEmptyState(false, string.Empty);
ApplyListMeta(data);
for (int i = 0; i < data.rankings.Count; i++)
{
GameObject rowObj = Instantiate(rkPrefab, rkParent.transform);
rkPrefab row = rowObj.GetComponent<rkPrefab>();
if (row == null)
{
continue;
}
LeaderboardEntry entry = data.rankings[i];
string formattedSubmitTime = FormatSubmitTime(entry);
if (isRoomRanking)
{
row.BindRoomRanking(entry, ResolveRankSprite(entry.rank), formattedSubmitTime);
}
else
{
row.Bind(entry, ResolveRankSprite(entry.rank), formattedSubmitTime);
}
}
}
private void HandleCloseClicked()
{
owner?.NotifyClosed(this);
Destroy(gameObject);
}
private void ClearRows()
{
if (rkParent == null)
{
return;
}
for (int i = rkParent.transform.childCount - 1; i >= 0; i--)
{
Destroy(rkParent.transform.GetChild(i).gameObject);
}
}
private void SetEmptyState(bool showEmpty, string message)
{
if (sv != null)
{
sv.SetActive(!showEmpty);
}
if (emptyText != null)
{
emptyText.gameObject.SetActive(showEmpty);
if (!string.IsNullOrEmpty(message))
{
emptyText.text = message;
}
}
}
private Sprite ResolveRankSprite(int rank)
{
if (btmSprites == null || btmSprites.Length == 0)
{
return null;
}
if (rank <= 1 && btmSprites.Length > 3)
{
return btmSprites[3];
}
if (rank == 2 && btmSprites.Length > 2)
{
return btmSprites[2];
}
if (rank == 3 && btmSprites.Length > 1)
{
return btmSprites[1];
}
return btmSprites[0];
}
private void ApplyListMeta(LeaderboardData data)
{
if (rankingListType != null)
{
rankingListType.text = isRoomRanking ? "房间排行榜" : "世界排行榜";
}
if (versionandServer != null)
{
versionandServer.text = isRoomRanking ? RoomRankingServerText : string.Empty;
}
if (yourPositionText == null)
{
return;
}
if (!isRoomRanking)
{
yourPositionText.text = string.Empty;
return;
}
if (data == null || data.rankings == null || data.rankings.Count == 0 || string.IsNullOrWhiteSpace(roomRankingLocalSteamId))
{
yourPositionText.text = "您的得分为--,目前排名第<color=green>--</color>位";
return;
}
LeaderboardEntry localEntry = null;
for (int i = 0; i < data.rankings.Count; i++)
{
LeaderboardEntry entry = data.rankings[i];
if (entry != null && string.Equals(entry.steam_id, roomRankingLocalSteamId, StringComparison.Ordinal))
{
localEntry = entry;
break;
}
}
if (localEntry == null)
{
yourPositionText.text = "您的得分为--,目前排名第<color=green>--</color>位";
return;
}
yourPositionText.text =
$"您的得分为{localEntry.total_score},目前排名第<color=green>{localEntry.rank}</color>位";
}
private static string FormatSubmitTime(LeaderboardEntry entry)
{
if (entry == null)
{
return string.Empty;
}
if (entry.achieved_at_unix > 0)
{
long unix = entry.achieved_at_unix;
if (unix > 9999999999L)
{
return DateTimeOffset.FromUnixTimeMilliseconds(unix).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
}
return DateTimeOffset.FromUnixTimeSeconds(unix).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
}
if (DateTimeOffset.TryParse(entry.achieved_at, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset parsed))
{
return parsed.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
}
return entry.achieved_at ?? string.Empty;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c406acddc1ad54a4c8d456483456a7c4
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5fbb630ba1ffa8349874f453d1c120dd
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,120 @@
using GameServer.Client;
using UnityEngine;
using UnityEngine.UI;
public class rkPrefab : MonoBehaviour
{
[SerializeField] private Image buttom_image;
[SerializeField] private Text listID;
[SerializeField] private Image userProfile;
[SerializeField] private Text this_username;
[SerializeField] private Text this_submitTime;
[SerializeField] private Text this_totalScore;
[SerializeField] private Text this_keepOnTime;
public void Bind(LeaderboardEntry entry, Sprite backgroundSprite, string formattedSubmitTime)
{
BindInternal(entry, backgroundSprite, formattedSubmitTime, false);
}
public void BindRoomRanking(LeaderboardEntry entry, Sprite backgroundSprite, string formattedSubmitTime)
{
BindInternal(entry, backgroundSprite, formattedSubmitTime, true);
}
private void BindInternal(LeaderboardEntry entry, Sprite backgroundSprite, string formattedSubmitTime, bool isRoomRanking)
{
if (entry == null)
{
return;
}
if (buttom_image != null)
{
buttom_image.sprite = backgroundSprite;
}
if (listID != null)
{
listID.text = entry.rank.ToString();
}
if (this_username != null)
{
string displayName = string.IsNullOrWhiteSpace(entry.display_name) ? entry.steam_id : entry.display_name;
this_username.text = TruncateToFit(this_username, displayName);
}
if (this_submitTime != null)
{
this_submitTime.text = formattedSubmitTime;
}
if (this_totalScore != null)
{
this_totalScore.text = entry.total_score.ToString();
}
if (this_keepOnTime != null)
{
this_keepOnTime.text = isRoomRanking
? (!string.IsNullOrWhiteSpace(entry.room_status_text) ? entry.room_status_text : "-")
: entry.keep_on_time.ToString();
}
if (userProfile != null)
{
userProfile.enabled = false;
}
}
private static string TruncateToFit(Text targetText, string source)
{
if (targetText == null || string.IsNullOrEmpty(source))
{
return source ?? string.Empty;
}
RectTransform rect = targetText.rectTransform;
if (rect == null || rect.rect.width <= 0f)
{
return source;
}
float maxWidth = rect.rect.width;
if (GetPreferredWidth(targetText, source) <= maxWidth)
{
return source;
}
const string ellipsis = "...";
if (GetPreferredWidth(targetText, ellipsis) > maxWidth)
{
return string.Empty;
}
int low = 0;
int high = source.Length;
while (low < high)
{
int mid = (low + high + 1) / 2;
string candidate = source.Substring(0, mid) + ellipsis;
if (GetPreferredWidth(targetText, candidate) <= maxWidth)
{
low = mid;
}
else
{
high = mid - 1;
}
}
return low <= 0 ? ellipsis : source.Substring(0, low) + ellipsis;
}
private static float GetPreferredWidth(Text targetText, string content)
{
TextGenerationSettings settings = targetText.GetGenerationSettings(Vector2.zero);
return targetText.cachedTextGeneratorForLayout.GetPreferredWidth(content, settings) / targetText.pixelsPerUnit;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a68b021f8f1d43e44b0e699730f4ae24
@@ -0,0 +1,748 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1202919405991223948
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4976417934945737035}
- component: {fileID: 9185173812383798283}
- component: {fileID: 8404339670641418553}
m_Layer: 0
m_Name: pf
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4976417934945737035
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1202919405991223948}
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: 7842164961978205412}
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: 25, y: 25}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9185173812383798283
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1202919405991223948}
m_CullTransparentMesh: 1
--- !u!114 &8404339670641418553
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1202919405991223948}
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: 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: 0}
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 &3848673281551182841
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3188298257810125285}
- component: {fileID: 8831211814588387856}
- component: {fileID: 6687610922055607062}
m_Layer: 0
m_Name: darkness_
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3188298257810125285
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3848673281551182841}
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: 3437894634699126015}
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: 0}
m_SizeDelta: {x: 1000, y: 40}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8831211814588387856
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3848673281551182841}
m_CullTransparentMesh: 1
--- !u!114 &6687610922055607062
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3848673281551182841}
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.19607843}
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: 28c51f93b6260224c867f45f411a0e85, 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: 4
--- !u!1 &4172849104164265243
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4493109035153815242}
- component: {fileID: 6334112776472291744}
- component: {fileID: 6017697758774510118}
m_Layer: 0
m_Name: listID
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4493109035153815242
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4172849104164265243}
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: 7842164961978205412}
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: 41.976, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6334112776472291744
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4172849104164265243}
m_CullTransparentMesh: 1
--- !u!114 &6017697758774510118
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4172849104164265243}
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: 1, g: 1, b: 1, 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: dfa10de3e2846524089b5073175cd151, type: 3}
m_FontSize: 18
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: 100
--- !u!1 &4305534998678093432
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1293959345365494561}
- component: {fileID: 548801232753630885}
- component: {fileID: 8887470777961154685}
m_Layer: 0
m_Name: score
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1293959345365494561
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4305534998678093432}
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: 7842164961978205412}
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: 241.959, y: 40}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &548801232753630885
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4305534998678093432}
m_CullTransparentMesh: 1
--- !u!114 &8887470777961154685
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4305534998678093432}
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: 1, g: 1, b: 1, 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: 8b8373b0af11dca46b89be60dbe469f8, type: 3}
m_FontSize: 27
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 40
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: 2000000
--- !u!1 &4439332187331332072
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6871938979269133948}
- component: {fileID: 5903337530726148902}
m_Layer: 0
m_Name: rkPrefab
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6871938979269133948
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4439332187331332072}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 18.954}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3437894634699126015}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 541.5, y: -25}
m_SizeDelta: {x: 1000, y: 40}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &5903337530726148902
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4439332187331332072}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a68b021f8f1d43e44b0e699730f4ae24, type: 3}
m_Name:
m_EditorClassIdentifier:
buttom_image: {fileID: 7594135340296542596}
listID: {fileID: 6017697758774510118}
userProfile: {fileID: 8404339670641418553}
this_username: {fileID: 2645129040745221173}
this_submitTime: {fileID: 1919077852137188376}
this_totalScore: {fileID: 8887470777961154685}
this_keepOnTime: {fileID: 540623326675905706}
--- !u!1 &4457460548878136444
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5568417350009506155}
- component: {fileID: 7014127935405271411}
- component: {fileID: 2645129040745221173}
m_Layer: 0
m_Name: username
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &5568417350009506155
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4457460548878136444}
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: 7842164961978205412}
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: 318.323, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7014127935405271411
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4457460548878136444}
m_CullTransparentMesh: 1
--- !u!114 &2645129040745221173
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4457460548878136444}
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: 1, g: 1, b: 1, 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: dfa10de3e2846524089b5073175cd151, type: 3}
m_FontSize: 18
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 40
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: "\u73A9\u5BB6\u7684\u540D\u5B57\u662F12123"
--- !u!1 &4773289945334786799
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6823984430671628200}
- component: {fileID: 8464744113657138046}
- component: {fileID: 1919077852137188376}
m_Layer: 0
m_Name: reachTime
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6823984430671628200
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4773289945334786799}
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: 7842164961978205412}
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: 229.827, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8464744113657138046
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4773289945334786799}
m_CullTransparentMesh: 1
--- !u!114 &1919077852137188376
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4773289945334786799}
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: 1, g: 1, b: 1, 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: dfa10de3e2846524089b5073175cd151, type: 3}
m_FontSize: 18
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 40
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: 1970-01-01 20:00:00
--- !u!1 &5987634680367655613
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3437894634699126015}
- component: {fileID: 986241107719408709}
- component: {fileID: 7594135340296542596}
m_Layer: 0
m_Name: btm
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3437894634699126015
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5987634680367655613}
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: 3188298257810125285}
- {fileID: 7842164961978205412}
m_Father: {fileID: 6871938979269133948}
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: 0}
m_SizeDelta: {x: 1000, y: 40}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &986241107719408709
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5987634680367655613}
m_CullTransparentMesh: 1
--- !u!114 &7594135340296542596
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5987634680367655613}
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: 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: 28c51f93b6260224c867f45f411a0e85, 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: 4
--- !u!1 &6495147886401793472
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5377276848171797507}
- component: {fileID: 7773371341055247645}
- component: {fileID: 540623326675905706}
m_Layer: 0
m_Name: howmany
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &5377276848171797507
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6495147886401793472}
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: 7842164961978205412}
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: 127.685, y: 35}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7773371341055247645
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6495147886401793472}
m_CullTransparentMesh: 1
--- !u!114 &540623326675905706
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6495147886401793472}
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: 1, g: 1, b: 1, 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: 8b8373b0af11dca46b89be60dbe469f8, type: 3}
m_FontSize: 24
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 40
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: 2000000
--- !u!1 &8179976138021131847
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7842164961978205412}
- component: {fileID: 7314414461183110699}
m_Layer: 0
m_Name: hori
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7842164961978205412
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8179976138021131847}
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: 4493109035153815242}
- {fileID: 4976417934945737035}
- {fileID: 5568417350009506155}
- {fileID: 6823984430671628200}
- {fileID: 1293959345365494561}
- {fileID: 5377276848171797507}
m_Father: {fileID: 3437894634699126015}
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: -44.5, y: 0}
m_SizeDelta: {x: 888.204, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &7314414461183110699
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8179976138021131847}
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
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5d56483f68516d94e9277bbac63f8525
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,478 @@
using System;
using System.Collections;
using System.Collections.Generic;
using EasyChart;
using EasyChart.UGUI;
using UnityEngine;
using UnityEngine.UI;
public class settleChart : MonoBehaviour
{
[Header("Chart")]
[SerializeField] private ChartProfile sourceProfile;
[SerializeField] private UGUIChartBridge chartBridge;
[SerializeField] private RectTransform chartHost;
[Header("Paging")]
[SerializeField] private Button previousPageButton;
[SerializeField] private Button nextPageButton;
[SerializeField, Min(1)] private int notesPerPage = 100;
[Header("Build")]
[SerializeField, Min(1)] private int buildBatchSize = 64;
[SerializeField] private bool includeMissAsZero = true;
[SerializeField] private bool useHoldEndOffsetAsFallback = true;
private readonly List<float> allOffsets = new List<float>(1024);
private readonly List<string> allLabels = new List<string>(1024);
private ChartProfile runtimeProfile;
private Coroutine buildRoutine;
private int currentPageIndex;
private bool buttonsHooked;
private void Start()
{
HookButtons();
settlementController.OnSettlementCompleted += HandleSettlementCompleted;
}
private void OnEnable()
{
HookButtons();
}
private void OnDestroy()
{
settlementController.OnSettlementCompleted -= HandleSettlementCompleted;
UnhookButtons();
if (buildRoutine != null)
{
StopCoroutine(buildRoutine);
buildRoutine = null;
}
if (runtimeProfile != null)
{
Destroy(runtimeProfile);
runtimeProfile = null;
}
}
private void HandleSettlementCompleted()
{
if (buildRoutine != null)
{
StopCoroutine(buildRoutine);
}
buildRoutine = StartCoroutine(BuildChartAsync());
}
private IEnumerator BuildChartAsync()
{
ResolveReferences();
EnsureChartBridge();
if (chartBridge == null || sourceProfile == null)
{
yield break;
}
allOffsets.Clear();
allLabels.Clear();
currentPageIndex = 0;
UpdateButtonState();
yield return null;
BeatmapManager beatmapManager = BeatmapManager.Instance != null
? BeatmapManager.Instance
: FindAnyObjectByType<BeatmapManager>();
NoteData[] notes = beatmapManager != null && beatmapManager.beatmap != null
? beatmapManager.beatmap.notes
: null;
if (notes == null || notes.Length == 0)
{
ApplyPage(0);
buildRoutine = null;
yield break;
}
int visibleIndex = 0;
for (int i = 0; i < notes.Length; i++)
{
NoteData note = notes[i];
if (note == null)
{
continue;
}
if (!TryExtractSignedOffset(note, out float signedOffset))
{
continue;
}
visibleIndex++;
allOffsets.Add(signedOffset);
allLabels.Add(visibleIndex.ToString());
if ((i + 1) % buildBatchSize == 0)
{
yield return null;
}
}
ApplyPage(0);
buildRoutine = null;
}
private bool TryExtractSignedOffset(NoteData note, out float signedOffset)
{
if (!float.IsNaN(note.judgeOffsetMs))
{
signedOffset = note.judgeOffsetMs;
return true;
}
if (useHoldEndOffsetAsFallback && !float.IsNaN(note.judgeOffsetMsEnd))
{
signedOffset = note.judgeOffsetMsEnd;
return true;
}
bool isMiss = string.Equals(note.judgeResult, "Miss", StringComparison.OrdinalIgnoreCase)
|| string.Equals(note.judgeResultEnd, "Miss", StringComparison.OrdinalIgnoreCase);
if (includeMissAsZero && isMiss)
{
signedOffset = 0f;
return true;
}
signedOffset = 0f;
return false;
}
public void ShowPreviousPage()
{
if (currentPageIndex <= 0)
{
return;
}
ApplyPage(currentPageIndex - 1);
}
public void ShowNextPage()
{
int totalPages = GetTotalPages();
if (currentPageIndex >= totalPages - 1)
{
return;
}
ApplyPage(currentPageIndex + 1);
}
private void ApplyPage(int pageIndex)
{
EnsureChartBridge();
if (chartBridge == null || sourceProfile == null)
{
UpdateButtonState();
return;
}
EnsureRuntimeProfile();
if (runtimeProfile == null)
{
UpdateButtonState();
return;
}
int totalPages = Mathf.Max(1, GetTotalPages());
currentPageIndex = Mathf.Clamp(pageIndex, 0, totalPages - 1);
AxisConfig xAxis = FindAxis(runtimeProfile, runtimeProfile.xAxisId);
AxisConfig yAxis = FindAxis(runtimeProfile, runtimeProfile.yAxisId);
Serie targetSeries = FindPrimaryBarSeries(runtimeProfile);
if (xAxis == null || yAxis == null || targetSeries == null)
{
UpdateButtonState();
return;
}
int start = currentPageIndex * notesPerPage;
int remaining = Mathf.Max(0, allOffsets.Count - start);
int count = Mathf.Min(notesPerPage, remaining);
List<string> labels = new List<string>(Mathf.Max(1, count));
List<SeriesData> seriesData = new List<SeriesData>(Mathf.Max(1, count));
float maxAbs = 1f;
if (count <= 0)
{
labels.Add("-");
seriesData.Add(new SeriesData
{
id = Guid.NewGuid().ToString("N"),
x = 0f,
value = 0f,
name = "-"
});
}
else
{
for (int i = 0; i < count; i++)
{
int globalIndex = start + i;
float value = allOffsets[globalIndex];
maxAbs = Mathf.Max(maxAbs, Mathf.Abs(value));
string label = allLabels[globalIndex];
labels.Add(label);
seriesData.Add(new SeriesData
{
id = Guid.NewGuid().ToString("N"),
x = i,
value = value,
name = label
});
}
}
float roundedRange = Mathf.Max(1f, Mathf.Ceil(maxAbs / 5f) * 5f);
xAxis.axisType = AxisType.Category;
xAxis.labels = labels;
xAxis.showLabels = true;
xAxis.labelPlacement = CategoryLabelPlacement.Tick;
yAxis.axisType = AxisType.Value;
yAxis.autoRangeMin = false;
yAxis.autoRangeMax = false;
yAxis.autoTicks = false;
yAxis.minValue = -roundedRange;
yAxis.maxValue = roundedRange;
yAxis.splitCount = 4;
targetSeries.visible = true;
targetSeries.type = SerieType.Bar;
targetSeries.seriesData = seriesData;
targetSeries.EnsureIntegrity();
for (int i = 0; i < runtimeProfile.series.Count; i++)
{
if (!ReferenceEquals(runtimeProfile.series[i], targetSeries))
{
runtimeProfile.series[i].visible = false;
}
}
chartBridge.Profile = runtimeProfile;
chartBridge.Refresh();
UpdateButtonState();
}
private void EnsureRuntimeProfile()
{
if (runtimeProfile != null)
{
if (!ReferenceEquals(chartBridge.Profile, runtimeProfile))
{
chartBridge.Profile = runtimeProfile;
}
return;
}
runtimeProfile = Instantiate(sourceProfile);
runtimeProfile.name = sourceProfile.name + "_Runtime";
runtimeProfile.hideFlags = HideFlags.DontSave;
chartBridge.Profile = runtimeProfile;
}
private void EnsureChartBridge()
{
ResolveReferences();
if (chartBridge != null)
{
return;
}
RectTransform host = chartHost != null ? chartHost : transform as RectTransform;
if (host == null)
{
return;
}
chartBridge = host.GetComponent<UGUIChartBridge>();
if (chartBridge == null)
{
chartBridge = host.gameObject.AddComponent<UGUIChartBridge>();
}
if (chartBridge != null && sourceProfile != null)
{
chartBridge.Profile = runtimeProfile != null ? runtimeProfile : sourceProfile;
}
}
private void ResolveReferences()
{
if (chartHost == null)
{
Transform host = transform.Find("btm");
chartHost = host as RectTransform;
if (chartHost == null)
{
chartHost = GetComponentInChildren<RectTransform>(true);
}
}
if (chartBridge == null && chartHost != null)
{
chartBridge = chartHost.GetComponent<UGUIChartBridge>();
}
TryAutoBindButtons();
}
private void HookButtons()
{
if (buttonsHooked)
{
return;
}
ResolveReferences();
if (previousPageButton != null)
{
previousPageButton.onClick.AddListener(ShowPreviousPage);
}
if (nextPageButton != null)
{
nextPageButton.onClick.AddListener(ShowNextPage);
}
buttonsHooked = true;
UpdateButtonState();
}
private void UnhookButtons()
{
if (!buttonsHooked)
{
return;
}
if (previousPageButton != null)
{
previousPageButton.onClick.RemoveListener(ShowPreviousPage);
}
if (nextPageButton != null)
{
nextPageButton.onClick.RemoveListener(ShowNextPage);
}
buttonsHooked = false;
}
private void TryAutoBindButtons()
{
if (previousPageButton != null && nextPageButton != null)
{
return;
}
Button[] buttons = FindObjectsByType<Button>(FindObjectsInactive.Include, FindObjectsSortMode.None);
for (int i = 0; i < buttons.Length; i++)
{
Button button = buttons[i];
if (button == null)
{
continue;
}
string lowerName = button.name.ToLowerInvariant();
if (previousPageButton == null && (lowerName.Contains("prev") || lowerName.Contains("previous") || lowerName.Contains("lastpage") || lowerName.Contains("uppage") || lowerName.Contains("ÉÏÒ»")))
{
previousPageButton = button;
continue;
}
if (nextPageButton == null && (lowerName.Contains("next") || lowerName.Contains("downpage") || lowerName.Contains("nextpage") || lowerName.Contains("ÏÂÒ»")))
{
nextPageButton = button;
}
}
}
private void UpdateButtonState()
{
int totalPages = GetTotalPages();
if (previousPageButton != null)
{
previousPageButton.interactable = currentPageIndex > 0;
}
if (nextPageButton != null)
{
nextPageButton.interactable = currentPageIndex < totalPages - 1;
}
}
private int GetTotalPages()
{
if (allOffsets.Count <= 0)
{
return 1;
}
return Mathf.CeilToInt(allOffsets.Count / (float)Mathf.Max(1, notesPerPage));
}
private static AxisConfig FindAxis(ChartProfile profile, AxisId axisId)
{
if (profile == null || profile.axes == null)
{
return null;
}
for (int i = 0; i < profile.axes.Count; i++)
{
AxisConfig axis = profile.axes[i];
if (axis != null && axis.id == axisId)
{
return axis;
}
}
return null;
}
private static Serie FindPrimaryBarSeries(ChartProfile profile)
{
if (profile == null || profile.series == null || profile.series.Count == 0)
{
return null;
}
for (int i = 0; i < profile.series.Count; i++)
{
Serie serie = profile.series[i];
if (serie != null && serie.type == SerieType.Bar)
{
return serie;
}
}
return profile.series[0];
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4c16e2bb12480ef45914dd26a660c8e8
@@ -7,11 +7,15 @@ using UnityEngine.SceneManagement;
using System.Collections;
using UnityEngine.Audio;
using DG.Tweening;
using GameServer.Client;
public class settlementController : MonoBehaviour
{
public static event Action OnSettlementCompleted;
[Header("ranking List")]
public rankingList rl;
[Header("Inspector")]
[SerializeField] private BeatmapManager bmm;
[SerializeField] private ScoreManager sm;
@@ -154,6 +158,7 @@ public class settlementController : MonoBehaviour
private bool hasCachedIntroMvpBaseLocalPos;
private bool settlementHistoryRecorded;
private bool settlementHeroStatsRecorded;
private bool roomScoreSubmitTriggered;
private void Awake()
{
@@ -168,6 +173,11 @@ public class settlementController : MonoBehaviour
exit_toSelectSongs.onClick.AddListener(OnExitButtonClicked);
}
if (display_rankList != null)
{
display_rankList.onClick.AddListener(OnDisplayRankListClicked);
}
// Documentation text normalized.
if (cdm != null)
{
@@ -216,6 +226,11 @@ public class settlementController : MonoBehaviour
exit_toSelectSongs.onClick.RemoveListener(OnExitButtonClicked);
}
if (display_rankList != null)
{
display_rankList.onClick.RemoveListener(OnDisplayRankListClicked);
}
// Documentation text normalized.
if (musicTransitionCoroutine != null)
{
@@ -252,6 +267,26 @@ public class settlementController : MonoBehaviour
}
}
private void OnDisplayRankListClicked()
{
if (rl == null)
{
Debug.LogWarning("[settlementController] rankingList reference is missing.");
return;
}
SongData songData = thisSong_so != null ? thisSong_so : (bmm != null ? bmm.assignedSongData : null);
string songId = songData != null ? songData.songID.ToString() : string.Empty;
string songName = songData != null ? songData.songName : string.Empty;
if (string.IsNullOrWhiteSpace(songId))
{
Debug.LogWarning("[settlementController] Cannot open ranking list because song id is missing.");
return;
}
rl.Open(songId, songName);
}
private void RegisterCurrentLineupDeployCount()
{
HashSet<int> uniqueHeroIds = new HashSet<int>();
@@ -491,6 +526,7 @@ public class settlementController : MonoBehaviour
settlementUiInitialized = true;
settlementHistoryRecorded = false;
settlementHeroStatsRecorded = false;
roomScoreSubmitTriggered = false;
OnSettlementCompleted?.Invoke();
equipSmelt.NotifySettlementCompleted();
@@ -605,6 +641,8 @@ public class settlementController : MonoBehaviour
float avg = sm.offsetCount > 0 ? sm.totalOffsetMs / sm.offsetCount : 0f;
avgOffset_Text.text = avg.ToString("F2") + " ms";
}
TrySubmitArenaRoomScore();
}
else Debug.LogError("score manager is null");
@@ -1813,17 +1851,70 @@ public class settlementController : MonoBehaviour
// Ensure time scale restored
try { Time.timeScale = 1f; } catch {}
string targetScene = "selectYourSongFirst";
ArenaRoomService arenaRoomService = ArenaRoomService.Instance;
if (arenaRoomService != null && arenaRoomService.IsInRoom)
{
arenaRoomService.RequestReturnToRoomUi();
targetScene = arenaRoomService.RoomSceneName;
}
// If GameManager and its blackMaskImage available, start coroutine on gm to fade to black then load
if (gm != null && gm.blackMaskImage != null)
{
gm.StartCoroutine(FadeToBlackAndLoadOnGM("selectYourSongFirst", 0.25f));
gm.StartCoroutine(FadeToBlackAndLoadOnGM(targetScene, 0.25f));
}
else
{
StartCoroutine(LoadSceneAsync("selectYourSongFirst"));
StartCoroutine(LoadSceneAsync(targetScene));
}
}
private void TrySubmitArenaRoomScore()
{
if (roomScoreSubmitTriggered)
{
return;
}
ArenaRoomService arenaRoomService = ArenaRoomService.Instance;
if (arenaRoomService == null || !arenaRoomService.IsInRoom)
{
return;
}
roomScoreSubmitTriggered = true;
_ = SubmitArenaRoomScoreAsync(arenaRoomService);
}
private async System.Threading.Tasks.Task SubmitArenaRoomScoreAsync(ArenaRoomService arenaRoomService)
{
try
{
await arenaRoomService.SubmitScore(
targetTotalScore,
GetArenaGrade(targetTotalScore),
targetPmScore,
targetIdolScore);
}
catch (Exception ex)
{
Debug.LogWarning($"[settlementController] Arena room score submit failed: {ex.Message}");
}
}
private static string GetArenaGrade(long totalScore)
{
if (totalScore >= 960000) return "SSS";
if (totalScore >= 920000) return "SS";
if (totalScore >= 880000) return "S";
if (totalScore >= 820000) return "A";
if (totalScore >= 720000) return "B";
if (totalScore >= 600000) return "C";
if (totalScore >= 400000) return "D";
return "F";
}
// Coroutine that will be started on the GameManager instance so that the gm's MonoBehaviour runs it
private IEnumerator FadeToBlackAndLoadOnGM(string sceneName, float duration)
{
@@ -133,7 +133,10 @@ public class teamUIController : MonoBehaviour
public void RecordDamage(int slotIndex, float amount)
{
if (slotIndex >= 0 && slotIndex < 5)
{
totalDamageDealt[slotIndex] += amount;
SkillBuilder.Instance?.NotifyAllyAttackDealt(slotIndex);
}
}
/// <summary>
@@ -860,6 +863,7 @@ public class teamUIController : MonoBehaviour
public ComboJudgeType comboJudgeType = ComboJudgeType.Perfect;
private int combo = 0;
public int CurrentCombo => combo;
public enum ComboJudgeType
{
@@ -1458,6 +1462,70 @@ public class teamUIController : MonoBehaviour
}
}
private void SetAllyObjectBySlot(int slotIndex, GameObject value)
{
switch (slotIndex)
{
case 0: ally01_object = value; break;
case 1: ally02_object = value; break;
case 2: ally03_object = value; break;
case 3: ally04_object = value; break;
case 4: ally05_object = value; break;
}
}
private void RefreshTeammateCharacterImageBySlotId(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= allySlotIds.Count) return;
int id = allySlotIds[slotIndex];
if (id <= 0) return;
AllyHero_SO[] all = Resources.LoadAll<AllyHero_SO>("");
for (int i = 0; i < all.Length; i++)
{
AllyHero_SO so = all[i];
if (so == null || so.ally_heroID != id) continue;
Sprite sprite = so.ally_heroProfile != null ? so.ally_heroProfile : so.ally_heroImage;
if (sprite != null)
{
SetTeammateCharacterImage(slotIndex, sprite);
}
return;
}
}
public void SwapAllyWithLowerAdjacent(int slotIndex)
{
int lowerSlot = slotIndex + 1;
if (slotIndex < 0 || lowerSlot > 4) return;
GameObject first = GetAllyObjectBySlot(slotIndex);
GameObject second = GetAllyObjectBySlot(lowerSlot);
int firstId = slotIndex < allySlotIds.Count ? allySlotIds[slotIndex] : 0;
int secondId = lowerSlot < allySlotIds.Count ? allySlotIds[lowerSlot] : 0;
SetAllyObjectBySlot(slotIndex, second);
SetAllyObjectBySlot(lowerSlot, first);
if (slotIndex < allySlotIds.Count) allySlotIds[slotIndex] = secondId;
if (lowerSlot < allySlotIds.Count) allySlotIds[lowerSlot] = firstId;
if (second != null)
{
AllyCombatant secondAlly = second.GetComponent<AllyCombatant>();
if (secondAlly != null) secondAlly.slotIndex = slotIndex;
}
if (first != null)
{
AllyCombatant firstAlly = first.GetComponent<AllyCombatant>();
if (firstAlly != null) firstAlly.slotIndex = lowerSlot;
}
RefreshTeammateCharacterImageBySlotId(slotIndex);
RefreshTeammateCharacterImageBySlotId(lowerSlot);
}
// ------------------- Enemy spawn & UI sync helpers -------------------
private void InitializeEnemyInstance()
{
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5c89b5503ccb0004ebc4aa6fbc50f9a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e149567326267f24e857701ac5f23521
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+135
View File
@@ -0,0 +1,135 @@
using System;
using System.Threading.Tasks;
using Bansonic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class roomAsk : MonoBehaviour
{
[Header("UI Elements")]
[SerializeField] private Button createRoomButton;
[SerializeField] private TMP_InputField roomCode_inputField;
[SerializeField] private Button enterRoomButton;
[SerializeField] private Button quitchoiceButton;
private Func<Task> _onCreate;
private Func<string, Task> _onJoin;
public void Bind(Func<Task> onCreate, Func<string, Task> onJoin)
{
_onCreate = onCreate;
_onJoin = onJoin;
TryAutoBindQuitButton();
if (createRoomButton != null)
{
createRoomButton.onClick.RemoveListener(HandleCreateClicked);
createRoomButton.onClick.AddListener(HandleCreateClicked);
}
if (enterRoomButton != null)
{
enterRoomButton.onClick.RemoveListener(HandleJoinClicked);
enterRoomButton.onClick.AddListener(HandleJoinClicked);
}
if (quitchoiceButton != null)
{
quitchoiceButton.onClick.RemoveListener(HandleQuitChoiceClicked);
quitchoiceButton.onClick.AddListener(HandleQuitChoiceClicked);
}
}
private void OnDestroy()
{
if (createRoomButton != null)
{
createRoomButton.onClick.RemoveListener(HandleCreateClicked);
}
if (enterRoomButton != null)
{
enterRoomButton.onClick.RemoveListener(HandleJoinClicked);
}
if (quitchoiceButton != null)
{
quitchoiceButton.onClick.RemoveListener(HandleQuitChoiceClicked);
}
}
private void TryAutoBindQuitButton()
{
if (quitchoiceButton != null)
{
return;
}
Button[] buttons = GetComponentsInChildren<Button>(true);
foreach (Button button in buttons)
{
if (button == null)
{
continue;
}
string name = button.gameObject.name;
if (string.Equals(name, "quitchoiceButton", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "closeButton", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "cancelButton", StringComparison.OrdinalIgnoreCase))
{
quitchoiceButton = button;
return;
}
}
}
private async void HandleCreateClicked()
{
if (_onCreate == null)
{
return;
}
try
{
await _onCreate.Invoke();
}
catch (Exception ex)
{
Debug.LogWarning($"[roomAsk] Create room failed: {ex.Message}");
}
}
private async void HandleJoinClicked()
{
if (_onJoin == null)
{
return;
}
string roomCode = roomCode_inputField != null ? roomCode_inputField.text?.Trim() : string.Empty;
if (string.IsNullOrWhiteSpace(roomCode))
{
Debug.LogWarning("[roomAsk] Room code is empty.");
gNotice.error.display("房间号不能为空");
return;
}
try
{
await _onJoin.Invoke(roomCode);
}
catch (Exception ex)
{
Debug.LogWarning($"[roomAsk] Join room failed: {ex.Message}");
}
}
private void HandleQuitChoiceClicked()
{
Destroy(gameObject);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 16c261a2a3f78ad46be3a6a68c10ed64
+544
View File
@@ -0,0 +1,544 @@
using System;
using System.Collections;
using Bansonic;
using GameServer.Client;
using UnityEngine;
using UnityEngine.UI;
public class roomDetails : MonoBehaviour
{
[Header("room Info")]
public Text roomID;
public Text roomRemainTime;
public Button quitRoom;
public Button quitRoom2;
public Button getReady;
public Button showRoomRankingList;
[Header("song info")]
public Text thisSongName;
public Text thisSongDifficulty;
[Header("objs")]
public GameObject roommatePrefab;
public Transform roommateParent;
[Header("sprites")]
public Sprite[] btmSprites;
private ArenaRoomService _service;
private Coroutine _countdownRoutine;
private GameObject _roomRankingPrefabTemplate;
private Transform _roomRankingSpawnParent;
private GameObject _spawnedRoomRankingInstance;
public void ConfigureRoomRanking(GameObject roomRankingPrefab, Transform roomRankingSpawnParent)
{
_roomRankingPrefabTemplate = roomRankingPrefab;
_roomRankingSpawnParent = roomRankingSpawnParent;
}
public void Bind(ArenaRoomService service)
{
_service = service;
if (_service == null)
{
Debug.LogWarning("[roomDetails] ArenaRoomService is missing.");
return;
}
_service.OnRoomSnapshotChanged -= HandleRoomSnapshotChanged;
_service.OnRoomSnapshotChanged += HandleRoomSnapshotChanged;
_service.OnKicked -= HandleKicked;
_service.OnKicked += HandleKicked;
_service.OnRoomDismissed -= HandleRoomDismissed;
_service.OnRoomDismissed += HandleRoomDismissed;
if (quitRoom != null)
{
quitRoom.onClick.RemoveListener(OnQuitClicked);
quitRoom.onClick.AddListener(OnQuitClicked);
}
if (quitRoom2 != null)
{
quitRoom2.onClick.RemoveListener(OnQuitClicked);
quitRoom2.onClick.AddListener(OnQuitClicked);
}
if (getReady != null)
{
getReady.onClick.RemoveListener(OnReadyButtonClicked);
getReady.onClick.AddListener(OnReadyButtonClicked);
}
if (showRoomRankingList != null)
{
showRoomRankingList.onClick.RemoveListener(OnShowRoomRankingClicked);
showRoomRankingList.onClick.AddListener(OnShowRoomRankingClicked);
}
Refresh(_service.CurrentRoom);
}
private void OnDestroy()
{
if (_service != null)
{
_service.OnRoomSnapshotChanged -= HandleRoomSnapshotChanged;
_service.OnKicked -= HandleKicked;
_service.OnRoomDismissed -= HandleRoomDismissed;
}
if (quitRoom != null)
{
quitRoom.onClick.RemoveListener(OnQuitClicked);
}
if (quitRoom2 != null)
{
quitRoom2.onClick.RemoveListener(OnQuitClicked);
}
if (getReady != null)
{
getReady.onClick.RemoveListener(OnReadyButtonClicked);
}
if (showRoomRankingList != null)
{
showRoomRankingList.onClick.RemoveListener(OnShowRoomRankingClicked);
}
if (_countdownRoutine != null)
{
StopCoroutine(_countdownRoutine);
_countdownRoutine = null;
}
if (_spawnedRoomRankingInstance != null)
{
Destroy(_spawnedRoomRankingInstance);
_spawnedRoomRankingInstance = null;
}
}
private void HandleRoomSnapshotChanged(ArenaRoomSnapshot snapshot)
{
if (snapshot == null)
{
Destroy(gameObject);
return;
}
Refresh(snapshot);
}
private void HandleKicked(string kickedSteamId, string bySteamId)
{
Destroy(gameObject);
}
private void HandleRoomDismissed(string roomCode, string bySteamId)
{
Destroy(gameObject);
}
private async void OnQuitClicked()
{
if (_service == null)
{
Destroy(gameObject);
return;
}
try
{
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
bool localIsHost = IsLocalHost(_service.CurrentRoom, localSteamId);
if (localIsHost)
{
await _service.DismissRoom();
}
else
{
await _service.LeaveRoom();
}
}
catch (Exception ex)
{
Debug.LogWarning($"[roomDetails] Leave room failed: {ex.Message}");
}
finally
{
Destroy(gameObject);
}
}
private void Refresh(ArenaRoomSnapshot snapshot)
{
if (snapshot == null)
{
return;
}
if (roomID != null)
{
roomID.text = snapshot.room_code;
}
if (thisSongName != null)
{
thisSongName.text = !string.IsNullOrWhiteSpace(snapshot.song_name)
? snapshot.song_name
: snapshot.song_id ?? string.Empty;
}
if (thisSongDifficulty != null)
{
thisSongDifficulty.text = FormatDifficulty(snapshot.difficulty);
}
if (_countdownRoutine != null)
{
StopCoroutine(_countdownRoutine);
}
_countdownRoutine = StartCoroutine(UpdateRemainTime(snapshot));
UpdateQuitButtonTexts(snapshot);
UpdateReadyButton(snapshot);
RenderParticipants(snapshot);
}
private IEnumerator UpdateRemainTime(ArenaRoomSnapshot snapshot)
{
while (snapshot != null && roomRemainTime != null)
{
long nowUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
long remainSeconds = Math.Max(0L, snapshot.expires_at_unix - nowUnix);
int remainMinutes = Mathf.CeilToInt(remainSeconds / 60f);
roomRemainTime.text = $"{remainMinutes}分钟内不开始游戏将自动关闭房间";
yield return new WaitForSeconds(1f);
}
}
private void RenderParticipants(ArenaRoomSnapshot snapshot)
{
if (roommateParent == null || roommatePrefab == null)
{
return;
}
for (int i = roommateParent.childCount - 1; i >= 0; i--)
{
Destroy(roommateParent.GetChild(i).gameObject);
}
string localSteamId = _service != null && NetworkManager.Instance != null
? NetworkManager.Instance.SteamId
: string.Empty;
bool localIsHost = IsLocalHost(snapshot, localSteamId);
if (snapshot.participants == null)
{
return;
}
for (int i = 0; i < snapshot.participants.Count; i++)
{
ArenaRoomParticipant participant = snapshot.participants[i];
GameObject item = Instantiate(roommatePrefab, roommateParent);
roomPrefab itemController = item.GetComponent<roomPrefab>();
if (itemController == null)
{
continue;
}
Sprite background = null;
if (participant != null && participant.is_host)
{
if (btmSprites != null && btmSprites.Length > 0)
{
background = btmSprites[0];
}
}
else if (btmSprites != null && btmSprites.Length > 1)
{
background = btmSprites[1];
}
itemController.Bind(participant, i + 1, localIsHost, localSteamId, background, HandleKickRequested);
}
}
private async void HandleKickRequested(string targetSteamId)
{
if (_service == null || string.IsNullOrWhiteSpace(targetSteamId))
{
return;
}
try
{
await _service.KickPlayer(targetSteamId);
}
catch (Exception ex)
{
Debug.LogWarning($"[roomDetails] Kick failed: {ex.Message}");
}
}
private async void OnReadyButtonClicked()
{
if (_service == null || _service.CurrentRoom == null)
{
return;
}
try
{
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
bool localIsHost = IsLocalHost(_service.CurrentRoom, localSteamId);
if (localIsHost)
{
if (CanHostStartGame(_service.CurrentRoom))
{
await _service.StartGame();
}
}
else
{
await _service.ToggleReady();
}
}
catch (Exception ex)
{
Debug.LogWarning($"[roomDetails] Ready/start failed: {ex.Message}");
}
}
private void OnShowRoomRankingClicked()
{
if (_service == null)
{
return;
}
GameObject template = _roomRankingPrefabTemplate;
Transform parent = _roomRankingSpawnParent != null ? _roomRankingSpawnParent : transform.parent;
if (template == null || parent == null)
{
Debug.LogWarning("[roomDetails] Room ranking prefab or parent is missing.");
return;
}
if (_spawnedRoomRankingInstance == null)
{
_spawnedRoomRankingInstance = Instantiate(template, parent, false);
RectTransform rect = _spawnedRoomRankingInstance.GetComponent<RectTransform>();
if (rect != null)
{
rect.localScale = Vector3.one;
rect.anchoredPosition3D = Vector3.zero;
}
}
rankingListPrefab ranking = _spawnedRoomRankingInstance.GetComponent<rankingListPrefab>();
if (ranking == null)
{
Debug.LogWarning("[roomDetails] Room ranking prefab is missing rankingListPrefab component.");
return;
}
ArenaRoomSnapshot snapshot = _service.CurrentRoom;
LeaderboardData roomRanking = _service.GetRoomRankingLeaderboardData();
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
ranking.BindRoomRanking(
snapshot != null ? snapshot.song_id : string.Empty,
snapshot != null ? snapshot.song_name : string.Empty,
roomRanking,
localSteamId);
}
private void UpdateQuitButtonTexts(ArenaRoomSnapshot snapshot)
{
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
bool localIsHost = IsLocalHost(snapshot, localSteamId);
string buttonText = localIsHost ? "解散房间" : "退出房间";
SetButtonText(quitRoom, buttonText);
SetButtonText(quitRoom2, buttonText);
}
private void UpdateReadyButton(ArenaRoomSnapshot snapshot)
{
if (getReady == null)
{
return;
}
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
bool localIsHost = IsLocalHost(snapshot, localSteamId);
int readyCount = CountReadyPlayers(snapshot);
int totalCount = snapshot != null
? Mathf.Max(snapshot.player_count, snapshot.participants != null ? snapshot.participants.Count : 0)
: 0;
bool roomStarted = IsRoomStarted(snapshot);
string buttonText;
bool interactable;
if (localIsHost)
{
buttonText = $"开始游戏({readyCount}/{totalCount}";
interactable = !roomStarted && CanHostStartGame(snapshot);
}
else
{
bool localReady = IsLocalReady(snapshot, localSteamId);
buttonText = localReady ? "取消准备" : "准备";
interactable = !roomStarted;
}
getReady.interactable = interactable;
SetButtonText(getReady, buttonText);
}
private static bool IsLocalHost(ArenaRoomSnapshot snapshot, string localSteamId)
{
if (snapshot == null || string.IsNullOrWhiteSpace(localSteamId))
{
return false;
}
if (string.Equals(snapshot.host_steam_id, localSteamId, StringComparison.Ordinal))
{
return true;
}
if (snapshot.participants == null)
{
return false;
}
foreach (ArenaRoomParticipant participant in snapshot.participants)
{
if (participant != null
&& string.Equals(participant.steam_id, localSteamId, StringComparison.Ordinal)
&& participant.is_host)
{
return true;
}
}
return false;
}
private static bool IsLocalReady(ArenaRoomSnapshot snapshot, string localSteamId)
{
if (snapshot == null || snapshot.participants == null || string.IsNullOrWhiteSpace(localSteamId))
{
return false;
}
foreach (ArenaRoomParticipant participant in snapshot.participants)
{
if (participant != null && string.Equals(participant.steam_id, localSteamId, StringComparison.Ordinal))
{
return participant.is_ready;
}
}
return false;
}
private static int CountReadyPlayers(ArenaRoomSnapshot snapshot)
{
if (snapshot == null || snapshot.participants == null)
{
return 0;
}
int count = 0;
foreach (ArenaRoomParticipant participant in snapshot.participants)
{
if (participant != null && participant.is_ready)
{
count++;
}
}
return count;
}
private static bool CanHostStartGame(ArenaRoomSnapshot snapshot)
{
if (snapshot == null || snapshot.participants == null || snapshot.participants.Count < 2)
{
return false;
}
foreach (ArenaRoomParticipant participant in snapshot.participants)
{
if (participant == null)
{
continue;
}
if (!participant.is_host && !participant.is_ready)
{
return false;
}
}
return true;
}
private static bool IsRoomStarted(ArenaRoomSnapshot snapshot)
{
if (snapshot == null || string.IsNullOrWhiteSpace(snapshot.status))
{
return false;
}
return string.Equals(snapshot.status, "STARTED", StringComparison.OrdinalIgnoreCase)
|| string.Equals(snapshot.status, "PLAYING", StringComparison.OrdinalIgnoreCase);
}
private static void SetButtonText(Button button, string text)
{
if (button == null)
{
return;
}
Text label = button.GetComponentInChildren<Text>(true);
if (label != null)
{
label.text = text;
}
}
private static string FormatDifficulty(string difficulty)
{
if (string.IsNullOrWhiteSpace(difficulty))
{
return string.Empty;
}
switch (difficulty.Trim().ToLowerInvariant())
{
case "ez":
return "EZ";
case "hd":
return "HD";
case "in":
return "IN";
case "im":
return "IM";
default:
return difficulty;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 57ad2789c4442384fb9566f031fd5e04
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2f8645fff0a5d034884405a8e5af53db
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+131
View File
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using GameServer.Client;
using UnityEngine;
using UnityEngine.UI;
public class roomPrefab : MonoBehaviour
{
public Image btmImg;
public Text roommateID;
public Image ownerImage;
public Text roommateUsername;
public Text playMode_inRoom;
public Dropdown usercando;
public Image roommate_readyStatus;
private Action<string> _onKick;
private string _targetSteamId;
private bool _dropdownBound;
public void Bind(ArenaRoomParticipant participant, int displayIndex, bool localIsHost,
string localSteamId, Sprite backgroundSprite, Action<string> onKick)
{
_onKick = onKick;
_targetSteamId = participant != null ? participant.steam_id : null;
if (btmImg != null)
{
btmImg.sprite = backgroundSprite;
}
if (roommateID != null)
{
roommateID.text = displayIndex.ToString();
}
if (ownerImage != null)
{
ownerImage.gameObject.SetActive(participant != null && participant.is_host);
}
if (roommateUsername != null)
{
roommateUsername.text = participant != null
? string.IsNullOrWhiteSpace(participant.display_name) ? participant.steam_id : participant.display_name
: string.Empty;
}
if (playMode_inRoom != null)
{
playMode_inRoom.text = "游玩";
}
if (roommate_readyStatus != null)
{
bool isReady = participant != null && participant.is_ready;
roommate_readyStatus.enabled = isReady;
roommate_readyStatus.gameObject.SetActive(isReady);
}
ConfigureDropdown(participant, localIsHost, localSteamId);
}
private void OnDestroy()
{
if (usercando != null && _dropdownBound)
{
usercando.onValueChanged.RemoveListener(OnDropdownValueChanged);
_dropdownBound = false;
}
}
private void ConfigureDropdown(ArenaRoomParticipant participant, bool localIsHost, string localSteamId)
{
if (usercando == null)
{
return;
}
if (_dropdownBound)
{
usercando.onValueChanged.RemoveListener(OnDropdownValueChanged);
_dropdownBound = false;
}
usercando.ClearOptions();
bool canKick = participant != null
&& localIsHost
&& !participant.is_host
&& !string.Equals(participant.steam_id, localSteamId, StringComparison.Ordinal);
if (canKick)
{
usercando.interactable = true;
usercando.AddOptions(new List<string> { "操作", "Kick" });
usercando.value = 0;
usercando.RefreshShownValue();
usercando.onValueChanged.AddListener(OnDropdownValueChanged);
_dropdownBound = true;
return;
}
usercando.interactable = false;
if (participant != null && participant.is_host)
{
usercando.AddOptions(new List<string> { "房主" });
}
else
{
usercando.AddOptions(new List<string> { "无权限" });
}
usercando.value = 0;
usercando.RefreshShownValue();
}
private void OnDropdownValueChanged(int index)
{
if (index != 1 || string.IsNullOrWhiteSpace(_targetSteamId))
{
return;
}
_onKick?.Invoke(_targetSteamId);
if (usercando != null)
{
usercando.value = 0;
usercando.RefreshShownValue();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7d8881087ab126243a4485290499a28d
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ecfff3d5dccb90846a8cbde12dbb8b70
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: