很多更新,服务端连接,新UI和系统
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
+2
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user