{"TotalCount":49,"Files":[{"Ident":"slfy.circleroyale","Path":"Game/CircleroyaleGame.cs","FileName":"CircleroyaleGame.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// CircleRoyale \u4E3B\u7CFB\u7EDF\uFF1A\u8FD0\u884C\u65F6\u751F\u6210\u6574\u4E2A\u4E16\u754C\uFF08\u76F8\u673A/HUD/\u80CC\u666F/\u7403\uFF09\uFF0C\u627F\u8F7D host \u6743\u5A01\u903B\u8F91\u3002\n/// GameObjectSystem \u4F1A\u5728\u573A\u666F\u52A0\u8F7D\u65F6\u81EA\u52A8\u5B9E\u4F8B\u5316\uFF08\u65E0\u9700\u6302\u8F7D\u5230\u573A\u666F\uFF09\uFF0C\u4F46\u7F16\u8F91\u5668\u7684\u7F16\u8F91\u6A21\u5F0F\u4E0B\n/// \u4E5F\u4F1A tick\uFF0C\u6240\u4EE5\u6240\u6709\u6E38\u620F\u903B\u8F91\u90FD\u7528 Game.IsPlaying \u62E6\u622A\u3002\u6A21\u677F\u573A\u666F\u5BF9\u8C61\u4F1A\u88AB\u6574\u4F53\u5C4F\u853D\u3002\n/// \u003C/summary\u003E\npublic sealed class CircleroyaleGame : GameObjectSystem\u003CCircleroyaleGame\u003E\n{\n\tGameObject _root;\n\tNeonCamera _camera;\n\tGameHud _hud;\n\tEndBoard _endBoard;\n\tLobbyPanel _lobby;\n\tRoomBrowser _browser;\n\tSettingsPanel _settings;\n\tRankingsPanel _rankings;\n\tFoodManager _food;\n\tPowerUpManager _power;\n\tNetworkManager _net;\n\tMainMenu _menu;\n\n\treadonly List\u003CBall\u003E _balls = new();\n\treadonly Dictionary\u003CGuid, Vector3\u003E _lastPos = new();   // host \u4F4D\u79FB\u6821\u9A8C\u7528\n\treadonly Dictionary\u003CGuid, TimeSince\u003E _teleportGrace = new();   // host\uFF1A\u5408\u6CD5\u77AC\u79FB\uFF08\u91CD\u751F\u6362\u4F4D/\u664B\u5347\uFF09\u540E\u7684\u6821\u9A8C\u5BBD\u9650\u671F\n\tTimeSince _sinceScan = 1f;         // \u7403\u6CE8\u518C\u626B\u63CF\n\tTimeSince _sinceFoodRequest = 1f;  // \u5BA2\u6237\u7AEF\u8BF7\u6C42\u98DF\u7269\u5168\u91CF\u7684\u8282\u6D41\uFF08\u9996\u5E27\u5373\u8BF7\u6C42\uFF09\n\tTimeSince _sinceMatchTick = 1f;    // \u5012\u8BA1\u65F6 1Hz \u6821\u65F6\u5E7F\u64AD\n\tTimeSince _sinceTerritoryWire = 1f;   // \u9886\u571F\u72B6\u6001\u53D8\u5316\u5E7F\u64AD\u8282\u6D41\uFF085Hz \u4E0A\u9650 \u002B 2s \u4FDD\u6D3B\uFF09\n\tbool _pendingFrontRespawn;            // \u672C\u673A\u4E0B\u6B21\u590D\u6D3B\u7684\u524D\u7EBF\u504F\u597D\uFF08\u9886\u571F M7.2\uFF0Cowner \u6362\u4F4D\u65F6\u6D88\u8D39\uFF09\n\tint _botNameIndex;\n\tint _teamSeq;                      // \u56E2\u961F\u8D5B\u8F6E\u8F6C\u5206\u961F\u8BA1\u6570\uFF08\u5F00\u5C40\u91CD\u7F6E\uFF09\n\n\t// ---- \u5206\u8EAB/\u5B62\u5B50\uFF08M3\uFF0C\u7EAF\u6570\u636E\u5B9E\u4F53\u89C1 CellPiece\uFF09----\n\treadonly List\u003CCellPiece\u003E _cells = new();                    // host \u6743\u5A01\u6A21\u62DF\uFF1B\u5BA2\u6237\u7AEF\u955C\u50CF\uFF08CellsState RPC\uFF09\n\treadonly Dictionary\u003CGuid, Vector2\u003E _steerDir = new();       // host\uFF1A\u6BCF\u7403\u5E73\u6ED1\u8F6C\u5411\uFF08\u5206\u8EAB\u8DDF\u968F\u7528\uFF09\n\treadonly Dictionary\u003Clong, int\u003E _teamScratch = new();        // \u56E2\u961F\u8D5B\u9635\u8425\u6620\u5C04\u590D\u7528\u7F13\u51B2\uFF08\u6BCF\u5E27 3 \u5904\u5148\u540E\u6784\u5EFA\uFF0C\u7701\u5806\u5206\u914D\uFF09\n\treadonly Dictionary\u003Cint, CellWire\u003E _wireById = new();       // CellsState \u5E94\u7528\u7F13\u51B2\uFF08\u6309 Id O(1) \u67E5\u627E\uFF09\n\treadonly HashSet\u003Cint\u003E _cellIdScratch = new();               // CellsState \u65B0\u589E\u5224\u5B9A\u7F13\u51B2\n\treadonly Dictionary\u003CGuid, TimeSince\u003E _sinceEject = new();   // host\uFF1A\u5410\u5B62\u5B50\u8282\u6D41\n\tTimeSince _sinceCellSync = 1f;\n\tTimeSince _sinceCellHeal;        // \u5206\u8EAB\u4E3B\u4EBA\u5931\u8054\u81EA\u6108\u7684 ScanBalls \u8282\u6D41\uFF08\u72EC\u7ACB\u8BA1\u65F6\uFF0C\u522B\u632A\u7528 _sinceScan \u6253\u4E71\u7403\u626B\u63CF\u8282\u594F\uFF09\n\tbool _cellsDirty;                                           // \u589E\u5220\u6807\u8BB0\uFF08\u5217\u8868\u6E05\u7A7A\u540E\u8FD8\u8981\u8865\u53D1\u4E00\u6B21\u7A7A\u5FEB\u7167\uFF09\n\tint _nextCellId = 1;\n\tVector2[] _spikes;                                          // \u7EFF\u523A\u4F4D\u7F6E\uFF08host \u751F\u6210\uFF1B\u5BA2\u6237\u7AEF\u7ECF SpikesFull \u540C\u6B65\uFF09\n\tfloat[] _spikeFed;                                          // \u6BCF\u6735\u523A\u7D2F\u8BA1\u5582\u5165\u8D28\u91CF\uFF08host \u6743\u5A01\uFF1B\u5BA2\u6237\u7AEF SpikeFed RPC \u955C\u50CF\uFF0Cv0.7.8.24\uFF09\n\treadonly List\u003CSpikeSpine\u003E _spines = new();                  // \u523A\u7206\u5F39\u5E55\uFF1Ahost=\u6743\u5A01\u6A21\u62DF\uFF08\u542B\u547D\u4E2D\uFF09\uFF0C\u5BA2\u6237\u7AEF=\u7EAF\u89C6\u89C9\u5F39\u9053\uFF08v0.7.8.24\uFF09\n\treadonly HashSet\u003Cstring\u003E _strayUiLogged = new();            // \u5B64\u513F UI \u8BA4\u9886\u65E5\u5FD7\u53BB\u91CD\uFF08ScanBalls \u6BCF\u79D2\u8DD1\uFF0C\u540C\u540D\u53EA\u62A5\u4E00\u6B21\uFF09\n\n\t/// \u003Csummary\u003E \u5168\u90E8\u7403\uFF08\u771F\u4EBA \u002B bot\uFF09 \u003C/summary\u003E\n\tpublic IReadOnlyList\u003CBall\u003E Balls =\u003E _balls;\n\n\t/// \u003Csummary\u003E \u672C\u5730\u7403\uFF08M2 \u8D77\uFF1A\u771F\u4EBA=\u81EA\u5DF1\u90A3\u9897\uFF0Cbot \u65E0\uFF09 \u003C/summary\u003E\n\tpublic Ball LocalBall { get; private set; }\n\n\t/// \u003Csummary\u003E \u672C\u5730\u73A9\u5BB6\u662F\u5426\u5904\u4E8E\u6B7B\u4EA1\u5F85\u91CD\u751F\u72B6\u6001\uFF08HUD \u663E\u793A\u6B7B\u4EA1\u9762\u677F\uFF09 \u003C/summary\u003E\n\tpublic bool IsLocalDead =\u003E _worldReady \u0026\u0026 LocalBall.IsValid() \u0026\u0026 !LocalBall.Alive;\n\n\t/// \u003Csummary\u003E \u98DF\u7269\u7BA1\u7406\uFF08\u6E32\u67D3\u4E0E AI \u8BFB\uFF09 \u003C/summary\u003E\n\tpublic FoodManager Food =\u003E _food;\n\n\t/// \u003Csummary\u003E \u9053\u5177\u7BA1\u7406\uFF08\u6E32\u67D3\u8BFB\uFF1Bhost \u6743\u5A01\uFF0C\u5BA2\u6237\u7AEF\u4E3A\u4E8B\u4EF6\u955C\u50CF\uFF09 \u003C/summary\u003E\n\tpublic PowerUpManager PowerUps =\u003E _power;\n\n\t/// \u003Csummary\u003E \u5168\u90E8\u5206\u8EAB/\u5B62\u5B50\uFF08\u6E32\u67D3\u8BFB\uFF1Bhost \u6743\u5A01\uFF0C\u5BA2\u6237\u7AEF\u4E3A 15Hz \u5FEB\u7167\u955C\u50CF\uFF09 \u003C/summary\u003E\n\tpublic IReadOnlyList\u003CCellPiece\u003E Cells =\u003E _cells;\n\n\t/// \u003Csummary\u003E \u7EFF\u523A\u4F4D\u7F6E\uFF08\u9759\u6001\u969C\u788D\uFF1Bhost \u751F\u6210\uFF0C\u5BA2\u6237\u7AEF\u7ECF SpikesFull \u5168\u91CF\u540C\u6B65\uFF09 \u003C/summary\u003E\n\tpublic Vector2[] Spikes =\u003E _spikes;\n\n\t/// \u003Csummary\u003E \u523A\u7684\u7D2F\u8BA1\u5582\u98DF\u91CF\uFF080~SpikeGrowTarget\uFF0C\u6E32\u67D3\u957F\u5927\u7528\uFF1Bhost \u6743\u5A01\uFF0C\u5BA2\u6237\u7AEF\u7ECF SpikeFed RPC \u955C\u50CF\uFF09 \u003C/summary\u003E\n\tpublic float SpikeFedAt( int index ) =\u003E\n\t\t_spikeFed is not null \u0026\u0026 (uint)index \u003C (uint)_spikeFed.Length ? _spikeFed[index] : 0f;\n\n\t/// \u003Csummary\u003E \u523A\u7206\u5F39\u5E55\uFF08\u6E32\u67D3\u8BFB\uFF1Bhost \u6743\u5A01\u6A21\u62DF\u542B\u547D\u4E2D\uFF0C\u5BA2\u6237\u7AEF\u4E3A\u7EAF\u89C6\u89C9\u5F39\u9053\uFF09 \u003C/summary\u003E\n\tpublic IReadOnlyList\u003CSpikeSpine\u003E Spines =\u003E _spines;\n\n\t/// \u003Csummary\u003E HUD\uFF08\u63D0\u4F9B\u5C4F\u5E55\u5149\u6807\u6865\u63A5\u7B49\uFF09 \u003C/summary\u003E\n\tpublic GameHud Hud =\u003E _hud;\n\n\t/// \u003Csummary\u003E \u8DDF\u968F\u76F8\u673A\uFF08\u5C4F\u5E55\u9707\u52A8\u89E6\u53D1\u5165\u53E3\uFF09 \u003C/summary\u003E\n\tpublic NeonCamera Camera =\u003E _camera;\n\n\t/// \u003Csummary\u003E \u623F\u95F4\u5217\u8868\u9875\uFF08\u4E3B\u83DC\u5355 [2] \u6253\u5F00\uFF0C\u70B9\u884C\u52A0\u5165\uFF09 \u003C/summary\u003E\n\tpublic RoomBrowser Browser =\u003E _browser;\n\n\t/// \u003Csummary\u003E host\uFF1A\u623F\u95F4\u5F00\u7740\uFF08\u7B49 START\uFF09\uFF1B\u6B64\u9636\u6BB5\u73A9\u5BB6\u53EF\u8C03\u53C2/\u8FDB\u51FA\uFF0C\u6BD4\u8D5B\u5C1A\u672A\u751F\u6210 \u003C/summary\u003E\n\tpublic bool IsLobbyOpen =\u003E _lobbyOpen;\n\n\t/// \u003Csummary\u003E \u6BD4\u8D5B\u662F\u5426\u5DF2\u5F00\u59CB\uFF08\u5BA2\u6237\u7AEF\u533A\u5206\u0022\u8FDB\u623F\u95F4\u7B49\u0022\u8FD8\u662F\u0022\u76F4\u63A5\u8FDB\u6E38\u620F\u0022\uFF09 \u003C/summary\u003E\n\tpublic bool IsMatchStarted =\u003E _gameStarted;\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u89C6\u89D2\uFF1A\u6B63\u5728\u4F1A\u8BDD\u91CC\uFF08\u623F\u95F4\u7B49\u5F85/\u5BF9\u5C40/\u52A0\u5165\u5012\u8BA1\u65F6\u4E2D\uFF09\u2014\u2014\u6389\u7EBF\u81EA\u52A8\u91CD\u8FDE\u5224\u5B9A\u7528 \u003C/summary\u003E\n\tpublic bool IsClientInSession =\u003E _lobbyWaiting || _gameStarted || _matchRunning || _joinCountdown \u003E= 0f;\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u662F\u5426\u505C\u5728\u7B49\u5F85\u9875\uFF08P \u9000\u5C40/\u6389\u7EBF\u6062\u590D\u4E2D\uFF09\u2014\u2014\u672C\u673A\u7403\u505C\u6B62\u64CD\u63A7\uFF08\u7403\u7559\u5728\u539F\u5730\uFF0C\u9632\u9694\u5C4F\u76F2\u5F00\uFF09 \u003C/summary\u003E\n\tpublic bool IsClientWaiting =\u003E _lobbyWaiting;\n\n\t/// \u003Csummary\u003E\n\t/// \u5BA2\u6237\u7AEF\u8FDB\u5165\u623F\u95F4\u7B49\u5F85\u6001\uFF08\u6240\u6709\u5165\u53E3\u7EDF\u4E00\u8D70\u8FD9\u91CC\uFF0Cv0.7.1.1\uFF09\uFF1A\n\t/// \u7F6E\u6807\u5FD7 \u002B **\u91CD\u7F6E/\u89E3\u9664\u623F\u95F4\u5FC3\u8DF3**\u2014\u2014\u8FDB\u5165\u7B49\u5F85\u7684\u65F6\u523B host \u4E0D\u4E00\u5B9A\u5728\u623F\u95F4\u9636\u6BB5\uFF08\u53EF\u80FD\u5BF9\u5C40\u8FDB\u884C\u4E2D\uFF0C\n\t/// \u6CA1\u6709\u540D\u5355\u5E7F\u64AD\uFF09\uFF0C\u65E7\u8BA1\u65F6\u5668/\u6B66\u88C5\u72B6\u6001\u4F1A\u7ACB\u523B\u8BEF\u5224\u8D85\u65F6\uFF0C\u5FC5\u987B\u7B49 host \u7684\u4E0B\u4E00\u6761 LobbyState \u91CD\u65B0\u6B66\u88C5\u3002\n\t/// \u003C/summary\u003E\n\tvoid EnterClientWaiting()\n\t{\n\t\tif ( _lobbyWaiting ) return;   // \u5DF2\u5728\u7B49\u5F85\u6001\uFF1A\u522B\u91CD\u7F6E\u8BA1\u65F6\uFF08\u5426\u5219\u5FC3\u8DF3\u6C38\u8FDC\u5224\u4E0D\u8D85\u65F6\uFF09\n\n\t\t_lobbyWaiting = true;\n\t\t_lobbyHeartbeatArmed = false;\n\t\t_sinceLobbyHeartbeat = 0;\n\t}\n\n\tstatic GameObject _sharedRoot;   // \u70ED\u91CD\u8F7D/\u91CD\u5EFA\u65F6\u590D\u7528\u540C\u4E00\u4E2A\u4E16\u754C\u6839\uFF0C\u907F\u514D\u5806\u53E0\u591A\u5957\u573A\u666F\n\n\tbool _worldReady;\n\tbool _networkReady;\n\tbool _gameStarted;              // \u6BD4\u8D5B\u8FDB\u884C\u4E2D\uFF08\u6A21\u62DF\u95E8\u63A7\uFF09\uFF1B\u623F\u95F4/\u83DC\u5355\u9636\u6BB5 false\n\tbool _menuDemo;                 // \u4E3B\u83DC\u5355\u80CC\u666F\u6F14\u793A\u8D5B\uFF1Aauthority \u79BB\u7EBF\u8DD1\u4E00\u573A\u7EAF bot \u5BF9\u6218\uFF08v0.6.3.0\uFF09\n\tbool _lobbyOpen;                // host\uFF1A\u623F\u95F4\u5F00\u7740\uFF08\u8FD8\u6CA1\u70B9 START\uFF09\n\tbool _lobbyWaiting;             // client\uFF1A\u5DF2\u8FDE\u5165\uFF0C\u5728\u623F\u95F4\u91CC\u7B49\u623F\u4E3B\u5F00\u5C40\n\tbool _joinRequested;            // \u5BA2\u6237\u7AEF\u70B9\u4E86\u0022\u52A0\u5165\u0022\uFF1A\u7B49\u4F1A\u8BDD\u4EE5\u5BA2\u6237\u7AEF\u8EAB\u4EFD\u6FC0\u6D3B\uFF08\u8FDE\u63A5\u524D\u51E0\u5E27 IsAuthority \u4E0D\u53EF\u4FE1\uFF09\n\tTimeSince _sinceJoinRequest;\n\tTimeSince _sinceMatchOver;      // \u7ED3\u7B97\u663E\u793A\u8BA1\u65F6\uFF0810 \u79D2\u540E\u81EA\u52A8\u56DE\u5927\u5385/\u4E3B\u83DC\u5355\uFF09\n\tbool _lobbyListDirty;           // \u623F\u95F4\u73A9\u5BB6\u540D\u5355\u53D8\u4E86\uFF08\u4E3B\u7EBF\u7A0B\u4E0B\u4E00\u62CD\u5E7F\u64AD LobbyState\uFF09\n\tbyte _lobbyMode;                // client\uFF1A\u623F\u4E3B\u63A8\u9001\u7684\u623F\u95F4\u53C2\u6570\uFF08LobbyState RPC\uFF09\n\tint _lobbyDuration;\n\tint _lobbyPlayers;\n\tstring[] _lobbyNames = System.Array.Empty\u003Cstring\u003E();\n\tfloat _joinCountdown = -1f;     // \u5BA2\u6237\u7AEF\u52A0\u5165\u5012\u8BA1\u65F6\uFF08\u22650 \u65F6\u623F\u95F4\u9875\u663E\u793A 3-2-1\uFF0C\u5F52\u96F6\u8FDB\u5165\u5BF9\u5C40\uFF09\n\tbool _matchRunning;             // \u5BA2\u6237\u7AEF\u89C6\u89D2\uFF1Ahost \u7684\u5BF9\u5C40\u8FDB\u884C\u4E2D\uFF08\u623F\u95F4\u9875\u4EAE JOIN GAME \u6309\u94AE\uFF09\n\n\t// host \u5FC3\u8DF3\uFF08v0.6.8.0\uFF09\uFF1Ahost \u623F\u95F4\u9636\u6BB5 3s \u5E7F\u64AD\u4E00\u6B21\u540D\u5355\u5F53\u5FC3\u8DF3\uFF1B\u5BA2\u6237\u7AEF\u6536\u5230\u5F52\u96F6\uFF0C\n\t// \u9759\u9ED8\u8D85\u65F6 = host \u6B7B\u4E86\uFF08\u8FDB\u7A0B\u88AB\u6740\u65F6\u5F15\u64CE\u4F1A\u8BDD\u53EF\u80FD\u4E0D\u81EA\u52A8\u5931\u6548\uFF09\uFF0C\u5F3A\u5236\u62C6\u7EBF\u8D70\u91CD\u8FDE\n\tTimeSince _sinceLobbyPing = 999f;   // host\uFF1A\u623F\u95F4\u5FC3\u8DF3\u8282\u62CD\n\tbool _lobbyHeartbeatArmed;          // client\uFF1A\u6536\u5230\u8FC7\u7B2C\u4E00\u6B21\u540D\u5355\u540E\u624D\u6B66\u88C5\uFF08\u9632\u521A\u8FDB\u623F\u5C31\u8BEF\u5224\uFF09\n\tTimeSince _sinceLobbyHeartbeat;\n\n\tpublic CircleroyaleGame( Scene scene ) : base( scene )\n\t{\n\t\tListen( Stage.UpdateBones, 0, Tick, \u0022CircleroyaleGame.Tick\u0022 );\n\n\t\t// \u6743\u5A01\u6A21\u62DF\u8D70\u56FA\u5B9A\u6B65\uFF08\u89C1 TickSimulation \u6CE8\u91CA\uFF09\uFF1A\u6302 FinishFixedUpdate \u800C\u975E StartFixedUpdate\uFF0C\n\t\t// \u4EE5\u6392\u5728\u7EC4\u4EF6 OnFixedUpdate \u4E4B\u540E\u2014\u2014\u7403\u672C\u6B65\u79FB\u52A8\u5B8C\uFF0C\u5403\u5224\u5B9A\u62FF\u5230\u672C\u6B65\u65B0\u4F4D\u7F6E\n\t\tListen( Stage.FinishFixedUpdate, 0, TickSimulation, \u0022CircleroyaleGame.TickSimulation\u0022 );\n\t}\n\n\tvoid Tick()\n\t{\n\t\tif ( !Game.IsPlaying ) return;\n\n\t\tEnsureWorld();\n\n\t\t// \u5BA2\u6237\u7AEF\uFF1A\u4F1A\u8BDD\u6FC0\u6D3B\u4F46\u4E16\u754C\u8FD8\u5728\u83DC\u5355\u6F14\u793A\u6001\uFF08\u65AD\u7EBF\u91CD\u8FDE\u65F6 EnsureWorld \u5728\u65AD\u7EBF\u671F\u628A\u5206\u652F\u8DD1\u6210\u4E86\n\t\t// \u6F14\u793A\u8D5B\uFF0C\u4E14\u91CD\u8FDE\u4E0D\u4F1A\u7F6E _joinRequested\uFF09\u2192 \u6536\u6F14\u793A\u8D5B\uFF0C\u56DE\u5230\u4F1A\u8BDD\u52A0\u5165\u6D41\n\t\tif ( !NetworkManager.IsAuthority \u0026\u0026 Networking.IsActive \u0026\u0026 _menuDemo )\n\t\t\tStopMenuDemo();\n\n\t\t// \u5BA2\u6237\u7AEF\uFF1A\u5728\u4F1A\u8BDD\u91CC\u4F46\u54EA\u4E2A\u754C\u9762\u90FD\u4E0D\u5728\uFF08\u91CD\u8FDE/\u70ED\u91CD\u8F7D\u540E\uFF09\u2192 \u56DE\u623F\u95F4\u7B49\u5F85\u5E76\u62C9\u5F53\u524D\u72B6\u6001\n\t\tif ( !NetworkManager.IsAuthority \u0026\u0026 Networking.IsActive \u0026\u0026 !_gameStarted\n\t\t\t\u0026\u0026 !_lobbyWaiting \u0026\u0026 _joinCountdown \u003C 0f )\n\t\t{\n\t\t\tEnterClientWaiting();\n\t\t\t_menu?.Hide();\n\t\t\t_browser?.Hide();\n\t\t\t_lobby?.ShowClient();\n\t\t\tNetworkManager.RequestMatchState();\n\t\t}\n\n\t\t// \u5BA2\u6237\u7AEF\uFF1AReplicated \u7684 cr_arena_size \u7531\u5F15\u64CE\u843D\u5230 ConvarArenaSize\uFF0C\u8FD9\u91CC\u540C\u6B65\u5230 ArenaHalfSize\u3002\n\t\t// host \u5728 OnNetworkReady \u505A\u540C\u6B3E\u94B3\u5236\uFF1B\u6B64\u524D\u5BA2\u6237\u7AEF\u4ECE\u4E0D\u8D4B\u503C\u2014\u2014\u7F51\u683C/\u8FB9\u754C/\u94B3\u5236\u4E00\u76F4\u7528\u9759\u6001\u9ED8\u8BA4\u503C\uFF0C\n\t\t// \u573A\u5730\u6539 3 \u500D\u540E\u4E0E host \u5B9E\u6D4B\u8131\u8282\uFF08host 2048 / \u5BA2\u6237\u7AEF 6144\uFF0C\u5BA2\u6237\u7AEF\u7F51\u683C\u5927\u4E86 3 \u500D\uFF09\n\t\tif ( !NetworkManager.IsAuthority )\n\t\t\tGameConfig.ArenaHalfSize = Math.Clamp( GameConfig.ConvarArenaSize, 1024f, 12288f );\n\n\t\t// \u5BA2\u6237\u7AEF\u52A0\u5165\u6D41\uFF1A\u70B9\u4E86\u0022\u52A0\u5165\u0022\u540E\u7B49\u4F1A\u8BDD\u4EE5\u5BA2\u6237\u7AEF\u8EAB\u4EFD\u6FC0\u6D3B\u3002\n\t\t// \u26A0\uFE0F \u4E0D\u80FD\u70B9\u5B8C\u5C31\u4FE1 IsAuthority\u2014\u2014\u8FDE\u63A5\u5EFA\u7ACB\u524D\u7684\u51E0\u5E27 IsActive=false\uFF0CIsAuthority \u6052\u4E3A true\n\t\tif ( _joinRequested \u0026\u0026 !_gameStarted )\n\t\t{\n\t\t\tif ( Networking.IsActive \u0026\u0026 !Networking.IsHost )\n\t\t\t{\n\t\t\t\tGameLog.Info( \u0022[menu] join: connected as client, entering room\u0022 );\n\t\t\t\t_joinRequested = false;\n\t\t\t\tEnterClientWaiting();\n\t\t\t\t_menu?.Hide();\n\t\t\t\t_browser?.Hide();   // \u623F\u95F4\u5217\u8868\u9875\u6536\u8D77\uFF08\u4ECE\u5217\u8868\u70B9\u8FDB\u6765\u7684\u6B63\u5E38\u6536\u5C3E\uFF09\n\t\t\t\t_lobby?.ShowClient();\n\t\t\t\tNetworkManager.RequestMatchState();\n\t\t\t}\n\t\t\telse if ( _sinceJoinRequest \u003E 12f )\n\t\t\t{\n\t\t\t\t_joinRequested = false;\n\t\t\t\t_menu?.SetStatus( \u0022JOIN FAILED \u2014 IS THE HOST UP? PRESS [2] TO RETRY\u0022 );\n\t\t\t\t_browser?.SetStatus( \u0022JOIN TIMED OUT \u2014 PICK ANOTHER ROOM OR REFRESH\u0022 );\n\t\t\t}\n\t\t}\n\n\t\t// \u5BA2\u6237\u7AEF\u5728\u4F1A\u8BDD\u754C\u9762\u91CC\u8FDE\u63A5\u65AD\u5F00\uFF1A**\u4E0D\u7ACB\u523B\u56DE\u83DC\u5355**\u2014\u2014\u754C\u9762\u7559\u5728\u539F\u5730\u7531 NetworkManager\n\t\t// \u81EA\u52A8\u91CD\u8FDE\uFF08TickDisconnectRecovery\uFF09\uFF0C\u91CD\u8BD5\u8017\u5C3D\u56DE\u8C03 OnReconnectFailed \u624D\u56DE\u4E3B\u83DC\u5355\uFF08v0.6.4.6\uFF09\n\n\t\t// \u623F\u95F4\u540D\u5355/\u53C2\u6570\u6709\u53D8\uFF08\u7F51\u7EDC\u7EBF\u7A0B\u53EA\u7F6E\u810F\u6807\uFF0C\u4E3B\u7EBF\u7A0B\u7EDF\u4E00\u5E7F\u64AD LobbyState\uFF09\n\t\tif ( _lobbyOpen \u0026\u0026 _lobbyListDirty )\n\t\t{\n\t\t\t_lobbyListDirty = false;\n\t\t\tBroadcastLobbyState();\n\t\t}\n\n\t\t// host \u623F\u95F4\u5FC3\u8DF3\uFF08v0.6.8.0\uFF09\uFF1A\u540D\u5355\u6CA1\u53D8\u4E5F\u5B9A\u65F6\u5E7F\u64AD\uFF0C\u5BA2\u6237\u7AEF\u9760\u5B83\u5224\u65AD\u0022host \u8FD8\u6D3B\u7740\u0022\n\t\tif ( _lobbyOpen \u0026\u0026 Networking.IsActive \u0026\u0026 _sinceLobbyPing \u003E 3f )\n\t\t{\n\t\t\t_sinceLobbyPing = 0;\n\t\t\tBroadcastLobbyState();\n\t\t}\n\n\t\t// host \u5FC3\u8DF3\u8D85\u65F6\uFF08\u5BA2\u6237\u7AEF\uFF0Cv0.6.8.0\uFF09\uFF1Ahost \u8FDB\u7A0B\u88AB\u6740\u65F6\u5F15\u64CE\u4F1A\u8BDD\u53EF\u80FD\u4E0D\u81EA\u52A8\u5931\u6548\n\t\t// \uFF08\u50F5\u5C38\u6001\uFF0CIsActive \u6052 true\uFF0C\u6389\u7EBF\u6062\u590D\u5FAA\u73AF\u4E0D\u89E6\u53D1\uFF09\u2014\u2014\u5BF9\u5C40/\u623F\u95F4\u5404\u81EA\u6709\u5FC3\u8DF3\uFF0C\n\t\t// \u9759\u9ED8\u8D85\u65F6\u4E3B\u52A8\u62C6\u7EBF\uFF0C\u4EA4\u7ED9 TickDisconnectRecovery\uFF08\u91CD\u8FDE\u8017\u5C3D\u56DE\u4E3B\u83DC\u5355\uFF09\n\t\t// v0.7.1.1 \u4FEE\uFF1A\u4E24\u6761\u5FC3\u8DF3**\u6309 host \u5B9E\u9645\u9636\u6BB5\u53D6\u4E00**\u2014\u2014host \u5728\u5BF9\u5C40\u9636\u6BB5\u4E0D\u53D1\u540D\u5355\u5E7F\u64AD\uFF0C\n\t\t// \u7B49\u5728\u623F\u95F4\u9875\u7684\u5BA2\u6237\u7AEF\u4E0D\u80FD\u62FF\u0022\u540D\u5355\u9759\u9ED8\u0022\u8BEF\u5224\uFF08\u6B64\u524D\u4E2D\u9014\u52A0\u5165 6s \u5373\u88AB\u8E22\u56DE\u4E3B\u83DC\u5355\uFF09\uFF1B\n\t\t// \u5BF9\u5C40\u5FC3\u8DF3\uFF08MatchTick 1Hz\uFF09\u53CD\u8FC7\u6765\u4E5F\u8986\u76D6\u0022\u5BA2\u6237\u7AEF\u5728\u623F\u95F4\u9875\u7B49\u3001host \u5BF9\u5C40\u8FD8\u5728\u8DD1\u0022\u7684\u60C5\u51B5\n\t\tif ( !NetworkManager.IsAuthority \u0026\u0026 Networking.IsActive \u0026\u0026 GameConfig.HostLivenessTimeout \u003E 0f )\n\t\t{\n\t\t\tbool matchSilent = ( _gameStarted || _matchRunning ) \u0026\u0026 !MatchState.MatchOver\n\t\t\t\t\u0026\u0026 MatchState.SinceLastTick \u003E GameConfig.HostLivenessTimeout;\n\t\t\tbool lobbySilent = _lobbyWaiting \u0026\u0026 _lobbyHeartbeatArmed \u0026\u0026 !_matchRunning\n\t\t\t\t\u0026\u0026 _sinceLobbyHeartbeat \u003E GameConfig.HostLivenessTimeout;\n\n\t\t\tif ( matchSilent || lobbySilent )\n\t\t\t{\n\t\t\t\tLog.Warning( \u0022[net] host silent beyond liveness timeout \u2014 forcing disconnect\u0022 );\n\t\t\t\ttry { Networking.Disconnect(); } catch { }\n\n\t\t\t\t// \u5BF9\u5C40\u5DF2\u6B7B\uFF1A\u7ACB\u523B\u9000\u51FA\u5BF9\u5C40\u6001\uFF0C\u6062\u590D\u671F\u505C\u5728\u623F\u95F4\u9875\uFF08CONNECTION LOST\uFF09\uFF0C\n\t\t\t\t// \u4E0D\u518D\u5BF9\u7740\u51BB\u7ED3\u753B\u9762\u5E72\u7B49\uFF08_gameStarted \u6302\u7740\u4F1A\u8BA9 HUD/\u51BB\u7ED3\u5BF9\u5C40\u4E00\u76F4\u5360\u5C4F\uFF09\n\t\t\t\t_gameStarted = false;\n\t\t\t\t_matchRunning = false;\n\t\t\t\t_joinCountdown = -1f;\n\t\t\t\t_lobbyHeartbeatArmed = false;\n\t\t\t\t_lobbyWaiting = true;   // \u4FDD\u4F4F IsClientInSession\uFF1A\u6389\u7EBF\u6062\u590D\u5FAA\u73AF\u8981\u9760\u5B83\u542F\u52A8\n\t\t\t\tLocalBall = null;\n\t\t\t}\n\t\t}\n\n\t\t// \u97F3\u4E50\u5FAA\u73AF\uFF08\u83DC\u5355/\u6218\u6597\u90FD\u8981\uFF09\uFF1A\u97F3\u91CF\u8DDF\u968F convar \u002B \u66F2\u7EC8\u81EA\u52A8\u91CD\u64AD\n\t\tGameMusic.Tick();\n\n\t\tReconcileUi();\n\n\t\t// P \u952E\u5FEB\u901F\u9000\u5C40\uFF08v0.6.9.0 \u7528\u6237\u9700\u6C42\uFF09\uFF1A\u5BF9\u5C40\u4E2D\u76F4\u63A5\u8FD4\u56DE\u5927\u5385\u2014\u2014host \u5E7F\u64AD\u53D6\u6D88\u8BA9\u5168\u5458\u4E00\u8D77\u56DE\u623F\uFF0C\n\t\t// \u5BA2\u6237\u7AEF\u81EA\u5DF1\u56DE\u7B49\u5F85\u9875\uFF1B\u5355\u673A\uFF08\u65E0\u4F1A\u8BDD\uFF09\u56DE\u4E3B\u83DC\u5355\u3002\u7ED3\u7B97\u671F\u4E0D\u53D7\u7406\uFF08EndBoard \u81EA\u52A8\u8FD4\u56DE\u5728\u7BA1\uFF09\n\t\tif ( _gameStarted \u0026\u0026 !MatchState.MatchOver \u0026\u0026 Input.Keyboard.Pressed( \u0022P\u0022 ) )\n\t\t{\n\t\t\tGameLog.Info( \u0022[match] quit to lobby via P\u0022 );\n\t\t\tif ( Networking.IsActive \u0026\u0026 NetworkManager.IsAuthority )\n\t\t\t\tNetworkManager.MatchCancelled();\n\t\t\tAfterSettlement();\n\t\t\treturn;\n\t\t}\n\n\t\t// \u5BA2\u6237\u7AEF\u52A0\u5165\u5012\u8BA1\u65F6\uFF08v0.6.4.0 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1AMatchStarted \u540E\u623F\u95F4\u9875 3-2-1\uFF0C\u5F52\u96F6\u8FDB\u5165\u5BF9\u5C40\n\t\tif ( _joinCountdown \u003E= 0f )\n\t\t{\n\t\t\t_joinCountdown -= Time.Delta;\n\t\t\t_lobby?.ShowJoinCountdown( MathF.Max( 0f, _joinCountdown ) );\n\t\t\tif ( _joinCountdown \u003C 0f )\n\t\t\t{\n\t\t\t\t_lobby?.HideJoinCountdown();\n\t\t\t\tOnMatchStartedLocal();\n\t\t\t}\n\t\t}\n\n\t\t// \u83DC\u5355\u9636\u6BB5\uFF1A\u96F6\u6A21\u62DF\uFF08\u98DF\u7269/bot/\u5403\u5224\u5B9A/\u540C\u6B65\u5168\u90FD\u4E0D\u8DD1\uFF09\u2014\u2014\u9664\u975E\u80CC\u666F\u6F14\u793A\u8D5B\u5728\u8DD1\uFF08v0.6.3.0\uFF09\n\t\tif ( !_gameStarted \u0026\u0026 !_menuDemo ) return;\n\n\t\t// \u6BD4\u8D5B\u8FDB\u7A0B\uFF08M5\uFF09\uFF1Ahost \u6743\u5A01\u5012\u8BA1\u65F6\uFF0C\u5F52\u96F6\u5E7F\u64AD\u7ED3\u7B97\uFF1B\u7ED3\u675F\u540E\u5168\u573A\u51BB\u7ED3\u7531\u7ED3\u7B97\u9762\u677F\u63A5\u7BA1\n\t\tTickMatch();\n\t\tif ( MatchState.MatchOver )\n\t\t{\n\t\t\t// \u7ED3\u7B97\u663E\u793A 10 \u79D2\u540E\u81EA\u52A8\u56DE\u5927\u5385\uFF08\u591A\u4EBA\uFF09/\u4E3B\u83DC\u5355\uFF08\u5355\u673A\uFF09\uFF1BEndBoard \u7A7A\u683C\u53EF\u63D0\u524D\n\t\t\tif ( _sinceMatchOver \u003E GameConfig.SettlementAutoReturnSeconds )\n\t\t\t\tAfterSettlement();\n\t\t\treturn;\n\t\t}\n\n\t\t// \u6743\u5A01\u7AEF\u6A21\u62DF\u5DF2\u79FB\u81F3\u56FA\u5B9A\u6B65\uFF08TickSimulation\uFF0C\u89C1\u5176\u6CE8\u91CA\uFF09\u2014\u2014\u6B64\u5904\u53EA\u5269\u6E32\u67D3\u5E27\u8BE5\u505A\u7684\u4E8B\n\t\t// \uFF08\u7EC6\u80DE\u6E32\u67D3\u5E73\u6ED1\u3001\u626B\u63CF\u3001UI/\u7F51\u7EDC\u7EF4\u62A4\u3001\u6BD4\u8D5B\u8FDB\u7A0B\uFF09\uFF0C\u4E0D\u518D\u968F\u5E27\u7387\u91CD\u590D\u8DD1\u5403\u5224\u5B9A\n\n\t\t// \u523A\u7206\u5F39\u5E55\uFF08\u5BA2\u6237\u7AEF\uFF09\uFF1A\u7EAF\u89C6\u89C9\u5F39\u9053\u63A8\u8FDB\uFF08host \u5728 TickSpikes \u6743\u5A01\u6A21\u62DF\u002B\u547D\u4E2D\uFF09\n\t\tif ( !NetworkManager.IsAuthority ) TickSpinesVisual();\n\n\t\t// \u7EC6\u80DE\u6E32\u67D3\u4F4D\u7F6E\u5E73\u6ED1\uFF1Ahost \u9010\u5E27\u6743\u5A01\u4F4D\u7F6E\u76F4\u63A5\u8D34\u9F50\uFF1B\u5BA2\u6237\u7AEF\u628A 15Hz \u5FEB\u7167\u63D2\u987A\n\t\tif ( _cells.Count \u003E 0 )\n\t\t{\n\t\t\tvar st = 1f - MathF.Exp( -12f * Time.Delta );\n\t\t\tforeach ( var c in _cells )\n\t\t\t\tc.DrawPos = NetworkManager.IsAuthority ? c.Pos : Vector3.Lerp( c.DrawPos, c.Pos, st );\n\t\t}\n\n\t\t// \u7403\u6CE8\u518C\uFF1A\u626B\u63CF\u573A\u666F\u6536\u7F16\uFF08\u7F51\u7EDC\u5BF9\u8C61\u7684 OnStart \u4E0D\u89E6\u53D1\uFF0C\u5B9E\u6D4B\uFF09\u002B \u6E05\u7406\u5931\u6548\u7403\n\t\tif ( _sinceScan \u003E 1f )\n\t\t{\n\t\t\t_sinceScan = 0;\n\t\t\tScanBalls();\n\t\t}\n\n\t\t// \u5BA2\u6237\u7AEF\uFF1A\u672C\u5730\u98DF\u7269\u5217\u8868\u672A\u5C31\u7EEA\uFF08\u70ED\u91CD\u8F7D/\u65B0 FoodManager\uFF09\u2192 \u8282\u6D41\u8BF7\u6C42\u5168\u91CF\u3002\n\t\t// \u7B49\u7403\u626B\u63CF\u5230\uFF08=\u573A\u666F\u5FEB\u7167\u5DF2\u6536\uFF09\u518D\u8BF7\u6C42\uFF1B\u521A\u8FDE\u5165\u65F6 host \u7684 OnActive \u4F1A\u4E3B\u52A8\u53D1\uFF0C\u65E0\u9700\u62A2\u8DD1\n\t\tif ( !NetworkManager.IsAuthority \u0026\u0026 _food.Foods is null \u0026\u0026 _balls.Count \u003E 0 \u0026\u0026 _sinceFoodRequest \u003E 3f )\n\t\t{\n\t\t\t_sinceFoodRequest = 0;\n\t\t\ttry\n\t\t\t{\n\t\t\t\tNetworkManager.RequestFoodFull();\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\tGameLog.Info( $\u0022[net] RequestFoodFull failed (session not ready): {e.Message}\u0022 );\n\t\t\t}\n\t\t}\n\n\t\tTickRespawn();\n\t\tTickTerritory();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// host \u6743\u5A01\u6A21\u62DF\uFF08**\u56FA\u5B9A\u6B65**\uFF0CStage.FinishFixedUpdate\uFF09\uFF1A\u98DF\u7269\u91CD\u751F / \u9053\u5177\u62FE\u53D6 / bot \u51B3\u7B56 /\n\t/// \u4F4D\u79FB\u6821\u9A8C / \u5206\u8EAB\u6A21\u62DF / \u5403\u5224\u5B9A / \u5C16\u523A / \u5206\u8EAB\u4E0E\u5B62\u5B50\u5FEB\u7167\u5E7F\u64AD\u3002\n\t///\n\t/// \u4E3A\u4EC0\u4E48\u5FC5\u987B\u662F\u56FA\u5B9A\u6B65\uFF08v0.7.8.105 \u4FEE\uFF0C\u5BA1\u8BA1 #11\uFF09\uFF1A\u8FD9\u4E9B\u5FAA\u73AF\u91CC\u6709\u5927\u91CF O(\u7403 \u00D7 \u5206\u8EAB) \u4E0E\n\t/// O(\u7403\u00B2) \u7684\u5224\u5B9A\uFF0C\u539F\u5148\u6302\u5728 Stage.UpdateBones\uFF08**\u6BCF\u6E32\u67D3\u5E27**\uFF09\u4E0A\uFF0C\u4E8E\u662F\u6A21\u62DF\u9891\u7387 = \u4E3B\u673A\u5E27\u7387\u2014\u2014\n\t/// 144Hz \u4E3B\u673A\u7684\u5224\u5B9A\u5F00\u9500\u4E0E\u5403\u5224\u5B9A\u89E6\u53D1\u9891\u7387\u662F 50Hz \u7684 2.9 \u500D\uFF0C\u4E14\u4E0E\u771F\u6B63\u8DD1\u5728 50Hz \u56FA\u5B9A\u6B65\u91CC\u7684\n\t/// \u79FB\u52A8/BotBrain \u91C7\u6837\u7387\u4E0D\u4E00\u81F4\uFF08\u79FB\u52A8\u4E0E\u5403\u5224\u5B9A\u770B\u5230\u7684\u4F4D\u7F6E\u5DEE\u6700\u9AD8\u4E00\u5E27\uFF09\u3002\n\t/// \u5F15\u64CE\u4FDD\u8BC1 StartFixedUpdate/FinishFixedUpdate \u5728 \u0060Time.Scope(step, 1/FixedUpdateFrequency)\u0060\n\t/// \u5185\u89E6\u53D1\uFF08FixedUpdate.cs:32 \u002B Time.cs:37-49\uFF09\uFF0C\u6240\u4EE5\u8FD9\u91CC\u8BFB\u5230\u7684 \u0060Time.Delta\u0060 \u6052\u4E3A 1/50 \u56FA\u5B9A\u6B65\u957F\uFF0C\n\t/// \u5404 Tick \u5185\u65E2\u6709\u7684 \u0060Time.Delta\u0060 \u7528\u6CD5\u65E0\u9700\u6539\u52A8\u3002\n\t/// \u987A\u5E8F\u4E0E\u672C\u65B9\u6CD5\u5728\u540C\u4E00\u56FA\u5B9A\u6B65\u5185\u7684\u76F8\u5BF9\u6B21\u5E8F\u5373\u539F Tick \u5185\u7684\u987A\u5E8F\uFF1B\u6302\u5728 FinishFixedUpdate\n\t/// \uFF08\u800C\u975E StartFixedUpdate\uFF09\u662F\u4E3A\u4E86\u6392\u5728\u7EC4\u4EF6 OnFixedUpdate \u4E4B\u540E\uFF1A\u7403\u672C\u6B65\u5DF2\u79FB\u52A8\uFF0C\u5403\u5224\u5B9A\u62FF\u5230\u672C\u6B65\u65B0\u4F4D\u7F6E\u3002\n\t/// \u003C/summary\u003E\n\tvoid TickSimulation()\n\t{\n\t\tif ( !Game.IsPlaying ) return;\n\t\tif ( !NetworkManager.IsAuthority ) return;\n\t\tif ( !_gameStarted \u0026\u0026 !_menuDemo ) return;   // \u83DC\u5355\u9636\u6BB5\u96F6\u6A21\u62DF\uFF08\u4E0E Tick \u540C\u6B3E\u95E8\u7981\uFF09\n\t\tif ( MatchState.MatchOver ) return;          // \u7ED3\u7B97\u671F\u5168\u573A\u51BB\u7ED3\n\n\t\t_food.Tick();\n\t\t_power?.Tick( _balls, _cells );\n\t\tTickBots();\n\t\tTickMovementValidation();\n\t\tTickCells();\n\t\tTickEating();\n\t\tTickSpikes();\n\n\t\t// \u5206\u8EAB/\u5B62\u5B50\u72B6\u6001 15Hz \u5FEB\u7167\u5E7F\u64AD\uFF08\u5217\u8868\u6E05\u7A7A\u540E\u8FD8\u8981\u8865\u53D1\u4E00\u6B21\u7A7A\u5FEB\u7167\u8BA9\u5BA2\u6237\u7AEF\u4E5F\u6E05\u7A7A\uFF09\n\t\tif ( _sinceCellSync \u003E GameConfig.CellSyncInterval )\n\t\t{\n\t\t\t_sinceCellSync = 0;\n\t\t\tif ( _cells.Count \u003E 0 || _cellsDirty ) BroadcastCells();\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u9886\u571F\u6A21\u5F0F\u6BCF\u5E27\uFF08M7\uFF09\uFF1Ahost \u68C0\u67E5\u5F81\u670D\u80DC\u5229 \u002B \u53D8\u5316/\u4FDD\u6D3B\u5E7F\u64AD 5Hz \u5FEB\u7167\uFF1B\u5BA2\u6237\u7AEF\u53EA\u7B49 ApplyRemote\u3002\n\t/// \u7EC8\u5C40\u5FEB\u7167\u5148\u884C\u2014\u2014\u5BA2\u6237\u7AEF\u7ED3\u7B97\u699C\u6309\u9886\u571F\u80DC\u8D1F\u663E\u793A\uFF0C\u6162\u4E00\u62CD\u5C31\u4F1A\u6309\u65E7\u683C\u6570\u5224\u9519\u51A0\u519B \u003C/summary\u003E\n\tvoid TickTerritory()\n\t{\n\t\tif ( !MatchState.IsTerritory || MatchState.MatchOver ) return;\n\n\t\tif ( !NetworkManager.IsAuthority )\n\t\t{\n\t\t\t_sinceTerritoryWire = 1f;\n\t\t\treturn;\n\t\t}\n\n\t\tTerritoryManager.TickPoints( Time.Delta );   // \u5360\u65D7\u79EF\u5206\uFF08\u6BCF\u65D7\u6BCF 3s \u002B1\uFF0CM7.4 \u7528\u6237\u5B9A\u7A3F\uFF09\n\t\tClassSkillManager.Tick();                    // \u5766\u514B\u4E34\u65F6\u8D28\u91CF\u5230\u671F\u56DE\u6536\n\n\t\tif ( TerritoryManager.ConquerWinner \u003E= 0 )\n\t\t{\n\t\t\tNetworkManager.TerritoryState( TerritoryManager.Snapshot(), TerritoryManager.PointsWire() );\n\t\t\tEndMatch();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( TerritoryManager.NeedsSync || _sinceTerritoryWire \u003E 2f )\n\t\t{\n\t\t\t_sinceTerritoryWire = 0;\n\t\t\tNetworkManager.TerritoryState( TerritoryManager.Snapshot(), TerritoryManager.PointsWire() );\n\t\t}\n\t}\n\n\t// ---- \u5403\u4E0E\u6210\u957F ----\n\n\tvoid TickEating()\n\t{\n\t\t// \u7403 \u00D7 \u98DF\u7269\uFF1A\u98DF\u7269\u4E2D\u5FC3\u8FDB\u5165\u7403\u4F53\u5373\u88AB\u5403\uFF08\u7A7A\u95F4\u54C8\u5E0C\u7C97\u7B5B \u002B \u7CBE\u786E\u590D\u6838\uFF0C4140 \u9897\u65F6\u7701 ~20 \u4E07\u6B21/\u5E27\uFF09\n\t\t// \u78C1\u94C1 buff\uFF08v0.7.3.0\uFF09\uFF1A\u5438\u98DF\u534A\u5F84\u7FFB\u500D\u2014\u2014\u9694\u7740\u4E00\u4E2A\u8EAB\u4F4D\u4E5F\u80FD\u628A\u98DF\u7269\u5438\u8FDB\u5634\u91CC\uFF08\u53EA\u5F71\u54CD\u98DF\u7269\uFF09\n\t\tvar foods = _food.Foods;\n\t\tforeach ( var ball in _balls )\n\t\t{\n\t\t\tif ( !ball.IsValid() || !ball.Alive ) continue;\n\n\t\t\tvar bp = ball.WorldPosition;\n\t\t\tvar r = ball.HasBuff( PowerUpManager.Kind.Magnet ) ? ball.Radius * 2f : ball.Radius;\n\t\t\tvar near = _food.Nearby( bp, r );\n\n\t\t\tfor ( int k = 0; k \u003C near.Count; k\u002B\u002B )\n\t\t\t{\n\t\t\t\tint i = near[k];\n\t\t\t\tif ( !foods[i].Alive ) continue;\n\n\t\t\t\tvar dx = foods[i].Pos.x - bp.x;\n\t\t\t\tvar dy = foods[i].Pos.y - bp.y;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E r * r ) continue;\n\n\t\t\t\tif ( _food.TryEat( i, ball.OwnerSteamId ) )\n\t\t\t\t{\n\t\t\t\t\tball.Mass = MathF.Min( GameConfig.MaxMass, ball.Mass \u002B GameConfig.FoodMass );\n\t\t\t\t\tNeonRenderer.Spark( new Vector3( foods[i].Pos.x, foods[i].Pos.y, 0f ), foods[i].ColorIndex );\n\t\t\t\t\tif ( GameSfx.IsMine( ball.OwnerSteamId ) )\n\t\t\t\t\t\tGameSfx.EatFood( new Vector3( foods[i].Pos.x, foods[i].Pos.y, 0f ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// \u8D28\u91CF\u8870\u51CF\uFF08v0.7.8.24 \u7403\u7403\u5927\u4F5C\u6218\u624B\u611F\uFF09\uFF1A\u5927\u7403\u6301\u7EED\u6389\u91CD\uFF0C\u538B\u5236\u6EDA\u96EA\u7403\u3001\u903C\u5927\u54E5\u4E0D\u505C\u884C\u52A8\u3002\n\t\t// \u5C0F\u7403\u514D\u8870\u51CF\uFF1B\u5206\u8EAB\u540C\u7387\uFF08TickCells\uFF09\u3002Mass \u662F [Sync(FromHost)]\uFF0C\u8870\u51CF\u503C\u81EA\u52A8\u4E0B\u53D1\u3002\n\t\t// \u9886\u571F\u6A21\u5F0F\uFF08M7 \u7528\u6237\u5B9A\u7A3F\u2461\uFF09\uFF1A\u5DF1\u65B9\u7403\u5728\u5DF1\u65B9\u5927\u672C\u8425\u5185\u514D\u8870\u51CF\uFF08\u56DE\u5BB6\u4FDD\u517B\u7684\u62C9\u626F\u70B9\uFF09\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.Alive ) continue;\n\t\t\tif ( b.Mass \u003E GameConfig.MassDecayMinMass\n\t\t\t\t\u0026\u0026 !( GameConfig.TerritoryHqStopsDecay \u0026\u0026 TerritoryManager.IsOwnHq( b.TeamIndex, b.WorldPosition ) ) )\n\t\t\t\tb.Mass = MathF.Max( GameConfig.MassDecayMinMass, b.Mass - b.Mass * GameConfig.MassDecayPerSecond * Time.Delta );\n\t\t}\n\n\t\t// \u654C\u8425\u5B89\u5168\u5C4B\uFF08M7.2\uFF09\uFF1A\u654C\u65B9\u7403\u95EF\u5927\u672C\u8425\u6301\u7EED\u6389\u91CD\u2014\u2014\u9A71\u8D76\u800C\u4E0D\u5904\u5211\uFF08\u4E0B\u9650\u51FA\u751F\u8D28\u91CF\uFF09\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.Alive ) continue;\n\t\t\tif ( TerritoryManager.InHostileHq( b.TeamIndex, b.WorldPosition ) )\n\t\t\t\tb.Mass = MathF.Max( GameConfig.StartMass, b.Mass - b.Mass * GameConfig.TerritoryHqHurtPerSecond * Time.Delta );\n\t\t}\n\n\t\t// \u7403 \u00D7 \u7403\uFF1A\u89E6\u78B0\u5373\u5403\uFF08\u8D28\u91CF\u6BD4\u95E8\u69DB\uFF1B\u62A4\u76FE\u671F/\u6B7B\u4EA1\u514D\u6218\uFF09\u3002\n\t\t// \u6B7B\u4EA1\u4E0D\u9500\u6BC1\u5BF9\u8C61\u2014\u2014MarkDead \u540E\u539F\u5730\u590D\u7528\uFF08\u8FDB\u884C\u4E2D NetworkSpawn \u4E0D\u590D\u5236\u5230\u5DF2\u8FDE\u63A5\u5BA2\u6237\u7AEF\uFF0C\u5B9E\u6D4B\uFF09\n\t\tfor ( int i = 0; i \u003C _balls.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = _balls[i];\n\t\t\tif ( !a.IsValid() || !a.Alive ) continue;\n\n\t\t\tfor ( int j = i \u002B 1; j \u003C _balls.Count; j\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar b = _balls[j];\n\t\t\t\tif ( !b.IsValid() || !b.Alive ) continue;\n\n\t\t\t\tvar bigger = a.Mass \u003E= b.Mass ? a : b;\n\t\t\t\tvar smaller = bigger == a ? b : a;\n\t\t\t\tif ( SameTeam( a, b ) ) continue;   // \u961F\u53CB\u514D\u4F24\uFF08\u56E2\u961F\u8D5B\uFF0Cv0.6.5.0\uFF09\n\t\t\t\tif ( bigger.Mass \u003C smaller.Mass * GameConfig.EatRatio ) continue;\n\t\t\t\tif ( smaller.IsProtected || bigger.IsProtected ) continue;   // \u62A4\u76FE\u671F\u53CC\u5411\u514D\u6218\n\t\t\t\tif ( smaller.HasBuff( PowerUpManager.Kind.Shield ) ) continue;   // \u9053\u5177\u62A4\u76FE\uFF1A\u4E0D\u53EF\u88AB\u5403\uFF08\u4E0D\u9632\u5C16\u523A\uFF09\n\n\t\t\t\tvar d = bigger.WorldPosition.Distance( smaller.WorldPosition );\n\t\t\t\tif ( d \u003E= bigger.Radius \u002B smaller.Radius ) continue;   // \u89E6\u78B0\u5373\u5403\uFF08\u7528\u6237\u5B9A\u7A3F\uFF09\n\n\t\t\t\t// \u541E\u5E76\u5C01\u9876\uFF1A\u4E0D\u8BBE\u4E0A\u9650\u7684\u8BDD\u8FDE\u9501\u541E\u566C\u4F1A\u8BA9\u534A\u5F84\u8D85\u8FC7\u573A\u5730\u4E00\u534A\uFF0C\n\t\t\t\t// \u8FB9\u754C\u94B3\u5236 min\u003Emax \u629B\u5F02\u5E38\uFF08\u6BCF\u5E27\u5237\u5C4F\uFF09\u4E14\u91CD\u751F\u8005\u79D2\u88AB\u5403\uFF08\u6B7B\u4EA1\u5FAA\u73AF\uFF09\u2014\u2014\u5B9E\u6D4B\n\t\t\t\tfloat gained = smaller.Mass;\n\t\t\t\tbigger.Mass = MathF.Min( GameConfig.MaxMass, bigger.Mass \u002B gained );\n\t\t\t\tSpawnPopFx( smaller );\n\t\t\t\t// \u541E\u7403\u97F3\u53EA\u5BF9\u53CC\u65B9\u64AD\uFF08\u5403\u8005 \u002B \u88AB\u5403\u8005\uFF09\u002B \u5168\u573A\u0022\u8C01\u5403\u4E86\u8C01\u0022\u64AD\u62A5\uFF08\u53CC\u7AEF\u7EDF\u4E00\u5165\u53E3\uFF09\n\t\t\t\tNotifyBallEaten( bigger.OwnerSteamId, smaller.OwnerSteamId, gained );\n\n\t\t\t\t// agar \u591A\u8EAB\u4F53\u89C4\u5219\uFF08v0.6.2.0 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u5355\u4E2A\u8EAB\u4F53\u6B7B\u4EA1\u4E0D\u5BFC\u81F4\u73A9\u5BB6\u6B7B\u4EA1\u2014\u2014\n\t\t\t\t// \u4E3B\u7403\u88AB\u5403\u65F6\u6700\u5927\u7684\u5206\u8EAB\u5347\u7EA7\u6210\u65B0\u4E3B\u7403\uFF0C\u5168\u90E8\u8EAB\u4F53\u6B7B\u5149\u624D\u7B97\u6B7B\n\t\t\t\tif ( TryPromoteFromPieces( smaller ) )\n\t\t\t\t{\n\t\t\t\t\tGameLog.Info( $\u0022[game] body lost: {smaller.PlayerName} \u003C- {bigger.PlayerName} (promoted)\u0022 );\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tsmaller.MarkDead();\n\t\t\t\tGameLog.Info( \u0022[game] eaten: \u0022 \u002B smaller.PlayerName \u002B \u0022 \u003C- \u0022 \u002B bigger.PlayerName );\n\t\t\t}\n\t\t}\n\n\t\t// ---- \u5206\u8EAB/\u5B62\u5B50\u53C2\u4E0E\u5403\uFF08M3\uFF09----\n\n\t\t// \u5206\u8EAB \u00D7 \u98DF\u7269\uFF08\u540C\u6B3E\u7A7A\u95F4\u54C8\u5E0C\u7C97\u7B5B\uFF09\n\t\tforeach ( var c in _cells )\n\t\t{\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\n\t\t\tvar cp = c.Pos;\n\t\t\tvar cr = c.Radius;\n\t\t\tvar near = _food.Nearby( cp, cr );\n\n\t\t\tfor ( int k = 0; k \u003C near.Count; k\u002B\u002B )\n\t\t\t{\n\t\t\t\tint i = near[k];\n\t\t\t\tif ( !foods[i].Alive ) continue;\n\n\t\t\t\tvar dx = foods[i].Pos.x - cp.x;\n\t\t\t\tvar dy = foods[i].Pos.y - cp.y;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E cr * cr ) continue;\n\n\t\t\t\tif ( _food.TryEat( i, c.OwnerSteamId ) )\n\t\t\t\t{\n\t\t\t\t\tc.Mass = MathF.Min( GameConfig.MaxMass, c.Mass \u002B GameConfig.FoodMass );\n\t\t\t\t\tNeonRenderer.Spark( new Vector3( foods[i].Pos.x, foods[i].Pos.y, 0f ), foods[i].ColorIndex );\n\t\t\t\t\tif ( GameSfx.IsMine( c.OwnerSteamId ) )\n\t\t\t\t\t\tGameSfx.EatFood( new Vector3( foods[i].Pos.x, foods[i].Pos.y, 0f ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// \u961F\u4F0D\u6620\u5C04\uFF08\u56E2\u961F\u8D5B\uFF09\uFF1A\u5B62\u5B50\u53EF\u98DF\u5EF6\u8FDF\u5206\u6863 \u002B \u4E0B\u65B9\u7403\u00D7\u5206\u8EAB\u514D\u4F24\u5171\u7528\n\t\tvar teamOf = BuildTeamMap();\n\n\t\t// \u5B62\u5B50\u88AB\u541E\uFF1A\u81EA\u5DF1\u002B\u961F\u53CB 0.2s \u6863\uFF08v0.7.8.22/23 \u7528\u6237\u5B9A\u7A3F\uFF0C\u7403\u7403\u5927\u4F5C\u6218\u5F0F\u77ED\u65F6\u4E92\u5582\u2014\u2014\n\t\t// \u81EA\u5DF1\u9A6C\u4E0A\u56DE\u6536\u3001\u5582\u961F\u53CB\u5927\u54E5\u8DDF\u81EA\u98DF\u4E00\u6837\u987A\uFF09\uFF1B\u654C\u4EBA 0.5s \u6863\u30020.2s \u53EA\u662F\u51FA\u751F\u51C0\u7A7A\uFF1A\n\t\t// \u5927\u7403\u5410\u7684\u5B62\u5B50\u534A\u5F84\u53EF\u8D85\u8FC7 16px \u51FA\u751F\u504F\u79FB\uFF0C\u96F6\u5EF6\u8FDF\u4F1A\u88AB\u5410\u51FA\u8005\u51FA\u751F\u540C\u5E27\u5438\u56DE\uFF0C\u5410\u4E1D\u76F4\u63A5\u5931\u6548\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar blob = _cells[i];\n\t\t\tif ( blob.PieceKind != CellPiece.Kind.EjectedMass ) continue;\n\n\t\t\tbool eaten = false;\n\n\t\t\t// \u5B62\u5B50\u5582\u9053\u5177\uFF08\u0022\u8840\u91CF\u6C60\u0022\uFF0Cv0.7.4.0 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u5B62\u5B50\u78B0\u9053\u5177\u88AB\u5438\u6536\u5145\u5165\u5171\u4EAB\u79EF\u5206\u6C60\uFF0C\n\t\t\t// \u79EF\u5206 \u2265 \u5582\u98DF\u8005\u5F53\u524D\u4F53\u91CD 5% \u5373\u89E6\u53D1\u6548\u679C\uFF08\u7ACB\u5373\u751F\u6548\uFF0C\u4E0D\u5165\u80CC\u5305\uFF09\u2014\u2014\u864E\u53E3\u593A\u98DF\u3002\n\t\t\t// \u26A0\uFE0F \u5224\u5B9A\u5FC5\u987B\u5728 EjectedEdibleDelay **\u4E4B\u524D**\uFF08v0.7.5.0 \u4FEE\u0022\u5582\u4E0D\u4E2D\u0022bug\uFF09\uFF1A\n\t\t\t// \u5B62\u5B50\u5410\u51FA\u6ED1\u884C ~355 \u8DDD\u79BB\uFF0C0.5s \u53EF\u98DF\u671F\u5230\u65F6\u5DF2\u98DE\u8FC7 236\u2014\u2014\u7ECF\u8FC7\u9053\u5177\u7684\u90A3\u4E00\u5E27\u51E0\u4E4E\n\t\t\t// \u603B\u5728\u5EF6\u8FDF\u671F\u5185\uFF0C\u7B49\u5EF6\u8FDF\u8FC7\u4E86\u5B62\u5B50\u65E9\u98DE\u8FDC\u4E86\u3002\u5582\u98DF\u65E0\u8D28\u91CF\u56DE\u6D41\uFF0C\u6CA1\u6709\u0022\u79D2\u5403\u81EA\u5DF1\u0022\u6F0F\u6D1E\u3002\n\t\t\tif ( _power?.Ready ?? false )\n\t\t\t{\n\t\t\t\tvar slots = _power.Slots;\n\t\t\t\tfor ( int s = 0; s \u003C slots.Count; s\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar slot = slots[s];\n\t\t\t\t\tif ( !slot.Alive ) continue;\n\n\t\t\t\t\tvar pp = new Vector3( slot.Pos.x, slot.Pos.y, 0f );\n\t\t\t\t\tif ( blob.Pos.Distance( pp ) \u003E= GameConfig.PowerUpRadius \u002B 8f ) continue;\n\n\t\t\t\t\teaten = true;\n\t\t\t\t\t_power.Feed( FindBallBySteamId( blob.OwnerSteamId ), s, blob.Mass );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif ( eaten )\n\t\t\t\t{\n\t\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t\t_cellsDirty = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tforeach ( var b in _balls )\n\t\t\t{\n\t\t\t\tif ( !b.IsValid() || !b.Alive ) continue;\n\t\t\t\tvar ally = SameSide( blob.OwnerSteamId, b.OwnerSteamId, teamOf );\n\t\t\t\tif ( blob.SinceSpawn \u003C ( ally ? GameConfig.EjectedAllyEdibleDelay : GameConfig.EjectedEdibleDelay ) ) continue;\n\t\t\t\tif ( b.WorldPosition.Distance( blob.Pos ) \u003C b.Radius \u002B blob.Radius )\n\t\t\t\t{\n\t\t\t\t\tb.Mass = MathF.Min( GameConfig.MaxMass, b.Mass \u002B blob.Mass );\n\t\t\t\t\teaten = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( !eaten )\n\t\t\t{\n\t\t\t\tforeach ( var c in _cells )\n\t\t\t\t{\n\t\t\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c == blob ) continue;\n\t\t\t\t\tvar ally = SameSide( blob.OwnerSteamId, c.OwnerSteamId, teamOf );\n\t\t\t\t\tif ( blob.SinceSpawn \u003C ( ally ? GameConfig.EjectedAllyEdibleDelay : GameConfig.EjectedEdibleDelay ) ) continue;\n\t\t\t\t\tif ( c.Pos.Distance( blob.Pos ) \u003C c.Radius \u002B blob.Radius )\n\t\t\t\t\t{\n\t\t\t\t\t\tc.Mass = MathF.Min( GameConfig.MaxMass, c.Mass \u002B blob.Mass );\n\t\t\t\t\t\teaten = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( eaten )\n\t\t\t{\n\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t_cellsDirty = true;\n\t\t\t}\n\t\t}\n\n\t\t// \u8DE8\u4E3B\u4EBA\uFF1A\u7403 \u00D7 \u5206\u8EAB\uFF08\u540C\u4E3B\u4EBA\u78B0\u649E\u8D70 TickCells \u7684\u5408\u5E76\uFF0C\u4E0D\u5728\u8FD9\u91CC\uFF09\n\t\t// \u961F\u53CB\u514D\u4F24\uFF08v0.6.5.0\uFF09\uFF1A\u56E2\u961F\u8D5B\u4E0B\u5206\u8EAB\u4E0D\u4E0E\u961F\u53CB\u7684\u4E3B\u7403\u4E92\u5403\u2014\u2014teamOf \u5DF2\u5728\u4E0A\u65B9\u5EFA\u597D\n\n\t\tfor ( int i = 0; i \u003C _balls.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar ball = _balls[i];\n\t\t\tif ( !ball.IsValid() || !ball.Alive ) continue;\n\t\t\tif ( ball.IsProtected ) continue;   // \u62A4\u76FE\u671F\u53CC\u5411\u514D\u6218\uFF08\u5206\u8EAB\u6CA1\u6709\u62A4\u76FE\uFF0C\u53EA\u4F1A\u88AB\u5403\uFF09\n\n\t\t\tfor ( int j = _cells.Count - 1; j \u003E= 0; j-- )\n\t\t\t{\n\t\t\t\tvar c = _cells[j];\n\t\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\t\tif ( c.OwnerSteamId == ball.OwnerSteamId ) continue;\n\t\t\t\tif ( MatchState.IsTeam\n\t\t\t\t\t\u0026\u0026 teamOf.TryGetValue( c.OwnerSteamId, out var pt )\n\t\t\t\t\t\u0026\u0026 pt == ball.TeamIndex ) continue;   // \u961F\u53CB\u7684\u5206\u8EAB/\u4E3B\u7403\u4E92\u4E0D\u5403\n\n\t\t\t\t// \u7403\u541E\u5206\u8EAB\n\t\t\t\tif ( ball.Mass \u003E= c.Mass * GameConfig.EatRatio\n\t\t\t\t\t\u0026\u0026 ball.WorldPosition.Distance( c.Pos ) \u003C ball.Radius \u002B c.Radius )\n\t\t\t\t{\n\t\t\t\t\tball.Mass = MathF.Min( GameConfig.MaxMass, ball.Mass \u002B c.Mass );\n\t\t\t\t\t_cells.RemoveAt( j );\n\t\t\t\t\t_cellsDirty = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// \u5206\u8EAB\u53CD\u541E\u4E3B\u7403\uFF08v0.6.2.0 agar \u89C4\u5219\uFF09\uFF1A\u53EA\u635F\u5931\u4E3B\u7403\u8FD9\u4E2A\u8EAB\u4F53\u2014\u2014\u6700\u5927\u5206\u8EAB\u5347\u7EA7\u6210\u65B0\u4E3B\u7403\n\t\t\t\t// \u5E26\u76FE\u4E3B\u7403\u514D\u75AB\uFF08v0.7.3.0\uFF09\uFF1A\u62A4\u76FE\u8FDE\u0022\u88AB\u5206\u8EAB\u541E\u0022\u4E5F\u6321\u4F4F\n\t\t\t\tif ( !ball.HasBuff( PowerUpManager.Kind.Shield )\n\t\t\t\t\t\u0026\u0026 c.Mass \u003E= ball.Mass * GameConfig.EatRatio\n\t\t\t\t\t\u0026\u0026 c.Pos.Distance( ball.WorldPosition ) \u003C c.Radius \u002B ball.Radius )\n\t\t\t\t{\n\t\t\t\t\tfloat gained = ball.Mass;\n\t\t\t\t\tc.Mass = MathF.Min( GameConfig.MaxMass, c.Mass \u002B gained );\n\t\t\t\t\tSpawnPopFx( ball );\n\t\t\t\t\tNotifyBallEaten( c.OwnerSteamId, ball.OwnerSteamId, gained );\n\n\t\t\t\t\tif ( TryPromoteFromPieces( ball ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tGameLog.Info( $\u0022[game] body lost: {ball.PlayerName} \u003C- cell#{c.Id} (promoted)\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tball.MarkDead();\n\t\t\t\t\t\tif ( !ball.IsBot ) PopPiecesOf( ball.OwnerSteamId );\n\t\t\t\t\t\tGameLog.Info( \u0022[game] eaten: \u0022 \u002B ball.PlayerName \u002B \u0022 \u003C- cell#\u0022 \u002B c.Id );\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// \u8DE8\u4E3B\u4EBA\uFF1A\u5206\u8EAB \u00D7 \u5206\u8EAB\uFF08i \u5347\u5E8F\u904D\u5386\uFF1B\u88AB\u541E\u7684\u81EA\u5DF1\u8981\u9000\u7D22\u5F15\uFF09\n\t\tfor ( int i = 0; i \u003C _cells.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = _cells[i];\n\t\t\tif ( a.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\n\t\t\tfor ( int j = _cells.Count - 1; j \u003E i; j-- )\n\t\t\t{\n\t\t\t\tvar b = _cells[j];\n\t\t\t\tif ( b.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\t\tif ( b.OwnerSteamId == a.OwnerSteamId ) continue;\n\t\t\t\tif ( MatchState.IsTeam\n\t\t\t\t\t\u0026\u0026 teamOf.TryGetValue( a.OwnerSteamId, out var ta )\n\t\t\t\t\t\u0026\u0026 teamOf.TryGetValue( b.OwnerSteamId, out var tb )\n\t\t\t\t\t\u0026\u0026 ta == tb ) continue;   // \u961F\u53CB\u5206\u8EAB\u4E92\u4E0D\u5403\uFF08v0.6.5.0\uFF09\n\n\t\t\t\tvar big = a.Mass \u003E= b.Mass ? a : b;\n\t\t\t\tvar small = big == a ? b : a;\n\t\t\t\tif ( big.Mass \u003C small.Mass * GameConfig.EatRatio ) continue;\n\t\t\t\tif ( big.Pos.Distance( small.Pos ) \u003E= big.Radius \u002B small.Radius ) continue;\n\n\t\t\t\tbig.Mass = MathF.Min( GameConfig.MaxMass, big.Mass \u002B small.Mass );\n\t\t\t\tNeonRenderer.Puff( small.Pos, small.ColorIndex );   // \u88AB\u541E\u53CD\u9988\uFF08v0.7.8.92\uFF09\uFF1Ahost \u4FA7\uFF1B\u5BA2\u6237\u7AEF\u7531\u5FEB\u7167\u79FB\u9664\u7EDF\u4E00\u8865\n\t\t\t\t_cellsDirty = true;\n\t\t\t\tif ( small == a )\n\t\t\t\t{\n\t\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t\ti--;   // \u4E0B\u4E00\u4E2A\u5143\u7D20\u79FB\u4F4D\u8865\u4F4D\uFF0C\u5916\u5C42 i\u002B\u002B \u540E\u6B63\u597D\u6307\u5411\u5B83\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t_cells.RemoveAt( j );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E bot \u6B7B\u4EA1\u540E\u539F\u5730\u590D\u6D3B\uFF08\u4E0D\u518D\u9500\u6BC1\u91CD\u5EFA\u2014\u2014\u8FDB\u884C\u4E2D NetworkSpawn \u4E0D\u590D\u5236\u5230\u5DF2\u8FDE\u63A5\u5BA2\u6237\u7AEF\uFF0C\u5B9E\u6D4B\uFF09\n\t/// \u26A0\uFE0F \u86F0\u4F0F\u7403\uFF08Dormant\uFF09\u4E0D\u662F\u6B7B\u4EA1\u2014\u2014\u590D\u6D3B\u5B83=\u51ED\u7A7A\u591A\u51FA\u65E0\u961F FFA bot\uFF08v0.7.8.25 \u4FEE\uFF09 \u003C/summary\u003E\n\tvoid TickBots()\n\t{\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.IsBot || b.Alive ) continue;\n\t\t\tif ( b.Dormant ) continue;   // \u86F0\u4F0F\u5907\u7528\u7403\uFF1A\u7B49\u5F00\u5C40\u6FC0\u6D3B\uFF0C\u7EDD\u4E0D\u81EA\u52A8\u590D\u6D3B\n\t\t\tif ( b.SinceDeath \u003C GameConfig.BotRespawnSeconds ) continue;\n\n\t\t\t// \u9886\u571F\u6A21\u5F0F bot \u81EA\u52A8\u4E0A\u524D\u7EBF\uFF08M7.2\uFF09\uFF1A\u6CA1\u524D\u7EBF\u683C\u65F6 TryFrontSpawn \u5185\u90E8\u56DE\u843D\u5927\u672C\u8425\n\t\t\tRespawnBall( b, frontLine: MatchState.IsTerritory );\n\t\t}\n\t}\n\n\tvoid TickRespawn()\n\t{\n\t\tif ( !LocalBall.IsValid() ) return;\n\t\tif ( LocalBall.Alive ) return;\n\t\tif ( !Input.Pressed( \u0022Jump\u0022 ) ) return;\n\n\t\tTryRespawnLocal();\n\t}\n\n\t/// \u003Csummary\u003E \u672C\u673A\u91CD\u751F\u8BF7\u6C42\uFF08\u7A7A\u683C / \u6B7B\u4EA1\u9762\u677F\u70B9\u51FB\u5171\u7528\uFF09\uFF1A\u6743\u5A01\u7AEF\u76F4\u63A5\u539F\u5730\u590D\u6D3B\uFF1B\n\t/// \u5BA2\u6237\u7AEF\u53D1\u9759\u6001 RPC\uFF08host \u7AEF Rpc.Caller \u627E\u56DE\u8FDE\u63A5\uFF0CAlive \u540C\u6B65\u4E0B\u53D1\uFF09\u3002\n\t/// \u9886\u571F\u6A21\u5F0F frontLine=true \u590D\u6D3B\u5230\u6700\u8FD1\u5DF1\u65B9\u5360\u9886\u683C\uFF08M7.2 \u4E8C\u9009\u4E00\uFF0C\u9ED8\u8BA4\u5927\u672C\u8425\uFF09\u3002\n\t/// \u83DC\u5355/\u623F\u95F4\u9636\u6BB5\u4E0D\u53D7\u7406\uFF08\u86F0\u4F0F\u7403\u662F\u6B7B\u7684\uFF0C\u4F46\u6BD4\u8D5B\u6CA1\u5F00\u59CB\uFF1B\u7A7A\u683C\u5728\u7ED3\u7B97\u9762\u677F\u53E6\u6709\u7528\u9014\uFF09 \u003C/summary\u003E\n\tpublic void TryRespawnLocal( bool frontLine = false )\n\t{\n\t\tif ( !_gameStarted || MatchState.MatchOver ) return;\n\t\tif ( !LocalBall.IsValid() || LocalBall.Alive ) return;\n\n\t\tGameLog.Info( \u0022[game] local respawn requested\u0022 );\n\t\tif ( NetworkManager.IsAuthority )\n\t\t{\n\t\t\tRespawnBall( LocalBall, frontLine: frontLine );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// \u8FDC\u7A0B\u7403\u7684\u4F4D\u7F6E\u7531 owner \u81EA\u5DF1\u7B97\uFF08Alive \u7FFB\u8F6C\u540E\u672C\u5730\u6362\u4F4D\uFF09\u2014\u2014\u524D\u7EBF\u504F\u597D\u968F\u70B9\u51FB\u5B58\u672C\u673A\uFF0Chost \u4F20\u53C2\u65E0\u610F\u4E49\n\t\t\t_pendingFrontRespawn = frontLine;\n\t\t\tNetworkManager.RequestRespawn( frontLine );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u53D6\u8D70\u672C\u673A\u4E0B\u6B21\u590D\u6D3B\u7684\u0022\u524D\u7EBF\u0022\u504F\u597D\uFF08owner \u6362\u4F4D\u65F6\u7528\uFF1B\u6BCF\u6B21\u91CD\u751F\u8BF7\u6C42\u90FD\u4F1A\u91CD\u5199\uFF0C\u6D88\u8D39\u5373\u6E05\uFF09 \u003C/summary\u003E\n\tpublic bool ConsumeFrontRespawn()\n\t{\n\t\tvar v = _pendingFrontRespawn;\n\t\t_pendingFrontRespawn = false;\n\t\treturn v;\n\t}\n\n\t/// \u003Csummary\u003E \u539F\u5730\u590D\u6D3B\u4E00\u9897\u7403\uFF1A\u91CD\u7F6E\u6570\u503C \u002B Alive=true\uFF08\u540C\u6B65\u4E0B\u53D1\uFF1B\u5BF9\u8C61\u6C38\u4E0D\u9500\u6BC1\uFF09\u3002\n\t/// teamIndex \u2265 0 \u65F6\u91CD\u5199\u961F\u4F0D\u53F7\uFF08\u5F00\u5C40\u6FC0\u6D3B\u65F6\u7684\u56E2\u961F\u5206\u914D\uFF1B\u666E\u901A\u8D5B -1 \u4E0D\u52A8\uFF09\u3002\n\t/// \u9886\u571F\u6A21\u5F0F frontLine=true \u590D\u6D3B\u5230\u6700\u8FD1\u5DF1\u65B9\u5360\u9886\u683C\uFF08M7.2 \u4E8C\u9009\u4E00\uFF09\uFF0C\u5426\u5219\u5927\u672C\u8425\u3002\n\t/// cue=false \u7528\u4E8E\u5F00\u5C40\u6279\u91CF\u6FC0\u6D3B\uFF08\u63D0\u793A\u97F3\u53EA\u7528\u4E8E\u6BD4\u8D5B\u4E2D\u7684\u590D\u6D3B\uFF0Cv0.7.8.11\uFF09 \u003C/summary\u003E\n\tvoid RespawnBall( Ball ball, int teamIndex = -1, bool cue = true, bool frontLine = false )\n\t{\n\t\tball.Mass = GameConfig.StartMass;\n\t\t// \u56E2\u961F\u8D5B\u515C\u5E95\uFF08v0.7.8.25\uFF09\uFF1A\u65E0\u961F\u7403\uFF08\u86F0\u4F0F\u8BEF\u590D\u6D3B/\u5386\u53F2\u5F02\u5E38\u6001\uFF09\u590D\u6D3B\u65F6\u8865\u7F16\u961F\uFF0C\u7EDD\u4E0D\u5E26 T=-1 \u4E0A\u573A\n\t\tif ( teamIndex \u003C 0 \u0026\u0026 MatchState.IsTeam \u0026\u0026 ball.TeamIndex \u003C 0 )\n\t\t\tteamIndex = NextTeamIndex();\n\t\tball.Init( ball.IsBot, teamIndex );   // \u91CD\u7F6E\u4FDD\u62A4\u671F/\u901F\u5EA6/\u989C\u8272\uFF08\u540C\u6B65\u4E0B\u53D1\uFF09\n\t\tball.Dormant = false;\n\t\tball.Alive = true;\n\n\t\t// \u9886\u571F\u804C\u4E1A\uFF08M7.4\uFF09\uFF1A\u7CFB\u7EDF\u968F\u673A\u5206\u914D\uFF0C\u6BCF\u6B21\u590D\u6D3B\u91CD\u63B7\u2014\u2014\u0022\u7531\u7CFB\u7EDF\u51B3\u5B9A\u0022\u4E14\u65E0\u8DE8\u5C40\u6B8B\u7559\u6001\n\t\tif ( MatchState.IsTerritory )\n\t\t\tball.TerritoryClass = (byte)Game.Random.Int( 0, 3 );\n\n\t\tClassSkillManager.ResetBall( ball );   // \u804C\u4E1A\u91CD\u63B7\u4E86\u5C31\u628A\u65E7\u51B7\u5374\u6E05\u6389\uFF08v0.7.8.87\uFF09\uFF0C\u5426\u5219\u65B0\u804C\u4E1A\u6309\u4E0D\u52A8\n\n\t\t// host \u81EA\u5DF1\u7684\u7403/bot\uFF1A\u76F4\u63A5\u6362\u4F4D\uFF1B\u771F\u4EBA\u5BA2\u6237\u7AEF\u7684\u7403\uFF1A\u8BE5\u5BA2\u6237\u7AEF\u5728 Alive\u2192true \u65F6\u81EA\u884C\u968F\u673A\u6362\u4F4D\uFF08owner \u6A21\u62DF\uFF09\n\t\tif ( ball.IsBot || ball.OwnerSteamId == Game.SteamId.Value )\n\t\t{\n\t\t\t// nearPos \u53D6\u6B7B\u4EA1\u4F4D\u7F6E\uFF08\u6B64\u523B\u8FD8\u6CA1\u88AB\u8986\u76D6\uFF09\u2014\u2014\u524D\u7EBF\u590D\u6D3B\u6311\u6700\u8FD1\u7684\u5360\u9886\u683C\uFF08M7.2\uFF09\n\t\t\tball.WorldPosition = RandomSpawnPos( ball.TeamIndex, frontLine, ball.WorldPosition );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// \u8FDC\u7A0B\u7403\uFF1Ahost \u4E0D\u4EE3\u6362\u4F4D\uFF0Cowner \u6536\u5230 Alive \u540E\u81EA\u884C\u77AC\u79FB\u2014\u2014\u5F00\u4F4D\u79FB\u6821\u9A8C\u5BBD\u9650\u671F\uFF0C\n\t\t\t// \u5426\u5219 owner \u7684\u5E72\u7EA7\u522B\u4F4D\u7F6E\u66F4\u65B0\u4F1A\u88AB TickMovementValidation \u6C38\u4E45\u62C9\u56DE\u65E7\u5904\uFF08v0.7.8.33 \u4FEE\uFF09\n\t\t\t_teleportGrace[ball.Id] = 0;\n\t\t}\n\n\t\tGameLog.Info( $\u0022[net] respawned \u0027{ball.PlayerName}\u0027\u0022 );\n\n\t\tif ( cue ) CueRespawn( ball.OwnerSteamId, ball.TeamIndex );\n\t}\n\n\t/// \u003Csummary\u003E \u672C\u673A\u662F\u5426\u8BE5\u542C\u5230\u8FD9\u58F0\u91CD\u751F\uFF1A**\u4EC5\u961F\u53CB**\u2014\u2014\u81EA\u5DF1\u90A3\u6761\u7531 Ball owner \u6A21\u62DF\u5206\u652F\u64AD\uFF0C\u8FD9\u91CC\u8DF3\u8FC7\u9632\u53E0\u97F3 \u003C/summary\u003E\n\tbool ShouldHearRespawn( long steamId, int teamIndex )\n\t{\n\t\tif ( GameSfx.IsMine( steamId ) ) return false;\n\t\tif ( !MatchState.IsTeam || teamIndex \u003C 0 ) return false;\n\n\t\tvar local = LocalBall;\n\t\treturn local.IsValid() \u0026\u0026 local.TeamIndex \u003E= 0 \u0026\u0026 local.TeamIndex == teamIndex;\n\t}\n\n\t/// \u003Csummary\u003E \u91CD\u751F\u63D0\u793A\u97F3\uFF08v0.7.8.11\uFF09\uFF1Abot/\u654C\u65B9\u91CD\u751F\u662F\u566A\u97F3\uFF0C\u53EA\u8BA9\u0022\u81EA\u5DF1\u002B\u961F\u53CB\u0022\u542C\u89C1\u2014\u2014\n\t/// host \u672C\u5730\u5224\u5B9A \u002B RespawnCue \u5E7F\u64AD\uFF0C\u5404\u7AEF\u540C\u4E00\u6761\u0022\u961F\u53CB\u624D\u54CD\u0022\u89C4\u5219 \u003C/summary\u003E\n\tvoid CueRespawn( long steamId, int teamIndex )\n\t{\n\t\tif ( ShouldHearRespawn( steamId, teamIndex ) )\n\t\t\tGameSfx.Respawn( Vector3.Zero );\n\t\tif ( Networking.IsActive ) NetworkManager.RespawnCue( steamId, teamIndex );\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u843D\u5730\uFF08RespawnCue \u5E7F\u64AD\uFF09 \u003C/summary\u003E\n\tpublic void OnRespawnCueRemote( long steamId, int teamIndex )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;   // host \u5DF2\u5728 CueRespawn \u64AD\u8FC7\uFF08\u6321\u5E7F\u64AD\u56DE\u58F0\uFF09\n\t\tif ( ShouldHearRespawn( steamId, teamIndex ) )\n\t\t\tGameSfx.Respawn( Vector3.Zero );\n\t}\n\n\t// ---- \u6BD4\u8D5B\u8FDB\u7A0B\uFF08M5\uFF09\uFF1Ahost \u6743\u5A01\u5012\u8BA1\u65F6\uFF0C\u5F52\u96F6\u5E7F\u64AD\u7ED3\u7B97\u699C ----\n\n\tvoid TickMatch()\n\t{\n\t\tif ( MatchState.MatchOver ) return;\n\n\t\t// \u53CC\u7AEF\u672C\u5730\u9012\u51CF\uFF1Ahost \u662F\u6743\u5A01\uFF0C\u5BA2\u6237\u7AEF 1Hz \u6821\u65F6\u540E\u81EA\u5DF1\u7EED\u8D70\uFF08\u89C2\u611F\u5E73\u6ED1\uFF09\n\t\tMatchState.TimeLeft = MathF.Max( 0f, MatchState.TimeLeft - Time.Delta );\n\n\t\tif ( !NetworkManager.IsAuthority ) return;\n\n\t\tif ( _sinceMatchTick \u003E 1f )\n\t\t{\n\t\t\t_sinceMatchTick = 0;\n\t\t\tNetworkManager.MatchTick( MatchState.TimeLeft );\n\t\t}\n\n\t\tif ( MatchState.TimeLeft \u003C= 0f ) EndMatch();\n\t}\n\n\t/// \u003Csummary\u003E \u6BD4\u8D5B\u7ED3\u675F\uFF08host\uFF09\uFF1A\u6309\u603B\u8D28\u91CF\uFF08\u4E3B\u7403\u002B\u5206\u8EAB\uFF09\u6392\u7ED3\u7B97\u699C\uFF0C\u5E7F\u64AD \u002B \u672C\u5730\u843D\u5730\u3002\n\t/// \u83DC\u5355\u80CC\u666F\u6F14\u793A\u8D5B\u65F6\u95F4\u5230\u5C31\u5C31\u5730\u91CD\u5F00\uFF0C\u4E0D\u8D70\u7ED3\u7B97\u3002 \u003C/summary\u003E\n\tvoid EndMatch()\n\t{\n\t\tif ( MatchState.MatchOver ) return;\n\n\t\tif ( _menuDemo )\n\t\t{\n\t\t\tMatchState.ActivateForMatch();   // \u6F14\u793A\u8D5B\u5FAA\u73AF\u91CD\u5F00\uFF08\u65E0\u8FDE\u63A5\uFF0CRPC \u7A7A\u53D1\u65E0\u5BB3\uFF09\n\t\t\tGameLog.Info( \u0022[menu] demo match restarted\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar rows = new List\u003CScoreWire\u003E();\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() ) continue;\n\t\t\t// \u53EA\u6536\u672C\u5C40\u771F\u6B63\u5728\u573A\u7684\u4EBA\uFF1A\u86F0\u4F0F\u7403\uFF08\u623F\u95F4\u9884\u7559\u3001\u672C\u5C40\u672A\u6FC0\u6D3B\uFF09\u4E0E\u5C38\u4F53\u4E0D\u8FDB\u699C\u3002\n\t\t\t// \u26A0\uFE0F v0.7.8.105 \u4FEE\uFF08\u5BA1\u8BA1 #2\uFF09\uFF1A\u539F\u5B9E\u73B0\u53EA\u67E5 IsValid\uFF0C\u800C StartMatchFromLobby \u53EA\u91CD\u7F6E\n\t\t\t// **\u88AB\u6FC0\u6D3B**\u7684 bot\uFF0C\u591A\u4F59\u7684\u90A3\u4E9B\u8D70 Alive=false;Dormant=true \u65F6**\u4FDD\u7559\u4E0A\u5C40\u8D28\u91CF**\u2014\u2014\n\t\t\t// \u4E8E\u662F\u628A\u623F\u95F4 PLAYERS \u8C03\u5C0F\u540E\uFF0C\u672A\u6FC0\u6D3B\u7684\u7403\u5E26\u7740\u51E0\u5343\u8D28\u91CF\u53C2\u4E0E\u6392\u540D\uFF1A\u62A2\u5360 arr[0] \u663E\u793A\u4E3A\n\t\t\t// \u0022bot \u593A\u51A0\u0022\u3001\u62FF\u6389\u672C\u673A champion/firstwin \u6210\u5C31\u3001\u5E76\u628A\u771F\u5B9E\u73A9\u5BB6\u7684\u540D\u6B21\u5956\u52B1\u6574\u4F53\u538B\u4F4E\n\t\t\t// \uFF08GameXp \u7684 1st/2~3rd/4~10th \u6863\uFF09\u3002HUD \u65E9\u5DF2\u8FC7\u6EE4 Alive\uFF08GameHud \u6392\u884C\u699C\uFF09\uFF0C\u6B64\u5904\u5BF9\u9F50\u3002\n\t\t\tif ( !b.Alive || b.Dormant ) continue;\n\t\t\trows.Add( new ScoreWire\n\t\t\t{\n\t\t\t\tSteamId = b.OwnerSteamId,\n\t\t\t\tName = b.PlayerName,\n\t\t\t\tMass = TotalMassFor( b ),\n\t\t\t\tTeam = (byte)Math.Max( 0, b.TeamIndex ),\n\t\t\t\tBot = b.IsBot,\n\t\t\t} );\n\t\t}\n\t\tvar arr = rows.ToArray();\n\t\tArray.Sort( arr, ( x, y ) =\u003E y.Mass.CompareTo( x.Mass ) );\n\n\t\t// \u9886\u571F\u6A21\u5F0F\uFF08M7.4 \u79EF\u5206\u5236\uFF09\uFF1A\u540D\u6B21\u6309\u5360\u65D7\u79EF\u5206\uFF08\u5F81\u670D=\u5360\u6EE1\u5168\u90E8\u53EF\u5360\u9886\u683C\uFF09\uFF0C\u79EF\u5206\u540C\u5206\u6BD4\u683C\u6570\u518D\u6BD4\u8D28\u91CF\uFF1B\n\t\t// \u7EC8\u5C40\u5FEB\u7167\u5148\u884C\u2014\u2014\u5BA2\u6237\u7AEF\u7ED3\u7B97\u6309\u9886\u571F\u80DC\u8D1F\u663E\u793A\uFF0C\u6162\u4E00\u62CD\u5C31\u4F1A\u6309\u65E7\u683C\u6570\u5224\u9519\u51A0\u519B\n\t\tif ( MatchState.IsTerritory )\n\t\t{\n\t\t\tNetworkManager.TerritoryState( TerritoryManager.Snapshot(), TerritoryManager.PointsWire() );\n\t\t\tArray.Sort( arr, ( x, y ) =\u003E\n\t\t\t{\n\t\t\t\tvar byPts = TerritoryManager.Points( y.Team ).CompareTo( TerritoryManager.Points( x.Team ) );\n\t\t\t\tif ( byPts != 0 ) return byPts;\n\t\t\t\tvar byCells = TerritoryManager.OwnedCount( y.Team ).CompareTo( TerritoryManager.OwnedCount( x.Team ) );\n\t\t\t\treturn byCells != 0 ? byCells : y.Mass.CompareTo( x.Mass );\n\t\t\t} );\n\t\t}\n\n\t\tNetworkManager.MatchOver( arr );   // \u5BA2\u6237\u7AEF\u5C55\u793A\u7528\n\t\tOnMatchOverRemote( arr );          // host \u7ACB\u5373\u843D\u5730\uFF08\u5E7F\u64AD\u56DE\u58F0\u88AB MatchOver \u5B88\u536B\u6321\u6389\uFF09\n\n\t\tGameLog.Info( $\u0022[match] over \u2014 {arr.Length} players, winner: {( arr.Length \u003E 0 ? arr[0].Name : \u0022?\u0022 )}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u6BD4\u8D5B\u7ED3\u675F\u843D\u5730\uFF08\u5168\u7AEF\uFF09\uFF1A\u51BB\u7ED3 \u002B \u7ED3\u7B97\u9762\u677F\uFF1B\u672C\u673A\u6210\u7EE9\u4E0A\u4E91\u3001\u672C\u5730\u699C\u843D\u76D8\uFF08\u5404\u7AEF\u5404\u81EA\u505A\uFF09\u3002\n\t/// 10 \u79D2\u540E\u81EA\u52A8\u56DE\u5927\u5385\uFF08\u591A\u4EBA\uFF09/\u4E3B\u83DC\u5355\uFF08\u5355\u673A\uFF09\uFF0C\u89C1 AfterSettlement\u3002 \u003C/summary\u003E\n\tpublic void OnMatchOverRemote( ScoreWire[] standings )\n\t{\n\t\tif ( MatchState.MatchOver ) return;   // host \u5E7F\u64AD\u56DE\u58F0\u9632\u91CD\u5165\n\t\tMatchState.MatchOver = true;\n\t\t_sinceMatchOver = 0;\n\n\t\tvar local = LocalBall;\n\t\t_endBoard?.Show( standings,\n\t\t\tlocal.IsValid() ? local.OwnerSteamId : 0,\n\t\t\tlocal.IsValid() ? local.PlayerName : \u0022\u0022 );\n\t\tLocalBoard.Record( standings );\n\t\tScoreUploader.SubmitOwn( this );\n\t\tbool won = GameAchievements.Settlement( standings, local.IsValid() ? local.OwnerSteamId : 0 );   // \u6210\u5C31\uFF1A\u593A\u51A0/\u767B\u9876\n\t\tGameXp.Settlement( standings, local.IsValid() ? local.OwnerSteamId : 0, won );                    // \u7ECF\u9A8C\u7ED3\u7B97\u4E0A\u62A5\uFF08v0.7.8.19\uFF09\n\t\tGameLog.Info( $\u0022[match] settlement shown ({standings?.Length ?? 0} rows)\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u541E\u7403\u901A\u77E5\uFF08host\uFF09\uFF1A\u5E7F\u64AD\u5BA2\u6237\u7AEF \u002B \u672C\u673A\u64AD\u62A5/\u97F3\u6548 \u003C/summary\u003E\n\tvoid NotifyBallEaten( long eaterSteamId, long eatenSteamId, float massGained )\n\t{\n\t\tif ( Networking.IsActive ) NetworkManager.BallEaten( eaterSteamId, eatenSteamId, massGained );   // \u65E0\u4F1A\u8BDD\uFF08\u83DC\u5355\u6F14\u793A\u8D5B\uFF09\u4E0D\u5E7F\u64AD\n\t\tOnBallEaten( eaterSteamId, eatenSteamId, massGained );\n\t}\n\n\t/// \u003Csummary\u003E \u541E\u7403\u4E8B\u4EF6\uFF08\u53CC\u7AEF\u7EDF\u4E00\u5165\u53E3\uFF09\uFF1A\u5168\u573A\u0022\u8C01\u5403\u4E86\u8C01\u0022\u64AD\u62A5\uFF1B\u97F3\u6548\u53EA\u5BF9\u53C2\u4E0E\u7684\u53CC\u65B9\uFF1B\n\t/// \u51FB\u6740\u8005=\u672C\u673A\u65F6\u5F39\u4E2A\u4EBA\u9AD8\u5149\u6A2A\u5E45\uFF08v0.7.6.0\uFF0C\u91D1\u8272\u827A\u672F\u5B57\uFF09 \u003C/summary\u003E\n\tpublic void OnBallEaten( long eaterSteamId, long eatenSteamId, float massGained )\n\t{\n\t\tvar eater = FindBallBySteamId( eaterSteamId );\n\t\tvar eaten = FindBallBySteamId( eatenSteamId );\n\n\t\t// \u541E\u7403\u6ED1\u5165\uFF08v0.7.8.28 juice\uFF09\uFF1A\u53CC\u7AEF\u7EDF\u4E00\u5165\u53E3\u2014\u2014\u88AB\u5403\u8005\u7F29\u56E2\u6ED1\u8FDB\u5403\u8005\u5634\u91CC\u3002\n\t\t// \u5206\u8EAB\u53CD\u541E\u4E3B\u7403\u65F6 eater \u662F\u5176\u4E3B\u7403\uFF08\u53D6\u4E0D\u5230\u5177\u4F53\u5206\u8EAB\uFF09\uFF0C\u6ED1\u5411\u5176\u4E3B\u961F\u8EAB\u4F53\uFF0C\u89C2\u611F\u4E3A\u0022\u88AB\u6536\u7F16\u0022\n\t\tif ( eater.IsValid() \u0026\u0026 eaten.IsValid() )\n\t\t\tNeonRenderer.Swallow( eaten.WorldPosition, eaten.ColorIndex, eater.WorldPosition, eaten.Radius );\n\n\t\tif ( GameSfx.IsMine( eaterSteamId ) || GameSfx.IsMine( eatenSteamId ) )\n\t\t\tGameSfx.EatBall( eaten.IsValid() ? eaten.WorldPosition : Vector3.Zero );\n\n\t\tif ( GameSfx.IsMine( eaterSteamId ) )\n\t\t{\n\t\t\t// \u51FB\u6740\u9AD8\u5149\u4E09\u8FDE\uFF08v0.7.7.0\uFF09\uFF1A\u8FDE\u51FB\u6A2A\u5E45\uFF08\u6587\u6848/\u5B57\u53F7\u968F\u6863\u4F4D\uFF09\u002B \u5347\u8C03\u97F3 \u002B \u5C4F\u5E55\u9707\u52A8\n\t\t\tint combo = GameSfx.RegisterKill();\n\t\t\t_hud?.AddKillBanner( combo, eaten?.PlayerName ?? \u0022???\u0022, massGained );\n\t\t\tGameSfx.ComboSound( combo );\n\t\t\t_camera?.Shake( MathF.Min( 8f \u002B combo * 1.5f, 14f ) );\n\n\t\t\tGameAchievements.LocalKill( combo, massGained, eatenSteamId );   // \u6210\u5C31\uFF1A\u51FB\u6740\u65CF\uFF08v0.7.8.14\uFF09\n\t\t}\n\t\telse if ( GameSfx.IsMine( eatenSteamId ) )\n\t\t{\n\t\t\tGameAchievements.LocalDeath( eaterSteamId );   // \u6210\u5C31\uFF1A\u9996\u6B21\u88AB\u5403 \u002B \u8BB0\u4EC7\u540D\u5355\n\t\t}\n\n\t\t_hud?.AddKillFeed( eater?.PlayerName ?? \u0022???\u0022,\n\t\t\teaten?.PlayerName ?? \u0022???\u0022,\n\t\t\tmassGained, eaterSteamId, eatenSteamId );\n\t}\n\n\t/// \u003Csummary\u003E host \u4F4D\u79FB\u6821\u9A8C\uFF1A\u8FDC\u7AEF\u6A21\u62DF\u7684\u7403\u5355\u5E27\u4F4D\u79FB\u8D85\u7406\u8BBA\u4E0A\u9650 3 \u500D\u5373\u62C9\u56DE\uFF08\u53CD\u77AC\u79FB\u6A61\u76AE\u7B4B\uFF09\u3002\n\t/// \u5408\u6CD5\u77AC\u79FB\uFF08\u91CD\u751F\u6362\u4F4D/\u5206\u8EAB\u664B\u5347\uFF09\u6709 TeleportGraceSeconds \u5BBD\u9650\u2014\u2014\u7A97\u53E3\u5185\u653E\u884C owner \u7684\n\t/// \u5E72\u7EA7\u522B\u4F4D\u7F6E\u66F4\u65B0\u5E76\u6301\u7EED\u951A\u5B9A _lastPos\uFF0C\u7A97\u53E3\u4E00\u8FC7\u6062\u590D\u5E38\u89C4\u6821\u9A8C\uFF08v0.7.8.33 \u4FEE\u0022\u664B\u5347/\u91CD\u751F\u540E\n\t/// host \u7AEF\u7403\u4F4D\u88AB\u9489\u6B7B\u5728\u65E7\u5904\u0022\uFF1A\u6B64\u524D _lastPos \u4ECE\u4E0D\u968F\u77AC\u79FB\u91CD\u7F6E\uFF0Cowner \u7684\u6BCF\u6B21\u66F4\u65B0\u90FD\u88AB\u62C9\u56DE\uFF09 \u003C/summary\u003E\n\tvoid TickMovementValidation()\n\t{\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() ) continue;\n\n\t\t\tvar key = b.Id;\n\t\t\tvar last = _lastPos.TryGetValue( key, out var lp ) ? lp : b.WorldPosition;\n\n\t\t\tbool grace = _teleportGrace.TryGetValue( key, out var g );\n\t\t\tif ( grace \u0026\u0026 g \u003E GameConfig.TeleportGraceSeconds )\n\t\t\t{\n\t\t\t\t_teleportGrace.Remove( key );\n\t\t\t\tgrace = false;\n\t\t\t}\n\n\t\t\t// \u6821\u9A8C\u5BF9\u8C61\u7528 IsRemoteOwned\uFF08v0.7.8.105\uFF0C\u5BA1\u8BA1 #9/#10\uFF09\uFF1A\u5B83\u662F host \u4FA7\u751F\u6210\u610F\u56FE\u6807\u8BB0\uFF0C\n\t\t\t// \u624D\u662F\u0022\u8FD9\u9897\u7403\u5F52\u8FDC\u7AEF\u6A21\u62DF\u0022\u7684\u53EF\u9760\u4FE1\u53F7\u3002\u539F\u6765\u7528 \u0060b.OwnerSteamId != mySteamId\u0060 \u5224\u5B9A\uFF0C\n\t\t\t// \u800C\u540C\u673A\u53CC\u5F00\u65F6\u8FDE\u63A5\u662F\u5408\u6210 SteamId\u3001\u4E0E Game.SteamId \u5BF9\u4E0D\u4E0A\uFF08\u672C\u6587\u4EF6\u591A\u5904\u5B9E\u6D4B\u6CE8\u91CA\uFF09\u2014\u2014\n\t\t\t// \u90A3\u79CD\u573A\u666F\u4E0B\u8FDC\u7AEF\u7403\u88AB\u5F53\u4F5C\u0022\u672C\u673A\u7684\u7403\u0022\u8DF3\u8FC7\u6821\u9A8C\uFF0C\u53CD\u4F5C\u5F0A\u5BF9\u6070\u597D\u5F00\u4E86\u53CC\u5F00\u7684\u4F5C\u5F0A\u8005\u5B8C\u5168\u5931\u6548\u3002\n\t\t\t// bot \u7684 IsRemoteOwned \u6052 false\uFF08SpawnBall \u91CC owner \u4E3A null\uFF09\uFF0C\u5929\u7136\u6392\u9664\u3002\n\t\t\tif ( !grace \u0026\u0026 b.IsRemoteOwned )\n\t\t\t{\n\t\t\t\t// \u5355\u6B65\u4F4D\u79FB\u4E0A\u9650 = \u7406\u8BBA\u901F\u5EA6 \u00D7 3\uFF08\u539F\u6CE8\u91CA\u5373\u0022\u8D85\u7406\u8BBA\u4E0A\u9650 3 \u500D\u5373\u62C9\u56DE\u0022\uFF09\u3002\n\t\t\t\t// \u26A0\uFE0F \u53BB\u6389\u539F\u6765\u7684 \u0060\u002B 16f\u0060 \u5E38\u6570\u9879\uFF08v0.7.8.105\uFF0C\u5BA1\u8BA1 #10\uFF09\uFF1A\u90A3\u4E2A\u5E38\u6570\u662F**\u6309\u5E27**\u52A0\u7684\uFF0C\n\t\t\t\t// \u800C\u6B64\u5904 Time.Delta \u73B0\u5728\u662F\u56FA\u5B9A 1/50 \u6B65\u957F\u2014\u201460fps \u4E0B 16 \u5355\u4F4D\u672C\u8EAB\u5DF2\u22481.4 \u500D\u5408\u6CD5\u4F4D\u79FB\uFF0C\n\t\t\t\t// \u5408\u8BA1\u628A\u5BB9\u5DEE\u62AC\u5230\u7EA6 4.8 \u500D\uFF0C\u7B49\u4E8E\u5141\u8BB8\u6301\u7EED\u8D85\u901F\u3002\u56FA\u5B9A\u6B65\u4E0B 3 \u500D\u4F59\u91CF\u5DF2\u80FD\u5438\u6536\n\t\t\t\t// \u8FDC\u7AEF\u53D1\u5305\u5408\u5E76\uFF08\u22483 \u6B65\u5408\u5E76\u4F4D\u79FB\uFF09\u7684\u6296\u52A8\u3002\n\t\t\t\tvar maxMove = b.Speed * Time.Delta * 3f;\n\t\t\t\tif ( b.WorldPosition.Distance( last ) \u003E maxMove )\n\t\t\t\t\tb.WorldPosition = last;\n\t\t\t}\n\n\t\t\t_lastPos[key] = b.WorldPosition;\n\n\t\t\t// \u8F6C\u5411\u65B9\u5411\uFF08\u5206\u8EAB\u8DDF\u968F\u7528\uFF09\uFF1A\u53D6\u672C\u5E27\u4F4D\u79FB\u65B9\u5411\u5E73\u6ED1\uFF1B\u9759\u6B62\u65F6\u4FDD\u6301\u539F\u65B9\u5411\u3002\n\t\t\t// \u5BBD\u9650\u671F\u5185\u4F4D\u79FB\u662F\u77AC\u79FB\u8DF3\u53D8\uFF0C\u4E0D\u4EE3\u8868\u771F\u5B9E\u822A\u5411\uFF0C\u8DF3\u8FC7\n\t\t\tif ( grace ) continue;\n\n\t\t\tvar delta = b.WorldPosition - last;\n\t\t\tif ( delta.Length \u003E 0.1f )\n\t\t\t{\n\t\t\t\tvar n = new Vector3( delta.x, delta.y, 0f ).Normal;\n\t\t\t\tvar prev = _steerDir.TryGetValue( key, out var sd ) ? sd : new Vector2( n.x, n.y );\n\t\t\t\t_steerDir[key] = new Vector2( prev.x \u002B ( n.x - prev.x ) * 0.4f, prev.y \u002B ( n.y - prev.y ) * 0.4f );\n\t\t\t}\n\t\t}\n\t}\n\n\tint CountPlayers( bool isBot )\n\t{\n\t\tint n = 0;\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 b.IsBot == isBot ) n\u002B\u002B;\n\t\t}\n\t\treturn n;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u626B\u63CF\u573A\u666F\u91CC\u7684 Ball \u5E76\u6536\u7F16\u8FDB\u5217\u8868\uFF08\u7F51\u7EDC\u5BF9\u8C61\u7684 OnStart \u4E0D\u53EF\u9760\uFF0C\u5B9E\u6D4B\u4E0D\u89E6\u53D1\uFF09\u3002\n\t/// \u672C\u673A\u7403\u5224\u5B9A\u7528\u6240\u6709\u6743\u5B57\u6BB5\uFF1A\u975E\u4EE3\u7406 \u002B \u975Ebot \u002B OwnerSteamId \u662F\u81EA\u5DF1\u7684 SteamId\u3002\n\t/// \u003C/summary\u003E\n\tvoid ScanBalls()\n\t{\n\t\t// \u5BA2\u6237\u7AEF\uFF1A\u5FEB\u7167\u4E16\u754C\u6839\u81EA\u6108\u542F\u7528\uFF08\u9996\u6B21 Tick \u53EF\u80FD\u65E9\u4E8E\u5FEB\u7167\u5E94\u7528\u5B8C\u6210\uFF0CEnsureWorld \u6251\u7A7A\u65F6\u515C\u4F4F\uFF09\n\t\tif ( !NetworkManager.IsAuthority )\n\t\t{\n\t\t\tAdoptSnapshotWorld();\n\t\t}\n\n\t\tforeach ( var b in Scene.GetAllComponents\u003CBall\u003E() )\n\t\t{\n\t\t\tif ( !b.IsValid() ) continue;\n\n\t\t\t// \u5B58\u91CF bot \u81EA\u6108\uFF1Av0.5.1.3 \u4E4B\u524D\u751F\u6210\u7684 bot OwnerSteamId=0\uFF08\u5206\u8EAB\u5934\u50CF/\u540D\u724C\u65E0\u6CD5\u5F52\u5C5E\uFF09\u2014\u2014\n\t\t\t// host \u8865\u53D1\u552F\u4E00\u5047 id\uFF08[Sync] \u81EA\u52A8\u4E0B\u53D1\u5BA2\u6237\u7AEF\uFF09\uFF0C\u70ED\u91CD\u8F7D/\u8001\u4F1A\u8BDD\u91CC\u7684 bot \u4E5F\u88AB\u8986\u76D6\n\t\t\tif ( NetworkManager.IsAuthority \u0026\u0026 b.IsBot \u0026\u0026 b.OwnerSteamId \u003C GameConfig.BotSteamIdBase )\n\t\t\t{\n\t\t\t\tb.OwnerSteamId = NextBotSteamId();\n\t\t\t\tGameLog.Info( $\u0022[game] legacy bot \u0027{b.PlayerName}\u0027 assigned steam id {b.OwnerSteamId}\u0022 );\n\t\t\t}\n\n\t\t\tif ( !_balls.Contains( b ) )\n\t\t\t{\n\t\t\t\t_balls.Add( b );\n\t\t\t}\n\n\t\t\t// \u672C\u673A\u7403 = \u81EA\u5DF1\u62E5\u6709\u7684\u90A3\u9897\uFF08IsProxy=false \u5373\u6240\u6709\u6743\uFF0C\u5929\u7136\u53EF\u9760\uFF09\u3002\n\t\t\t// \u4E0D\u8981\u7528 OwnerSteamId \u5339\u914D\uFF1A\u540C\u673A\u53CC\u5F00\u65F6\u8FDE\u63A5\u662F\u5408\u6210 ID\uFF0C\u4E0E Game.SteamId \u5BF9\u4E0D\u4E0A\uFF08\u5B9E\u6D4B\u8E29\u5751\uFF09\u3002\n\t\t\t// \u26A0\uFE0F \u6BCF\u6B21\u626B\u63CF\u90FD\u8981\u65AD\u8A00\uFF08\u4E0D\u80FD\u53EA\u5728\u65B0\u6536\u7F16\u65F6\uFF09\uFF1A\u5BA2\u6237\u7AEF\u7684 Ball.OnStart \u8DD1\u5728\u5FEB\u7167\u671F\uFF0C\n\t\t\t// \u65E9\u4E8E\u672C\u7AEF EnsureWorld \u5EFA\u76F8\u673A\uFF0C\u9996\u6B21 SetLocalBall \u65F6 _camera \u4E3A null \u88AB\u8DF3\u8FC7\uFF1B\n\t\t\t// SetTarget \u5185\u90E8\u6709\u540C\u7403\u5B88\u536B\uFF0C\u91CD\u590D\u8C03\u7528\u96F6\u5F00\u9500\u3002\n\t\t\tif ( !b.IsProxy \u0026\u0026 !b.IsBot )\n\t\t\t{\n\t\t\t\tSetLocalBall( b );\n\t\t\t}\n\t\t}\n\n\t\tfor ( int i = _balls.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tif ( !_balls[i].IsValid() ) _balls.RemoveAt( i );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u7403\u6CE8\u518C\uFF08Ball.OnStart \u5728\u6240\u6709\u673A\u5668\u4E0A\u8C03\u7528\uFF1Bhost \u751F\u6210\u4E0E\u5BA2\u6237\u7AEF\u590D\u5236\u7EDF\u4E00\u5165\u53E3\uFF09 \u003C/summary\u003E\n\tpublic void RegisterBall( Ball ball )\n\t{\n\t\tif ( !_balls.Contains( ball ) ) _balls.Add( ball );\n\t}\n\n\t/// \u003Csummary\u003E \u6807\u8BB0\u672C\u673A\u62E5\u6709\u7684\u7403\uFF0C\u5E76\u8BA9\u8DDF\u968F\u76F8\u673A\u9501\u5B9A\u5B83\u3002\n\t/// host \u4E0A\u4E3A\u8FDC\u7A0B\u8FDE\u63A5\u751F\u6210\u7684\u7403\uFF08IsRemoteOwned\uFF09\u7EDD\u4E0D\u5145\u5F53\u672C\u673A\u7403\u2014\u2014\u540C\u8D26\u53F7\u53CC\u5F00\u65F6\n\t/// \u5B83\u4EEC\u4E0E host \u7403\u540C\u540D\u540C SteamId\uFF0C\u552F\u4E00\u53EF\u9760\u7684\u533A\u522B\u5C31\u662F\u751F\u6210\u610F\u56FE\uFF08\u5B9E\u6D4B\u8E29\u5751\uFF09\u3002 \u003C/summary\u003E\n\tpublic void SetLocalBall( Ball ball )\n\t{\n\t\tif ( ball is null || ball.IsRemoteOwned ) return;\n\n\t\tLocalBall = ball;\n\t\t_camera?.SetTarget( ball );\n\t}\n\n\t/// \u003Csummary\u003E \u573A\u5730\u5185\u968F\u673A\u51FA\u751F\u70B9\uFF1A**agar \u5F0F\u5B89\u5168\u51FA\u751F**\u2014\u2014\u91C7\u6837\u591A\u70B9\uFF0C\u9009\u79BB\u0022\u80FD\u5403\u6389\u51FA\u751F\u8D28\u91CF\u0022\u7684\n\t/// \u6700\u8FD1\u5A01\u80C1\u6700\u8FDC\u7684\u70B9\u3002\u968F\u673A\u88F8\u751F\u4F1A\u88AB\u8DEF\u8FC7\u7684\u4E2D bot \u79D2\u5403\uFF08\u5B9E\u6D4B\uFF1A\u52A0\u5165 1.6s \u88AB\u5403\uFF09 \u003C/summary\u003E\n\t/// \u003Csummary\u003E \u573A\u5730\u5185\u51FA\u751F\u70B9\uFF1A\u8D70\u6E38\u620F\u7CFB\u7EDF\u7684\u5B89\u5168\u91C7\u6837\u3002\u9886\u571F\u6A21\u5F0F\uFF08M7\uFF09\u7ED9\u961F\u53F7\u65F6\u5728\u5927\u672C\u8425\u683C\u5185\u91C7\u6837\uFF08\u51FA\u751F\u5373\u56DE\u8425\uFF09\u3002\n\t/// teamIndex \u5C0F\u4E8E 0 \u6216\u975E\u9886\u571F\u6A21\u5F0F = \u5168\u573A\u968F\u673A\uFF08\u65E2\u6709\u884C\u4E3A\uFF09 \u003C/summary\u003E\n\t/// \u003Csummary\u003E \u573A\u5730\u5185\u51FA\u751F\u70B9\uFF1A\u8D70\u6E38\u620F\u7CFB\u7EDF\u7684\u5B89\u5168\u91C7\u6837\u3002\u9886\u571F\u6A21\u5F0F\uFF08M7\uFF09\u7ED9\u961F\u53F7\u65F6\u5728\u5927\u672C\u8425\u683C\u5185\u91C7\u6837\uFF1B\n\t/// frontLine=true\uFF08M7.2 \u590D\u6D3B\u4E8C\u9009\u4E00\uFF09\u6539\u6700\u8FD1\u5DF1\u65B9\u5360\u9886\u683C\uFF08\u6CA1\u6709\u524D\u7EBF\u683C\u56DE\u843D\u5927\u672C\u8425\uFF09\u3002\n\t/// teamIndex \u5C0F\u4E8E 0 \u6216\u975E\u9886\u571F\u6A21\u5F0F = \u5168\u573A\u968F\u673A\uFF08\u65E2\u6709\u884C\u4E3A\uFF09 \u003C/summary\u003E\n\tpublic Vector3 RandomSpawnPos( int teamIndex = -1, bool frontLine = false, Vector3 nearPos = default )\n\t{\n\t\tvar half = GameConfig.ArenaHalfSize - 128f;\n\n\t\tfloat minX = -half, maxX = half, minY = -half, maxY = half;\n\t\tif ( MatchState.IsTerritory \u0026\u0026 teamIndex \u003E= 0 )\n\t\t{\n\t\t\tif ( frontLine \u0026\u0026 TerritoryManager.TryFrontSpawn( teamIndex, nearPos, out var front ) )\n\t\t\t\treturn front;\n\t\t\tTerritoryManager.HqBounds( teamIndex, out minX, out maxX, out minY, out maxY );\n\t\t}\n\n\t\tVector3 best = new Vector3( Game.Random.Float( minX, maxX ), Game.Random.Float( minY, maxY ), 0f );\n\t\tvar bestScore = float.MinValue;\n\n\t\tfor ( int i = 0; i \u003C GameConfig.SpawnSafeSamples; i\u002B\u002B )\n\t\t{\n\t\t\tvar p = new Vector3( Game.Random.Float( minX, maxX ), Game.Random.Float( minY, maxY ), 0f );\n\t\t\tvar score = float.MaxValue;\n\n\t\t\tforeach ( var b in _balls )\n\t\t\t{\n\t\t\t\tif ( !b.IsValid() || !b.Alive ) continue;\n\t\t\t\tif ( b.Mass \u003C GameConfig.StartMass * GameConfig.EatRatio ) continue;   // \u5403\u4E0D\u52A8\u51FA\u751F\u7403\u7684\u5FFD\u7565\n\n\t\t\t\tvar d = p.Distance( b.WorldPosition ) - b.Radius;\n\t\t\t\tif ( d \u003C score ) score = d;\n\t\t\t}\n\n\t\t\tif ( score \u003E bestScore )\n\t\t\t{\n\t\t\t\tbestScore = score;\n\t\t\t\tbest = p;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\tstatic string LocalPlayerName()\n\t{\n\t\ttry { return Connection.Local?.DisplayName ?? \u0022You\u0022; }\n\t\tcatch { return \u0022You\u0022; }\n\t}\n\n\t// ---- \u5206\u8EAB/\u5B62\u5B50\uFF1Ahost \u6743\u5A01\u6A21\u62DF\uFF08M3\uFF09----\n\n\t/// \u003Csummary\u003E\n\t/// \u5206\u8EAB/\u5B62\u5B50\u6BCF\u5E27\u6A21\u62DF\uFF1A\u51B2\u91CF\u8870\u51CF \u2192 \u8DDF\u968F\u4E3B\u4EBA\u8F6C\u5411 \u2192 \u573A\u5730\u94B3\u5236 \u2192 \u5408\u5E76\u68C0\u67E5 \u2192 \u6E05\u7406\u3002\n\t/// \u5B62\u5B50\u65E0\u8F6C\u5411\uFF08\u6ED1\u884C\u5230\u505C\uFF09\uFF0C\u5206\u8EAB\u6309\u4E3B\u4EBA\u7403\u7684\u5E73\u6ED1\u8F6C\u5411\u65B9\u5411\u8DDF\u968F\u79FB\u52A8\u3002\n\t/// \u003C/summary\u003E\n\tvoid TickCells()\n\t{\n\t\tif ( _cells.Count == 0 ) return;\n\n\t\tvar dt = Time.Delta;\n\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar c = _cells[i];\n\n\t\t\t// \u4E3B\u4EBA\u7403\u5F15\u7528\u5931\u6548\uFF08\u70ED\u91CD\u8F7D\u6362\u7A0B\u5E8F\u96C6\u5BF9\u8C61 / \u4E3B\u4EBA\u771F\u6CA1\u4E86\uFF09\u2192 \u5148\u6309 SteamId \u91CD\u6302\u81EA\u6108\n\t\t\t// \uFF08v0.7.8.29\uFF1A\u70ED\u91CD\u8F7D\u4F1A\u628A\u7EC4\u4EF6\u6362\u6210\u65B0\u7A0B\u5E8F\u96C6\u5BF9\u8C61\uFF0C\u65E7 OwnerBall \u5F15\u7528\u5168\u5931\u6548\uFF0C\n\t\t\t// \u65E7\u903B\u8F91\u76F4\u63A5\u6D88\u6563 = \u6BCF\u6B21\u70ED\u91CD\u8F7D\u5206\u8EAB\u51ED\u7A7A\u84B8\u53D1\uFF09\uFF1B\u91CD\u6302\u4E0D\u4E0A\u624D\u6D88\u6563\uFF08\u5B62\u5B50\u4E2D\u6027\u5B58\u6D3B\uFF0C\u4E0D\u6E05\u7406\uFF09\n\t\t\tif ( c.PieceKind != CellPiece.Kind.EjectedMass\n\t\t\t\t\u0026\u0026 ( !c.OwnerBall.IsValid() || !c.OwnerBall.Alive ) )\n\t\t\t{\n\t\t\t\tvar re = FindBallBySteamId( c.OwnerSteamId );\n\t\t\t\tif ( ( !re.IsValid() || !re.Alive ) \u0026\u0026 _sinceCellHeal \u003E 0.5f )\n\t\t\t\t{\n\t\t\t\t\t_sinceCellHeal = 0;\n\t\t\t\t\tScanBalls();   // \u70ED\u91CD\u8F7D\u7A97\u53E3\uFF1A\u4E3B\u4EBA\u7403\u5176\u5B9E\u5728\u573A\u666F\u91CC\uFF08\u65B0\u7A0B\u5E8F\u96C6\u5BF9\u8C61\uFF09\uFF0C\u7ACB\u523B\u6536\u7F16\u518D\u8BD5\n\t\t\t\t\tre = FindBallBySteamId( c.OwnerSteamId );\n\t\t\t\t}\n\t\t\t\tif ( re.IsValid() \u0026\u0026 re.Alive )\n\t\t\t\t{\n\t\t\t\t\tc.OwnerBall = re;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t\t_cellsDirty = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// \u5C16\u523A\u5206\u8EAB\u5230\u671F\uFF08v0.7.4.0 \u7528\u6237\u5B9A\u7A3F B\uFF09\uFF1A\u53D8\u56DE\u666E\u901A\u5206\u8EAB\u2014\u2014\u8D28\u91CF\u4FDD\u7559\uFF0C\n\t\t\t// \u6309\u8D28\u91CF\u53D6\u5408\u5E76\u51B7\u5374\uFF08v0.7.8.27 \u66F2\u7EBF\uFF09\u540E\u6309\u666E\u901A\u5206\u8EAB\u89C4\u5219\u5408\u4F53\u56DE\u6536\uFF1B\u6B64\u540E\u4E0D\u518D\u662F\u6B66\u5668\u3002\n\t\t\t// MaxLife \u8986\u76D6\uFF08M7.4 \u62A4\u536B\u56FA\u5B88\u5C16\u523A 8s\uFF09\uFF1A\u5230\u671F\u540C\u65F6\u89E3\u9664\u951A\u5B9A\n\t\t\tif ( c.PieceKind == CellPiece.Kind.SpikeMinion\n\t\t\t\t\u0026\u0026 c.SinceSpawn \u003E ( c.MaxLife \u003E 0f ? c.MaxLife : GameConfig.SpikeMinionLife ) )\n\t\t\t{\n\t\t\t\tc.PieceKind = CellPiece.Kind.SplitPiece;\n\t\t\t\tc.Anchored = false;\n\t\t\t\tc.SinceSpawn = 0;\n\t\t\t\tc.MergeCooldown = GameConfig.MergeCooldownFor( c.Mass );\n\t\t\t\tNeonRenderer.Puff( c.Pos, c.ColorIndex );\n\t\t\t\t// \u4E3B\u4EBA\u63D0\u793A\uFF08v0.7.8.92\uFF09\uFF1A\u5C16\u523A\u5230\u671F\u0022\u53D8\u56DE\u5206\u8EAB\u0022\u662F\u5B9A\u7A3F\u8BBE\u8BA1\uFF0C\u4F46\u4E0D\u63D0\u793A\u7684\u8BDD\u89C2\u611F\u5C31\u662F\n\t\t\t\t// \u0022\u6211\u7684\u5C16\u523A\u83AB\u540D\u5176\u5999\u53D8\u6210\u4E86\u5206\u8EAB\u7403\u0022\u2014\u2014\u53EA\u7ED9\u4E3B\u4EBA\u81EA\u5DF1\u51FA\u6A2A\u5E45\n\t\t\t\tif ( GameSfx.IsMine( c.OwnerSteamId ) )\n\t\t\t\t\tCircleroyaleGame.Current?.Hud?.AddBanner( \u0022SPIKE \u2192 CLONE\u0022,\n\t\t\t\t\t\tPowerUpManager.ColorOf( (byte)PowerUpManager.Kind.Spike ) );\n\t\t\t\t_cellsDirty = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// \u8D28\u91CF\u8870\u51CF\uFF08v0.7.8.24 \u7403\u7403\u5927\u4F5C\u6218\u624B\u611F\uFF09\uFF1A\u5206\u8EAB\u4E0E\u7403\u540C\u7387\u6301\u7EED\u6389\u91CD\uFF08\u5B62\u5B50\u4E0D\u8870\u51CF\uFF09\n\t\t\tif ( c.PieceKind != CellPiece.Kind.EjectedMass \u0026\u0026 c.Mass \u003E GameConfig.MassDecayMinMass )\n\t\t\t{\n\t\t\t\tc.Mass = MathF.Max( GameConfig.MassDecayMinMass, c.Mass - c.Mass * GameConfig.MassDecayPerSecond * dt );\n\t\t\t}\n\n\t\t\tc.Impulse *= MathF.Exp( -( c.PieceKind == CellPiece.Kind.EjectedMass\n\t\t\t\t? GameConfig.EjectImpulseDecay : GameConfig.SplitImpulseDecay ) * dt );\n\n\t\t\tif ( c.PieceKind != CellPiece.Kind.EjectedMass )\n\t\t\t{\n\t\t\t\tvar dir = SteerDirOf( c.OwnerBall );\n\t\t\t\tvar target = new Vector3( dir.x, dir.y, 0f ) * c.Speed;\n\n\t\t\t\t// \u5012\u6D41\u5F15\u529B\uFF08\u627918\u2463\uFF0C\u0022\u79D2\u5408\u0022\u7684\u7269\u7406\u57FA\u7840\uFF09\uFF1A\u5206\u8EAB\u6BD4\u4E3B\u7403\u91CD\uFF08\u5410\u7403\u51CF\u91CD\u53EF\u5236\u9020\u4F53\u91CD\u5DEE\uFF09\n\t\t\t\t// \u6216\u51FA\u751F\u8D85\u8FC7 PieceHomeAfterSeconds \u2192 \u81EA\u52A8\u8F6C\u5411\u4E3B\u7403\u6EDA\u56DE\uFF0C\u4E0E\u6447\u6746\u8F93\u5165\u6309\u6743\u91CD\u6DF7\u5408\u3002\n\t\t\t\t// \u5C16\u523A\u5206\u8EAB\u6392\u9664\u2014\u201420 \u79D2\u6B66\u5668\u671F\u7EDD\u4E0D\u5012\u6D41\u5408\u4F53\uFF08\u7528\u6237\u5B9A\u7A3F A/B\uFF09\n\t\t\t\tvar owner = c.OwnerBall;\n\t\t\t\tif ( owner.IsValid() \u0026\u0026 owner.Alive \u0026\u0026 c.PieceKind == CellPiece.Kind.SplitPiece \u0026\u0026 c.IsMergeReady\n\t\t\t\t\t\u0026\u0026 ( c.Mass \u003E owner.Mass || c.SinceSpawn \u003E GameConfig.PieceHomeAfterSeconds ) )\n\t\t\t\t{\n\t\t\t\t\tvar to = owner.WorldPosition - c.Pos;\n\t\t\t\t\tif ( to.Length \u003E 1f )\n\t\t\t\t\t{\n\t\t\t\t\t\tto = to.Normal;\n\t\t\t\t\t\tvar blended = new Vector3(\n\t\t\t\t\t\t\tto.x * GameConfig.PieceAttractWeight \u002B dir.x,\n\t\t\t\t\t\t\tto.y * GameConfig.PieceAttractWeight \u002B dir.y, 0f );\n\t\t\t\t\t\tif ( blended.Length \u003E 0.001f )\n\t\t\t\t\t\t\ttarget = blended.Normal * c.Speed;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// \u5206\u8EAB\u8F6C\u5411\u54CD\u5E94\u540C\u6837\u968F\u8D28\u91CF\u8870\u51CF\uFF08\u4E0E\u4E3B\u7403\u540C\u66F2\u7EBF\uFF0C\u5927\u5206\u8EAB\u4E00\u6837\u7B28\u62D9\uFF09\n\t\t\t\tvar turn = MathF.Max( GameConfig.MinTurnSpeed,\n\t\t\t\t\tGameConfig.BaseTurnSpeed * MathF.Pow( GameConfig.StartMass / c.Mass, GameConfig.TurnCurve ) );\n\t\t\t\tvar t = 1f - MathF.Exp( -turn * dt );\n\t\t\t\tc.SteerVel = Vector3.Lerp( c.SteerVel, target, t );\n\t\t\t}\n\n\t\t\t// \u62A4\u536B\u56FA\u5B88\u5C16\u523A\uFF08M7.4\uFF09\uFF1A\u951A\u5B9A\u5206\u8EAB\u539F\u5730\u4E0D\u52A8\uFF08\u4E0D\u8DDF\u968F\u4E3B\u4EBA\u3001\u65E0\u8F6C\u5411\uFF09\n\t\t\tif ( c.Anchored ) c.SteerVel = Vector3.Zero;\n\n\t\t\tvar pos = c.Pos \u002B ( c.Impulse \u002B c.SteerVel ) * dt;\n\t\t\tvar half = MathF.Max( 0f, GameConfig.ArenaHalfSize - c.Radius );\n\t\t\tpos.x = Math.Clamp( pos.x, -half, half );\n\t\t\tpos.y = Math.Clamp( pos.y, -half, half );\n\t\t\tpos.z = 0f;\n\t\t\tc.Pos = pos;\n\n\t\t\t// \u9886\u571F\u5582\u65D7\uFF08M7\uFF09\uFF1A\u5B62\u5B50\u843D\u8FDB\u65D7\u5708\u5373\u8BA1\u5206\u5E76\u6D88\u8017\uFF08\u9A7B\u519B\u6A21\u578B\uFF0Chost \u6743\u5A01\uFF09\u3002\n\t\t\t// \u6CA1\u8FDB\u65D7\u5708\u6216\u89C4\u5219\u4E0D\u5141\u8BB8\uFF08\u975E\u90BB\u63A5/\u5927\u672C\u8425\uFF09\u65F6 Feed \u8FD4\u56DE false\uFF0C\u5B62\u5B50\u7EE7\u7EED\u5F53\u666E\u901A\u5B62\u5B50\u98DE\uFF0C\u8D28\u91CF\u4E0D\u767D\u8D39\n\t\t\tif ( c.PieceKind == CellPiece.Kind.EjectedMass \u0026\u0026 MatchState.IsTerritory )\n\t\t\t{\n\t\t\t\tif ( TerritoryManager.Feed( c.TeamIndex, c.Pos, c.Mass ) )\n\t\t\t\t{\n\t\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t\t_cellsDirty = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// \u540C\u4E3B\u4EBA\u5408\u5E76\uFF08\u51B7\u5374\u6309\u8D28\u91CF\u66F2\u7EBF v0.7.8.27\uFF1A\u5C0F\u5206\u8EAB 1s\u3001\u5927\u5206\u8EAB\u6700\u957F 14s\uFF09\uFF1A**\u78B0\u5230\u5373\u5408\u5E76**\uFF08\u5706\u7F18\u76F8\u89E6\uFF0C\n\t\t// \u4E0D\u518D\u8981\u6C42\u5D4C\u8FDB\u5706\u5FC3\u2014\u2014\u7528\u6237\u5B9E\u6D4B\u53CD\u9988\uFF1A\u9760\u5728\u4E00\u8D77\u4E0D\u52A8\u4E0D\u7B97\uFF0C\u89C2\u611F\u5C31\u662F\u0022\u4E0D\u80FD\u5408\u5E76\u0022\uFF09\u3002\u4F18\u5148\u5E76\u56DE\u4E3B\u7403\uFF0C\u5176\u6B21\u5927\u5206\u8EAB\u541E\u5C0F\u5206\u8EAB\u3002\n\t\t// \u26A0\uFE0F \u5185\u5C42 j \u53EA\u5411\u4E0A\u626B\uFF08j \u003E i\uFF09\uFF1Aj \u6389\u5230 i \u4EE5\u4E0B\u518D RemoveAt(i) \u4F1A\u7D22\u5F15\u8D8A\u754C\n\t\t//\uFF08\u4E0B\u65B9\u5143\u7D20\u88AB\u5220\u540E i \u5DF2\u6F02\u79FB\uFF0C\u5B9E\u6D4B ArgumentOutOfRangeException \u5237\u5C4F\uFF09\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar a = _cells[i];\n\t\t\tif ( a.PieceKind != CellPiece.Kind.SplitPiece || !a.IsMergeReady ) continue;\n\n\t\t\tvar owner = a.OwnerBall;\n\t\t\tif ( owner.IsValid() \u0026\u0026 owner.Alive \u0026\u0026 owner.WorldPosition.Distance( a.Pos ) \u003C owner.Radius \u002B a.Radius )\n\t\t\t{\n\t\t\t\towner.Mass = MathF.Min( GameConfig.MaxMass, owner.Mass \u002B a.Mass );\n\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t_cellsDirty = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor ( int j = i - 1; j \u003E= 0; j-- )\n\t\t\t{\n\t\t\t\tvar b = _cells[j];\n\t\t\t\tif ( b.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\t\tif ( b.OwnerSteamId != a.OwnerSteamId || !b.IsMergeReady ) continue;\n\n\t\t\t\tvar big = a.Mass \u003E= b.Mass ? a : b;\n\t\t\t\tvar small = big == a ? b : a;\n\t\t\t\tif ( big.Pos.Distance( small.Pos ) \u003E= big.Radius \u002B small.Radius ) continue;\n\n\t\t\t\tbig.Mass = MathF.Min( GameConfig.MaxMass, big.Mass \u002B small.Mass );\n\t\t\t\t_cells.RemoveAt( j );   // \u53EA\u5220 j \u003C i \u7684\uFF0Ca \u7684\u7D22\u5F15 i \u6052\u5B9A\u5B89\u5168\n\t\t\t\t_cellsDirty = true;\n\t\t\t\tbreak;   // \u6BCF\u5E27\u6BCF\u5206\u8EAB\u5E76\u4E00\u4E2A\uFF0C\u4E0B\u4E00\u5E27\u7EE7\u7EED\n\t\t\t}\n\t\t}\n\n\t\t// \u654C\u65B9\u5206\u8EAB\u76F8\u4E92\u63A8\u6324\uFF08v0.7.8.24 \u7403\u7403\u5927\u4F5C\u6218\u624B\u611F\uFF09\uFF1A\u5403\u4E0D\u52A8=\u5B9E\u5FC3\uFF0C\u6309\u8D28\u91CF\u6743\u91CD\u4E92\u76F8\u8BA9\u4F4D\u3002\n\t\t// \u540C\u4E3B\u4EBA\u8D70\u4E0A\u65B9\u5408\u5E76\uFF1B\u56E2\u961F\u8D5B\u961F\u53CB\u53EF\u91CD\u53E0\uFF1B\u5C16\u523A\u5206\u8EAB\u662F\u6B66\u5668\u4E0D\u53C2\u4E0E\uFF08\u78B0\u5230\u8D70\u6B66\u5668\u5224\u5B9A\uFF09\n\t\tvar pushTeamOf = BuildTeamMap();\n\t\tfor ( int i = 0; i \u003C _cells.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = _cells[i];\n\t\t\tif ( a.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\tfor ( int j = i \u002B 1; j \u003C _cells.Count; j\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar b = _cells[j];\n\t\t\t\tif ( b.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\t\tif ( a.OwnerSteamId == b.OwnerSteamId ) continue;\n\t\t\t\tif ( MatchState.IsTeam\n\t\t\t\t\t\u0026\u0026 pushTeamOf.TryGetValue( a.OwnerSteamId, out var at )\n\t\t\t\t\t\u0026\u0026 pushTeamOf.TryGetValue( b.OwnerSteamId, out var bt )\n\t\t\t\t\t\u0026\u0026 at == bt ) continue;\n\t\t\t\t// \u80FD\u5403\u5C31\u5403\uFF08TickEating \u5904\u7406\uFF09\uFF0C\u4E0D\u63A8\u2014\u2014\u5426\u5219\u63A8\u6324\u4F1A\u628A\u53EF\u541E\u7684\u5BF9\u624B\u6321\u5F00\uFF0C\u5927\u5206\u8EAB\u6C38\u8FDC\u5403\u4E0D\u6389\u5C0F\u5206\u8EAB\n\t\t\t\tvar hi = MathF.Max( a.Mass, b.Mass );\n\t\t\t\tvar lo = MathF.Min( a.Mass, b.Mass );\n\t\t\t\tif ( hi \u003E= lo * GameConfig.EatRatio ) continue;\n\n\t\t\t\tvar dx = b.Pos.x - a.Pos.x;\n\t\t\t\tvar dy = b.Pos.y - a.Pos.y;\n\t\t\t\tvar rr = a.Radius \u002B b.Radius;\n\t\t\t\tvar d2 = dx * dx \u002B dy * dy;\n\t\t\t\tif ( d2 \u003E= rr * rr ) continue;\n\t\t\t\tvar d = MathF.Sqrt( MathF.Max( d2, 0.0001f ) );\n\t\t\t\tvar push = MathF.Min( rr - d, GameConfig.PushMaxStep );\n\t\t\t\tvar nx = d \u003E 0.001f ? dx / d : 1f;\n\t\t\t\tvar ny = d \u003E 0.001f ? dy / d : 0f;\n\t\t\t\tvar wa = b.Mass / ( a.Mass \u002B b.Mass );   // \u8D8A\u91CD\u8BA9\u5F97\u8D8A\u5C11\n\t\t\t\ta.Pos = new Vector3( a.Pos.x - nx * push * wa, a.Pos.y - ny * push * wa, 0f );\n\t\t\t\tb.Pos = new Vector3( b.Pos.x \u002B nx * push * ( 1f - wa ), b.Pos.y \u002B ny * push * ( 1f - wa ), 0f );\n\t\t\t\t_cellsDirty = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u6267\u884C\u5206\u88C2\uFF08host \u6743\u5A01\uFF1Bowner \u672C\u5730\u76F4\u8C03 / \u5BA2\u6237\u7AEF\u7ECF RequestSplit RPC\uFF09\u3002\n\t/// **agar \u6807\u51C6\u5168\u5458\u5206\u88C2\uFF08v0.7.1.0 \u7528\u6237\u9700\u6C42\uFF09**\uFF1A\u7A7A\u683C = \u4E3B\u7403 \u002B \u6BCF\u4E2A\u591F\u91CD\u7684\u5206\u8EAB**\u540C\u65F6\u5BF9\u534A\u5206\u88C2**\uFF0C\n\t/// \u65B0\u8EAB\u4F53**\u5404\u81EA\u671D\u5149\u6807\u5F39\u51FA**\uFF08v0.7.8.7 \u624B\u611F\uFF1AaimPoint \u4E3A\u5149\u6807\u4E16\u754C\u5750\u6807\uFF0C\u4E3B\u7403\u4E0E\u6BCF\u4E2A\u5206\u8EAB\n\t/// \u72EC\u7ACB\u7B97\u6307\u5411\uFF0Cagar \u6807\u51C6\u6247\u5F62\u94FA\u5F00\uFF09\u2014\u2014\u8FDE\u6309\u53EF\u5FEB\u901F\u94FA\u5F00\u5230\u5206\u8EAB\u4E0A\u9650\uFF081\u21922\u21924\u21928\u219216 \u53EA\u8981 4 \u6309\uFF09\u3002\n\t/// \u6BCF\u9897\u8EAB\u4F53\u72EC\u7ACB\u8FC7\u8D28\u91CF\u95E8\u69DB\uFF08\u5BF9\u534A\u540E\u4E0D\u51FB\u7A7F\u8D28\u91CF\u4E0B\u9650 1\uFF09\uFF0C\u69FD\u6EE1\u5373\u505C\u3002\n\t/// \u003C/summary\u003E\n\tpublic void DoSplit( Ball ball, Vector2 aimPoint )\n\t{\n\t\tif ( !GameConfig.EnableSplit || !NetworkManager.IsAuthority ) return;\n\t\tif ( ball is null || !ball.IsValid() || !ball.Alive ) return;\n\t\tif ( ball.Mass \u003C GameConfig.SplitMinMass ) return;\n\t\tif ( ClassSkillManager.IsTankSurging( ball ) ) return;   // \u5766\u514B\u7206\u53D1\u671F\u7981\u5206\u88C2\uFF08\u4E34\u65F6\u8D28\u91CF\u5206\u88C2=\u5957\u73B0\uFF0CM7.4\uFF09\n\n\t\tvar mainDir = FallbackDir( ball, DirTo( ball.WorldPosition, aimPoint ) );\n\n\t\t// \u26A0\uFE0F \u5148\u6355\u83B7\u65E7\u5206\u8EAB\u6570\u91CF\u518D\u751F\u6210\u4E3B\u7403\u65B0\u5206\u8EAB\uFF1ASpawnPiece \u662F\u5C3E\u90E8\u8FFD\u52A0\uFF0C\u82E5\u5FAA\u73AF\u4ECE\n\t\t// _cells.Count-1 \u8D77\u6B65\u4F1A\u7B2C\u4E00\u8F6E\u5C31\u904D\u5386\u5230\u521A\u5F39\u51FA\u7684\u65B0\u5206\u8EAB\u628A\u5B83\u518D\u5288\u4E00\u6B21\u2014\u2014\n\t\t// \u4E00\u6B21\u7A7A\u683C\u51FA 3 \u9897\uFF08\u672C\u4F53\u002B\u65B0\u5206\u8EAB\u002B\u65B0\u5206\u8EAB\u7684\u5B6A\u751F\uFF09\uFF0C\u5B9E\u6D4B v0.7.8.26 \u4FEE\n\t\tint oldCount = _cells.Count;\n\n\t\t// \u4E3B\u7403\u5BF9\u534A\n\t\tif ( CountPiecesOf( ball.OwnerSteamId ) \u003C GameConfig.MaxSplitPieces )\n\t\t{\n\t\t\tvar pieceMass = ball.Mass * 0.5f;\n\t\t\tball.Mass -= pieceMass;\n\t\t\tSpawnPiece( ball, mainDir, pieceMass );\n\t\t\tNeonRenderer.Puff( ball.WorldPosition, ball.ColorIndex );\n\t\t}\n\n\t\t// \u6BCF\u4E2A\u591F\u91CD\u7684**\u65E7**\u5206\u8EAB\u4E5F\u5BF9\u534A\uFF08**\u5404\u81EA\u671D\u5149\u6807\u5F39\u51FA**\uFF1B\u5012\u5E8F\u53EA\u904D\u5386\u5230 oldCount\uFF0C\n\t\t// \u672C\u8F6E\u65B0\u751F\u6210\u7684\u4E3B\u7403\u5206\u8EAB/\u5B6A\u751F\u4E00\u5F8B\u4E0D\u53C2\u4E0E\u518D\u5206\u88C2\uFF09\u3002\n\t\t// \u65B0\u5206\u8EAB\u540C\u6837\u8D34\u7740\u6BCD\u5206\u8EAB**\u5916\u7F18**\u751F\u6210\uFF08v0.7.2.0\uFF09\uFF0C\u5426\u5219 1s \u51B7\u5374\u4E00\u5230\u5C31\u88AB\u5438\u56DE\u53BB\n\t\tfor ( int i = oldCount - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar c = _cells[i];\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c.OwnerSteamId != ball.OwnerSteamId ) continue;\n\t\t\tif ( c.Mass \u003C GameConfig.SplitMinMass * 2f ) continue;\n\t\t\tif ( CountPiecesOf( ball.OwnerSteamId ) \u003E= GameConfig.MaxSplitPieces ) break;\n\n\t\t\tvar half = c.Mass * 0.5f;\n\t\t\tc.Mass -= half;\n\t\t\tvar twinRadius = GameConfig.StartRadius * MathF.Sqrt( half / GameConfig.StartMass );\n\t\t\tvar dir = DirOr( c.Pos, aimPoint, mainDir );\n\n\t\t\tvar twin = new CellPiece\n\t\t\t{\n\t\t\t\tId = _nextCellId\u002B\u002B,\n\t\t\t\tPieceKind = CellPiece.Kind.SplitPiece,\n\t\t\t\tOwnerSteamId = c.OwnerSteamId,\n\t\t\t\tOwnerBall = c.OwnerBall,\n\t\t\t\tColorIndex = c.ColorIndex,\n\t\t\t\tMass = half,\n\t\t\t\tPos = c.Pos \u002B new Vector3( dir.x, dir.y, 0f ) * ( c.Radius \u002B twinRadius ),\n\t\t\t\tDrawPos = c.Pos,\n\t\t\t\tImpulse = new Vector3( dir.x, dir.y, 0f ) * GameConfig.SplitImpulseSpeed,\n\t\t\t\tSinceSpawn = 0,\n\t\t\t\tMergeCooldown = GameConfig.MergeCooldownFor( half ),   // v0.7.8.27 \u66F2\u7EBF\n\t\t\t};\n\t\t\tClampToArena( twin );\n\t\t\t_cells.Add( twin );\n\t\t\t_cellsDirty = true;\n\t\t}\n\n\t\tGameLog.Info( $\u0022[game] split-all \u0027{ball.PlayerName}\u0027\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u751F\u6210\u4E00\u4E2A\u5206\u88C2\u5206\u8EAB\uFF08\u8C03\u7528\u65B9\u5DF2\u6263\u4E3B\u7403\u8D28\u91CF\uFF09\u3002\n\t/// **\u8D34\u7740\u4E3B\u7403\u5916\u7F18\u751F\u6210\uFF08v0.7.2.0\uFF09**\uFF1A\u539F 0.4\u00D7\u534A\u5F84\u5728\u7403\u4F53\u5185\u90E8\uFF0C\u5927\u7403\u5206\u8EAB\u98DE\u4E0D\u51FA\u5408\u5E76\u5438\u9644\u8303\u56F4\uFF0C\n\t/// 1s \u51B7\u5374\u4E00\u5230\u5C31\u88AB\u5438\u56DE\u4E3B\u7403\u2014\u2014\u73A9\u5BB6\u5B9E\u9645\u6C38\u8FDC\u53EA\u6709\u4E00\u9897\u8EAB\u4F53\uFF0C\u4E3B\u7403\u4E00\u6B7B\u65E0\u8EAB\u53EF\u664B\u5347\u76F4\u63A5\u771F\u6B7B \u003C/summary\u003E\n\tCellPiece SpawnPiece( Ball ball, Vector2 dir, float mass )\n\t{\n\t\tvar pieceRadius = GameConfig.StartRadius * MathF.Sqrt( mass / GameConfig.StartMass );\n\t\tvar c = new CellPiece\n\t\t{\n\t\t\tId = _nextCellId\u002B\u002B,\n\t\t\tPieceKind = CellPiece.Kind.SplitPiece,\n\t\t\tOwnerSteamId = ball.OwnerSteamId,\n\t\t\tOwnerBall = ball,\n\t\t\tColorIndex = ball.ColorIndex,\n\t\t\tMass = mass,\n\t\t\tPos = ball.WorldPosition \u002B new Vector3( dir.x, dir.y, 0f ) * ( ball.Radius \u002B pieceRadius ),\n\t\t\tDrawPos = ball.WorldPosition,\n\t\t\tImpulse = new Vector3( dir.x, dir.y, 0f ) * GameConfig.SplitImpulseSpeed,\n\t\t\tSinceSpawn = 0,\n\t\t\tMergeCooldown = GameConfig.MergeCooldownFor( mass ),   // v0.7.8.27 \u66F2\u7EBF\uFF1A\u5206\u8EAB\u8D8A\u5927\u5408\u56DE\u8D8A\u4E45\n\t\t};\n\t\tClampToArena( c );\n\t\t_cells.Add( c );\n\t\t_cellsDirty = true;\n\t\treturn c;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u649E\u523A\u70B8\u88C2\uFF1A\u8FDE\u73AF\u5F3A\u5236\u5BF9\u534A\u5206\u88C2\uFF0C\u76F4\u5230\u5206\u8EAB\u69FD\u6EE1\u6216\u8D28\u91CF\u6389\u5230\u5206\u88C2\u95E8\u69DB\u4EE5\u4E0B\u3002\n\t/// \u8FD4\u56DE\u662F\u5426\u771F\u70B8\u4E86\u4E1C\u897F\u2014\u2014\u5927\u7403\u505C\u5728\u523A\u4E0A\u4F1A\u6BCF\u5E27\u89E6\u53D1\uFF0C\u9760\u5B83\u6321\u4F4F\u7279\u6548/\u65E5\u5FD7\u5237\u5C4F\u3002\n\t/// \u003C/summary\u003E\n\tbool DoBurst( Ball ball )\n\t{\n\t\tvar slots = GameConfig.MaxSplitPieces - CountPiecesOf( ball.OwnerSteamId );\n\t\tif ( slots \u003C= 0 || ball.Mass \u003C GameConfig.SplitMinMass * 2f ) return false;\n\n\t\tint n = 0;\n\t\twhile ( n \u003C slots \u0026\u0026 ball.Mass \u003E= GameConfig.SplitMinMass * 2f )\n\t\t{\n\t\t\tvar pieceMass = ball.Mass * 0.5f;\n\t\t\tball.Mass -= pieceMass;\n\t\t\tvar p = SpawnPiece( ball, RandomDir(), pieceMass );\n\t\t\tp.MergeCooldown = GameConfig.SpikePieceMergeCooldown;   // \u649E\u523A\u788E\u7247\uFF1A\u77ED\u51B7\u5374\uFF0C\u4E3B\u4EBA\u53EF\u4EE5\u5F88\u5FEB\u5403\u56DE\u6765\n\t\t\tn\u002B\u002B;\n\t\t}\n\n\t\tSpawnPopFx( ball );\n\t\tGameLog.Info( $\u0022[game] burst on spike: \u0027{ball.PlayerName}\u0027 into {n} pieces\u0022 );\n\t\treturn true;\n\t}\n\n\t/// \u003Csummary\u003E \u5927\u5206\u8EAB\u649E\u523A\uFF1A\u5BF9\u534A\u88C2\u6210\u4E24\u4E2A\uFF08\u5404\u81EA\u91CD\u7F6E\u5408\u5E76\u51B7\u5374\uFF09\uFF1B\u69FD\u6EE1\u5219\u88AB\u523A\u5438\u6536 \u003C/summary\u003E\n\tvoid BurstPiece( CellPiece c )\n\t{\n\t\tvar slots = GameConfig.MaxSplitPieces - CountPiecesOf( c.OwnerSteamId );\n\t\tif ( slots \u003C= 0 )\n\t\t{\n\t\t\t_cells.Remove( c );\n\t\t\t_cellsDirty = true;\n\t\t\treturn;\n\t\t}\n\n\t\tvar dir = RandomDir();\n\t\tvar half = c.Mass * 0.5f;\n\n\t\tc.Mass = half;\n\t\tc.SinceSpawn = 0;\n\t\tc.MergeCooldown = GameConfig.SpikePieceMergeCooldown;\n\t\tc.Impulse = new Vector3( dir.x, dir.y, 0f ) * GameConfig.SplitImpulseSpeed;\n\n\t\tvar d2 = -dir;\n\t\tvar other = new CellPiece\n\t\t{\n\t\t\tId = _nextCellId\u002B\u002B,\n\t\t\tPieceKind = CellPiece.Kind.SplitPiece,\n\t\t\tOwnerSteamId = c.OwnerSteamId,\n\t\t\tOwnerBall = c.OwnerBall,\n\t\t\tColorIndex = c.ColorIndex,\n\t\t\tMass = half,\n\t\t\tPos = c.Pos \u002B new Vector3( d2.x, d2.y, 0f ) * ( c.Radius * 0.5f ),\n\t\t\tDrawPos = c.Pos,\n\t\t\tImpulse = new Vector3( d2.x, d2.y, 0f ) * GameConfig.SplitImpulseSpeed,\n\t\t\tSinceSpawn = 0,\n\t\t\tMergeCooldown = GameConfig.SpikePieceMergeCooldown,\n\t\t};\n\t\tClampToArena( other );\n\t\t_cells.Add( other );\n\t\t_cellsDirty = true;\n\n\t\tNeonRenderer.Pop( c.Pos, c.ColorIndex, c.Radius * 2f );\n\t\tif ( GameSfx.IsMine( c.OwnerSteamId ) )\n\t\t\tGameSfx.Pop( c.Pos );\n\t\tNetworkManager.PoppedEffect( c.Pos, (byte)c.ColorIndex, c.Radius * 2f, c.OwnerSteamId );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u5C16\u523A\u5224\u5B9A\uFF08host\uFF0Cv0.5.1.4 \u7528\u6237\u5B9A\u7A3F agar \u75C5\u6BD2\u89C4\u5219\uFF09\uFF1A\n\t/// - \u7EC6\u80DE**\u6BD4\u523A\u5C0F**\uFF1A\u78B0\u5230\u5373\u88AB\u5C16\u523A\u5403\u6389\uFF08\u73A9\u5BB6\u6B7B\u4EA1 / \u5206\u8EAB\u6D88\u5931\uFF09\uFF1B\n\t/// - \u7EC6\u80DE**\u6BD4\u523A\u5927**\uFF1A\u628A\u5C16\u523A\u5403\u6389\uFF08\u002BSpikeMass\uFF09\u5E76\u5F3A\u5236\u89E6\u53D1\u5206\u88C2\uFF08DoBurst \u8FDE\u73AF\u70B8\uFF09\uFF0C\u5C16\u523A\u5728\u522B\u5904\u91CD\u751F\uFF08\u573A\u4E0A\u6052 12 \u9897\uFF09\uFF1B\n\t/// - \u5B62\u5B50\u78B0\u523A\u82AF\u88AB\u5438\u6536\uFF08\u5582\u523A\uFF09\u3002\u51FA\u751F\u4FDD\u62A4\u671F\u5185\u514D\u75AB\u5C16\u523A\u3002\n\t/// \u003C/summary\u003E\n\tvoid TickSpikes()\n\t{\n\t\tvar spikes = _spikes;\n\t\tif ( spikes is null ) return;\n\n\t\tvar touch = GameConfig.SpikeRadius * GameConfig.SpikeTouchFactor;\n\n\t\t// \u7403 \u00D7 \u523A\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.Alive || b.IsProtected ) continue;   // \u62A4\u76FE\u671F\u514D\u75AB\u5C16\u523A\n\n\t\t\tvar bp = b.WorldPosition;\n\t\t\tfor ( int i = 0; i \u003C spikes.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar dx = spikes[i].x - bp.x;\n\t\t\t\tvar dy = spikes[i].y - bp.y;\n\t\t\t\tvar rr = b.Radius \u002B touch;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E rr * rr ) continue;\n\n\t\t\t\tif ( b.Radius \u003E GameConfig.SpikeRadius )\n\t\t\t\t{\n\t\t\t\t\t// \u5403\u523A\uFF1A\u002B100 \u8D28\u91CF \u002B \u5F3A\u5236\u5206\u88C2\uFF0C\u523A\u91CD\u751F\u5230\u65B0\u4F4D\u7F6E\n\t\t\t\t\tb.Mass = MathF.Min( GameConfig.MaxMass, b.Mass \u002B GameConfig.SpikeMass );\n\t\t\t\t\tDoBurst( b );\n\t\t\t\t\tRespawnSpike( i );\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t// \u88AB\u523A\u5403\uFF08v0.6.2.0 agar \u89C4\u5219\uFF09\uFF1A\u53EA\u635F\u5931\u8FD9\u4E2A\u8EAB\u4F53\u2014\u2014\u6700\u5927\u5206\u8EAB\u5347\u7EA7\u6210\u65B0\u4E3B\u7403\n\t\t\t\t\tSpawnPopFx( b );\n\n\t\t\t\t\tif ( TryPromoteFromPieces( b ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tGameLog.Info( $\u0022[game] body lost to spike: {b.PlayerName} (promoted)\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tb.MarkDead();\n\t\t\t\t\t\tif ( !b.IsBot ) PopPiecesOf( b.OwnerSteamId );\n\t\t\t\t\t\tGameLog.Info( \u0022[game] eaten by spike: \u0022 \u002B b.PlayerName );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// \u5206\u8EAB \u00D7 \u523A\uFF08\u540C\u89C4\u5219\uFF1B\u5012\u5E8F\u904D\u5386\u2014\u2014\u88AB\u5403\u8981\u79FB\u9664\uFF09\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar c = _cells[i];\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\n\t\t\tfor ( int sIdx = 0; sIdx \u003C spikes.Length; sIdx\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar dx = spikes[sIdx].x - c.Pos.x;\n\t\t\t\tvar dy = spikes[sIdx].y - c.Pos.y;\n\t\t\t\tvar rr = c.Radius \u002B touch;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E rr * rr ) continue;\n\n\t\t\t\tif ( c.Radius \u003E GameConfig.SpikeRadius )\n\t\t\t\t{\n\t\t\t\t\tc.Mass = MathF.Min( GameConfig.MaxMass, c.Mass \u002B GameConfig.SpikeMass );\n\t\t\t\t\tBurstPiece( c );   // \u5F3A\u5236\u5BF9\u534A\u88C2\uFF08\u788E\u7247\u77ED\u51B7\u5374\uFF0C\u4E3B\u4EBA\u80FD\u5FEB\u901F\u5403\u56DE\uFF09\n\t\t\t\t\tRespawnSpike( sIdx );\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tNeonRenderer.Puff( c.Pos, c.ColorIndex );\n\t\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t\t_cellsDirty = true;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// \u5B62\u5B50\u5582\u523A\uFF08v0.7.8.24 \u7403\u7403\u5927\u4F5C\u6218\u5F0F\uFF09\uFF1A\u5438\u6536\u5145\u80FD \u2192 \u5582\u6EE1\u7206\u5F00\u5C04\u523A\u3002\u523A\u4E0D\u6362\u4F4D\u4E0D\u91CD\u751F\uFF0C\n\t\t// \u5582\u98DF\u91CF\u5F52\u96F6\u91CD\u65B0\u6512\u2014\u2014\u0022\u5728\u654C\u4EBA\u8EAB\u8FB9\u517B\u523A\u0022\u7684\u6218\u672F\u6838\u5FC3\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar c = _cells[i];\n\t\t\tif ( c.PieceKind != CellPiece.Kind.EjectedMass ) continue;\n\n\t\t\tfor ( int sIdx = 0; sIdx \u003C spikes.Length; sIdx\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar dx = spikes[sIdx].x - c.Pos.x;\n\t\t\t\tvar dy = spikes[sIdx].y - c.Pos.y;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E touch * touch ) continue;\n\n\t\t\t\tif ( _spikeFed is not null \u0026\u0026 (uint)sIdx \u003C (uint)_spikeFed.Length )\n\t\t\t\t{\n\t\t\t\t\t_spikeFed[sIdx] \u002B= c.Mass;\n\t\t\t\t\tNetworkManager.SpikeFed( sIdx, _spikeFed[sIdx] );   // \u5BA2\u6237\u7AEF\u955C\u50CF\uFF08\u523A\u957F\u5927\u663E\u793A\u7528\uFF09\n\t\t\t\t\tif ( _spikeFed[sIdx] \u003E= GameConfig.SpikeGrowTarget )\n\t\t\t\t\t\tBurstSpike( sIdx );\n\t\t\t\t}\n\n\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\t_cellsDirty = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tTickSpines();\n\t\tTickSpikeMinions( spikes );\n\t}\n\n\t/// \u003Csummary\u003E \u523A\u7206\uFF08host \u6743\u5A01\uFF0Cv0.7.8.24\uFF09\uFF1A\u5582\u98DF\u91CF\u6E05\u96F6 \u002B \u5747\u5300\u89D2\u5EA6\u5C04\u51FA\u5F39\u5E55\uFF08\u547D\u4E2D\u5728 TickSpines\uFF09\u3002\n\t/// \u89D2\u5EA6\u516C\u5F0F SpineDir \u4E0E\u5BA2\u6237\u7AEF OnSpikeBurstRemote \u5171\u7528\uFF08\u786E\u5B9A\u6027\uFF0C\u89C6\u89C9\u5F39\u9053=\u6743\u5A01\u5F39\u9053\uFF09 \u003C/summary\u003E\n\tvoid BurstSpike( int index )\n\t{\n\t\tif ( _spikes is null || _spikeFed is null || (uint)index \u003E= (uint)_spikes.Length ) return;\n\t\t_spikeFed[index] = 0f;\n\n\t\tvar pos = _spikes[index];\n\t\tvar n = GameConfig.SpikeBurstCount;\n\t\tfor ( int k = 0; k \u003C n; k\u002B\u002B )\n\t\t{\n\t\t\t_spines.Add( new SpikeSpine\n\t\t\t{\n\t\t\t\tPos = new Vector2( pos.x, pos.y ),\n\t\t\t\tVel = SpineDir( k, n, index ) * GameConfig.SpineSpeed,\n\t\t\t\tSinceSpawn = 0,   // TimeSince \u9ED8\u8BA4\u503C\u22600\uFF08\u8BB0\u5F55\u65F6\u523B 0 \u2192 \u8BFB\u51FA\u5DE8\u5927\u503C\u7ACB\u5373\u8FC7\u671F\uFF09\uFF0C\u5FC5\u987B\u663E\u5F0F\u5F52\u96F6\n\t\t\t} );\n\t\t}\n\t\tNetworkManager.SpikeBurst( index );   // \u5168\u7AEF\u7206\u6548\uFF1B\u5BA2\u6237\u7AEF\u5728\u540C\u6B3E\u89D2\u5EA6\u751F\u6210\u89C6\u89C9\u5F39\u5E55\n\t}\n\n\t/// \u003Csummary\u003E \u5F39\u5E55\u65B9\u5411\uFF08\u786E\u5B9A\u6027\u516C\u5F0F\uFF0Chost \u6743\u5A01\u4E0E\u5BA2\u6237\u7AEF\u89C6\u89C9\u5171\u7528\uFF09\uFF1A\u5747\u5300\u5706\u5468 \u002B \u6309\u523A\u5E8F\u53F7\u9519\u5F00 \u003C/summary\u003E\n\tstatic Vector2 SpineDir( int k, int n, int index )\n\t{\n\t\tvar ang = k / (float)n * MathF.PI * 2f \u002B index * 0.7f;\n\t\treturn new Vector2( MathF.Cos( ang ), MathF.Sin( ang ) );\n\t}\n\n\t/// \u003Csummary\u003E \u523A\u7206\u5F39\u5E55\u63A8\u8FDB\uFF08host \u6743\u5A01\uFF09\uFF1A\u547D\u4E2D\u7403/\u5206\u8EAB\u6263\u8D28\u91CF\uFF08\u62A4\u76FE\u4E0D\u9632\u5C16\u523A\u65CF\uFF1B\u51FA\u751F\u4FDD\u62A4\u514D\u75AB\uFF09\uFF0C\n\t/// \u5230\u671F\u6D88\u6563\u3002\u4E2D\u7ACB\u4F24\u5BB3\u2014\u2014\u547D\u4E2D\u8C01\u90FD\u6263\uFF08\u5305\u62EC\u5582\u523A\u8005\u81EA\u5DF1\uFF09\uFF0C\u5F39\u5E55\u672C\u4F53\u6D88\u5931 \u003C/summary\u003E\n\tvoid TickSpines()\n\t{\n\t\tvar dt = Time.Delta;\n\t\tfor ( int i = _spines.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar s = _spines[i];\n\t\t\tif ( s.SinceSpawn \u003E GameConfig.SpineLife ) { _spines.RemoveAt( i ); continue; }\n\t\t\ts.Pos \u002B= s.Vel * dt;\n\n\t\t\tbool consumed = false;\n\n\t\t\tforeach ( var b in _balls )\n\t\t\t{\n\t\t\t\tif ( !b.IsValid() || !b.Alive || b.IsProtected ) continue;\n\t\t\t\tvar dx = b.WorldPosition.x - s.Pos.x;\n\t\t\t\tvar dy = b.WorldPosition.y - s.Pos.y;\n\t\t\t\tvar rr = b.Radius \u002B GameConfig.SpineRadius;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E= rr * rr ) continue;\n\n\t\t\t\tb.Mass = MathF.Max( GameConfig.StartMass, b.Mass * ( 1f - GameConfig.SpineHitMassFraction ) );\n\t\t\t\tNetworkManager.SpineHit( s.Pos.x, s.Pos.y, (byte)Math.Clamp( b.ColorIndex, 0, 255 ) );\n\t\t\t\tconsumed = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( consumed ) { _spines.RemoveAt( i ); continue; }\n\n\t\t\tfor ( int j = _cells.Count - 1; j \u003E= 0; j-- )\n\t\t\t{\n\t\t\t\tvar c = _cells[j];\n\t\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\t\tvar dx = c.Pos.x - s.Pos.x;\n\t\t\t\tvar dy = c.Pos.y - s.Pos.y;\n\t\t\t\tvar rr = c.Radius \u002B GameConfig.SpineRadius;\n\t\t\t\tif ( dx * dx \u002B dy * dy \u003E= rr * rr ) continue;\n\n\t\t\t\tc.Mass = MathF.Max( GameConfig.StartMass, c.Mass * ( 1f - GameConfig.SpineHitMassFraction ) );\n\t\t\t\t_cellsDirty = true;\n\t\t\t\tNetworkManager.SpineHit( s.Pos.x, s.Pos.y, (byte)Math.Clamp( c.ColorIndex, 0, 255 ) );\n\t\t\t\t_spines.RemoveAt( i );\n\t\t\t\tconsumed = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( consumed ) continue;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u523A\u7206\u5F39\u5E55\uFF08\u5BA2\u6237\u7AEF\u955C\u50CF\uFF09\uFF1A\u7EAF\u89C6\u89C9\u5F39\u9053\u63A8\u8FDB \u002B \u5230\u671F\u6D88\u6563\uFF0C\u4E0D\u5224\u547D\u4E2D\uFF08\u6263\u8D28\u91CF\u7531 host \u6743\u5A01\u4E0B\u53D1\uFF09 \u003C/summary\u003E\n\tvoid TickSpinesVisual()\n\t{\n\t\tvar dt = Time.Delta;\n\t\tfor ( int i = _spines.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar s = _spines[i];\n\t\t\tif ( s.SinceSpawn \u003E GameConfig.SpineLife ) { _spines.RemoveAt( i ); continue; }\n\t\t\ts.Pos \u002B= s.Vel * dt;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u5C16\u523A\u5206\u8EAB\u5224\u5B9A\uFF08v0.7.4.0\uFF09\uFF1A\u654C\u65B9\u73A9\u5BB6/\u5206\u8EAB\u78B0\u5230\u6309\u91CE\u523A\u89C4\u5219\u2014\u2014\u6BD4\u523A\u5927\u88AB\u70B8\uFF08\u8FDE\u73AF\u5BF9\u534A\u002B\u523A\u88AB\u5403\uFF09\uFF0C\n\t/// \u6BD4\u523A\u5C0F\u88AB\u523A\u5403\uFF08\u6B7B/\u664B\u5347\uFF0C\u523A\u4E0D\u6D88\u8017\uFF09\u3002**\u540C\u4E3B\u4EBA\u4E0E\u56E2\u961F\u8D5B\u961F\u53CB\u514D\u75AB**\uFF08\u6B66\u5668\u4E0D\u4F24\u81EA\u5DF1\u4EBA\uFF09\u3002\n\t/// \u70B8\u6389\u0022\u6BD4\u81EA\u5DF1\uFF08\u4E3B\u4EBA\uFF09\u5927\u7684\u7403\u0022\u2192 \u4E3B\u4EBA\u5956 3 \u79D2\u65E0\u654C\uFF08\u7528\u6237\u5B9A\u7A3F C\uFF1A\u6B66\u5668\u4E13\u7ED9\u5C0F\u7403\u53CD\u5927\u7403\uFF0C\n\t/// \u5927\u7403\u7528\u5B83\u70B8\u5C0F\u7403\u96F6\u5956\u52B1\uFF09\u3002\u5224\u5B9A\u82AF\u4E0E\u91CE\u523A\u540C\u6B3E\uFF08SpikeRadius \u00D7 SpikeTouchFactor\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tvoid TickSpikeMinions( Vector2[] spikes )\n\t{\n\t\tvar touch = GameConfig.SpikeRadius * GameConfig.SpikeTouchFactor;\n\t\tvar teamOf = BuildTeamMap();\n\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar minion = _cells[i];\n\t\t\tif ( minion.PieceKind != CellPiece.Kind.SpikeMinion ) continue;\n\n\t\t\tvar owner = minion.OwnerBall;\n\t\t\tbool ownerAlive = owner.IsValid() \u0026\u0026 owner.Alive;\n\n\t\t\tbool consumed = false;\n\n\t\t\t// \u654C\u65B9\u7403 \u00D7 \u5C16\u523A\u5206\u8EAB\n\t\t\tforeach ( var b in _balls )\n\t\t\t{\n\t\t\t\tif ( !b.IsValid() || !b.Alive ) continue;\n\t\t\t\tif ( b == owner ) continue;   // \u4E3B\u4EBA\u514D\u75AB\n\t\t\t\tif ( MatchState.IsTeam \u0026\u0026 ownerAlive \u0026\u0026 SameTeam( b, owner ) ) continue;\n\n\t\t\t\tvar d = b.WorldPosition.Distance( minion.Pos );\n\t\t\t\tif ( d \u003E= b.Radius \u002B touch ) continue;\n\n\t\t\t\tif ( b.Radius \u003E GameConfig.SpikeRadius )\n\t\t\t\t{\n\t\t\t\t\t// \u5927\u7403\u88AB\u70B8\uFF1A\u5403\u6389\u5C16\u523A\u5206\u8EAB\u7684\u8D28\u91CF \u002B \u8FDE\u73AF\u5BF9\u534A\u70B8\u88C2\uFF1B\n\t\t\t\t\t// \u70B8\u6389\u0022\u6BD4\u4E3B\u4EBA\u5927\u7684\u7403\u0022 \u2192 \u4E3B\u4EBA 3s \u65E0\u654C\uFF08\u590D\u7528\u62A4\u76FE buff\uFF09\n\t\t\t\t\tfloat gained = minion.Mass;\n\t\t\t\t\tb.Mass = MathF.Min( GameConfig.MaxMass, b.Mass \u002B gained );\n\t\t\t\t\tDoBurst( b );\n\t\t\t\t\tNotifyBallEaten( minion.OwnerSteamId, b.OwnerSteamId, gained );\n\n\t\t\t\t\tif ( ownerAlive \u0026\u0026 b.Mass \u003E owner.Mass )\n\t\t\t\t\t{\n\t\t\t\t\t\towner.ApplyBuff( (byte)PowerUpManager.Kind.Shield, GameConfig.SpikeMinionRewardSeconds );\n\t\t\t\t\t\tGameAchievements.SpikeMinionBlast( minion.OwnerSteamId );   // \u6210\u5C31\uFF1A\u4EE5\u5C0F\u535A\u5927\uFF08v0.7.8.13\uFF09\n\t\t\t\t\t\tGameLog.Info( $\u0022[game] spike minion burst \u0027{b.PlayerName}\u0027 -\u003E \u0027{owner.PlayerName}\u0027 invincible {GameConfig.SpikeMinionRewardSeconds}s\u0022 );\n\t\t\t\t\t}\n\n\t\t\t\t\tNeonRenderer.Pop( minion.Pos, minion.ColorIndex, minion.Radius * 3f );\n\t\t\t\t\tif ( Networking.IsActive ) NetworkManager.PoppedEffect( minion.Pos, (byte)minion.ColorIndex, minion.Radius * 3f, minion.OwnerSteamId );\n\t\t\t\t\tconsumed = true;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t// \u6BD4\u523A\u5C0F\uFF1A\u88AB\u523A\u5403\uFF08\u523A\u4FDD\u7559\uFF0C\u4E0E\u91CE\u523A\u4E00\u81F4\uFF09\n\t\t\t\t\tSpawnPopFx( b );\n\t\t\t\t\tif ( !TryPromoteFromPieces( b ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tb.MarkDead();\n\t\t\t\t\t\tif ( !b.IsBot ) PopPiecesOf( b.OwnerSteamId );\n\t\t\t\t\t}\n\t\t\t\t\tGameLog.Info( $\u0022[game] eaten by spike minion: {b.PlayerName}\u0022 );\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// \u654C\u65B9\u5206\u8EAB \u00D7 \u5C16\u523A\u5206\u8EAB\uFF08\u5927\u5206\u8EAB\u5403\u523A\u002B\u70B8\u88C2\uFF0C\u5C0F\u5206\u8EAB\u88AB\u5403\uFF09\n\t\t\tif ( !consumed )\n\t\t\t{\n\t\t\t\tfor ( int j = _cells.Count - 1; j \u003E= 0; j-- )\n\t\t\t\t{\n\t\t\t\t\tvar c = _cells[j];\n\t\t\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c == minion ) continue;\n\t\t\t\t\tif ( c.OwnerSteamId == minion.OwnerSteamId ) continue;   // \u4E3B\u4EBA\u514D\u75AB\n\t\t\t\t\tif ( MatchState.IsTeam\n\t\t\t\t\t\t\u0026\u0026 teamOf.TryGetValue( c.OwnerSteamId, out var pt )\n\t\t\t\t\t\t\u0026\u0026 ownerAlive \u0026\u0026 owner.TeamIndex \u003E= 0 \u0026\u0026 pt == owner.TeamIndex ) continue;\n\n\t\t\t\t\tif ( c.Pos.Distance( minion.Pos ) \u003E= c.Radius \u002B touch ) continue;\n\n\t\t\t\t\tif ( c.Radius \u003E GameConfig.SpikeRadius )\n\t\t\t\t\t{\n\t\t\t\t\t\tc.Mass = MathF.Min( GameConfig.MaxMass, c.Mass \u002B minion.Mass );\n\t\t\t\t\t\tBurstPiece( c );\n\t\t\t\t\t\tNeonRenderer.Pop( minion.Pos, minion.ColorIndex, minion.Radius * 3f );\n\t\t\t\t\t\tconsumed = true;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tNeonRenderer.Puff( c.Pos, c.ColorIndex );\n\t\t\t\t\t\t_cells.Remove( c );   // \u6309\u5F15\u7528\u79FB\u9664\uFF1ABurstPiece \u5185\u90E8\u53EF\u80FD\u5DF2\u5220\u8FC7\u5143\u7D20\uFF0C\u7D22\u5F15\u4E0D\u53EF\u4FE1\uFF08v0.7.8.87\uFF09\n\t\t\t\t\t\t_cellsDirty = true;\n\t\t\t\t\t\tif ( j \u003C i ) i--;   // \u5DF2\u79FB\u9664\u7684\u5206\u8EAB\u5728\u5F53\u524D\u904D\u5386\u4F4D\u4E4B\u524D\uFF0C\u8865\u7D22\u5F15\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( consumed )\n\t\t\t{\n\t\t\t\t// \u6309\u5F15\u7528\u79FB\u9664\uFF08v0.7.8.87 \u4FEE\uFF09\uFF1A\u4E0A\u9762 BurstPiece \u5728\u81EA\u5DF1\u4EBA\u6EE1 16 \u5206\u8EAB\u65F6\u4E5F\u4F1A\u4ECE _cells \u5220\u5143\u7D20\uFF0C\n\t\t\t\t// \u539F\u6765\u6309\u7D22\u5F15 RemoveAt(i) \u4F1A\u5220\u9519\u8EAB\u4F53\u6216\u8D8A\u754C\u629B\u5F02\u5E38\n\t\t\t\t_cells.Remove( minion );\n\t\t\t\t_cellsDirty = true;\n\t\t\t\ti = Math.Min( i, _cells.Count );   // \u79FB\u9664\u540E\u5217\u8868\u53D8\u77ED\uFF0C\u94B3\u5230\u6709\u6548\u8303\u56F4\uFF08\u5FAA\u73AF\u8FD8\u4F1A\u518D --\uFF09\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u523A\u88AB\u5403\u6389\u540E\u5728\u522B\u5904\u91CD\u751F\uFF08\u907F\u5F00\u4E2D\u5FC3\u51FA\u751F\u533A\uFF09\uFF0C\u5168\u91CF\u5E7F\u64AD\uFF0812 \u4E2A\u4F4D\u7F6E\u5C0F\u5305\uFF0C\u5E42\u7B49\uFF09 \u003C/summary\u003E\n\tvoid RespawnSpike( int index )\n\t{\n\t\t_spikes[index] = RandomSpikePos();\n\t\tif ( Networking.IsActive ) NetworkManager.SpikesFull( _spikes );   // \u65E0\u4F1A\u8BDD\u4E0D\u5E7F\u64AD\n\t}\n\n\tVector2 RandomSpikePos()\n\t{\n\t\tvar half = GameConfig.ArenaHalfSize - GameConfig.FoodMargin - GameConfig.SpikeRadius;\n\t\tVector2 p;\n\t\tdo\n\t\t{\n\t\t\tp = new Vector2( Game.Random.Float( -half, half ), Game.Random.Float( -half, half ) );\n\t\t}\n\t\twhile ( p.Length \u003C 512f );   // \u4E2D\u5FC3\u533A\u7559\u7A7A\uFF08\u51FA\u751F/\u91CD\u751F\u4E0D\u81F3\u4E8E\u8138\u4E0A\u6709\u523A\uFF09\n\t\treturn p;\n\t}\n\n\t/// \u003Csummary\u003E host \u751F\u6210\u5C16\u523A\uFF08\u907F\u5F00\u4E2D\u5FC3\u51FA\u751F\u533A\uFF09\u5E76\u5E7F\u64AD\uFF1B\u65B0\u8FDE\u63A5\u8D70 OnActive \u5B9A\u5411\u8865\u53D1 \u003C/summary\u003E\n\tvoid GenerateSpikes()\n\t{\n\t\t_spikes = new Vector2[GameConfig.SpikeCount];\n\t\t_spikeFed = new float[GameConfig.SpikeCount];\n\t\t_spines.Clear();\n\t\tfor ( int i = 0; i \u003C _spikes.Length; i\u002B\u002B )\n\t\t{\n\t\t\t_spikes[i] = RandomSpikePos();\n\t\t}\n\t\tif ( Networking.IsActive ) NetworkManager.SpikesFull( _spikes );   // \u65E0\u4F1A\u8BDD\uFF08\u83DC\u5355\u6F14\u793A\u8D5B\uFF09\u4E0D\u5E7F\u64AD\n\t\tGameLog.Info( $\u0022[game] spikes generated: {_spikes.Length}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u6267\u884C\u5410\u5B62\u5B50\uFF08host \u6743\u5A01\uFF0C**agar \u6807\u51C6\u591A\u8EAB\u4F53\u5582\u7403**\uFF09\uFF1A\u5BF9\u73A9\u5BB6\u7684\u6BCF\u4E00\u4E2A\u8EAB\u4F53\u2014\u2014\u4E3B\u7403\u548C\u6240\u6709\n\t/// \u591F\u8D28\u91CF\u7684\u5206\u8EAB\u2014\u2014\u5404\u5410\u4E00\u9897\u4E2D\u6027\u5B62\u5B50\uFF08v0.7.8.7 \u624B\u611F\uFF1AaimPoint \u4E3A\u5149\u6807\u4E16\u754C\u5750\u6807\uFF0C\n\t/// \u6BCF\u9897\u8EAB\u4F53**\u72EC\u7ACB\u671D\u5149\u6807**\u5F39\u51FA\uFF0C\u5B62\u5B50\u6247\u5F62\u6563\u5F00\uFF09\u3002\u6309\u4F4F\u8FDE\u5410\u3002\n\t/// \u003C/summary\u003E\n\tpublic void DoEject( Ball ball, Vector2 aimPoint )\n\t{\n\t\tif ( !GameConfig.EnableEject || !NetworkManager.IsAuthority ) return;\n\t\tif ( ball is null || !ball.IsValid() || !ball.Alive ) return;\n\t\tif ( _sinceEject.TryGetValue( ball.Id, out var t ) \u0026\u0026 t \u003C GameConfig.EjectCooldownSeconds ) return;\n\t\tif ( !HasEjectMass( ball ) ) return;   // \u8D28\u91CF\u4E0D\u591F\u5C31\u522B\u5403\u8FD9\u6B21\u8282\u6D41\uFF08v0.7.8.87\uFF1A\u539F\u6765\u5148\u8BB0\u51B7\u5374\uFF0C\u767D\u541E 0.15s\uFF09\n\t\t_sinceEject[ball.Id] = 0;\n\n\t\tvar mainDir = FallbackDir( ball, DirTo( ball.WorldPosition, aimPoint ) );\n\n\t\tEjectFrom( ball, mainDir );\n\n\t\t// \u4E24\u9636\u6BB5\uFF08\u5148\u6536\u96C6\u540E\u751F\u6210\uFF09\uFF1ASpawnBlob \u8D85\u5B62\u5B50\u4E0A\u9650\u4F1A _cells.Remove \u6324\u6389\u6700\u8001\u4E00\u9897\uFF0C\n\t\t// \u8FB9\u904D\u5386 _cells \u8FB9\u751F\u6210 = \u0022Collection was modified\u0022\uFF08\u5B9E\u6D4B\u70B8 bot \u5582\u5927\u54E5\u8DEF\u5F84\uFF0Cv0.7.8.8 \u4FEE\uFF09\n\t\tList\u003CCellPiece\u003E ejectors = null;\n\n\t\tforeach ( var c in _cells )\n\t\t{\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c.OwnerSteamId != ball.OwnerSteamId ) continue;\n\t\t\tif ( EjectCost( c.Mass ) \u003C= 0f ) continue;\n\n\t\t\t( ejectors ??= new List\u003CCellPiece\u003E() ).Add( c );\n\t\t}\n\n\t\tif ( ejectors is null ) return;\n\n\t\tforeach ( var c in ejectors )\n\t\t{\n\t\t\tvar cost = EjectCost( c.Mass );   // \u4E24\u9636\u6BB5\u95F4\u8D28\u91CF\u4E0D\u4F1A\u88AB\u6539\uFF0C\u91CD\u7B97\u5373\u5F97\n\t\t\tif ( cost \u003C= 0f ) continue;\n\n\t\t\tc.Mass -= cost;\n\t\t\tSpawnBlob( c.Pos, DirOr( c.Pos, aimPoint, mainDir ), c.Radius, c.OwnerSteamId, ball.TeamIndex, c.ColorIndex, cost * GameConfig.EjectBlobEfficiency );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u4E3B\u7403\u662F\u5426\u8FD8\u6709\u53EF\u5410\u8D28\u91CF\uFF08\u65E0\u5206\u8EAB\u65F6\u51B3\u5B9A\u8FD9\u6B21\u6309\u952E\u7B97\u4E0D\u7B97\u4E00\u6B21\u6709\u6548\u6295\u63B7\uFF1Bv0.7.8.87\uFF09 \u003C/summary\u003E\n\tstatic bool HasEjectMass( Ball ball ) =\u003E EjectCost( ball.Mass ) \u003E 0f;\n\n\t/// \u003Csummary\u003E \u8EAB\u4F53 \u2192 \u5149\u6807\u7684\u5355\u4F4D\u6307\u5411\uFF1B\u5149\u6807\u6B63\u8D34\u8BE5\u8EAB\u4F53\u65F6\u8FD4\u56DE fallback\uFF08\u4E3B\u7403\u65B9\u5411\uFF09 \u003C/summary\u003E\n\tstatic Vector2 DirOr( Vector3 from, Vector2 aimPoint, Vector2 fallback )\n\t{\n\t\tvar d = DirTo( from, aimPoint );\n\t\treturn d.Length \u003E 0.001f ? d : fallback;\n\t}\n\n\t/// \u003Csummary\u003E \u8EAB\u4F53 \u2192 \u7784\u51C6\u70B9\u7684\u5355\u4F4D\u6307\u5411\uFF1B\u6B63\u8D34\u65F6\u8FD4\u56DE Zero\uFF08\u8C03\u7528\u65B9\u8D70 FallbackDir \u515C\u5E95\uFF09 \u003C/summary\u003E\n\tstatic Vector2 DirTo( Vector3 from, Vector2 aimPoint )\n\t{\n\t\tvar dx = aimPoint.x - from.x;\n\t\tvar dy = aimPoint.y - from.y;\n\t\tvar len = MathF.Sqrt( dx * dx \u002B dy * dy );\n\t\treturn len \u003E 0.001f ? new Vector2( dx / len, dy / len ) : Vector2.Zero;\n\t}\n\n\t/// \u003Csummary\u003E \u8BE5\u8D28\u91CF\u5410\u4E00\u9897\u7684\u6210\u672C\uFF08\u7EA6 1% \u968F\u4F53\u91CF\u7F29\u653E\uFF0C\u4E0B\u9650 1\uFF09\uFF1B\u8FD4\u56DE 0 = \u4F1A\u51FB\u7A7F\u8D28\u91CF\u4E0B\u9650 1\uFF0C\u4E0D\u5141\u8BB8 \u003C/summary\u003E\n\tstatic float EjectCost( float mass )\n\t{\n\t\tvar cost = MathF.Max( GameConfig.EjectMinCost, mass * GameConfig.EjectMassPercent );\n\t\treturn mass - cost \u003E= GameConfig.EjectMinMass ? cost : 0f;\n\t}\n\n\t/// \u003Csummary\u003E \u4E3B\u7403\u5410\u51FA\u4E00\u9897\u5B62\u5B50 \u003C/summary\u003E\n\tvoid EjectFrom( Ball ball, Vector2 dir )\n\t{\n\t\tvar cost = EjectCost( ball.Mass );\n\t\tif ( cost \u003C= 0f ) return;\n\n\t\tball.Mass -= cost;\n\t\tSpawnBlob( ball.WorldPosition, dir, ball.Radius, ball.OwnerSteamId, ball.TeamIndex, ball.ColorIndex, cost * GameConfig.EjectBlobEfficiency );\n\t}\n\n\t/// \u003Csummary\u003E \u751F\u6210\u4E00\u9897\u4E2D\u6027\u5B62\u5B50\uFF08\u8D28\u91CF=\u6D88\u8017\u00D7\u8FD4\u8FD8\u7387\uFF09\u5E76\u505A\u603B\u91CF\u4E0A\u9650\u6536\u655B\uFF08\u6324\u6389\u6700\u8001\uFF09\u3002\n\t/// team = \u5410\u7403\u8005\u961F\u4F0D\uFF08\u9886\u571F\u5582\u65D7\u8BA1\u5206\u6309\u961F\u4F0D\uFF1Bbot \u4E0D\u6309 OwnerSteamId \u67E5\u2014\u2014bot \u7684 SteamId \u5168\u662F 0 \u4F1A\u4E32\u961F\uFF09 \u003C/summary\u003E\n\tvoid SpawnBlob( Vector3 pos, Vector2 dir, float radius, long owner, int team, int colorIndex, float mass )\n\t{\n\t\tvar c = new CellPiece\n\t\t{\n\t\t\tId = _nextCellId\u002B\u002B,\n\t\t\tPieceKind = CellPiece.Kind.EjectedMass,\n\t\t\tOwnerSteamId = owner,\n\t\t\tTeamIndex = team,\n\t\t\tColorIndex = colorIndex,\n\t\t\tMass = mass,\n\t\t\tPos = pos \u002B new Vector3( dir.x, dir.y, 0f ) * ( radius \u002B 16f ),\n\t\t\tDrawPos = pos,\n\t\t\tImpulse = new Vector3( dir.x, dir.y, 0f ) * GameConfig.EjectImpulseSpeed,\n\t\t\tSinceSpawn = 0,\n\t\t};\n\t\tClampToArena( c );\n\t\t_cells.Add( c );\n\t\t_cellsDirty = true;\n\t\tNeonRenderer.Puff( c.Pos, colorIndex );\n\n\t\t// \u5B62\u5B50\u603B\u91CF\u4E0A\u9650\uFF1A\u6324\u6389\u6700\u8001\u7684\u4E00\u9897\uFF08TimeSince \u8D8A\u5927\u8D8A\u8001\uFF09\n\t\tint blobs = 0;\n\t\tCellPiece oldest = null;\n\t\tforeach ( var x in _cells )\n\t\t{\n\t\t\tif ( x.PieceKind != CellPiece.Kind.EjectedMass ) continue;\n\t\t\tblobs\u002B\u002B;\n\t\t\tif ( oldest is null || x.SinceSpawn \u003E oldest.SinceSpawn ) oldest = x;\n\t\t}\n\t\tif ( blobs \u003E GameConfig.MaxEjectedBlobs \u0026\u0026 oldest is not null )\n\t\t{\n\t\t\t_cells.Remove( oldest );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E host \u2192 \u5168\u4F53\uFF1A\u5206\u8EAB/\u5B62\u5B50\u72B6\u6001\u5FEB\u7167\uFF08\u5168\u91CF\u66FF\u6362\u5F0F\uFF0C\u91CF\u5C0F\u9891\u7387\u4F4E\uFF0C\u4E0D\u505A\u589E\u91CF\uFF09 \u003C/summary\u003E\n\tvoid BroadcastCells()\n\t{\n\t\t_cellsDirty = false;\n\n\t\tvar wires = new CellWire[_cells.Count];\n\t\tfor ( int i = 0; i \u003C _cells.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar c = _cells[i];\n\t\t\twires[i] = new CellWire\n\t\t\t{\n\t\t\t\tId = c.Id,\n\t\t\t\tKind = (byte)c.PieceKind,\n\t\t\t\tColorIndex = (byte)c.ColorIndex,\n\t\t\t\tOwner = c.OwnerSteamId,\n\t\t\t\tX = c.Pos.x,\n\t\t\t\tY = c.Pos.y,\n\t\t\t\tMass = c.Mass,\n\t\t\t};\n\t\t}\n\t\tNetworkManager.CellsState( wires );\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u5E94\u7528\u5FEB\u7167\uFF1A\u6309 Id \u5C31\u5730\u66F4\u65B0\uFF08\u4FDD\u7559 DrawPos \u5E73\u6ED1\uFF09\uFF0C\u65B0\u589E\u8865\u5165\uFF0C\u6D88\u5931\u79FB\u9664\u3002\n\t/// \u7ECF\u7F13\u51B2\u5B57\u5178 O(n\u002Bm)\uFF08\u539F\u53CC\u91CD\u5168\u626B O(n\u00D7m)\uFF0C\u6EE1\u914D 800\u002B \u5206\u8EAB/\u5B62\u5B50\u65F6\u662F 15Hz \u70ED\u8DEF\u5F84\uFF09 \u003C/summary\u003E\n\tpublic void OnCellsStateRemote( CellWire[] wires )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;   // host \u56DE\u58F0\u6321\u6389\n\n\t\t_wireById.Clear();\n\t\tif ( wires is not null )\n\t\t{\n\t\t\tforeach ( var w in wires )\n\t\t\t\t_wireById[w.Id] = w;\n\t\t}\n\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar c = _cells[i];\n\t\t\tif ( !_wireById.TryGetValue( c.Id, out var w ) )\n\t\t\t{\n\t\t\t\t// \u6D88\u5931\u4E5F\u8981\u7ED9\u53CD\u9988\uFF08v0.7.8.92\uFF09\uFF1A\u88AB\u541E/\u5E76\u56DE/\u4E3B\u4EBA\u9635\u4EA1\u5728\u5BA2\u6237\u7AEF\u53EA\u662F\u4ECE\u5FEB\u7167\u91CC\u9759\u9ED8\u6D88\u5931\uFF0C\n\t\t\t\t// \u89C2\u611F\u5C31\u662F\u0022\u5206\u8EAB\u51ED\u7A7A\u6D88\u5931\u0022\u2014\u2014\u539F\u5730\u8865\u4E00\u6735\u5C0F\u55B7\u53D1\uFF08\u5408\u5E76\u56DE\u6536\u4E5F\u4F1A\u51FA\uFF0C\u5C5E\u53EF\u63A5\u53D7\u7684\u5438\u6536\u53CD\u9988\uFF09\n\t\t\t\tNeonRenderer.Puff( c.DrawPos, c.ColorIndex );\n\t\t\t\t_cells.RemoveAt( i );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tc.Pos = new Vector3( w.X, w.Y, 0f );\n\t\t\tc.Mass = w.Mass;\n\t\t\tc.ColorIndex = w.ColorIndex;\n\t\t\tc.OwnerSteamId = w.Owner;\n\t\t\tc.PieceKind = (CellPiece.Kind)w.Kind;\n\t\t}\n\n\t\t_cellIdScratch.Clear();\n\t\tforeach ( var c in _cells )\n\t\t\t_cellIdScratch.Add( c.Id );\n\n\t\tforeach ( var kv in _wireById )\n\t\t{\n\t\t\tif ( _cellIdScratch.Contains( kv.Key ) ) continue;\n\n\t\t\tvar w = kv.Value;\n\t\t\tvar pos = new Vector3( w.X, w.Y, 0f );\n\t\t\t_cells.Add( new CellPiece\n\t\t\t{\n\t\t\t\tId = w.Id,\n\t\t\t\tPieceKind = (CellPiece.Kind)w.Kind,\n\t\t\t\tColorIndex = w.ColorIndex,\n\t\t\t\tOwnerSteamId = w.Owner,\n\t\t\t\tMass = w.Mass,\n\t\t\t\tPos = pos,\n\t\t\t\tDrawPos = pos,\n\t\t\t} );\n\t\t\tNeonRenderer.Puff( pos, w.ColorIndex );   // \u5BA2\u6237\u7AEF\u8865\u4E0A\u751F\u6210\u55B7\u53D1\uFF08host \u5DF2\u5728\u672C\u5730\u653E\u8FC7\uFF09\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u6B7B\u4EA1\u7206\u88C2\u7279\u6548\uFF1Ahost \u672C\u5730\u751F\u6210 \u002B \u5E7F\u64AD\uFF08\u5BA2\u6237\u7AEF\u7ECF PoppedEffect RPC \u751F\u6210\u540C\u6B3E\uFF09\u3002\n\t/// \u7206\u88C2\u97F3\u53EA\u64AD\u0022\u81EA\u5DF1\u6B7B\u4EA1/\u7206\u88C2\u0022\u7684\uFF08\u89C6\u89C9\u7279\u6548\u4ECD\u5168\u573A\u64AD\uFF0C\u58F0\u97F3\u6536\u7A84\u5230\u672C\u4EBA\uFF09 \u003C/summary\u003E\n\tvoid SpawnPopFx( Ball ball )\n\t{\n\t\tvar pos = ball.WorldPosition;\n\t\tNeonRenderer.Pop( pos, ball.ColorIndex, ball.Radius );\n\t\tif ( GameSfx.IsMine( ball.OwnerSteamId ) )\n\t\t\tGameSfx.Pop( pos );\n\t\tif ( Networking.IsActive ) NetworkManager.PoppedEffect( pos, (byte)ball.ColorIndex, ball.Radius, ball.OwnerSteamId );   // \u65E0\u4F1A\u8BDD\u4E0D\u5E7F\u64AD\n\t}\n\n\t/// \u003Csummary\u003E \u6E05\u6389\u67D0\u4E3B\u4EBA\u7684\u5168\u90E8\u5206\u8EAB\uFF08\u4E3B\u7403\u6B7B\u4EA1/\u65AD\u7EBF\u65F6\u8C03\u7528\uFF1B\u5B62\u5B50\u4FDD\u7559\uFF09 \u003C/summary\u003E\n\tpublic void PopPiecesOf( long ownerSteamId )\n\t{\n\t\tfor ( int i = _cells.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar c = _cells[i];\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c.OwnerSteamId != ownerSteamId ) continue;\n\t\t\t_cells.RemoveAt( i );\n\t\t\t_cellsDirty = true;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u5C16\u523A\u5206\u8EAB\uFF08v0.7.4.0 \u9053\u5177\u6548\u679C\uFF09\uFF1A\u4ECE\u4E3B\u4EBA\u73B0\u6263 SpikeMinionMassPercent \u7684\u8D28\u91CF\u751F\u6210\u4E00\u9897\u8DDF\u968F\u5206\u8EAB\uFF0C\n\t/// 20 \u79D2\u5185\u662F\u0022\u523A\u0022\u2014\u2014\u654C\u65B9\u73A9\u5BB6\u78B0\u5230\u6309\u5C16\u523A\u89C4\u5219\u5904\u7406\uFF08\u5927\u7403\u88AB\u70B8/\u5C0F\u7403\u6B7B\uFF09\uFF0C\u5230\u671F\u53D8\u56DE\u666E\u901A\u5206\u8EAB\u5408\u4F53\u56DE\u6536\u8D28\u91CF\u3002\n\t/// \u751F\u6210\u5373\u5E26\u4E3B\u4EBA\u8F6C\u5411\u8DDF\u968F\uFF08TickCells \u7684 SpikeMinion \u5206\u652F\uFF09\u3002\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003E \u62A4\u536B\u56FA\u5B88\u5C16\u523A\uFF08M7.4 \u804C\u4E1A\uFF09\uFF1A\u539F\u5730\u751F\u6210\u951A\u5B9A\u5C16\u523A\u5206\u8EAB\uFF088 \u79D2\uFF0C\u4E0D\u6263\u4E3B\u4EBA\u8D28\u91CF\uFF09\uFF0C\n\t/// \u654C\u65B9\u89E6\u78B0\u8D70\u5C16\u523A\u5206\u8EAB\u65E2\u6709\u5224\u5B9A\uFF1B\u5230\u671F\u53D8\u56DE\u666E\u901A\u5206\u8EAB\u5F00\u59CB\u8DDF\u968F\u4E3B\u4EBA \u003C/summary\u003E\n\tpublic void SpawnGuardSpike( Ball ball )\n\t{\n\t\tif ( ball is null || !ball.IsValid() || !ball.Alive ) return;\n\n\t\tvar c = new CellPiece\n\t\t{\n\t\t\tId = _nextCellId\u002B\u002B,\n\t\t\tPieceKind = CellPiece.Kind.SpikeMinion,\n\t\t\tOwnerSteamId = ball.OwnerSteamId,\n\t\t\tOwnerBall = ball,\n\t\t\tTeamIndex = ball.TeamIndex,\n\t\t\tColorIndex = ball.ColorIndex,\n\t\t\tMass = MathF.Max( 10f, ball.Mass * 0.15f ),\n\t\t\tPos = ball.WorldPosition,\n\t\t\tDrawPos = ball.WorldPosition,\n\t\t\tSinceSpawn = 0,\n\t\t\tAnchored = true,\n\t\t\tMaxLife = GameConfig.ClassGuardLife,\n\t\t};\n\t\t_cells.Add( c );\n\t\t_cellsDirty = true;\n\t\tNeonRenderer.Puff( c.Pos, c.ColorIndex );\n\t}\n\n\tpublic void SpawnSpikeMinion( Ball ball )\n\t{\n\t\tif ( ball is null || !ball.IsValid() || !ball.Alive ) return;\n\n\t\tvar mass = MathF.Max( 10f, ball.Mass * GameConfig.SpikeMinionMassPercent );\n\t\tmass = MathF.Min( mass, ball.Mass - 1f );\n\t\tif ( mass \u003C 10f ) return;   // \u8D28\u91CF\u592A\u4F4E\uFF08\u6781\u5C0F\u7403\uFF09\u6263\u4E0D\u51FA 10\uFF0C\u653E\u5F03\uFF08\u9053\u5177\u767D\u62FF\u2014\u2014\u53EF\u63A5\u53D7\u7684\u6781\u7AEF\u8FB9\u754C\uFF09\n\n\t\tball.Mass -= mass;\n\n\t\tvar dir = RandomDir();\n\t\tvar c = new CellPiece\n\t\t{\n\t\t\tId = _nextCellId\u002B\u002B,\n\t\t\tPieceKind = CellPiece.Kind.SpikeMinion,\n\t\t\tOwnerSteamId = ball.OwnerSteamId,\n\t\t\tOwnerBall = ball,\n\t\t\tColorIndex = ball.ColorIndex,\n\t\t\tMass = mass,\n\t\t\tPos = ball.WorldPosition \u002B new Vector3( dir.x, dir.y, 0f ) * ( ball.Radius \u002B 40f ),\n\t\t\tDrawPos = ball.WorldPosition,\n\t\t\tSinceSpawn = 0,\n\t\t};\n\t\tClampToArena( c );\n\t\t_cells.Add( c );\n\t\t_cellsDirty = true;\n\n\t\tNeonRenderer.Puff( c.Pos, ball.ColorIndex );\n\t\tGameLog.Info( $\u0022[game] spike minion spawned for \u0027{ball.PlayerName}\u0027 (mass {mass:0})\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// agar \u591A\u8EAB\u4F53\u89C4\u5219\uFF08v0.6.2.0 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u4E3B\u7403\u88AB\u5403/\u649E\u523A\u65F6\uFF0C\u628A\u6700\u5927\u7684\u5206\u8EAB\u0022\u5347\u7EA7\u0022\u6210\u65B0\u4E3B\u7403\n\t/// \u2014\u2014\u7403\u5BF9\u8C61\u539F\u5730\u7EED\u547D\uFF08\u8D28\u91CF/\u4F4D\u7F6E\u6362\u6210\u8BE5\u5206\u8EAB\u7684\uFF09\uFF0C\u73A9\u5BB6\u7EE7\u7EED\u73A9\uFF1B\u5168\u90E8\u8EAB\u4F53\u6B7B\u5149\u624D\u7B97\u771F\u6B7B\u3002\n\t/// \u4E3B\u7403\u4E0E\u5206\u8EAB\u5728\u540C\u4E00 tick \u5185\u5B8C\u6210\u4EA4\u63A5\uFF0CAlive \u4ECE\u4E0D\u7FFB\u8F6C\uFF0C\u5BA2\u6237\u7AEF\u4E0D\u4F1A\u770B\u5230\u6B7B\u4EA1/\u91CD\u7F6E\u3002\n\t/// \u8FD4\u56DE\u662F\u5426\u6210\u529F\uFF08\u6CA1\u6709\u5206\u8EAB = \u771F\u6B7B\u4EA1\uFF0C\u8D70\u539F MarkDead \u6D41\u7A0B\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tbool TryPromoteFromPieces( Ball ball )\n\t{\n\t\tif ( ball.IsBot ) return false;   // bot \u4E0D\u5206\u88C2\uFF0C\u65E0\u5206\u8EAB\u53EF\u664B\u5347\n\n\t\tCellPiece best = null;\n\t\tforeach ( var c in _cells )\n\t\t{\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c.OwnerSteamId != ball.OwnerSteamId ) continue;\n\t\t\tif ( best is null || c.Mass \u003E best.Mass ) best = c;\n\t\t}\n\t\tif ( best is null ) return false;\n\n\t\tfloat mass = best.Mass;\n\t\tVector3 pos = best.Pos;\n\t\t_cells.Remove( best );\n\t\t_cellsDirty = true;\n\n\t\tball.Mass = mass;\n\t\tball.Alive = true;\n\t\tball.WorldPosition = pos;   // host \u81EA\u5DF1\u7684\u7403\uFF1Aowner \u6A21\u62DF\u4ECE\u8FD9\u91CC\u7EE7\u7EED\n\t\t_lastPos[ball.Id] = pos;   // \u4F4D\u79FB\u6821\u9A8C\u951A\u70B9\u540C\u6B65\u77AC\u79FB\uFF0C\u5426\u5219\u4E0B\u4E00\u5E27\u88AB\u5F53\u77AC\u79FB\u4F5C\u5F0A\u62C9\u56DE\uFF08v0.7.8.33 \u4FEE\uFF09\n\n\t\t// \u8FDC\u7A0B\u73A9\u5BB6\u7684\u7403\u5F52\u5176\u4E3B\u4EBA\u6A21\u62DF\uFF0C\u4F4D\u7F6E\u8981\u901A\u77E5\u4E3B\u4EBA\u77AC\u79FB\u8FC7\u53BB\uFF08\u5426\u5219\u4E0B\u4E00\u5E27\u88AB\u4E3B\u4EBA\u7684\u6A21\u62DF\u5199\u56DE\u65E7\u4F4D\u7F6E\uFF09\n\t\tif ( ball.IsRemoteOwned || ball.OwnerSteamId != Game.SteamId.Value )\n\t\t{\n\t\t\tforeach ( var conn in Connection.All )\n\t\t\t{\n\t\t\t\tif ( conn is null || !conn.IsActive ) continue;\n\t\t\t\tif ( conn.SteamId.Value != ball.OwnerSteamId ) continue;\n\n\t\t\t\tusing ( Rpc.FilterInclude( conn ) )\n\t\t\t\t{\n\t\t\t\t\tNetworkManager.MainPromoted( pos.x, pos.y );\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tGameLog.Info( $\u0022[game] promoted piece -\u003E main: {ball.PlayerName} mass={mass:0}\u0022 );\n\t\treturn true;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u89C6\u89D2\u7126\u70B9\uFF08v0.6.2.0 \u7528\u6237\u5B9A\u7A3F\u0022\u89C6\u89D2\u8DDF\u968F\u6700\u5927\u7684\u5206\u8EAB\u0022\uFF09\uFF1A\u4E3B\u7403\u4E0E\u81EA\u5DF1\u6240\u6709\u5206\u8EAB\u4E2D\n\t/// \u8D28\u91CF\u6700\u5927\u7684\u90A3\u4E2A\u8EAB\u4F53\u7684\u4F4D\u7F6E/\u8D28\u91CF\u3002\n\t/// \u003C/summary\u003E\n\tpublic Vector3 FocusPositionOf( Ball ball, out float biggestMass )\n\t{\n\t\tbiggestMass = ball.IsValid() ? ball.Mass : 0f;\n\t\tvar pos = ball.IsValid() ? ball.WorldPosition : Vector3.Zero;\n\t\tif ( !ball.IsValid() ) return pos;\n\n\t\tforeach ( var c in _cells )\n\t\t{\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c.OwnerSteamId != ball.OwnerSteamId ) continue;\n\t\t\tif ( c.Mass \u003E biggestMass )\n\t\t\t{\n\t\t\t\tbiggestMass = c.Mass;\n\t\t\t\tpos = c.Pos;\n\t\t\t}\n\t\t}\n\t\treturn pos;\n\t}\n\n\tint CountPiecesOf( long ownerSteamId )\n\t{\n\t\tint n = 0;\n\t\tforeach ( var c in _cells )\n\t\t{\n\t\t\tif ( c.PieceKind == CellPiece.Kind.SplitPiece \u0026\u0026 c.OwnerSteamId == ownerSteamId ) n\u002B\u002B;\n\t\t}\n\t\treturn n;\n\t}\n\n\t/// \u003Csummary\u003E \u8BE5\u73A9\u5BB6\u662F\u5426\u8FD8\u6709\u5206\u8EAB\u62A4\u4F53\uFF08\u4E3B\u7403\u88AB\u5403\u4FDD\u62A4 \u002B \u62A4\u76FE\u865A\u73AF\u6E32\u67D3\u7528\uFF09 \u003C/summary\u003E\n\tpublic bool HasPieces( long ownerSteamId ) =\u003E CountPiecesOf( ownerSteamId ) \u003E 0;\n\n\t/// \u003Csummary\u003E \u73A9\u5BB6\u603B\u8D28\u91CF = \u4E3B\u7403 \u002B \u5168\u90E8\u5206\u8EAB\uFF08\u6392\u884C\u699C/HUD \u7528\uFF1B\u5206\u8EAB\u7684\u8D28\u91CF\u4E5F\u662F\u771F\u5B9E\u6218\u529B\uFF09 \u003C/summary\u003E\n\tpublic float TotalMassFor( Ball ball )\n\t{\n\t\tif ( ball is null || !ball.IsValid() ) return 0f;\n\n\t\tfloat m = ball.Mass;\n\t\tvar sid = ball.OwnerSteamId;\n\t\tforeach ( var c in _cells )\n\t\t{\n\t\t\tif ( c.PieceKind == CellPiece.Kind.SplitPiece \u0026\u0026 c.OwnerSteamId == sid ) m \u002B= c.Mass;\n\t\t}\n\t\treturn m;\n\t}\n\n\tVector2 SteerDirOf( Ball ball )\n\t{\n\t\tif ( !ball.IsValid() ) return Vector2.Zero;\n\t\treturn _steerDir.TryGetValue( ball.Id, out var d ) ? d : Vector2.Zero;\n\t}\n\n\t/// \u003Csummary\u003E \u5F39\u5C04\u65B9\u5411\uFF1A\u4F18\u5148\u7528\u8F93\u5165\u65B9\u5411\uFF0C\u6CA1\u7ED9\uFF08\u9759\u6B62\u5F00\u706B\uFF09\u5C31\u6CBF\u7528\u7403\u7684\u79FB\u52A8\u65B9\u5411\uFF0C\u518D\u9000\u968F\u673A \u003C/summary\u003E\n\tVector2 FallbackDir( Ball ball, Vector2 dir )\n\t{\n\t\tif ( dir.Length \u003E 0.001f ) return dir.Normal;\n\t\tvar d = SteerDirOf( ball );\n\t\tif ( d.Length \u003E 0.001f ) return d.Normal;\n\n\t\treturn RandomDir();\n\t}\n\n\t/// \u003Csummary\u003E \u5747\u5300\u968F\u673A\u5355\u4F4D\u65B9\u5411 \u003C/summary\u003E\n\tVector2 RandomDir()\n\t{\n\t\tvar a = Game.Random.Float( 0f, MathF.PI * 2f );\n\t\treturn new Vector2( MathF.Cos( a ), MathF.Sin( a ) );\n\t}\n\n\tvoid ClampToArena( CellPiece c )\n\t{\n\t\tvar half = MathF.Max( 0f, GameConfig.ArenaHalfSize - c.Radius );\n\t\tc.Pos.x = Math.Clamp( c.Pos.x, -half, half );\n\t\tc.Pos.y = Math.Clamp( c.Pos.y, -half, half );\n\t\tc.Pos.z = 0f;\n\t}\n\n\t// ---- \u4E16\u754C\u642D\u5EFA ----\n\n\t/// \u003Csummary\u003E\n\t/// UI \u5BF9\u8D26\uFF08v0.6.4.5\uFF09\uFF1A\u7531\u72B6\u6001\u6807\u5FD7**\u63A8\u5BFC**\u5404\u9762\u677F\u5E94\u6709\u7684\u53EF\u89C1\u6027\uFF0C\u6BCF\u5E27\u5F3A\u5236\u6267\u884C\u3002\n\t/// \u4FEE\u0022\u5BA2\u6237\u7AEF\u52A0\u5165\u540E UI \u5168\u6D88\u5931 / \u623F\u95F4\u548C\u4E3B\u83DC\u5355\u53E0\u5728\u4E00\u8D77\u0022\u2014\u2014\u6B64\u524D Show/Hide \u5168\u9760\u5404\u8DEF\u5F84\u81EA\u89C9\u8C03\u7528\uFF0C\n\t/// \u4EFB\u4F55\u4E00\u6761\u8DEF\u5F84\u6F0F\u8C03\uFF08\u4E2D\u9014\u52A0\u5165\u3001\u70ED\u91CD\u8F7D\u91CD\u5EFA\u9762\u677F\u3001\u6B8B\u7559\u5012\u8BA1\u65F6\u5361\u4F4F\u6062\u590D\u5206\u652F\uFF09\u5C31\u6C38\u8FDC\u505C\u5728\u574F\u754C\u9762\u3002\n\t/// \u003C/summary\u003E\n\tvoid ReconcileUi()\n\t{\n\t\tbool inSession = Networking.IsActive;\n\n\t\t// \u6B8B\u7559\u52A0\u5165\u5012\u8BA1\u65F6\u4F1A\u8BA9\u5BA2\u6237\u7AEF\u6062\u590D\u5206\u652F\uFF08_joinCountdown\u003C0 \u624D\u8D70\uFF09\u6C38\u4E45\u5931\u6548\u2014\u2014\u5BF9\u5C40\u5DF2\u4E0D\u5728\u5C31\u6E05\u6389\n\t\tif ( _joinCountdown \u003E= 0f \u0026\u0026 ( !_matchRunning || _gameStarted || !inSession ) )\n\t\t\t_joinCountdown = -1f;\n\n\t\tif ( _gameStarted )\n\t\t{\n\t\t\t_menu?.Hide();\n\t\t\t_lobby?.Hide();\n\t\t\t_browser?.Hide();\n\t\t\tif ( !MatchState.MatchOver )\n\t\t\t{\n\t\t\t\t// \u5BF9\u5C40\u4E2D HUD \u5FC5\u987B\u5728\uFF1A\u6CA1\u5EFA\u8FC7\u5C31\u5EFA\uFF08\u70ED\u91CD\u8F7D\u91CD\u5EFA\u4E16\u754C\u540E\u5F15\u7528\u5931\u6548 \u2192 \u91CD\u5EFA\u81EA\u6108\uFF09\n\t\t\t\tif ( _hud.IsValid() )\n\t\t\t\t{\n\t\t\t\t\tif ( !_hud.GameObject.Enabled ) _hud.GameObject.Enabled = true;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tCreateHud();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _lobbyOpen )\n\t\t{\n\t\t\t_menu?.Hide();\n\t\t\t_lobby?.ShowHost();   // \u5E42\u7B49\uFF1A\u5DF2\u663E\u793A\u4E0D\u91CD\u7F6E\u72B6\u6001\n\t\t\treturn;\n\t\t}\n\n\t\tif ( inSession \u0026\u0026 !NetworkManager.IsAuthority )\n\t\t{\n\t\t\t// \u5BA2\u6237\u7AEF\u5728\u4F1A\u8BDD\u91CC\u4F46\u6CA1\u8FDB\u5BF9\u5C40\uFF1A\u5FC5\u987B\u505C\u5728\u623F\u95F4\u9875\uFF08\u7B49\u5F85 / JOIN GAME \u6309\u94AE / 3-2-1\uFF09\n\t\t\t_menu?.Hide();\n\t\t\t_browser?.Hide();\n\t\t\tif ( !_lobbyWaiting )\n\t\t\t{\n\t\t\t\tEnterClientWaiting();\n\t\t\t\tNetworkManager.RequestMatchState();\n\t\t\t}\n\t\t\t_lobby?.ShowClient();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !inSession \u0026\u0026 NetworkManager.IsReconnecting )\n\t\t{\n\t\t\t// \u6389\u7EBF\u81EA\u52A8\u91CD\u8FDE\u4E2D\uFF1A\u623F\u95F4\u9875\u7559\u5728\u539F\u5730\u663E\u793A \u0022CONNECTION LOST \u2014 RECONNECTING\u0022\uFF0C\n\t\t\t// \u91CD\u8BD5\u8017\u5C3D OnReconnectFailed \u624D\u56DE\u4E3B\u83DC\u5355\uFF08v0.6.4.6\uFF09\n\t\t\t_lobby?.ShowClient();\n\t\t\treturn;\n\t\t}\n\n\t\t_lobby?.Hide();\n\n\t\t// \u7EC8\u6001\u4FDD\u8BC1\uFF08v0.6.8.2\uFF09\uFF1A\u65E0\u4F1A\u8BDD\u4E14\u4E0D\u5728\u4EFB\u4F55\u6D41\u7A0B \u2192 **\u4E3B\u83DC\u5355\u5FC5\u987B\u53EF\u89C1**\u3002\n\t\t// \u4E4B\u524D\u53EA Hide \u623F\u95F4\u4E0D\u4EAE\u83DC\u5355\u2014\u2014\u4EFB\u4F55\u8DEF\u5F84\u6F0F\u6389 Show\uFF08\u70ED\u91CD\u8F7D\u91CD\u5EFA\u3001\u5F02\u5E38\u4E2D\u65AD\uFF09\u5C31\u662F\u5168\u7A7A\u5C4F\n\t\tif ( _browser.IsValid() \u0026\u0026 _browser.IsOpen ) return;   // \u623F\u95F4\u5217\u8868\u9875\u5F00\u7740\uFF1A\u4E3B\u83DC\u5355\u8BA9\u4F4D\n\t\tif ( _settings.IsValid() \u0026\u0026 _settings.IsOpen ) return;   // \u8BBE\u7F6E\u9875\u5F00\u7740\uFF1A\u4E3B\u83DC\u5355\u8BA9\u4F4D\uFF08v0.7.8.16\uFF09\n\t\tif ( _rankings.IsValid() \u0026\u0026 _rankings.IsOpen ) return;   // \u6392\u884C\u9875\u5F00\u7740\uFF1A\u4E3B\u83DC\u5355\u8BA9\u4F4D\uFF08v0.7.8.19\uFF09\n\t\tif ( _menu.IsValid() \u0026\u0026 !_menu.GameObject.Enabled ) _menu.Show();\n\t}\n\n\tvoid EnsureWorld()\n\t{\n\t\tif ( _worldReady ) return;\n\t\t_worldReady = true;\n\n\t\t// \u590D\u7528\u65E7\u7684\u4E16\u754C\u6839\uFF08\u70ED\u91CD\u8F7D\u65F6\u907F\u514D\u5806\u53E0\u591A\u5957\u4E16\u754C\uFF09\uFF0C\u5E76\u6E05\u6389\u5176\u52A8\u6001\u5B50\u7269\u4F53\u3002\n\t\t// \u6CE8\u610F\uFF1APlay \u505C\u6B62\u540E static \u5F15\u7528\u53EF\u80FD\u6307\u5411\u5DF2\u5378\u8F7D\u7684\u65E7\u573A\u666F\u5BF9\u8C61\uFF08IsValid \u4ECD\u4E3A\u771F\uFF09\uFF0C\n\t\t// \u5FC5\u987B\u6821\u9A8C\u573A\u666F\u5F52\u5C5E\uFF0C\u5426\u5219\u5411\u65E7\u573A\u666F\u6839\u6302\u5B50\u7269\u4F53\u76F4\u63A5\u65AD\u8A00\u5931\u8D25\n\t\tif ( _sharedRoot.IsValid() \u0026\u0026 _sharedRoot.Scene == Scene )\n\t\t{\n\t\t\t_root = _sharedRoot;\n\t\t\tforeach ( var child in _root.Children.ToArray() )\n\t\t\t{\n\t\t\t\tif ( child.IsValid() ) child.Destroy();\n\t\t\t}\n\t\t\t_balls.Clear();\n\t\t\t_lastPos.Clear();\n\t\t\t_teleportGrace.Clear();\n\t\t\t_cells.Clear();\n\t\t\t_steerDir.Clear();\n\t\t\t_sinceEject.Clear();\n\t\t\t_nextCellId = 1;\n\t\t\tLocalBall = null;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_sharedRoot = null;\n\t\t\t// \u547D\u540D\u907F\u5F00 \u0022CircleroyaleGame\u0022\uFF1A\u5BA2\u6237\u7AEF\u4E0A\u5FEB\u7167\u4E16\u754C\u91CC host \u7684\u4E16\u754C\u6839\u4E5F\u53EB\u8FD9\u4E2A\u540D\n\t\t\t_root = new GameObject( true, \u0022CircleroyaleGameLocal\u0022 );\n\t\t\t_sharedRoot = _root;\n\t\t}\n\n\t\tif ( NetworkManager.IsAuthority )\n\t\t{\n\t\t\t// \u5C4F\u853D\u573A\u666F\u91CC\u6A21\u677F\u7559\u4E0B\u7684\u6240\u6709\u5BF9\u8C61\uFF08\u793A\u4F8B\u76F8\u673A\u3001\u65B9\u5757\u7B49\uFF09\uFF0C\u53EA\u8DD1\u6211\u4EEC\u751F\u6210\u7684\u4E16\u754C\u3002\n\t\t\t// Scene \u672C\u8EAB\u5C31\u662F\u6839 GameObject\uFF0C\u9876\u5C42\u5BF9\u8C61\u90FD\u5728\u5B83\u7684 Children \u91CC\u3002\n\t\t\t// \u26A0\uFE0F \u8C41\u514D\u542B ScreenPanel \u7684\u9876\u5C42\u7269\u4F53\uFF1AUI GO \u82E5\u56E0 Parent= \u9759\u9ED8\u5931\u6548\u53D8\u6210\u9876\u5C42\u5B64\u513F\uFF0C\n\t\t\t// \u7981\u7528\u5B83\u4EEC\u4F1A\u5F15\u53D1\u7981\u7528\u2194\u542F\u7528\u5FAA\u73AF\uFF08PanelComponent \u6BCF\u6B21\u91CD\u65B0\u542F\u7528\u91CD\u5EFA\u9762\u677F\u6811=\u7206\u95EA\uFF09\n\t\t\tforeach ( var go in Scene.Children )\n\t\t\t{\n\t\t\t\tif ( !go.IsValid() || go == _root ) continue;\n\t\t\t\tvar hasUi = false;\n\t\t\t\tforeach ( var sp in go.GetComponentsInChildren\u003CScreenPanel\u003E( true ) ) { hasUi = true; break; }\n\t\t\t\tif ( hasUi ) continue;\n\t\t\t\tgo.Enabled = false;\n\t\t\t}\n\n\t\t\tCreateBackdrop();\n\t\t\tCreateNeon();\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// \u5BA2\u6237\u7AEF\uFF1A**\u76F4\u63A5\u542F\u7528\u5FEB\u7167\u4E16\u754C\u6839**\uFF08host \u7684\u4E16\u754C\uFF0C\u7403\u90FD\u5728\u91CC\u9762\uFF09\uFF0C\u53EA\u5265\u6389\u5B83\u81EA\u5E26\u7684\n\t\t\t// \u76F8\u673A/HUD\uFF08\u7528\u672C\u673A\u7684\uFF0C\u907F\u514D\u53CC\u76F8\u673A\u53CC HUD\uFF09\u3002\u26A0\uFE0F \u4E0D\u80FD\u8D70\u0022\u628A\u7403 reparent \u5230\u672C\u673A\u6839\u0022\n\t\t\t// \u7684\u8DEF\u7EBF\u2014\u2014\u5F15\u64CE\u4E0D\u5141\u8BB8\u5BA2\u6237\u7AEF\u6539 host \u62E5\u6709\u5BF9\u8C61\u7684\u5C42\u7EA7\uFF0CParent= \u9759\u9ED8\u5931\u6548\uFF0C\u7403\u6C38\u8FDC\n\t\t\t// Active=false\uFF1A\u5934\u50CF\u4E0D\u6E32\u67D3\u3001OnUpdate \u4E0D\u8DD1\uFF08\u6B7B\u4EA1\u5934\u50CF\u9690\u85CF\u5931\u6548\u2192\u0022\u5C38\u4F53\u5708\u0022\uFF09\n\t\t\t// \u2014\u20142026-09-05 \u63A2\u9488\u5B9E\u6D4B\uFF08actFalse=32\uFF0C\u81EA\u6108\u626B\u63CF\u6BCF\u79D2\u8DD1\u4E5F\u65E0\u6D4E\u4E8E\u4E8B\uFF09\u3002\n\t\t\tAdoptSnapshotWorld();\n\t\t}\n\n\t\tCreateCamera();\n\t\tCreateMenu();\n\t\tCreateLobbyPanel();\n\t\tCreateRoomBrowser();\n\t\tCreateSettingsPanel();\n\t\tCreateRankingsPanel();\n\n\t\t// \u98DF\u7269\u7BA1\u7406\u4E0E\u8054\u673A\u7BA1\u7406\uFF1A**\u6BCF\u53F0\u673A\u5668\u4E00\u4EFD\u672C\u5730\u5B9E\u4F8B\uFF0C\u81EA\u8EAB\u4E0D\u8054\u7F51**\u3002\n\t\t// \u98DF\u7269\u72B6\u6001\u7ECF\u9759\u6001 RPC \u540C\u6B65\uFF08FoodFull \u5B9A\u5411\u5168\u91CF / FoodEaten / FoodRespawned \u5E7F\u64AD\u589E\u91CF\uFF09\uFF1B\n\t\t// \u6B64\u524D\u6302\u5728\u7F51\u7EDC\u5BF9\u8C61\uFF08GameNet\uFF09\u4E0A\u7684 NetList \u65E0\u6CD5\u590D\u5236\u5230\u5BA2\u6237\u7AEF\uFF0C\u98DF\u7269/\u91CD\u751F\u5168\u5E9F\u2014\u2014\u5B9E\u6D4B\u3002\n\t\t// M4 \u4E3B\u83DC\u5355\uFF1A\u58F3\u5230\u6B64\u4E3A\u6B62\u2014\u2014\u5EFA\u5927\u5385\u8FD8\u662F\u8FDE\u4EBA\u7531\u83DC\u5355\u70B9\u51FB\u51B3\u5B9A\uFF08\u65E7\u6D41\u7A0B\u5F00\u673A\u81EA\u52A8\u5EFA\u5927\u5385\uFF0C\n\t\t// \u53CC\u5F00\u7B2C\u4E8C\u5B9E\u4F8B\u5FC5\u987B\u8D76\u5728 CreateLobby \u5B8C\u6210\u524D\u8FDE\u4E0A\u624D\u6709\u0022\u5BA2\u6237\u7AEF\u0022\u8EAB\u4EFD\uFF0C\u5929\u7136\u7ADE\u6001\uFF09\u3002\n\t\t_food = new FoodManager();\n\t\t_power = new PowerUpManager();\n\t\t_net = _root.AddComponent\u003CNetworkManager\u003E();\n\t\tGameMusic.PlayMenu();   // \u83DC\u5355/\u5927\u5385\u9636\u6BB5\u64AD journey\uFF082026-09-09 \u7528\u6237\u5B9A\u7A3F\uFF09\n\n\t\t// \u4E3B\u83DC\u5355\u80CC\u666F\u6F14\u793A\u8D5B\uFF08v0.6.3.0\uFF09\uFF1A\u79BB\u7EBF authority \u8DD1\u4E00\u573A\u7EAF bot \u5BF9\u6218\u5F53\u80CC\u666F\n\t\tif ( NetworkManager.IsAuthority \u0026\u0026 !Networking.IsActive )\n\t\t{\n\t\t\tStartMenuDemo();\n\t\t}\n\t\telse if ( !NetworkManager.IsAuthority \u0026\u0026 Networking.IsActive )\n\t\t{\n\t\t\t// \u5BA2\u6237\u7AEF\u5728\u4F1A\u8BDD\u91CC\uFF08\u91CD\u8FDE/\u70ED\u91CD\u8F7D/\u81EA\u52A8\u91CD\u8FDE\u540E\u91CD\u5EFA\u4E16\u754C\uFF09\uFF1A\u4E0D\u4EAE\u4E3B\u83DC\u5355\u2014\u2014\n\t\t\t// \u76F4\u63A5\u56DE\u623F\u95F4\u7B49\u5F85\u6216\u5BF9\u5C40\uFF08\u5411 host \u8981\u5F53\u524D\u72B6\u6001\uFF0C\u89C1 RequestMatchState\uFF09\n\t\t\t_menu?.Hide();\n\t\t\tEnterClientWaiting();\n\t\t\t_lobby?.ShowClient();\n\t\t\tNetworkManager.RequestMatchState();\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u4E3B\u83DC\u5355\u52A8\u4F5C\uFF080=\u5EFA\u670D 1=\u5FEB\u901F\u52A0\u5165 2=\u4EBA\u673A\u5BF9\u6218 3=\u9000\u51FA\uFF09\u3002\n\t/// HOST / VS BOTS \u90FD\u5148\u8FDB**\u623F\u95F4**\uFF08M5 \u4E8C\u7EA7\u754C\u9762\uFF09\uFF1A\u8C03\u53C2\u3001\u770B\u73A9\u5BB6\u3001\u70B9 START \u624D\u5F00\u5C40 \u003C/summary\u003E\n\tpublic void MenuAction( int index )\n\t{\n\t\tif ( _gameStarted || _lobbyOpen ) return;\n\n\t\tswitch ( index )\n\t\t{\n\t\t\tcase 0:   // HOST\uFF1A\u672C\u673A\u5F53\u623F\u4E3B\uFF08\u5EFA\u516C\u5F00\u5927\u5385\uFF0C\u522B\u4EBA\u53EF\u52A0\u5165\uFF09\u2192 \u8FDB\u623F\u95F4\n\t\t\t\tStopMenuDemo();\n\t\t\t\tOpenHostLobby();\n\t\t\t\tGameLog.Info( \u0022[menu] host game (room)\u0022 );\n\t\t\t\t_net.RunHostFlow();\n\t\t\t\tbreak;\n\n\t\t\tcase 1:   // JOIN\uFF1A\u623F\u95F4\u5217\u8868\u9875\uFF08v0.6.7.0\uFF09\u2014\u2014\u5217\u51FA Steam \u516C\u5F00\u5927\u5385\u70B9\u9009\u52A0\u5165\uFF1BJOIN LOCAL \u662F\u540C\u673A\u8C03\u8BD5\u515C\u5E95\n\t\t\t\tStopMenuDemo();\n\t\t\t\t_menu?.Hide();\n\t\t\t\t_browser?.Show();\n\t\t\t\tbreak;\n\n\t\t\tcase 2:   // VS BOTS\uFF1A\u4EBA\u673A\u5BF9\u6218\uFF08\u4E0D\u5EFA\u5927\u5385\uFF0C\u79BB\u7EBF\u623F\u95F4\uFF0C\u4E00\u6837\u8C03\u53C2\u540E START\uFF09\n\t\t\t\tStopMenuDemo();\n\t\t\t\tOpenHostLobby();\n\t\t\t\tGameLog.Info( \u0022[menu] vs bots (offline room)\u0022 );\n\t\t\t\t_net.RunHostFlow( createLobby: false );\n\t\t\t\tbreak;\n\n\t\t\tcase 3:   // QUIT\uFF1A\u7F16\u8F91\u5668\u91CC\u9000\u4E0D\u51FA\u5E94\u7528\uFF0C\u53EA\u63D0\u793A\n\t\t\t\tif ( Game.IsEditor )\n\t\t\t\t\t_menu?.SetStatus( \u0022QUIT IS STANDALONE-ONLY (THIS IS THE EDITOR)\u0022 );\n\t\t\t\telse\n\t\t\t\t\tGame.Close();\n\t\t\t\tbreak;\n\n\t\t\tcase 4:   // SETTINGS\uFF1A\u8BBE\u7F6E\u9875\uFF08\u6CDB\u5149\u5F00\u5173/\u97F3\u4E50\u97F3\u91CF\uFF1BBACK \u56DE\u83DC\u5355\uFF09\n\t\t\t\t_menu?.Hide();\n\t\t\t\t_settings?.Show();\n\t\t\t\tbreak;\n\n\t\t\tcase 5:   // RANKINGS\uFF1A\u5168\u7403 XP \u6392\u884C\u9875\uFF08v0.7.8.19\uFF1BBACK \u56DE\u83DC\u5355\uFF09\n\t\t\t\t_menu?.Hide();\n\t\t\t\t_rankings?.Show();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u4E3B\u83DC\u5355\u80CC\u666F\u6F14\u793A\u8D5B\uFF08v0.6.3.0 \u7528\u6237\u9700\u6C42\uFF09\uFF1Aauthority \u79BB\u7EBF\u8DD1\u4E00\u573A\u7EAF bot \u5BF9\u6218\u5F53\u83DC\u5355\u80CC\u666F\uFF0C\n\t/// \u955C\u5934\u89C2\u6218\u6700\u5927 bot\uFF1B\u89C4\u5219\u968F\u83DC\u5355\u8BBE\u7F6E\uFF0C\u65F6\u95F4\u5230\u5C31\u5730\u91CD\u5F00\u3002\u8FDB\u623F\u95F4/\u52A0\u5165\u65F6 StopMenuDemo \u6536\u573A\u3002\n\t/// \u003C/summary\u003E\n\tvoid StartMenuDemo()\n\t{\n\t\tif ( _menuDemo ) return;\n\t\t_menuDemo = true;\n\n\t\tGameConfig.ArenaHalfSize = Math.Clamp( GameConfig.ConvarArenaSize, 1024f, 12288f );\n\t\tMatchState.ActivateForMatch();   // \u6309\u5F53\u524D\u83DC\u5355\u8BBE\u7F6E\u8DD1\uFF08\u65E0\u8FDE\u63A5\uFF0CRPC \u7A7A\u53D1\u65E0\u5BB3\uFF09\n\t\t_food.Init();\n\t\tGenerateSpikes();\n\t\t_power.Init();\n\n\t\tvar target = Math.Clamp( MatchState.PlayerTarget, 2, 64 );\n\t\tfor ( int i = 0; i \u003C target; i\u002B\u002B )\n\t\t{\n\t\t\tSpawnBall( RandomSpawnPos(), isBot: true, owner: null );\n\t\t}\n\t\tGameLog.Info( $\u0022[menu] demo bots started ({target})\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u6536\u6F14\u793A\u8D5B\uFF1A\u6E05 bot/\u5206\u8EAB\uFF0C\u91CD\u7F6E\u521D\u59CB\u5316\u6807\u8BB0\uFF08\u771F\u5B9E\u5F00\u5C40\u7684 OnLobbyOpened \u4F1A\u91CD\u65B0\u6295\u98DF\uFF09 \u003C/summary\u003E\n\tvoid StopMenuDemo()\n\t{\n\t\tif ( !_menuDemo ) return;\n\t\t_menuDemo = false;\n\n\t\tforeach ( var b in _balls.ToArray() )\n\t\t{\n\t\t\tif ( b.IsValid() ) b.GameObject.Destroy();\n\t\t}\n\t\t_balls.Clear();\n\t\t_cells.Clear();\n\t\t_lastPos.Clear();\n\t\t_teleportGrace.Clear();\n\t\t_steerDir.Clear();\n\t\t_sinceEject.Clear();\n\t\t_power?.Reset();\n\t\t_networkReady = false;\n\t\tGameLog.Info( \u0022[menu] demo stopped\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E host \u5F00\u623F\uFF1A\u85CF\u83DC\u5355\u3001\u4EAE\u623F\u95F4\u754C\u9762\uFF08\u86F0\u4F0F\u7403\u7531 OnLobbyOpened \u5728\u4F1A\u8BDD\u5C31\u7EEA\u540E\u751F\u6210\uFF09 \u003C/summary\u003E\n\tvoid OpenHostLobby()\n\t{\n\t\t_lobbyOpen = true;\n\n\t\t// \u5F00\u65B0\u623F\u9ED8\u8BA4\u9886\u571F\u6218\u4E89\uFF08v0.7.8.85 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u663E\u5F0F\u5F52\u4F4D\u800C\u4E0D\u662F\u53EA\u9760\u9759\u6001\u521D\u59CB\u5316\u5668\u2014\u2014\n\t\t// \u70ED\u91CD\u8F7D\u4FDD\u7559\u65E7\u9759\u6001\u503C\uFF0C\u5F00\u623F\u65F6\u91CD\u8BBE\u624D\u80FD\u81EA\u6108\uFF1B\u540C\u4E00\u623F\u95F4\u8FDE\u7EED\u591A\u5C40\n\t\t// \uFF08P \u9000\u5C40/\u7ED3\u7B97\u56DE\u623F\uFF09\u4E0D\u8D70\u8FD9\u91CC\uFF0C\u73A9\u5BB6\u6539\u8FC7\u7684\u9009\u62E9\u4FDD\u7559\n\t\tMatchState.PendingMode = MatchState.Mode.Territory;\n\n\t\t_menu?.Hide();\n\t\t_lobby?.ShowHost();\n\t}\n\n\t/// \u003Csummary\u003E \u83DC\u5355\u72B6\u6001\u884C\uFF08NetworkManager \u7684\u5F02\u6B65\u6D41\u7A0B\u4E5F\u7528\u5B83\u62A5\u8FDB\u5EA6\uFF09 \u003C/summary\u003E\n\tpublic void MenuStatus( string text ) =\u003E _menu?.SetStatus( text );\n\n\t/// \u003Csummary\u003E \u5F00\u5C40\u516C\u5171\u5C3E\u5DF4\uFF1A\u85CF\u83DC\u5355\u3001\u4E0A HUD\uFF08\u56DE\u83DC\u5355\u540E\u518D\u5F00\u5C40\u590D\u7528\u5DF2\u6709 HUD\uFF0C\u4E0D\u91CD\u590D\u5EFA\uFF09\u3001\u5207\u6218\u6597\u66F2 \u003C/summary\u003E\n\tvoid StartGame()\n\t{\n\t\t_joinRequested = false;\n\t\t_menu?.Hide();\n\n\t\tif ( _hud.IsValid() )\n\t\t{\n\t\t\t_hud.GameObject.Enabled = true;\n\t\t\tif ( _endBoard.IsValid() ) _endBoard.Hide();\n\t\t}\n\t\telse\n\t\t{\n\t\t\tCreateHud();\n\t\t}\n\n\t\tGameMusic.PlayBattle();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u5BA2\u6237\u7AEF\uFF1A\u542F\u7528\u5FEB\u7167\u4E16\u754C\u6839\uFF08\u5E42\u7B49\uFF1BEnsureWorld \u8C03\u4E00\u6B21 \u002B ScanBalls \u6BCF\u79D2\u81EA\u6108\u2014\u2014\n\t/// \u9996\u6B21 Tick \u53EF\u80FD\u65E9\u4E8E\u5FEB\u7167\u5E94\u7528\u5B8C\u6210\uFF0C\u6251\u7A7A\u540E\u9760\u81EA\u6108\u515C\u4F4F\uFF09\u3002\n\t/// \u542B\u7403\u5BF9\u8C61\u7684\u9876\u5C42 GO \u89C6\u4E3A\u5FEB\u7167\u4E16\u754C\uFF1A\u542F\u7528\u4E4B\uFF0C\u5E76\u5265\u6389\u5176\u76F8\u673A/HUD \u5B50\u7269\u4F53\uFF1B\u5176\u4F59\u9876\u5C42\u5BF9\u8C61\n\t/// \uFF08\u6A21\u677F\u6B8B\u7559\uFF09\u7167\u65E7\u7981\u7528\u3002\n\t/// \u003C/summary\u003E\n\tvoid AdoptSnapshotWorld()\n\t{\n\t\tforeach ( var go in Scene.Children.ToArray() )\n\t\t{\n\t\t\tif ( !go.IsValid() || go == _root ) continue;\n\n\t\t\tvar hasBalls = false;\n\t\t\tforeach ( var b in go.GetComponentsInChildren\u003CBall\u003E( true ) )\n\t\t\t{\n\t\t\t\thasBalls = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif ( !hasBalls )\n\t\t\t{\n\t\t\t\t// \u26A0\uFE0F \u53EA\u7981\u7528\u0022\u771F\u6B63\u7684\u6A21\u677F\u6B8B\u7559\u0022\u2014\u2014\u542B ScreenPanel \u7684\u662F\u6211\u4EEC\u81EA\u5DF1\u7684 UI \u9762\u677F\uFF08\u5BA2\u6237\u7AEF\u4E0A UI GO\n\t\t\t\t// \u53EF\u80FD\u56E0 Parent= \u9759\u9ED8\u5931\u6548\u53D8\u6210\u9876\u5C42\u5B64\u513F\uFF09\uFF0C\u7981\u7528\u5B83\u4EEC\u4F1A\u89E6\u53D1\u6BCF\u79D2\u7981\u7528\u2194\u542F\u7528\u5FAA\u73AF\uFF1A\n\t\t\t\t// PanelComponent \u6BCF\u6B21\u91CD\u65B0\u542F\u7528\u90FD\u91CD\u5EFA\u9762\u677F\u6811 = HUD \u6BCF\u79D2\u6574\u6811\u91CD\u5EFA = \u5BA2\u6237\u7AEF\u7206\u95EA\uFF08\u5B9E\u6D4B\uFF09\n\t\t\t\tvar hasUi = false;\n\t\t\t\tforeach ( var sp in go.GetComponentsInChildren\u003CScreenPanel\u003E( true ) ) { hasUi = true; break; }\n\t\t\t\tif ( hasUi )\n\t\t\t\t{\n\t\t\t\t\tif ( _strayUiLogged.Add( go.Name ) )\n\t\t\t\t\t\tLog.Info( $\u0022[game] keep stray top-level UI \u0027{go.Name}\u0027 (no balls, has ScreenPanel)\u0022 );\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( go.Enabled )\n\t\t\t\t{\n\t\t\t\t\tLog.Info( $\u0022[game] disable stray top-level \u0027{go.Name}\u0027 (no balls)\u0022 );\n\t\t\t\t\tgo.Enabled = false;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( !go.Enabled )\n\t\t\t{\n\t\t\t\tgo.Enabled = true;\n\t\t\t\tGameLog.Info( \u0022[game] snapshot world root enabled\u0022 );\n\t\t\t}\n\n\t\t\tforeach ( var child in go.Children.ToArray() )\n\t\t\t{\n\t\t\t\tif ( !child.IsValid() ) continue;\n\t\t\t\t// \u5265\u6389 host \u5FEB\u7167\u91CC\u81EA\u5E26\u7684\u76F8\u673A\u4E0E\u5168\u90E8 UI \u9762\u677F\uFF08\u4F1A\u968F\u5FEB\u7167\u590D\u5236\u8FC7\u6765\uFF0C\u5BA2\u6237\u7AEF\u7559\u7740\u5C31\u662F\n\t\t\t\t// \u53CC\u4EFD\u754C\u9762/host \u89C6\u89D2\u7684\u9875\u9762\uFF09\u2014\u2014\u53EA\u7528\u672C\u673A\u81EA\u5DF1\u5EFA\u7684\u90A3\u5957\u3002\n\t\t\t\t// \u26A0\uFE0F \u65B0\u589E UI \u9762\u677F\u5FC5\u987B\u540C\u6B65\u52A0\u8FDB\u8FD9\u4E2A\u540D\u5355\uFF08RoomBrowser/Settings/Rankings \u66FE\u6F0F\uFF0Cv0.7.8.33 \u8865\uFF09\n\t\t\t\tif ( child.Name == \u0022Camera\u0022 || child.Name == \u0022Hud\u0022 || child.Name == \u0022MainMenu\u0022\n\t\t\t\t\t|| child.Name == \u0022LobbyPanel\u0022 || child.Name == \u0022EndBoard\u0022\n\t\t\t\t\t|| child.Name == \u0022RoomBrowser\u0022 || child.Name == \u0022SettingsPanel\u0022 || child.Name == \u0022RankingsPanel\u0022 )\n\t\t\t\t{\n\t\t\t\t\tvar name = child.Name;\n\t\t\t\t\t// \u8BCA\u65AD\uFF08\u4E34\u65F6\uFF09\uFF1ALog.Info \u5BA2\u6237\u7AEF\u4E5F\u6253\u5370\u2014\u2014\u786E\u8BA4\u5265\u79BB\u5230\u5E95\u5220\u7684\u662F\u8C01\u7684 UI\uFF08\u5BBF\u4E3B\u5FEB\u7167 vs \u672C\u673A\u7684\uFF09\n\t\t\t\t\tLog.Info( $\u0022[game] stripped snapshot \u0027{name}\u0027 (parent=\u0027{go.Name}\u0027)\u0022 );\n\t\t\t\t\tchild.Destroy();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u623F\u95F4\u5C31\u7EEA\uFF08host \u5EFA\u5B8C\u5927\u5385 / \u79BB\u7EBF\u623F\u95F4\u65F6\u8C03\u7528\uFF0CM5\uFF09\uFF1A\u751F\u6210**\u86F0\u4F0F\u7403**\u2014\u2014host \u7403 \u002B \u6EE1\u7F16\u9884\u5907 bot\uFF0C\n\t/// Alive=false \u4E0D\u53EF\u89C1\u4E0D\u6A21\u62DF\u3002\u5F15\u64CE\u7EA6\u675F\uFF1A\u0022\u8FDB\u884C\u4E2D NetworkSpawn \u4E0D\u590D\u5236\u5230\u5DF2\u8FDE\u63A5\u5BA2\u6237\u7AEF\u0022\uFF08\u5B9E\u6D4B\uFF09\uFF0C\n\t/// \u6240\u6709\u7403\u5FC5\u987B\u5728\u5404\u7AEF\u521D\u59CB\u5FEB\u7167\u524D\u5C31\u5B58\u5728\uFF0C\u5F00\u5C40\uFF08StartMatchFromLobby\uFF09\u53EA\u6FC0\u6D3B\u3001\u4E0D\u518D\u65B0\u5EFA\u3002\n\t/// \u98DF\u7269/\u5C16\u523A\u662F\u7EAF RPC \u6570\u636E\uFF08\u4E0D\u7ECF\u5FEB\u7167\uFF09\uFF0C\u5F00\u5C40\u65F6\u624D\u751F\u6210\u3002\u5BA2\u6237\u7AEF\u4E0D\u8C03\u7528\u3002\n\t/// \u003C/summary\u003E\n\tpublic void OnLobbyOpened()\n\t{\n\t\tif ( _networkReady ) return;\n\t\t_networkReady = true;\n\n\t\tGameLog.Info( $\u0022[game] CircleRoyale {GameConfig.Version} lobby opened auth={NetworkManager.IsAuthority}\u0022 );\n\t\tif ( !NetworkManager.IsAuthority ) return;\n\n\t\t// \u623F\u95F4\u8BBE\u7F6E\uFF1Aconvar \u8986\u5199\u9ED8\u8BA4\u503C\uFF08arena \u7ECF Replicated \u540C\u6B65\u7ED9\u5BA2\u6237\u7AEF\uFF09\n\t\tGameConfig.ArenaHalfSize = Math.Clamp( GameConfig.ConvarArenaSize, 1024f, 12288f );\n\n\t\tSpawnBall( Vector3.Zero, isBot: false, owner: null, dormant: true );\n\t\tfor ( int i = 1; i \u003C GameConfig.LobbyReserveBalls; i\u002B\u002B )\n\t\t{\n\t\t\tSpawnBall( RandomSpawnPos(), isBot: true, owner: null, dormant: true );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u623F\u95F4\u5185 START\uFF08host\uFF09\uFF1A\u89C4\u5219\u751F\u6548\u3001\u6295\u98DF/\u5C16\u523A\u3001\u6FC0\u6D3B\u86F0\u4F0F\u7403\uFF08\u8FDC\u7A0B\u771F\u4EBA/host/bot \u8865\u9F50\u5230\u76EE\u6807\u6570\uFF09\u3001\n\t/// \u7ED9\u7B49\u5728\u623F\u95F4\u91CC\u7684\u5BA2\u6237\u7AEF\u5B9A\u5411\u8865\u53D1\u98DF\u7269\u5C16\u523A\u3001\u5E7F\u64AD\u5F00\u5C40\u3002\u961F\u4F0D\u5206\u914D\u4E5F\u5728\u8FD9\u91CC\u505A\uFF08\u6B64\u523B\u6A21\u5F0F\u624D\u786E\u5B9A\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tpublic void StartMatchFromLobby()\n\t{\n\t\tif ( !_lobbyOpen || _gameStarted ) return;\n\t\t_lobbyOpen = false;\n\t\t_gameStarted = true;\n\n\t\tMatchState.ActivateForMatch();\n\t\t_teamSeq = 0;\n\t\tClearMatchState();   // \u5F00\u5C40\u4E5F\u6E05\u4E00\u904D\uFF08v0.7.8.87\uFF09\uFF1A\u4ECE\u83DC\u5355\u76F4\u63A5\u5F00\u7684\u7B2C\u4E00\u5C40\u3001\u4EBA\u673A\u5C40\u4E0D\u7ECF\u8FC7 ResetToLobby\n\n\t\t_food.Init();\n\t\tGenerateSpikes();\n\t\t_power.Init();\n\n\t\t// \u6BCF\u5C40\u7403\u7EC4\u590D\u4F4D\uFF08\u7403\u5BF9\u8C61\u6C38\u4E0D\u9500\u6BC1\u2014\u2014v0.6.4.2 \u8FDE\u7EED\u591A\u5C40\uFF09\uFF1A\u771F\u4EBA\u7403\uFF08\u8FDC\u7A0B\u002Bhost\uFF09\u5168\u90E8\u590D\u4F4D\uFF0C\n\t\t// bot \u6309\u5269\u4F59\u540D\u989D\u6FC0\u6D3B\u3001\u591A\u4F59\u7684\u86F0\u4F0F\uFF1B\u4E0A\u4E00\u5C40\u7684\u8D28\u91CF/\u751F\u6B7B\u6001\u4E00\u5E76\u6E05\u96F6\n\t\tint remotes = 0;\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() ) continue;\n\t\t\tif ( !b.IsRemoteOwned \u0026\u0026 b.IsBot ) continue;   // bot \u8D70\u4E0B\u9762\u7684\u540D\u989D\u8865\u9F50\n\n\t\t\tb.TeamIndex = -1;   // \u6E05\u4E0A\u4E00\u5C40\u6B8B\u7559\u961F\u4F0D\u53F7\uFF08v0.7.8.87\uFF09\uFF1A\u975E\u56E2\u961F\u5C40\u5FC5\u987B\u5F52 -1\uFF0C\u5426\u5219\u540C\u53F7\u4E92\u4E0D\u5403\n\t\t\tRespawnBall( b, NextTeamIndex(), cue: false );\n\t\t\tif ( b.IsRemoteOwned ) remotes\u002B\u002B;\n\t\t}\n\n\t\tint botsWanted = Math.Clamp( MatchState.PlayerTarget - 1 - remotes, 0, GameConfig.LobbyReserveBalls - 1 );\n\t\tint bots = 0;\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.IsBot ) continue;\n\n\t\t\tb.TeamIndex = -1;   // \u540C\u4E0A\uFF1Abot \u7403\u4E5F\u662F\u8DE8\u5C40\u590D\u7528\u7684\uFF08\u86F0\u4F0F\u7684\u4E5F\u8981\u6E05\uFF0C\u907F\u514D\u4E0B\u6B21\u6FC0\u6D3B\u5E26\u65E7\u961F\u53F7\uFF09\n\n\t\t\tif ( bots \u003C botsWanted )\n\t\t\t{\n\t\t\t\tRespawnBall( b, NextTeamIndex(), cue: false );\n\t\t\t\tbots\u002B\u002B;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tb.Alive = false;   // \u591A\u4F59 bot \u86F0\u4F0F\uFF08\u4E0D\u53EF\u89C1\u4E0D\u6A21\u62DF\uFF09\n\t\t\t\tb.Dormant = true;\n\t\t\t\tb.Mass = GameConfig.StartMass;   // \u26A0\uFE0F \u5FC5\u987B\u6E05\u96F6\uFF08v0.7.8.105\uFF0C\u5BA1\u8BA1 #2\uFF09\uFF1A\n\t\t\t\t// \u86F0\u4F0F\u7403\u8DE8\u5C40\u590D\u7528\u4E14\u4E0D\u53C2\u4E0E\u6A21\u62DF\uFF0C\u4FDD\u7559\u4E0A\u5C40\u8D28\u91CF\u4F1A\u6C61\u67D3\u7ED3\u7B97\u6392\u540D\uFF08\u89C1 EndMatch \u6CE8\u91CA\uFF09\uFF0C\n\t\t\t\t// \u4E5F\u4F1A\u8BA9\u5B83\u88AB\u6FC0\u6D3B\u65F6\u4EE5\u5DE8\u7403\u5F00\u5C40\n\t\t\t}\n\t\t}\n\n\t\t// \u7B49\u5728\u623F\u95F4\u91CC\u7684\u5BA2\u6237\u7AEF\u6CA1\u6536\u5230\u8FC7\u98DF\u7269/\u5C16\u523A\uFF08OnActive \u65F6\u8FD8\u6CA1\u751F\u6210\uFF09\uFF1A\u9010\u4E00\u5B9A\u5411\u8865\u53D1\n\t\tvar foods = _food.Foods;\n\t\tvar spikes = _spikes;\n\t\tvar powerWires = _power.ToWireForSync();\n\t\tforeach ( var conn in Connection.All )\n\t\t{\n\t\t\tif ( conn is null || !conn.IsActive ) continue;\n\t\t\tif ( Connection.Local is not null \u0026\u0026 conn.Id == Connection.Local.Id ) continue;\n\n\t\t\tusing ( Rpc.FilterInclude( conn ) )\n\t\t\t{\n\t\t\t\tif ( foods is not null ) NetworkManager.FoodFull( foods );\n\t\t\t\tif ( spikes is not null ) NetworkManager.SpikesFull( spikes );\n\t\t\t\tif ( powerWires is not null ) NetworkManager.PowerUpFull( powerWires );\n\t\t\t}\n\t\t}\n\n\t\tif ( Networking.IsActive ) NetworkManager.MatchStarted();   // \u65E0\u4F1A\u8BDD\u4E0D\u5E7F\u64AD\n\t\tOnMatchStartedLocal();\n\n\t\tGameLog.Info( $\u0022[match] started \u2014 mode={MatchState.CurrentMode} target={MatchState.PlayerTarget} remotes={remotes} bots={bots}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u56E2\u961F\u8D5B\u6309\u5757\u5206\u961F\uFF08v0.6.5.0 \u4FEE bug\uFF1A\u539F %TeamSize \u8F6E\u8F6C=\u53EA\u6709 TeamSize \u4E2A\u961F\u3001\u6BCF\u961F\u5341\u51E0\u4EBA\uFF1B\n\t/// \u7528\u6237\u8BED\u4E49\u662F\u0022\u6BCF\u961F N \u4EBA\u0022\u2192 48 \u4EBA 3 \u4EBA\u961F\u5E94\u5206\u51FA 16 \u961F\uFF09\u3002\u666E\u901A\u8D5B\u6052 -1 \u003C/summary\u003E\n\tint NextTeamIndex()\n\t{\n\t\tif ( !MatchState.IsTeam ) return -1;\n\t\tvar team = _teamSeq\u002B\u002B / MatchState.TeamSize;\n\t\t// \u9886\u571F\u56FA\u5B9A 4 \u961F\uFF08v0.7.8.35\uFF09\uFF1A\u771F\u4EBA\u8D85\u7F16\uFF08\u5927\u4E8E 16\uFF09\u65F6\u6324\u8FDB\u65E2\u6709\u961F\uFF0C\u7EDD\u4E0D\u5F00\u7B2C 5 \u961F\u2014\u2014\u5927\u672C\u8425\u53EA\u6709\u56DB\u89D2\n\t\treturn MatchState.IsTerritory ? Math.Min( team, 3 ) : team;\n\t}\n\n\t/// \u003Csummary\u003E \u4E24\u9897\u7403\u662F\u5426\u540C\u961F\uFF08TeamIndex \u5C0F\u4E8E 0 = \u666E\u901A\u8D5B/\u672A\u5206\u961F\uFF0C\u4E0D\u7B97\u540C\u961F\uFF09\u3002\n\t/// \u26A0\uFE0F \u5FC5\u987B\u5E26 MatchState.IsTeam \u95E8\u7981\uFF08v0.7.8.87 \u4FEE\uFF09\uFF1A\u7403\u5BF9\u8C61\u8DE8\u5C40\u590D\u7528\uFF0C\u56E2\u961F/\u9886\u571F\u5C40\u7559\u4E0B\u7684\n\t/// TeamIndex \u4F1A\u4E00\u76F4\u6302\u5728\u7403\u4E0A\uFF0C\u666E\u901A\u8D5B\u91CC\u0022\u540C\u53F7\u5373\u540C\u961F\u0022\u4F1A\u8BA9\u6574\u7EC4\u4EBA\u4E92\u76F8\u5403\u4E0D\u52A8\uFF08\u63A8\u6324\u4ECD\u751F\u6548\uFF0C\u53EA\u63A8\u4E0D\u5403\uFF09 \u003C/summary\u003E\n\tstatic bool SameTeam( Ball a, Ball b ) =\u003E\n\t\tMatchState.IsTeam \u0026\u0026 a.TeamIndex \u003E= 0 \u0026\u0026 a.TeamIndex == b.TeamIndex;\n\n\t/// \u003Csummary\u003E \u63A8\u6324\u7528\uFF1A\u4E24\u4E2A SteamId \u662F\u5426\u540C\u9635\u8425\uFF08\u540C\u4E00 SteamId=\u81EA\u5DF1\u7684\u8EAB\u4F53\uFF1B\u56E2\u961F\u8D5B\u6309\u7403\u4E0A\u7684 TeamIndex \u67E5\uFF09\u3002\n\t/// \u540C\u9635\u8425\u4E0D\u63A8\u6324\uFF08BoB \u60EF\u4F8B\uFF1A\u81EA\u5DF1\u548C\u961F\u53CB\u53EF\u91CD\u53E0\uFF09 \u003C/summary\u003E\n\tpublic bool SameTeamSteam( long a, long b )\n\t{\n\t\tif ( a == b ) return true;\n\t\tif ( !MatchState.IsTeam ) return false;\n\t\tvar ta = TeamIndexOfSteam( a );\n\t\treturn ta \u003E= 0 \u0026\u0026 ta == TeamIndexOfSteam( b );\n\t}\n\n\t/// \u003Csummary\u003E \u56E2\u961F\u8D5B\u9635\u8425\u6620\u5C04\uFF08SteamId \u2192 \u961F\u4F0D\u53F7\uFF09\uFF1A\u8FD4\u56DE**\u590D\u7528\u7F13\u51B2**\u2014\u2014\u6BCF\u5E27 TickEating/TickCells/\n\t/// TickSpikeMinions \u4E09\u5904\u5148\u540E\u6784\u5EFA\u4F7F\u7528\uFF0C\u8C03\u7528\u65B9\u5F53\u5E27\u7528\u5B8C\u5373\u5F03\uFF0C\u4E0D\u5F97\u8DE8\u65B9\u6CD5/\u8DE8\u5E27\u6301\u6709\uFF1B\n\t/// \u666E\u901A\u8D5B\u8FD4\u56DE\u7A7A\u6620\u5C04 \u003C/summary\u003E\n\tDictionary\u003Clong, int\u003E BuildTeamMap()\n\t{\n\t\t_teamScratch.Clear();\n\t\tif ( !MatchState.IsTeam ) return _teamScratch;\n\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 b.TeamIndex \u003E= 0 ) _teamScratch[b.OwnerSteamId] = b.TeamIndex;\n\t\t}\n\t\treturn _teamScratch;\n\t}\n\n\tint TeamIndexOfSteam( long sid )\n\t{\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 b.OwnerSteamId == sid \u0026\u0026 b.TeamIndex \u003E= 0 ) return b.TeamIndex;\n\t\t}\n\t\treturn -1;\n\t}\n\n\t/// \u003Csummary\u003E eater \u4E0E\u5B62\u5B50\u4E3B\u4EBA\u662F\u5426\u540C\u9635\u8425\uFF08v0.7.8.23\uFF09\uFF1A\u540C\u4E00 SteamId\uFF08\u81EA\u5DF1\uFF09\uFF0C\n\t/// \u6216\u56E2\u961F\u8D5B\u4E0B\u67E5 teamOf \u540C\u961F\u3002\u540C\u9635\u8425\u8D70 0.2s \u5FEB\u6863\uFF0C\u654C\u4EBA 0.5s\u3002\n\t/// \u6CE8\uFF1Abot \u7684 OwnerSteamId \u5168\u662F 0\u2014\u2014bot \u4E4B\u95F4\u7684 blob \u6052\u5224\u0022\u81EA\u5DF1\u0022\u5FEB\u6863\uFF08\u65E2\u6709 SteamId 0 \u8BBE\u8BA1\u7684\u65E2\u6709\u6A21\u7CCA\uFF0C\u65E0\u5BB3\uFF09 \u003C/summary\u003E\n\tstatic bool SameSide( long blobOwner, long eater, Dictionary\u003Clong, int\u003E teamOf )\n\t{\n\t\tif ( eater == blobOwner ) return true;\n\t\treturn MatchState.IsTeam\n\t\t\t\u0026\u0026 teamOf.TryGetValue( eater, out var et )\n\t\t\t\u0026\u0026 teamOf.TryGetValue( blobOwner, out var bt )\n\t\t\t\u0026\u0026 et == bt;\n\t}\n\n\t/// \u003Csummary\u003E \u5F00\u5C40\u843D\u5730\uFF08\u672C\u673A\uFF09\uFF1A\u51FA\u623F\u95F4\u3001\u4E0A HUD\u3001\u5207\u6218\u6597\u66F2\uFF08host \u76F4\u8C03\uFF1B\u5BA2\u6237\u7AEF\u7ECF RPC\uFF09\u3002\n\t/// \u5BA2\u6237\u7AEF\u5FC5\u987B\u5728\u8FD9\u91CC\u7F6E _gameStarted\u2014\u2014host \u7684\u7F6E\u4F4D\u5728 MenuAction \u91CC\uFF0C\u5BA2\u6237\u7AEF\u6CA1\u6709\u90A3\u6761\u8DEF\u5F84\uFF0C\n\t/// \u4E0D\u7F6E\u4F4D\u7684\u8BDD Tick \u7684\u0022\u8FDE\u4E0A\u4E86\u5C31\u8FDB\u623F\u95F4\u0022\u5224\u5B9A\u4F1A\u5728 MatchStarted \u4E4B\u540E\u628A\u623F\u95F4\u9875\u76D6\u56DE\u6E38\u620F\u4E0A\uFF08\u5B9E\u6D4B\uFF09\u3002\u003C/summary\u003E\n\tvoid OnMatchStartedLocal()\n\t{\n\t\t_lobbyWaiting = false;\n\t\t_lobbyHeartbeatArmed = false;   // \u51FA\u623F\u95F4\uFF1A\u623F\u95F4\u5FC3\u8DF3\u8BA9\u4F4D\u7ED9\u5BF9\u5C40\u5FC3\u8DF3\uFF08MatchTick\uFF09\n\t\t_lobby?.Hide();\n\t\tStartGame();\n\t\t_gameStarted = true;\n\t\t_joinRequested = false;\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\uFF1A\u623F\u4E3B\u70B9\u4E86 START\uFF08host \u56DE\u58F0\u7528 IsAuthority \u6321\u6389\u2014\u2014host \u5DF2\u76F4\u8C03\uFF09 \u003C/summary\u003E\n\tpublic void OnMatchStartedRemote()\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\tGameLog.Info( \u0022[match] host started the game\u0022 );\n\n\t\t// \u7528\u6237\u5B9A\u7A3F\uFF08v0.6.4.1\uFF09\uFF1A\u5728\u623F\u95F4\u91CC\u7684\u5BA2\u6237\u7AEF**\u4E0D\u81EA\u52A8\u8FDB\u5C40**\u2014\u2014\u623F\u95F4\u9875\u4EAE\u8D77 JOIN GAME \u6309\u94AE\uFF0C\n\t\t// \u7531\u73A9\u5BB6\u70B9\u51FB\u540E\u8D70 3 \u79D2\u52A0\u5165\u5012\u8BA1\u65F6\u518D\u8FDB\u5165\uFF1B\u4E0D\u5728\u623F\u95F4\uFF08\u5F02\u5E38\u6001\uFF09\u5219\u76F4\u63A5\u8FDB\n\t\tif ( _lobbyWaiting )\n\t\t{\n\t\t\t_matchRunning = true;\n\t\t\t_lobby?.SetMatchRunning();\n\t\t\treturn;\n\t\t}\n\t\tOnMatchStartedLocal();\n\t}\n\n\t/// \u003Csummary\u003E host \u53D6\u6D88\u5BF9\u5C40\uFF08P \u952E\u5FEB\u901F\u9000\u5C40\u7684\u5E7F\u64AD\u843D\u5730\uFF0C\u4EC5\u5BA2\u6237\u7AEF\uFF09\uFF1A\u56DE\u623F\u95F4\u7B49\u5F85\u9875 \u003C/summary\u003E\n\tpublic void OnMatchCancelledRemote()\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;   // host \u81EA\u5DF1\u5DF2\u76F4\u63A5\u5904\u7406\n\t\tGameLog.Info( \u0022[match] host cancelled the match \u2014 back to lobby\u0022 );\n\t\tResetToLobby();\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u70B9\u51FB\u623F\u95F4\u9875\u7684 JOIN GAME\uFF1A\u5BF9\u5C40\u8FDB\u884C\u4E2D \u2192 3 \u79D2\u52A0\u5165\u5012\u8BA1\u65F6 \u2192 \u8FDB\u5165 \u003C/summary\u003E\n\tpublic void ClientJoinMatch()\n\t{\n\t\tif ( !_matchRunning || _gameStarted || _joinCountdown \u003E= 0f ) return;\n\t\tif ( !_lobbyWaiting ) return;\n\n\t\t_joinCountdown = GameConfig.JoinCountdownSeconds;\n\t\t_lobby?.ShowJoinCountdown( GameConfig.JoinCountdownSeconds );\n\t\tGameLog.Info( \u0022[match] client joining via button\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u81EA\u52A8\u91CD\u8FDE watchdog \u53D1\u8D77\u91CD\u8FDE\uFF08NetworkManager \u8C03\u7528\uFF09\uFF1A\u91CD\u65B0\u8D70\u6807\u51C6\u52A0\u5165\u6D41 \u003C/summary\u003E\n\tpublic void NotifyReconnectStarted()\n\t{\n\t\t_joinRequested = true;\n\t\t_sinceJoinRequest = 0;\n\t}\n\n\t// ---- \u52A0\u5165\u6D41\u8F85\u52A9\uFF08\u623F\u95F4\u5217\u8868\u9875 v0.6.7.0\uFF09----\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u53D1\u8D77\u52A0\u5165\uFF08\u70B9\u623F\u95F4/\u5FEB\u901F\u52A0\u5165/\u672C\u5730\u76F4\u8FDE\u5171\u7528\uFF09\uFF1A\u7F6E\u8BF7\u6C42\u6807\uFF0CTick \u6355\u83B7\u4F1A\u8BDD\u6FC0\u6D3B\u6536\u5C3E \u003C/summary\u003E\n\tpublic void OnJoinAttemptStarted()\n\t{\n\t\t_joinRequested = true;\n\t\t_sinceJoinRequest = 0;\n\t}\n\n\t/// \u003Csummary\u003E \u52A0\u5165\u5931\u8D25\u63D0\u793A\uFF1A\u6D4F\u89C8\u5668\u9875\u5F00\u7740\u663E\u793A\u5728\u90A3\uFF0C\u4E3B\u83DC\u5355\u72B6\u6001\u884C\u662F\u515C\u5E95\u3002\n\t/// \u26A0\uFE0F \u5FC5\u987B\u540C\u65F6\u590D\u4F4D\u623F\u95F4\u5217\u8868\u9875\u7684 _joining\uFF08v0.7.8.105\uFF0C\u5BA1\u8BA1 #4\uFF09\uFF1A\u5B83\u53EA\u5728 Show/Hide \u91CC\u6E05\uFF0C\n\t/// \u5931\u8D25\u540E\u4E00\u76F4\u4E3A true \u2192 \u6574\u4E2A\u623F\u95F4\u9875\u6240\u6709\u884C\u4E0E REFRESH/JOIN LOCAL \u5168\u90E8\u70B9\u51FB\u5931\u6548\u3002 \u003C/summary\u003E\n\tpublic void OnJoinAttemptFailed( string message )\n\t{\n\t\t_browser?.OnJoinDenied();\n\t\t_browser?.SetStatus( message );\n\t}\n\n\t/// \u003Csummary\u003E \u540C\u673A\u8C03\u8BD5\u76F4\u8FDE\uFF08\u623F\u95F4\u5217\u8868\u9875 JOIN LOCAL\uFF09\uFF1Alocal \u5730\u5740 = \u672C\u673A host \u003C/summary\u003E\n\tpublic void JoinLocalRoom()\n\t{\n\t\tOnJoinAttemptStarted();\n\t\t_net?.StartJoin( GameConfig.DefaultJoinAddress );\n\t\t_browser?.SetStatus( \u0022CONNECTING TO LOCAL HOST ...\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u623F\u95F4\u5217\u8868\u9875\u8FD4\u56DE\u4E3B\u83DC\u5355\uFF08BACK\uFF09 \u003C/summary\u003E\n\tpublic void CloseBrowser()\n\t{\n\t\t_browser?.Hide();\n\t\t_menu?.Show();\n\n\t\t// JOIN \u6253\u5F00\u5217\u8868\u65F6 StopMenuDemo \u6E05\u6389\u4E86\u80CC\u666F\u6F14\u793A\u8D5B\u2014\u2014\u56DE\u83DC\u5355\u5FC5\u987B\u91CD\u65B0\u5F00\u8D77\u6765\uFF0C\u5426\u5219\u80CC\u666F\u4E0D\u52A8\uFF08\u7528\u6237\u5B9E\u6D4B\uFF09\n\t\tif ( !_gameStarted \u0026\u0026 !_lobbyOpen \u0026\u0026 NetworkManager.IsAuthority \u0026\u0026 !Networking.IsActive )\n\t\t\tStartMenuDemo();\n\t}\n\n\t/// \u003Csummary\u003E \u8BBE\u7F6E\u9875\u6536\u8D77\uFF08SettingsPanel BACK \u8C03\u7528\uFF09\uFF1A\u56DE\u4E3B\u83DC\u5355 \u003C/summary\u003E\n\tpublic void CloseSettings()\n\t{\n\t\t_settings?.Hide();\n\t\t_menu?.Show();\n\t}\n\n\t/// \u003Csummary\u003E \u6392\u884C\u9875\u6536\u8D77\uFF08RankingsPanel BACK \u8C03\u7528\uFF09\uFF1A\u56DE\u4E3B\u83DC\u5355 \u003C/summary\u003E\n\tpublic void CloseRankings()\n\t{\n\t\t_rankings?.Hide();\n\t\t_menu?.Show();\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u6389\u7EBF\uFF0C\u81EA\u52A8\u91CD\u8FDE\u5F00\u59CB\uFF08NetworkManager \u8C03\u7528\uFF09\uFF1A\u754C\u9762\u7559\u5728\u539F\u5730 \u002B \u623F\u95F4\u9875\u72B6\u6001\u884C\u63D0\u793A \u003C/summary\u003E\n\tpublic void OnDisconnectRecoveryStarted()\n\t{\n\t\tif ( _lobbyWaiting ) _lobby?.SetConnectionLost();\n\t\tLog.Warning( \u0022[net] connection lost \u2014 staying put, auto reconnecting\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u81EA\u52A8\u91CD\u8FDE\u8017\u5C3D\uFF08NetworkManager \u8C03\u7528\uFF09\uFF1A\u56DE\u4E3B\u83DC\u5355\uFF08v0.6.4.6 \u7528\u6237\u5B9A\u7A3F\uFF09 \u003C/summary\u003E\n\tpublic void OnReconnectFailed()\n\t{\n\t\tResetToMenu();\n\t\t_menu?.SetStatus( \u0022CONNECTION LOST \u2014 COULD NOT REACH HOST\u0022 );\n\t}\n\n\t// ---- \u623F\u95F4\u4FE1\u606F\u540C\u6B65\uFF08host \u2192 \u7B49\u5F85\u4E2D\u7684\u5BA2\u6237\u7AEF\uFF09----\n\n\t/// \u003Csummary\u003E \u623F\u95F4\u73A9\u5BB6\u540D\u5355\uFF1Ahost \u81EA\u5DF1 \u002B \u6240\u6709\u6D3B\u8DC3\u8FDE\u63A5\uFF08\u987A\u5E8F\u5373\u5217\u8868\u987A\u5E8F\uFF0C0 \u53F7\u662F\u623F\u4E3B\uFF09 \u003C/summary\u003E\n\tpublic List\u003Cstring\u003E LobbyPlayerNames()\n\t{\n\t\tvar list = new List\u003Cstring\u003E();\n\t\ttry\n\t\t{\n\t\t\tif ( Connection.Local is not null ) list.Add( Connection.Local.DisplayName );\n\n\t\t\tforeach ( var c in Connection.All )\n\t\t\t{\n\t\t\t\tif ( c is null || !c.IsActive ) continue;\n\t\t\t\tif ( Connection.Local is not null \u0026\u0026 c.Id == Connection.Local.Id ) continue;\n\t\t\t\tlist.Add( c.DisplayName );\n\t\t\t}\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\t// \u4F1A\u8BDD\u672A\u5C31\u7EEA\u65F6\u7ED9\u7A7A\u540D\u5355\uFF0C\u754C\u9762\u663E\u793A\u5360\u4F4D\n\t\t}\n\t\treturn list;\n\t}\n\n\t/// \u003Csummary\u003E \u540D\u5355/\u53C2\u6570\u53D8\u4E86\uFF08\u7F51\u7EDC\u7EBF\u7A0B\u53EA\u7F6E\u810F\u6807\uFF0C\u4E3B\u7EBF\u7A0B Tick \u7EDF\u4E00\u5E7F\u64AD\uFF09 \u003C/summary\u003E\n\tpublic void MarkLobbyDirty() =\u003E _lobbyListDirty = true;\n\n\tvoid BroadcastLobbyState()\n\t{\n\t\tNetworkManager.LobbyState( (byte)MatchState.PendingMode, (int)MatchState.PendingDuration,\n\t\t\tMatchState.PendingPlayerTarget, LobbyPlayerNames().ToArray() );\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u5E94\u7528\u623F\u4E3B\u63A8\u9001\u7684\u623F\u95F4\u4FE1\u606F\uFF08host \u56DE\u58F0\u7528 IsAuthority \u6321\u6389\uFF09 \u003C/summary\u003E\n\tpublic void OnLobbyStateRemote( byte mode, int duration, int players, string[] names )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\n\t\t// \u623F\u95F4\u5FC3\u8DF3\uFF1Ahost \u6BCF 3s \u63A8\u4E00\u6B21\uFF08\u540D\u5355\u6CA1\u53D8\u4E5F\u63A8\uFF09\uFF0C\u6536\u5230\u5373\u5F52\u96F6\n\t\tif ( _lobbyWaiting )\n\t\t{\n\t\t\t_lobbyHeartbeatArmed = true;\n\t\t\t_sinceLobbyHeartbeat = 0;\n\t\t}\n\n\t\t_lobbyMode = mode;\n\t\t_lobbyDuration = duration;\n\t\t_lobbyPlayers = players;\n\t\t_lobbyNames = names ?? Array.Empty\u003Cstring\u003E();\n\t\t_lobby?.UpdateFromHost( mode, duration, players, names );\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\uFF1A\u4E3B\u7403\u88AB\u5403\uFF0C\u6700\u5927\u5206\u8EAB\u5DF2\u664B\u5347\u6210\u65B0\u4E3B\u7403\u2014\u2014\u628A\u7403\u77AC\u79FB\u5230\u664B\u5347\u4F4D\u7F6E\u7EE7\u7EED\u6A21\u62DF\n\t/// \uFF08\u8D28\u91CF\u7ECF [Sync] \u5230\u8FBE\uFF1Bhost \u56DE\u58F0\u7528 IsAuthority \u6321\u6389\uFF09\u3002 \u003C/summary\u003E\n\tpublic void OnMainPromotedRemote( float x, float y )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\n\t\tvar ball = LocalBall;\n\t\tif ( !ball.IsValid() ) return;\n\n\t\tball.TeleportTo( new Vector3( x, y, 0f ) );\n\t\tGameLog.Info( $\u0022[game] main promoted -\u003E continue at ({x:0},{y:0})\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E host\uFF1A\u6807\u8BB0\u4E00\u6B21\u5408\u6CD5\u77AC\u79FB\uFF08\u4F4D\u79FB\u6821\u9A8C\u951A\u5230\u65B0\u4F4D\u7F6E \u002B \u5F00\u5BBD\u9650\u671F\uFF09\u2014\u2014\u6307\u6325\u5B98\u6280\u80FD/\u664B\u5347\u5171\u7528\u8BED\u4E49 \u003C/summary\u003E\n\tpublic void MarkTeleport( Ball ball, Vector3 pos )\n\t{\n\t\t_lastPos[ball.Id] = pos;\n\t\t_teleportGrace[ball.Id] = 0;\n\t}\n\n\t/// \u003Csummary\u003E \u9759\u6001 RPC \u843D\u5730\uFF1A\u6307\u6325\u5B98\u6280\u80FD\u4F20\u9001\u2014\u2014\u672C\u673A\u7403\u77AC\u79FB\u5230\u961F\u53CB\u8EAB\u8FB9\uFF08host \u5DF2\u5148\u951A\u5B9A\u6821\u9A8C\u4F4D\uFF09 \u003C/summary\u003E\n\tpublic void OnClassTeleportRemote( long steamId, float x, float y )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\n\t\t// \u8EAB\u4EFD\u5224\u5B9A\u8D70\u672C\u673A\u7403\u7684 OwnerSteamId\uFF08v0.7.8.87 \u4FEE\uFF09\uFF1A\u53D1\u5305\u65B9\u7528\u7684\u662F Connection.SteamId\uFF0C\n\t\t// \u540C\u673A\u53CC\u5F00\u65F6\u5B83\u4E0E Game.SteamId \u5BF9\u4E0D\u4E0A\uFF08\u672C\u6587\u4EF6\u4E0E NetworkManager \u90FD\u6709\u8BE5\u5B9E\u6D4B\u6CE8\u91CA\uFF09\uFF0C\n\t\t// \u539F\u6765\u6309 Game.SteamId \u6BD4\u5BF9\u4F1A\u8BA9\u53CC\u5F00\u5BA2\u6237\u7AEF\u7684\u4F20\u9001\u9759\u9ED8\u5931\u6548\n\t\tvar ball = LocalBall;\n\t\tif ( !ball.IsValid() ) return;\n\t\tif ( ball.OwnerSteamId != steamId ) return;\n\n\t\tball.TeleportTo( new Vector3( x, y, 0f ) );\n\t\tGameLog.Info( $\u0022[class] commander teleport -\u003E ({x:0},{y:0})\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E host\uFF1A\u4E3A\u6307\u5B9A\u73A9\u5BB6\u65BD\u653E\u804C\u4E1A\u6280\u80FD\uFF08\u5BA2\u6237\u7AEF RPC \u5165\u53E3\uFF1B\u51B7\u5374/\u6761\u4EF6\u5728\u7BA1\u7406\u5668\u5185\u6821\u9A8C\uFF09 \u003C/summary\u003E\n\tpublic void CastClassSkillFor( long steamId )\n\t{\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 !b.IsBot \u0026\u0026 b.Alive \u0026\u0026 b.OwnerSteamId == steamId )\n\t\t\t{\n\t\t\t\tClassSkillManager.Cast( b );\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u9759\u6001 RPC \u843D\u5730\uFF1A\u4E3A\u6307\u5B9A\u8FDE\u63A5\u539F\u5730\u590D\u6D3B\u5176\u7403\uFF08\u6309 OwnerSteamId \u5339\u914D\uFF09\u3002\n\t/// \u26A0\uFE0F \u95E8\u7981\uFF08v0.7.8.105\uFF0C\u5BA1\u8BA1 #6\uFF09\uFF1A\u5FC5\u987B\u0022\u5BF9\u5C40\u8FDB\u884C\u4E2D\u4E14\u672A\u7ED3\u7B97\u0022\uFF0C\u4E14\u4E0D\u78B0\u86F0\u4F0F\u7403\u3002\n\t/// \u6B64\u524D\u53EA\u67E5\u8FDE\u63A5\u6709\u6548\u2014\u2014\u623F\u95F4\u671F\u6BCF\u9897\u7403\u90FD\u662F Alive=false,Dormant=true\uFF0C\u4EFB\u4F55\u5BA2\u6237\u7AEF\u90FD\u80FD\u5728 host\n\t/// \u70B9 START \u4E4B\u524D\u8C03\u7528 RequestRespawn \u628A\u81EA\u5DF1\u590D\u6D3B\u5E76\u5206\u5230\u961F\u4F0D\uFF1B\u7ED3\u7B97\u671F\u540C\u7406\uFF08\u51BB\u7ED3\u88AB\u7ED5\u8FC7\uFF09\u3002\n\t/// \u5BA2\u6237\u7AEF\u4FA7 TryRespawnLocal \u672C\u6765\u6709\u95E8\u7981\uFF0C\u4F46 RPC \u5165\u53E3\u5B8C\u5168\u7ED5\u8FC7\u4E86\u5B83\u3002 \u003C/summary\u003E\n\tpublic void RespawnConnection( Connection conn, bool frontLine = false )\n\t{\n\t\tif ( conn is null || !conn.IsActive ) return;\n\t\tif ( !_gameStarted || MatchState.MatchOver ) return;\n\n\t\tvar steamId = conn.SteamId.Value;\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 !b.IsBot \u0026\u0026 !b.Alive \u0026\u0026 !b.Dormant \u0026\u0026 b.OwnerSteamId == steamId )\n\t\t\t{\n\t\t\t\tRespawnBall( b, frontLine: frontLine );\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tGameLog.Info( $\u0022[net] respawn request: no dead ball for \u0027{conn.DisplayName}\u0027\u0022 );\n\t}\n\n\t// ---- \u98DF\u7269\u8FDC\u7AEF\u4E8B\u4EF6\u5E94\u7528\uFF08\u4EC5\u5BA2\u6237\u7AEF\uFF1Bhost \u7684\u5E7F\u64AD\u56DE\u58F0\u7528 IsAuthority \u6321\u6389\uFF09----\n\n\tpublic void OnFoodEatenRemote( int index, long eaterSteamId )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\n\t\t// \u5E94\u7528\u524D\u5148\u53D6\u4F4D\u7F6E/\u989C\u8272\u653E\u706B\u82B1\uFF08ApplyEaten \u4F1A\u7F6E Alive=false\uFF0C\u4F46\u6570\u636E\u4ECD\u5728\u2014\u2014\u53D6\u66F4\u4FDD\u9669\uFF09\n\t\tvar fs = _food?.Foods;\n\t\tif ( fs is not null \u0026\u0026 (uint)index \u003C (uint)fs.Length )\n\t\t{\n\t\t\tvar pos = new Vector3( fs[index].Pos.x, fs[index].Pos.y, 0f );\n\t\t\tNeonRenderer.Spark( pos, fs[index].ColorIndex );\n\t\t\tif ( GameSfx.IsMine( eaterSteamId ) )\n\t\t\t\tGameSfx.EatFood( pos );\n\t\t}\n\n\t\t_food?.ApplyEaten( index );\n\t}\n\n\tpublic void OnFoodRespawnedRemote( int index, FoodData food )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\t_food?.ApplyRespawned( index, food );\n\t}\n\n\tpublic void OnFoodFullRemote( FoodData[] foods )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\t_food?.ApplyFull( foods );\n\t\tGameLog.Info( $\u0022[game] food full sync received: {foods.Length}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u5E94\u7528\u7EFF\u523A\u5168\u91CF\uFF08\u9759\u6001\u6570\u636E\uFF0C\u6536\u5230\u5373\u7528\uFF09 \u003C/summary\u003E\n\tpublic void OnSpikesFullRemote( Vector2[] spikes )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\t_spikes = spikes;\n\n\t\t// \u5582\u98DF\u91CF\u955C\u50CF\uFF1A**\u957F\u5EA6\u4E00\u81F4\u5C31\u4FDD\u7559**\uFF08v0.7.8.87 \u4FEE\uFF09\u3002SpikesFull \u5728\u6BCF\u6B21\u523A\u88AB\u5403/\u91CD\u5237\u65F6\u90FD\u4F1A\u91CD\u53D1\n\t\t// \uFF08\u53EA\u5E26\u4F4D\u7F6E\u3001\u4E0D\u5E26\u5582\u98DF\u91CF\uFF09\uFF0C\u539F\u6765\u65E0\u6761\u4EF6\u5F52\u96F6 \u2192 \u5BA2\u6237\u7AEF\u523A\u7684\u5582\u517B\u8FDB\u5EA6\u88AB\u53CD\u590D\u62B9\u6389\u3001\u4E0E host \u957F\u671F\u4E0D\u4E00\u81F4\uFF1B\n\t\t// \u53EA\u6709\u957F\u5EA6\u53D8\u4E86\uFF08\u65B0\u5C40/\u9996\u6B21\u540C\u6B65\uFF09\u624D\u91CD\u5EFA\n\t\tvar n = spikes is null ? 0 : spikes.Length;\n\t\tif ( _spikeFed is null || _spikeFed.Length != n ) _spikeFed = new float[n];\n\t\t// \u4E0D\u518D\u6E05 _spines\uFF1A\u5168\u91CF\u540C\u6B65\u6BCF\u6735\u523A\u91CD\u5237\u90FD\u4F1A\u53D1\u4E00\u6B21\uFF0C\u6E05\u5F39\u5E55\u4F1A\u628A\u5728\u98DE\u7684\u523A\u7206\u62B9\u6389\uFF08v0.7.8.87\uFF09\uFF1B\n\t\t// \u8DE8\u5C40\u7684\u65E7\u5F39\u5E55\u7531\u5F00\u5C40\u6E05\u7406\u8DEF\u5F84\uFF08StartMatchFromLobby / ResetToLobby\uFF09\u8D1F\u8D23\n\t}\n\n\t// ---- \u5582\u523A/\u523A\u7206\u8FDC\u7AEF\u4E8B\u4EF6\uFF08v0.7.8.24\uFF09----\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u955C\u50CF\uFF1A\u67D0\u6735\u523A\u7684\u7D2F\u8BA1\u5582\u98DF\u91CF\uFF08host \u5DF2\u76F4\u63A5\u5199\u5165\uFF0C\u56DE\u58F0\u6321\u6389\uFF09 \u003C/summary\u003E\n\tpublic void OnSpikeFedRemote( int index, float fed )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\tif ( _spikeFed is not null \u0026\u0026 (uint)index \u003C (uint)_spikeFed.Length ) _spikeFed[index] = fed;\n\t}\n\n\t/// \u003Csummary\u003E \u5168\u7AEF\u7206\u6548\uFF1B\u5BA2\u6237\u7AEF\u7528\u4E0E host \u76F8\u540C\u7684\u786E\u5B9A\u6027\u89D2\u5EA6\u751F\u6210\u89C6\u89C9\u5F39\u5E55\uFF08host \u7684\u6743\u5A01\u5F39\u5E55\u5728 BurstSpike \u5DF2\u751F\u6210\uFF09 \u003C/summary\u003E\n\tpublic void OnSpikeBurstRemote( int index )\n\t{\n\t\tif ( _spikes is null || (uint)index \u003E= (uint)_spikes.Length ) return;\n\t\tvar pos = _spikes[index];\n\n\t\tif ( NetworkManager.IsAuthority )\n\t\t{\n\t\t\tGameSfx.Pop( new Vector3( pos.x, pos.y, 0f ) );   // host \u7206\u6548\u58F0\uFF08\u6743\u5A01\u5F39\u5E55\u5DF2\u5728 BurstSpike \u751F\u6210\uFF0C\u6E32\u67D3\u76F4\u63A5\u8BFB\uFF09\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _spikeFed is not null \u0026\u0026 (uint)index \u003C (uint)_spikeFed.Length ) _spikeFed[index] = 0f;\n\t\tvar n = GameConfig.SpikeBurstCount;\n\t\tfor ( int k = 0; k \u003C n; k\u002B\u002B )\n\t\t{\n\t\t\t_spines.Add( new SpikeSpine\n\t\t\t{\n\t\t\t\tPos = new Vector2( pos.x, pos.y ),\n\t\t\t\tVel = SpineDir( k, n, index ) * GameConfig.SpineSpeed,\n\t\t\t\tSinceSpawn = 0,   // \u540C BurstSpike\uFF1ATimeSince \u5FC5\u987B\u663E\u5F0F\u5F52\u96F6\n\t\t\t} );\n\t\t}\n\t\tGameSfx.Pop( new Vector3( pos.x, pos.y, 0f ) );\n\t}\n\n\t/// \u003Csummary\u003E \u523A\u7206\u5F39\u5E55\u547D\u4E2D\u7279\u6548\uFF08\u5168\u7AEF\uFF1ABroadcast \u672C\u5730\u4E5F\u6267\u884C\uFF0Chost \u4E0D\u9884\u64AD\uFF09 \u003C/summary\u003E\n\tpublic void OnSpineHitRemote( float x, float y, byte colorIndex )\n\t{\n\t\tvar pos = new Vector3( x, y, 0f );\n\t\tNeonRenderer.Puff( pos, colorIndex );\n\t\tGameSfx.Pop( pos );\n\t}\n\n\t// ---- \u9053\u5177\u8FDC\u7AEF\u4E8B\u4EF6\u5E94\u7528\uFF08\u4EC5\u5BA2\u6237\u7AEF\uFF1Bhost \u7684\u5E7F\u64AD\u56DE\u58F0\u7528 IsAuthority \u6321\u6389\uFF09----\n\n\tpublic void OnPowerUpFullRemote( PowerUpWire[] wires )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\t_power?.ApplyFull( wires );\n\t}\n\n\tpublic void OnPowerUpPickedRemote( int slot, byte kind, long pickerSteamId )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\n\t\t// \u5E94\u7528\u524D\u5148\u53D6\u4F4D\u7F6E/\u79CD\u7C7B\u653E\u706B\u82B1\uFF08ApplyPicked \u4F1A\u7F6E\u7A7A\u69FD\uFF09\n\t\tif ( _power?.Ready ?? false )\n\t\t{\n\t\t\tvar slots = _power.Slots;\n\t\t\tif ( (uint)slot \u003C (uint)slots.Count )\n\t\t\t{\n\t\t\t\tvar s = slots[slot];\n\t\t\t\tNeonRenderer.Spark( new Vector3( s.Pos.x, s.Pos.y, 0f ), PowerUpManager.ColorOf( kind ) );\n\t\t\t\tif ( GameSfx.IsMine( pickerSteamId ) )\n\t\t\t\t{\n\t\t\t\t\tGameSfx.EatFood( new Vector3( s.Pos.x, s.Pos.y, 0f ) );\n\t\t\t\t\tGameAchievements.PowerStored();   // \u6210\u5C31\uFF1A\u7D2F\u8BA1\u6361 5 \u4E2A\u9053\u5177\u5165\u5305\uFF08\u5BA2\u6237\u7AEF\u4FA7\u8BA1\u6570\uFF09\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_power?.ApplyPicked( slot );\n\t}\n\n\tpublic void OnPowerUpRespawnedRemote( int slot, byte kind, float x, float y )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\t_power?.ApplyRespawned( slot, kind, new Vector2( x, y ) );\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u4E2A\u4EBA\u9AD8\u5149\u6A2A\u5E45\u843D\u5730\uFF08host \u5DF2\u672C\u5730\u64AD\u8FC7\uFF0C\u56DE\u58F0\u6321\u6389\uFF1B\u53EA\u64AD\u0022\u662F\u6211\u7684\u0022\u90A3\u6761\uFF09 \u003C/summary\u003E\n\tpublic void OnPowerBannerRemote( byte kind, long ownerSteamId, byte mode )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\tif ( !GameSfx.IsMine( ownerSteamId ) ) return;\n\n\t\tvar text = mode == 2 ? $\u0022{PowerUpManager.NameOf( kind )} READY (Q)\u0022   // Q \u5145\u80FD\u5230\u70B9\uFF08v0.7.8.31\uFF09\n\t\t\t: mode == 0 ? $\u0022{PowerUpManager.NameOf( kind )} STORED\u0022 : GameHud.ActivationText( kind );\n\t\t_hud?.AddBanner( text, GameHud.KindColor( kind ) );\n\t\tGameSfx.Pickup();\n\n\t\tif ( mode == 1 ) GameAchievements.FeedTriggered( ownerSteamId );   // \u6210\u5C31\uFF1A\u9996\u6B21\u5582\u98DF\u89E6\u53D1\uFF08mode1=\u6FC0\u6D3B/\u5582\u517B\uFF09\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u5E94\u7528\u6B7B\u4EA1\u7206\u88C2\u7279\u6548\uFF08host \u5DF2\u5728\u672C\u5730\u751F\u6210\u8FC7\uFF0C\u56DE\u58F0\u7528 IsAuthority \u6321\u6389\uFF09\u3002\n\t/// \u89C6\u89C9\u5168\u573A\u64AD\uFF1B\u7206\u88C2\u97F3\u53EA\u64AD\u81EA\u5DF1\u6B7B\u4EA1/\u7206\u88C2\u7684\uFF08ownerSteamId \u5339\u914D\u672C\u673A\uFF09 \u003C/summary\u003E\n\tpublic void OnPoppedEffectRemote( Vector3 pos, byte colorIndex, float radius, long ownerSteamId )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\tNeonRenderer.Pop( pos, colorIndex, radius );\n\t\tif ( GameSfx.IsMine( ownerSteamId ) )\n\t\t\tGameSfx.Pop( pos );\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u541E\u7403\u4E8B\u4EF6\uFF08host \u56DE\u58F0\u7528 IsAuthority \u6321\u6389\u2014\u2014host \u5DF2\u5728 NotifyBallEaten \u76F4\u8C03\u8FC7\uFF09 \u003C/summary\u003E\n\tpublic void OnBallEatenRemote( long eaterSteamId, long eatenSteamId, float massGained )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\tOnBallEaten( eaterSteamId, eatenSteamId, massGained );\n\t}\n\n\tvoid CreateBackdrop()\n\t{\n\t\tvar go = new GameObject( true, \u0022GridBackdrop\u0022 );\n\t\tgo.Parent = _root;\n\t\tgo.AddComponent\u003CGridBackdrop\u003E();\n\t\tgo.AddComponent\u003CTerritoryRenderer\u003E();   // \u9886\u571F\u683C\u5E95\u8272/\u8FB9\u754C/\u65D7\u6807\uFF08M7\uFF1B\u975E\u9886\u571F\u6A21\u5F0F\u81EA\u6E05\u7A7A\u4E0D\u753B\uFF09\n\t}\n\n\t/// \u003Csummary\u003E \u5168\u573A\u666F\u4E00\u4E2A\u7684\u50CF\u7D20\u6279\u91CF\u753B\u7B14\uFF1A\u6BCF\u5E27\u628A\u6240\u6709\u7403/\u5206\u8EAB\u5199\u8FDB\u5404\u6279\u6B21\u9876\u70B9\uFF08v0.7.8.40 \u5206\u8EAB\u5934\u50CF\u4E5F\u5E76\u5165\uFF09 \u003C/summary\u003E\n\tvoid CreateNeon()\n\t{\n\t\tvar go = new GameObject( true, \u0022Neon\u0022 );\n\t\tgo.Parent = _root;\n\t\tgo.AddComponent\u003CNeonRenderer\u003E();\n\t}\n\n\tvoid CreateCamera()\n\t{\n\t\t// \u9632\u5FA1\uFF1A\u70ED\u91CD\u8F7D/\u91CD\u5165\u53EF\u80FD\u6B8B\u7559\u65E7\u76F8\u673A\u2014\u2014\u4E24\u4E2A IsMainCamera \u4F1A\u62A2\u6E32\u67D3\uFF0C\u800C\u65E7\u7684\u90A3\u4E2A\n\t\t// \u59FF\u6001\u5DF2\u6B7B\uFF08NeonCamera \u7F13\u5B58\u7684 _target \u5931\u6548\u65E9\u9000\uFF09\uFF0C\u6E32\u67D3\u9009\u4E2D\u5B83\u5C31\u662F\u753B\u9762\u50F5\u6B7B\u4E0D\u8DDF\u968F\n\t\tforeach ( var c in Scene.GetAllComponents\u003CCameraComponent\u003E().ToArray() )\n\t\t{\n\t\t\tif ( c.IsValid() \u0026\u0026 c.GameObject.IsValid() )\n\t\t\t{\n\t\t\t\tGameLog.Info( \u0022[game] destroying stale camera\u0022 );\n\t\t\t\tc.GameObject.Destroy();\n\t\t\t}\n\t\t}\n\n\t\tvar go = new GameObject( true, \u0022Camera\u0022 );\n\t\tgo.Parent = _root;\n\n\t\tvar cam = go.AddComponent\u003CCameraComponent\u003E();\n\t\tcam.IsMainCamera = true;\n\t\tcam.Priority = -100;\n\t\tcam.Orthographic = true;\n\t\tcam.OrthographicHeight = GameConfig.CameraBaseView;\n\t\tcam.ClearFlags = ClearFlags.All;\n\t\tcam.BackgroundColor = new Color( 0.918f, 0.957f, 0.988f );   // #EAF4FC \u6D45\u84DD\u5E95\uFF08\u50CF\u7D20\u98CE\uFF0Cv0.7.8.36\uFF09\n\t\tcam.EnablePostProcessing = true;\n\n\t\t// Bloom \u4FDD\u7559\u7EC4\u4EF6\uFF08cr_bloom convar/\u8BBE\u7F6E\u9875\u8FD8\u7BA1\u5B83\uFF09\uFF0C\u9ED8\u8BA4\u5173\u2014\u2014\u52A0\u8272\u6CDB\u5149\u4F1A\u7CCA\u6389\u7CD6\u679C\u8272\u4E0E\u50CF\u7D20\u9897\u7C92\n\t\tvar bloom = go.AddComponent\u003CBloom\u003E();\n\t\tbloom.Mode = SceneCamera.BloomAccessor.BloomMode.Additive;\n\t\tbloom.Strength = 1.5f;\n\t\tbloom.Threshold = 0.55f;\n\n\t\t// \u50CF\u7D20\u98CE\u53BB\u6389 ACES \u8272\u8C03\u6620\u5C04\uFF08\u4F1A\u628A\u9A6C\u5361\u9F99\u8272\u538B\u7070\uFF09\uFF0C\u4FDD\u6301\u8D34\u56FE\u539F\u8272\n\t\t_camera = go.AddComponent\u003CNeonCamera\u003E();\n\t\t_camera.Cam = cam;\n\t}\n\n\tvoid CreateHud()\n\t{\n\t\tvar go = new GameObject( true, \u0022Hud\u0022 );\n\t\tgo.Parent = _root;\n\t\t// \u8BCA\u65AD\uFF08\u4E34\u65F6\uFF0C\u5BA2\u6237\u7AEF\u7206\u95EA\uFF09\uFF1AParent= \u53EF\u80FD\u9759\u9ED8\u5931\u6548\uFF08\u76EE\u6807\u662F host \u62E5\u6709\u5BF9\u8C61\u65F6\uFF09\u2014\u2014\u56DE\u8BFB\u5B9E\u9645\u7236\u7269\u4F53\n\t\tvar rootName = _root.IsValid() ? _root.Name : \u0022NULL\u0022;\n\t\tvar realParent = go.Parent.IsValid() ? go.Parent.Name : \u0022NONE\u0022;\n\t\tint ballCount = 0;\n\t\tif ( _root.IsValid() ) foreach ( var b in _root.GetComponentsInChildren\u003CBall\u003E( true ) ) ballCount\u002B\u002B;\n\t\tLog.Info( $\u0022[game] CreateHud: _root=\u0027{rootName}\u0027 ballsUnderRoot={ballCount} hudParentNow=\u0027{realParent}\u0027\u0022 );\n\t\tgo.AddComponent\u003CScreenPanel\u003E();\n\t\t_hud = go.AddComponent\u003CGameHud\u003E();\n\n\t\t// \u7ED3\u7B97\u9762\u677F\uFF08M5\uFF09\uFF1A\u72EC\u7ACB ScreenPanel \u53E0\u5728 HUD \u4E4B\u4E0A\uFF0C\u5E73\u65F6\u9690\u85CF\n\t\tvar ego = new GameObject( true, \u0022EndBoard\u0022 );\n\t\tego.Parent = _root;\n\t\tego.AddComponent\u003CScreenPanel\u003E();\n\t\t_endBoard = ego.AddComponent\u003CEndBoard\u003E();\n\t}\n\n\t/// \u003Csummary\u003E \u4E3B\u83DC\u5355\uFF08\u5F00\u5C40\u540E\u7531 StartGame \u7981\u7528\u6574\u4E2A GameObject\uFF09 \u003C/summary\u003E\n\tvoid CreateMenu()\n\t{\n\t\tvar go = new GameObject( true, \u0022MainMenu\u0022 );\n\t\tgo.Parent = _root;\n\t\tgo.AddComponent\u003CScreenPanel\u003E();\n\t\t_menu = go.AddComponent\u003CMainMenu\u003E();\n\t}\n\n\t/// \u003Csummary\u003E \u6E38\u620F\u623F\u95F4\uFF08M5 \u4E8C\u7EA7\u754C\u9762\uFF09\uFF1AHOST/VS BOTS \u5F00\u623F\u540E\u663E\u793A\uFF0CSTART/\u8FDB\u5C40\u540E\u9690\u85CF \u003C/summary\u003E\n\tvoid CreateLobbyPanel()\n\t{\n\t\tvar go = new GameObject( true, \u0022LobbyPanel\u0022 );\n\t\tgo.Parent = _root;\n\t\tgo.AddComponent\u003CScreenPanel\u003E();\n\t\t_lobby = go.AddComponent\u003CLobbyPanel\u003E();\n\t}\n\n\t/// \u003Csummary\u003E \u8BBE\u7F6E\u9875\uFF08v0.7.8.16 \u4E8C\u7EA7\u754C\u9762\uFF09\uFF1A\u4E3B\u83DC\u5355 [4] \u8FDB\u5165\u2014\u2014\u6CDB\u5149\u5F00\u5173/\u97F3\u4E50\u97F3\u91CF\uFF0CBACK \u56DE\u83DC\u5355 \u003C/summary\u003E\n\tvoid CreateSettingsPanel()\n\t{\n\t\tvar go = new GameObject( true, \u0022SettingsPanel\u0022 );\n\t\tgo.Parent = _root;\n\t\tgo.AddComponent\u003CScreenPanel\u003E();\n\t\t_settings = go.AddComponent\u003CSettingsPanel\u003E();\n\t}\n\n\t/// \u003Csummary\u003E \u6392\u884C\u9875\uFF08v0.7.8.19 \u4E8C\u7EA7\u754C\u9762\uFF09\uFF1A\u4E3B\u83DC\u5355 [5] \u8FDB\u5165\u2014\u2014\u5168\u7403 XP \u699C\uFF0CBACK \u56DE\u83DC\u5355 \u003C/summary\u003E\n\tvoid CreateRankingsPanel()\n\t{\n\t\tvar go = new GameObject( true, \u0022RankingsPanel\u0022 );\n\t\tgo.Parent = _root;\n\t\tgo.AddComponent\u003CScreenPanel\u003E();\n\t\t_rankings = go.AddComponent\u003CRankingsPanel\u003E();\n\t}\n\n\t/// \u003Csummary\u003E \u623F\u95F4\u5217\u8868\u9875\uFF08v0.6.7.0\uFF09\uFF1A\u4E3B\u83DC\u5355 [2] JOIN GAME \u6253\u5F00\uFF0C\u5217\u51FA Steam \u516C\u5F00\u5927\u5385 \u003C/summary\u003E\n\tvoid CreateRoomBrowser()\n\t{\n\t\tvar go = new GameObject( true, \u0022RoomBrowser\u0022 );\n\t\tgo.Parent = _root;\n\t\tgo.AddComponent\u003CScreenPanel\u003E();\n\t\t_browser = go.AddComponent\u003CRoomBrowser\u003E();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u751F\u6210\u4E00\u9897\u7403\u3002M0 \u76F4\u63A5\u4EE3\u7801\u5EFA\u5B9E\u4F53\uFF1BM2 \u8D77\u771F\u4EBA\u7403\u4F18\u5148\u7528 Ball \u9884\u5236\u4F53\n\t/// \uFF08\u7528\u6237\u5728\u7F16\u8F91\u5668\u521B\u5EFA\uFF0C\u4EE3\u7801\u6309\u8DEF\u5F84 Clone\uFF0C\u7F3A\u5931\u65F6\u56DE\u9000\u4EE3\u7801\u6784\u5EFA\uFF09\u3002\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// \u751F\u6210\u4E00\u9897\u7403\uFF08\u4EC5\u6743\u5A01\u7AEF\u8C03\u7528\uFF09\uFF1A\u4F18\u5148\u514B\u9686\u7528\u6237\u521B\u5EFA\u7684 ball.prefab\uFF08\u6839\u8282\u70B9\u9700\u6302 Ball\uFF09\uFF0C\n\t/// \u7F3A\u5931\u56DE\u9000\u7EAF\u4EE3\u7801\u6784\u5EFA\u3002owner=null \u2192 host \u62E5\u6709\uFF08\u623F\u4E3B\u81EA\u5DF1/bot\uFF09\uFF1B\n\t/// \u4F20\u8FDE\u63A5 \u2192 \u8BE5\u5BA2\u6237\u7AEF\u62E5\u6709\uFF0C\u79FB\u52A8\u7531\u5176\u672C\u5730\u6A21\u62DF\uFF08owner \u6A21\u62DF\u67B6\u6784\uFF09\u3002\n\t/// dormant=true\uFF1A\u623F\u95F4\u86F0\u4F0F\u7403\uFF08Alive=false \u4E0D\u53EF\u89C1\uFF0C\u5F00\u5C40\u7531 StartMatchFromLobby \u6FC0\u6D3B\uFF09\u2014\u2014\n\t/// \u5F15\u64CE\u0022\u8FDB\u884C\u4E2D NetworkSpawn \u4E0D\u590D\u5236\u5230\u5DF2\u8FDE\u63A5\u5BA2\u6237\u7AEF\u0022\uFF0C\u7403\u5FC5\u987B\u5728\u5404\u7AEF\u5FEB\u7167\u524D\u5B58\u5728\u3002\n\t/// \u003C/summary\u003E\n\tpublic Ball SpawnBall( Vector3 position, bool isBot, Connection owner = null, bool dormant = false )\n\t{\n\t\t// \u7ED9\u8FDC\u7A0B\u8FDE\u63A5\u751F\u6210\u7684\u7403\u5FC5\u987B\u7F51\u7EDC\u5316\u624D\u6709\u610F\u4E49\uFF1A\u4F1A\u8BDD\u672A\u6FC0\u6D3B\u65F6 NetworkSpawn \u4F1A\u88AB\u8DF3\u8FC7\uFF0C\n\t\t// \u7403\u6CA6\u4E3A host \u672C\u5730\u5E7D\u7075\u7403\uFF08\u4E0D\u5E7F\u64AD \u2192 \u5BA2\u6237\u7AEF\u6536\u4E0D\u5230\uFF1Bproxy=false \u2192 \u5E72\u6270\u672C\u673A\u7403\u5224\u5B9A\uFF09\u2014\u2014\u5B9E\u6D4B\u5927\u5751\u3002\n\t\t// OnActive \u7ADE\u6001\uFF08\u5927\u5385\u91CD\u5EFA\u4E2D\u89E6\u53D1\u56DE\u8C03\uFF09\u5C31\u843D\u5728\u8FD9\u91CC\uFF0C\u76F4\u63A5\u62D2\u7EDD\u751F\u6210\u3002\n\t\tif ( owner is not null \u0026\u0026 !Networking.IsActive )\n\t\t{\n\t\t\tLog.Warning( \u0022[game] SpawnBall: session not active, refuse ghost ball for remote owner\u0022 );\n\t\t\treturn null;\n\t\t}\n\n\t\tGameObject go = null;\n\t\tBall ball = null;\n\n\t\tvar prefab = PrefabFile.Load( GameConfig.BallPrefabPath );\n\t\tif ( prefab is not null )\n\t\t{\n\t\t\tgo = GameObject.Clone( GameConfig.BallPrefabPath, new Transform( position ), _root );\n\t\t\tball = go.Components.Get\u003CBall\u003E();\n\t\t\tif ( ball is null )\n\t\t\t{\n\t\t\t\tgo.Destroy();   // \u9884\u5236\u4F53\u6839\u8282\u70B9\u6CA1\u6302 Ball\uFF0C\u89C6\u4E3A\u65E0\u6548\u914D\u7F6E\uFF0C\u56DE\u9000\u4EE3\u7801\u6784\u5EFA\n\t\t\t\tgo = null;\n\t\t\t}\n\t\t}\n\n\t\tif ( go is null )\n\t\t{\n\t\t\tgo = new GameObject( true, isBot ? \u0022Bot\u0022 : \u0022Player\u0022 );\n\t\t\tgo.Parent = _root;\n\t\t\tgo.WorldPosition = position;\n\t\t\tball = go.AddComponent\u003CBall\u003E();\n\t\t}\n\n\t\tball.PlayerName = isBot ? NextBotName() : ( owner?.DisplayName ?? LocalPlayerName() );\n\t\tball.OwnerSteamId = owner?.SteamId.Value ?? ( isBot ? NextBotSteamId() : Game.SteamId.Value );\n\t\tball.IsRemoteOwned = owner is not null;   // host \u4FA7\uFF1A\u8FDC\u7A0B\u8FDE\u63A5\u7684\u7403\u4E0D\u53C2\u4E0E\u672C\u673A\u7403\u5224\u5B9A\n\t\t// \u961F\u4F0D\u5206\u914D\uFF1A\u86F0\u4F0F\u7403\uFF08\u623F\u95F4\u9636\u6BB5\uFF09\u4E0D\u5360\u961F\u53F7\uFF0C\u5F00\u5C40\u6FC0\u6D3B\u65F6\u7EDF\u4E00\u6309\u5757\u5206\u961F\uFF1B\n\t\t// \u5BF9\u5C40\u4E2D\u9014\u52A0\u5165\u7684\u7403\uFF08dormant=false \u4E14\u6BD4\u8D5B\u5DF2\u5F00\u59CB\uFF09\u8865\u5206\u5230\u4E0B\u4E00\u4E2A\u5757\uFF08\u56E2\u961F\u8D5B\uFF0Cv0.6.5.0\uFF09\n\t\tball.Init( isBot, !dormant \u0026\u0026 _gameStarted \u0026\u0026 MatchState.IsTeam ? NextTeamIndex() : -1 );\n\t\tball.Dormant = dormant;\n\t\tif ( dormant ) ball.Alive = false;   // \u5FC5\u987B\u5728 NetworkSpawn \u4E4B\u524D\u7F6E false\uFF0C\u968F\u521D\u59CB\u5FEB\u7167\u4E0B\u53D1\n\n\t\tif ( isBot ) go.AddComponent\u003CBotBrain\u003E();\n\n\t\t// host \u4FA7\u76F4\u63A5\u6CE8\u518C\uFF08Ball.OnStart \u5728\u7F51\u7EDC\u5BF9\u8C61\u4E0A\u4E0D\u89E6\u53D1\uFF0C\u89C1 ScanBalls \u515C\u5E95\u6CE8\u91CA\uFF09\n\t\t_balls.Add( ball );\n\t\tif ( !isBot \u0026\u0026 owner is null )\n\t\t\tSetLocalBall( ball );\n\n\t\tgo.NetworkMode = NetworkMode.Object;\n\t\tif ( Networking.IsActive )\n\t\t\tgo.NetworkSpawn( owner );\n\n\t\treturn ball;\n\t}\n\n\t/// \u003Csummary\u003E \u53D6\u4E00\u4E2A\u672A\u88AB\u5360\u7528\u7684 bot \u540D\uFF08\u4ECE\u6E38\u6807\u8D77\u626B\uFF0C\u8DF3\u8FC7\u573A\u4E0A\u4EFB\u4F55\u7403\u2014\u2014\u542B\u86F0\u4F0F/\u5C38\u4F53\u2014\u2014\u6B63\u7528\u7740\u7684\uFF1B\n\t/// 48 \u6EE1\u7F16\u5C0F\u4E8E 64 \u540D\u5B57\u6C60\uFF0C\u6B63\u5E38\u6C38\u8FDC\u53D6\u5F97\u5230\uFF1B\u6C60\u5B50\u771F\u88AB\u5360\u6EE1\u624D\u5FAA\u73AF\u590D\u7528\u515C\u5E95\uFF09 \u003C/summary\u003E\n\tstring NextBotName()\n\t{\n\t\tfor ( int i = 0; i \u003C GameConfig.BotNames.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar name = GameConfig.BotNames[( _botNameIndex \u002B i ) % GameConfig.BotNames.Length];\n\t\t\tif ( FindBallByPlayerName( name ) is null )\n\t\t\t{\n\t\t\t\t_botNameIndex \u002B= i \u002B 1;\n\t\t\t\treturn name;\n\t\t\t}\n\t\t}\n\t\treturn GameConfig.BotNames[_botNameIndex\u002B\u002B % GameConfig.BotNames.Length];\n\t}\n\n\t/// \u003Csummary\u003E \u6309\u540D\u5B57\u627E\u7403\uFF08bot \u53D6\u540D\u67E5\u91CD\u7528\uFF09 \u003C/summary\u003E\n\tpublic Ball FindBallByPlayerName( string name )\n\t{\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 b.PlayerName == name ) return b;\n\t\t}\n\t\treturn null;\n\t}\n\n\tint _botSeq;   // bot \u72EC\u7ACB\u5047 SteamId \u5E8F\u5217\uFF08\u914D\u5408 BotSteamIdBase\uFF09\n\n\t/// \u003Csummary\u003E \u5206\u914D\u4E00\u4E2A\u672A\u88AB\u5360\u7528\u7684 bot \u5047 SteamId\uFF08\u70ED\u91CD\u8F7D\u4F1A\u91CD\u7F6E _botSeq\uFF0C\u626B\u63CF\u67E5\u91CD\u9632\u78B0\u649E\uFF09 \u003C/summary\u003E\n\tlong NextBotSteamId()\n\t{\n\t\tvar id = GameConfig.BotSteamIdBase \u002B _botSeq\u002B\u002B;\n\t\twhile ( FindBallBySteamId( id ) is not null )\n\t\t\tid = GameConfig.BotSteamIdBase \u002B _botSeq\u002B\u002B;\n\t\treturn id;\n\t}\n\n\t/// \u003Csummary\u003E \u6309 SteamId \u627E\u7403\uFF08\u5206\u8EAB\u5934\u50CF/\u540D\u724C\u5F52\u5C5E\u89E3\u6790\u7528\uFF1Bbot \u73B0\u5728\u5404\u6709\u72EC\u7ACB\u5047 SteamId\uFF09 \u003C/summary\u003E\n\tpublic Ball FindBallBySteamId( long steamId )\n\t{\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 b.OwnerSteamId == steamId ) return b;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003E \u5168\u573A\u603B\u8D28\u91CF\uFF08\u4E3B\u7403\u002B\u5206\u8EAB\uFF09\u6700\u9AD8\u7684\u6D3B\u7403\u2014\u2014\u83DC\u5355\u6F14\u793A\u8D5B\u89C2\u6218\u955C\u5934/\u6B7B\u4EA1\u89C2\u6218\u7528 \u003C/summary\u003E\n\tpublic Ball TopMassBall()\n\t{\n\t\tBall best = null;\n\t\tfloat bestMass = -1f;\n\t\tforeach ( var b in _balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.Alive ) continue;\n\n\t\t\tvar m = TotalMassFor( b );\n\t\t\tif ( m \u003C= bestMass ) continue;\n\n\t\t\tbestMass = m;\n\t\t\tbest = b;\n\t\t}\n\t\treturn best;\n\t}\n\n\t/// \u003Csummary\u003E \u7403\u9500\u6BC1\u65F6\u7531 Ball.OnDestroy \u56DE\u8C03\u6CE8\u9500 \u003C/summary\u003E\n\tpublic void RemoveBall( Ball ball )\n\t{\n\t\t_balls.Remove( ball );\n\t\t_lastPos.Remove( ball.Id );\n\t\t_teleportGrace.Remove( ball.Id );\n\t\t_steerDir.Remove( ball.Id );\n\t\t_sinceEject.Remove( ball.Id );\n\t\tif ( LocalBall == ball ) LocalBall = null;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u7ED3\u7B97\u7ED3\u675F\uFF08\u81EA\u52A8 10 \u79D2 / \u7A7A\u683C\u63D0\u524D\uFF09\uFF1A\u591A\u4EBA\u56DE **host \u7684\u623F\u95F4**\uFF08\u4F1A\u8BDD\u4FDD\u7559\uFF0C\u7B49\u4E0B\u4E00\u5C40\uFF09\uFF0C\n\t/// \u5355\u673A\u56DE\u4E3B\u83DC\u5355\u3002\n\t/// \u003C/summary\u003E\n\tpublic void AfterSettlement()\n\t{\n\t\tif ( Networking.IsActive )\n\t\t\tResetToLobby();\n\t\telse\n\t\t\tResetToMenu();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u56DE\u5230\u623F\u95F4\uFF08\u591A\u4EBA\uFF0Cv0.6.4.2 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A**\u4F1A\u8BDD\u4FDD\u7559\u4E0D\u6E05**\u2014\u2014host \u56DE\u623F\u95F4\u63A7\u5236\u7AEF\n\t/// \uFF08\u53EF\u8C03\u53C2\u3001\u518D START \u5F00\u4E0B\u4E00\u5C40\uFF09\uFF0C\u5BA2\u6237\u7AEF\u56DE\u623F\u95F4\u7B49\u5F85\u9875\uFF08RequestMatchState \u62C9\u73A9\u5BB6\u5217\u8868\uFF09\u3002\n\t/// \u4E0A\u4E00\u5C40\u7684\u7403\u7559\u7740\u5F53\u80CC\u666F\uFF08\u4E0B\u5C40 START \u65F6\u7EDF\u4E00\u9500\u6BC1\u91CD\u5EFA\uFF09\uFF0C\u6A21\u62DF\u5DF2\u51BB\u7ED3\u4E0D\u4F1A\u7EE7\u7EED\u5403\u3002\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// \u5C40\u95F4\u6E05\u7406\uFF08v0.7.8.87\uFF09\uFF1A\u4E00\u5C40\u7ED3\u675F/\u56DE\u5927\u5385\u65F6\uFF0C\u51E1\u662F\u0022\u6309\u7403\u8BB0\u8D26\u0022\u6216\u0022\u672C\u5C40\u4E34\u65F6\u0022\u7684\u72B6\u6001\u90FD\u8981\u6E05\u3002\n\t/// **\u7403\u5BF9\u8C61\u662F\u8DE8\u5C40\u590D\u7528\u7684**\uFF08\u6C38\u4E0D\u9500\u6BC1\u3001\u4E0B\u5C40\u539F\u5730\u590D\u6D3B\uFF09\uFF0C\u6240\u4EE5\u4E0D\u6E05\u7684\u540E\u679C\u90FD\u4F1A\u6F0F\u5230\u4E0B\u4E00\u5C40\uFF1A\n\t/// \u5206\u8EAB/\u5B62\u5B50\u88AB\u7EE7\u7EED\u6A21\u62DF\uFF08\u5F00\u5C40\u767D\u9001\u8D28\u91CF\uFF09\u3001\u4F4D\u79FB\u6821\u9A8C\u951A\u70B9/\u77AC\u79FB\u5BBD\u9650\u9519\u4F4D\u3001\n\t/// \u804C\u4E1A\u51B7\u5374\u4E0E Q \u5145\u80FD\u6B8B\u7559\uFF08\u5F00\u5C40\u6309\u4E0D\u52A8 / \u767D\u9001\u6280\u80FD\uFF09\u3001\u8FDE\u51FB\u4E0E\u51FB\u6740\u8BA1\u6570\u4E32\u5C40\u3001\n\t/// \u523A\u7206\u5F39\u5E55\u6B8B\u7559\u3001\u524D\u7EBF\u590D\u6D3B\u504F\u597D\u6B8B\u7559\u3002\u56DE\u4E3B\u83DC\u5355\u7684 ResetToMenu \u5DF2\u6709\u4E00\u4EFD\u66F4\u5F7B\u5E95\u7684\u6E05\u7406\uFF0C\u8FD9\u91CC\u5BF9\u9F50\u3002\n\t/// \u003C/summary\u003E\n\tvoid ClearMatchState()\n\t{\n\t\t_cells.Clear();\n\t\t_cellsDirty = true;\n\t\t_lastPos.Clear();\n\t\t_teleportGrace.Clear();\n\t\t_steerDir.Clear();\n\t\t_sinceEject.Clear();\n\t\t_spines.Clear();\n\t\t_pendingFrontRespawn = false;\n\n\t\tClassSkillManager.ResetAll();\n\t\tGameSfx.ResetMatchStats();\n\t\tGameXp.ResetMatchKills();\n\t\tGameHud.ResetHints();\n\t}\n\n\tpublic void ResetToLobby()\n\t{\n\t\tMatchState.Reset();\n\t\t_matchRunning = false;\n\t\t_joinCountdown = -1f;\n\t\t_gameStarted = false;\n\n\t\tClearMatchState();   // \u5C40\u95F4\u6E05\u7406\uFF08v0.7.8.87\uFF09\uFF1A\u7403\u5BF9\u8C61\u8DE8\u5C40\u590D\u7528\uFF0C\u89C1\u65B9\u6CD5\u6CE8\u91CA\n\n\t\t_endBoard?.Hide();\n\t\tif ( _hud.IsValid() ) _hud.GameObject.Enabled = false;\n\t\t_menu?.Hide();\n\n\t\tif ( NetworkManager.IsAuthority )\n\t\t{\n\t\t\t_lobbyOpen = true;\n\t\t\t_lobby?.ShowHost();\n\t\t\t_net?.ResetSession();\n\t\t\tMarkLobbyDirty();   // \u5E7F\u64AD\u623F\u95F4\u73A9\u5BB6\u5217\u8868\n\t\t}\n\t\telse\n\t\t{\n\t\t\tEnterClientWaiting();\n\t\t\t_browser?.Hide();\n\t\t\t_lobby?.ShowClient();\n\t\t\tNetworkManager.RequestMatchState();\n\t\t}\n\n\t\tGameMusic.PlayMenu();   // \u56DE\u5927\u5385\u56DE\u5230\u83DC\u5355\u66F2\n\t\tGameLog.Info( $\u0022[match] back to lobby (auth={NetworkManager.IsAuthority})\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u7ED3\u7B97\u540E\u56DE\u4E3B\u83DC\u5355\uFF08M5\uFF09\uFF1A\u65AD\u7F51\u3001\u6E05\u573A\uFF08\u7403\u9500\u6BC1/\u5206\u8EAB\u6E05\u7A7A/\u6BD4\u8D5B\u6001\u590D\u4F4D\uFF09\u3001\u83DC\u5355\u91CD\u51FA\uFF1B\n\t/// \u4E16\u754C\u58F3\uFF08\u7F51\u683C/\u76F8\u673A\uFF09\u7559\u7740\u5F53\u83DC\u5355\u80CC\u666F\uFF0C\u4E0B\u6B21\u5F00\u5C40\u6CBF\u7528\uFF08StartGame \u590D\u7528 HUD\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tpublic void ResetToMenu()\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( Networking.IsActive ) Networking.Disconnect();\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\t// \u65AD\u4E0D\u5F00\u4E5F\u65E0\u59A8\uFF1Ahost \u4FA7\u4E0B\u5C40\u6CBF\u7528\u65E7\u4F1A\u8BDD\u7167\u5E38\u80FD\u5F00\n\t\t}\n\n\t\tforeach ( var b in _balls.ToArray() )\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tif ( b.IsValid() ) b.GameObject.Destroy();\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\t// \u5BA2\u6237\u7AEF\u9500\u6BC1 host \u62E5\u6709\u7684\u5BF9\u8C61\u53EF\u80FD\u88AB\u5F15\u64CE\u62D2\u7EDD\u2014\u2014\u4E0D\u80FD\u8BA9\u5B83\u4E2D\u65AD\u56DE\u83DC\u5355\u6D41\u7A0B\uFF08\u5B9E\u6D4B\u8FC7\u5361\u4F4F\u4E0D\u8FD4\u56DE\uFF09\n\t\t\t\tLog.Warning( $\u0022[menu] destroy ball failed: {e.Message}\u0022 );\n\t\t\t}\n\t\t}\n\t\t_balls.Clear();\n\t\t_cells.Clear();\n\t\t_lastPos.Clear();\n\t\t_teleportGrace.Clear();\n\t\t_steerDir.Clear();\n\t\t_sinceEject.Clear();\n\t\t_power?.Reset();\n\t\tClearMatchState();   // \u9759\u6001\u8BA1\u6570/\u5F15\u5BFC\u51B7\u5374/\u804C\u4E1A\u51B7\u5374\u4E00\u5E76\u6E05\uFF08v0.7.8.87\uFF09\n\t\t_teamSeq = 0;\n\t\tLocalBall = null;\n\n\t\t_gameStarted = false;\n\t\t_lobbyOpen = false;\n\t\t_lobbyWaiting = false;\n\t\t_lobbyHeartbeatArmed = false;\n\t\t_lobbyListDirty = false;\n\t\t_joinRequested = false;\n\t\t_networkReady = false;\n\t\t_joinCountdown = -1f;\n\t\t_matchRunning = false;\n\t\tMatchState.Reset();\n\n\t\t_net?.ResetSession();\n\t\t_lobby?.Hide();\n\t\t_browser?.Hide();\n\t\t_endBoard?.Hide();\n\t\tif ( _hud.IsValid() ) _hud.GameObject.Enabled = false;\n\t\t_menu?.Show();\n\t\tGameMusic.PlayMenu();\n\t\tGameLog.Info( \u0022[menu] back to menu\u0022 );\n\n\t\t// \u83DC\u5355\u80CC\u666F\u6F14\u793A\u8D5B\u91CD\u65B0\u5F00\u8D77\u6765\n\t\tif ( NetworkManager.IsAuthority \u0026\u0026 !Networking.IsActive )\n\t\t\tStartMenuDemo();\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Rendering/NeonCamera.cs","FileName":"NeonCamera.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\n\n/// \u003Csummary\u003E\n/// \u4FEF\u89C6\u8DDF\u968F\u76F8\u673A\uFF1A\u6B63\u4EA4\u6295\u5F71\u3001\u6307\u6570\u5E73\u6ED1\u8DDF\u968F\u672C\u5730\u7403\uFF1B\u89C6\u53E3\u9AD8\u5EA6\u968F\u8D28\u91CF\u589E\u5927\uFF08\u955C\u5934\u62C9\u8FDC\uFF09\u3002\n/// \u76F8\u673A\u671D\u5411\u56FA\u5B9A\u5782\u76F4\u5411\u4E0B\uFF0C\u9F20\u6807\u2192\u4E16\u754C\u65B9\u5411\u7684\u6620\u5C04\u89C1 BallInput\u3002\n/// \u003C/summary\u003E\npublic sealed class NeonCamera : Component\n{\n\tconst float FollowSpeed = 6f;\n\tconst float ZoomSpeed = 2.5f;\n\n\t/// \u003Csummary\u003E \u7531 CircleroyaleGame \u6CE8\u5165\uFF08\u907F\u514D\u6BCF\u5E27 GetComponent\uFF09 \u003C/summary\u003E\n\tpublic CameraComponent Cam { get; set; }\n\n\tBall _target;\n\tBall _spectate;      // \u6B7B\u4EA1\u89C2\u6218\u76EE\u6807\uFF08\u672C\u5730\u7403\u6D3B\u7740\u65F6\u6052\u4E3A null\uFF09\n\tVector3 _focus;\n\tfloat _shakeAmp;     // \u5C4F\u5E55\u9707\u52A8\u5E45\u5EA6\uFF08\u51FB\u6740/\u653E\u6280\u80FD\u89E6\u53D1\uFF0C\u6307\u6570\u8870\u51CF\uFF0Cv0.7.7.0\uFF09\n\tBloom _bloom;        // \u6CDB\u5149\u7EC4\u4EF6\uFF08cr_bloom convar \u5373\u65F6\u5F00\u5173\uFF0Cv0.7.8.15\uFF09\n\n\t/// \u003Csummary\u003E \u89E6\u53D1\u5C4F\u5E55\u8F7B\u5FAE\u9707\u52A8\uFF08\u51FB\u6740\u8FDE\u64AD/\u653E\u6280\u80FD\u7684\u9AD8\u5149\u53CD\u9988\uFF1B\u5E45\u5EA6\u6309\u4E8B\u4EF6\u5B9A\uFF0C\u81EA\u52A8\u6307\u6570\u8870\u51CF\uFF09 \u003C/summary\u003E\n\tpublic void Shake( float amplitude ) =\u003E _shakeAmp = MathF.Max( _shakeAmp, amplitude );\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\n\t\t// \u7ACB\u5373\u6446\u597D\u9ED8\u8BA4\u4FEF\u89C6\u59FF\u6001\uFF08\u7126\u70B9=\u573A\u5730\u4E2D\u5FC3\uFF09\u3002\u6CA1\u6709\u672C\u673A\u7403\u4E4B\u524D OnUpdate \u4F1A\u56E0\n\t\t// !_target.IsValid() \u76F4\u63A5 return\u2014\u2014\u76F8\u673A\u4FDD\u6301\u5F15\u64CE\u9ED8\u8BA4\u59FF\u6001\uFF08\u539F\u70B9\u6C34\u5E73\u770B\uFF09\uFF0C\n\t\t// \u6574\u4E2A\u573A\u5730\u5E73\u9762\u4FA7\u89C6\u584C\u6210\u4E00\u6761\u7EBF\uFF1A\u4E3B\u83DC\u5355\u80CC\u666F\u5B9E\u6D4B\u5C31\u662F\u8FD9\u6837\uFF08\u5BA2\u6237\u7AEF\u7ADE\u6001\u671F\u540C\u6837\u4E2D\u62DB\uFF09\n\t\tApplyPose();\n\t\tif ( Cam.IsValid() )\n\t\t{\n\t\t\tCam.OrthographicHeight = GameConfig.CameraBaseView;\n\t\t\t_bloom = Cam.GetComponent\u003CBloom\u003E();\n\t\t}\n\t}\n\n\tpublic void SetTarget( Ball ball )\n\t{\n\t\tif ( _target == ball ) return;   // \u540C\u4E00\u9897\u7403\uFF1A\u4E0D\u91CD\u590D SnapTo\uFF08\u626B\u63CF\u4F1A\u53CD\u590D\u65AD\u8A00\uFF09\n\t\t_target = ball;\n\t\tSnapTo();\n\t}\n\n\tpublic void SnapTo()\n\t{\n\t\tif ( !_target.IsValid() ) return;\n\t\t_focus = _target.WorldPosition;\n\t\t_focus.z = 0f;\n\t\tApplyPose();\n\t\tif ( Cam.IsValid() ) Cam.OrthographicHeight = ViewForMass( _target.Mass );\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\n\t\t// \u6CDB\u5149\u5F00\u5173\uFF08v0.7.8.15\uFF09\uFF1A\u63A7\u5236\u53F0 cr_bloom 0/1 \u5373\u65F6\u751F\u6548\uFF08\u5E42\u7B49\u8D4B\u503C\uFF0C\u65E0\u6296\u52A8\uFF09\uFF1B\n\t\t// \u70ED\u91CD\u8F7D\u6362\u7A0B\u5E8F\u96C6\u540E\u7F13\u5B58\u5931\u6548 \u2192 \u60F0\u6027\u91CD\u53D6\n\t\tif ( Cam.IsValid() )\n\t\t{\n\t\t\tif ( !_bloom.IsValid() ) _bloom = Cam.GetComponent\u003CBloom\u003E();\n\t\t\tif ( _bloom.IsValid() ) _bloom.Enabled = GameConfig.ConvarBloom;\n\t\t}\n\n\t\t// \u81EA\u6108\uFF1A\u70ED\u91CD\u8F7D\u4F1A\u628A\u573A\u666F\u7EC4\u4EF6\u6362\u6210\u65B0\u7A0B\u5E8F\u96C6\u5BF9\u8C61\uFF0C\u7F13\u5B58\u7684 _target \u53EF\u80FD\u53D8\u6210\u5931\u6548\u5F15\u7528\u2014\u2014\n\t\t// OnUpdate \u9996\u884C\u65E9\u9000\u4F1A\u8BA9\u76F8\u673A\u6C38\u4E45\u51BB\u7ED3\uFF08\u5B9E\u6D4B\uFF1A\u5BA2\u6237\u7AEF\u590D\u6D3B\u7740\u3001\u5206\u88C2\u7740\uFF0C\u955C\u5934\u50F5\u6B7B\u4E0D\u8DDF\uFF09\u3002\n\t\t// \u4ECE\u6E38\u620F\u7CFB\u7EDF\u91CD\u65B0\u8BA4\u9886\u672C\u673A\u7403\uFF1B\u8FDE\u672C\u673A\u7403\u90FD\u6CA1\u6709\u4E5F\u4FDD\u6301\u4FEF\u89C6\u59FF\u6001\uFF0C\u7EDD\u4E0D\u50F5\u6B7B\n\t\tif ( !_target.IsValid() )\n\t\t{\n\t\t\tvar local = CircleroyaleGame.Current?.LocalBall;\n\t\t\tif ( local.IsValid() )\n\t\t\t{\n\t\t\t\tSetTarget( local );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t// \u83DC\u5355\u80CC\u666F\u6F14\u793A\u8D5B\uFF08v0.6.3.0\uFF09\uFF1A\u6CA1\u6709\u672C\u673A\u7403\u65F6\u89C2\u6218\u5168\u573A\u6700\u5927\u8EAB\u4F53\u7684 bot\u3002\n\t\t\t\t// \u7C98\u6EDE\u540C\u6B7B\u4EA1\u89C2\u6218\uFF1A\u5F53\u524D\u5BF9\u8C61\u8FD8\u6D3B\u7740\u4E14\u6311\u6218\u8005\u6CA1\u9886\u5148 30% \u2192 \u4E0D\u6362\u955C\u5934\n\t\t\t\tvar g = CircleroyaleGame.Current;\n\t\t\t\tvar best = g?.TopMassBall();\n\n\t\t\t\tif ( _spectate.IsValid() \u0026\u0026 _spectate.Alive\n\t\t\t\t\t\u0026\u0026 best.IsValid() \u0026\u0026 best != _spectate \u0026\u0026 g is not null )\n\t\t\t\t{\n\t\t\t\t\tif ( g.TotalMassFor( best ) \u003C= g.TotalMassFor( _spectate ) * GameConfig.SpectateOvertakeRatio )\n\t\t\t\t\t\tbest = _spectate;\n\t\t\t\t}\n\n\t\t\t\tif ( best.IsValid() )\n\t\t\t\t{\n\t\t\t\t\t_spectate = best;\n\t\t\t\t\tvar tp = 1f - MathF.Exp( -FollowSpeed * Time.Delta );\n\t\t\t\t\tvar fp = g.FocusPositionOf( best, out var fm );\n\t\t\t\t\t_focus = Vector3.Lerp( _focus, fp, tp );\n\t\t\t\t\t_focus.z = 0f;\n\t\t\t\t\tif ( Cam.IsValid() )\n\t\t\t\t\t{\n\t\t\t\t\t\tCam.OrthographicHeight = MathX.Lerp( Cam.OrthographicHeight, ViewForMass( fm ),\n\t\t\t\t\t\t\t1f - MathF.Exp( -ZoomSpeed * Time.Delta ) );\n\t\t\t\t\t}\n\t\t\t\t\tApplyPose();\n\t\t\t\t\tUpdateTagPositions( g );   // \u59FF\u6001\u4E0E\u53D8\u7126\u90FD\u5199\u5B8C\uFF0C\u672C\u5E27\u6295\u5F71\u4E00\u6B21\u5373\u53EF\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tApplyPose();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// \u6B7B\u4EA1\u89C2\u6218\uFF08M4\uFF09\uFF1A\u672C\u5730\u7403\u9635\u4EA1\u540E\u955C\u5934\u8DDF\u4F4F\u5F53\u524D\u699C\u9996\uFF0C\u91CD\u751F\u77AC\u95F4\u5F39\u56DE\u81EA\u5DF1\n\t\tvar cur = SpectateTarget();\n\n\t\t// \u89C6\u89D2\u8DDF\u968F\u6700\u5927\u7684\u5206\u8EAB\uFF08v0.6.2.0 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u7126\u70B9\u4E0E\u53D8\u7126\u57FA\u51C6\u90FD\u53D6\n\t\t// \u0022\u4E3B\u7403 \u002B \u81EA\u5DF1\u5206\u8EAB\u0022\u4E2D\u8D28\u91CF\u6700\u5927\u7684\u8EAB\u4F53\n\t\tvar game = CircleroyaleGame.Current;\n\t\tVector3 focusPos = cur.WorldPosition;\n\t\tfloat followMass = cur.Mass;\n\t\tif ( game is not null )\n\t\t\tfocusPos = game.FocusPositionOf( cur, out followMass );\n\n\t\tvar t = 1f - MathF.Exp( -FollowSpeed * Time.Delta );\n\t\t_focus = Vector3.Lerp( _focus, focusPos, t );\n\t\t_focus.z = 0f;\n\n\t\t// \u53D8\u7126\u5148\u5199\uFF08\u6295\u5F71\u7528\u7684 OrthoHeight \u5FC5\u987B\u662F\u672C\u5E27\u6700\u7EC8\u503C\uFF09\uFF0C\u518D\u6446\u4F4D\u3001\u518D\u6295\u5F71\u2014\u2014\u6BCF\u5E27\u4E00\u6B21\uFF0C\n\t\t// \u4E0D\u91CD\u590D\u8C03\uFF08ApplyPose \u6BCF\u591A\u8C03\u4E00\u6B21 shake \u8870\u51CF\u5C31\u5FEB\u4E00\u500D\uFF0C\u540D\u724C\u91CD\u590D\u6295\u5F71\u4E5F\u662F\u767D\u505A\uFF09\n\t\tif ( Cam.IsValid() )\n\t\t{\n\t\t\tvar tz = 1f - MathF.Exp( -ZoomSpeed * Time.Delta );\n\t\t\tCam.OrthographicHeight = MathX.Lerp( Cam.OrthographicHeight, ViewWithCells( cur, followMass ), tz );\n\t\t}\n\n\t\tApplyPose();\n\n\t\tif ( Cam.IsValid() )\n\t\t\tUpdateTagPositions( game );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u6B7B\u4EA1\u89C2\u6218\u76EE\u6807\uFF1A\u56E2\u961F\u8D5B\u4F18\u5148\u8DDF**\u6700\u5927\u7684\u5B58\u6D3B\u961F\u53CB**\uFF08v0.7.8.28 \u7528\u6237\u5B9A\u7A3F\u2014\u2014\u0022\u6211\u6B7B\u4E86\u961F\u4F0D\u8FD8\u5728\u6253\u0022\uFF0C\n\t/// \u8DDF\u7740\u5927\u54E5\u770B\u6218\u5C40\uFF1B\u961F\u53CB\u5168\u706D\u624D\u56DE\u843D\u5168\u573A\u699C\u9996\uFF09\u3002\u5E26\u7C98\u6EDE\uFF08v0.6.3.1\uFF09\u2014\u2014\u51E0\u4E2A\u76EE\u6807\u4EA4\u66FF\u9886\u5148\u65F6\n\t/// \u4E0D\u518D\u6BCF\u6B21\u53CD\u8D85\u90FD\u5207\u955C\u5934\uFF0C\u6311\u6218\u8005\u987B\u9886\u5148 SpectateOvertakeRatio \u624D\u63A5\u7BA1\u3002\n\t/// \u003C/summary\u003E\n\tBall SpectateTarget()\n\t{\n\t\tif ( _target.Alive )\n\t\t{\n\t\t\tif ( _spectate.IsValid() )\n\t\t\t{\n\t\t\t\t_spectate = null;\n\t\t\t\tSpectateName = null;\n\t\t\t\tSnapTo();   // \u91CD\u751F\uFF1A\u955C\u5934\u77AC\u79FB\u56DE\u81EA\u5DF1\uFF08\u4E0D\u4ECE\u699C\u9996\u4F4D\u7F6E\u957F\u8DDD\u79BB\u6ED1\u8FC7\u6765\uFF09\n\t\t\t}\n\t\t\treturn _target;\n\t\t}\n\n\t\tvar game = CircleroyaleGame.Current;\n\t\tBall best = null;\n\n\t\t// \u56E2\u961F\u8D5B\u961F\u53CB\u4F18\u5148\uFF08\u6700\u5927\u5B58\u6D3B\u961F\u53CB\uFF09\uFF1B\u6CA1\u6709\u624D\u770B\u5168\u573A\u699C\u9996\n\t\tif ( MatchState.IsTeam \u0026\u0026 _target.TeamIndex \u003E= 0 \u0026\u0026 game is not null )\n\t\t{\n\t\t\tfloat bestMass = -1f;\n\t\t\tforeach ( var b in game.Balls )\n\t\t\t{\n\t\t\t\tif ( !b.IsValid() || !b.Alive || b == _target ) continue;\n\t\t\t\tif ( b.TeamIndex != _target.TeamIndex ) continue;\n\t\t\t\tvar m = game.TotalMassFor( b );\n\t\t\t\tif ( m \u003E bestMass ) { bestMass = m; best = b; }\n\t\t\t}\n\t\t}\n\t\tif ( !best.IsValid() )\n\t\t\tbest = game?.TopMassBall();\n\n\t\t// \u7C98\u6EDE\uFF1A\u5F53\u524D\u89C2\u6218\u5BF9\u8C61\u8FD8\u6D3B\u7740\uFF0C\u4E14\u6311\u6218\u8005\u6CA1\u9886\u5148\u5230\u9608\u503C \u2192 \u4E0D\u6362\u955C\u5934\n\t\tif ( _spectate.IsValid() \u0026\u0026 _spectate.Alive\n\t\t\t\u0026\u0026 best.IsValid() \u0026\u0026 best != _spectate \u0026\u0026 game is not null )\n\t\t{\n\t\t\tif ( game.TotalMassFor( best ) \u003C= game.TotalMassFor( _spectate ) * GameConfig.SpectateOvertakeRatio )\n\t\t\t\treturn _spectate;\n\t\t}\n\n\t\t_spectate = best.IsValid() ? best : _target;\n\t\tSpectateName = _spectate.IsValid() \u0026\u0026 _spectate != _target ? _spectate.PlayerName : null;\n\t\treturn _spectate;\n\t}\n\n\t/// \u003Csummary\u003E \u5F53\u524D\u6B7B\u4EA1\u89C2\u6218\u76EE\u6807\u540D\uFF08\u672C\u673A\u6D3B\u7740=null\uFF09\uFF0CHUD \u6B7B\u4EA1\u9762\u677F\u63D0\u793A\u7528 \u003C/summary\u003E\n\tpublic string SpectateName { get; private set; }\n\n\t/// \u003Csummary\u003E\n\t/// \u89C6\u53E3\u9AD8\u5EA6\uFF1A\u6309\u0022\u6700\u5927\u8EAB\u4F53\u0022\u8D28\u91CF\u53D6\u57FA\u51C6\uFF1B\u6709\u81EA\u5BB6\u5206\u8EAB\u65F6\u62C9\u8FDC\u5230\u80FD\u76D6\u4F4F\u6700\u8FDC\u7684\u90A3\u4E2A\n\t/// \uFF08\u5206\u88C2\u540E\u7EC6\u80DE\u51B2\u51FA\u5C4F\u5E55\u5916\u5C31\u770B\u4E0D\u89C1\u4E86\uFF0C\u53D6\u666F\u8981\u628A\u81EA\u5BB6\u5206\u8EAB\u5305\u8FDB\u6765\uFF09\u3002\n\t/// \u5BA2\u6237\u7AEF\u5339\u914D\u7528 OwnerSteamId\uFF1A\u5206\u8EAB\u4E0E\u4E3B\u7403\u5E26\u540C\u4E00\u4E2A\u540C\u6B65\u503C\uFF0C\u53CC\u5F00\u5408\u6210 ID \u4E5F\u80FD\u5BF9\u4E0A\u3002\n\t/// \u003C/summary\u003E\n\tfloat ViewWithCells( Ball cur, float baseMass )\n\t{\n\t\tvar view = ViewForMass( baseMass );\n\n\t\tvar game = CircleroyaleGame.Current;\n\t\tif ( game is null ) return view;\n\n\t\tvar sid = cur.OwnerSteamId;\n\t\tfloat maxDist = 0f;\n\t\tforeach ( var c in game.Cells )\n\t\t{\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece || c.OwnerSteamId != sid ) continue;\n\t\t\tmaxDist = MathF.Max( maxDist, cur.WorldPosition.Distance( c.DrawPos ) );\n\t\t}\n\n\t\tif ( maxDist \u003E 0f )\n\t\t\tview = Math.Clamp( ( cur.Radius \u002B maxDist ) * 2.6f, view, GameConfig.CameraViewMax );\n\n\t\treturn view;\n\t}\n\n\tvoid ApplyPose()\n\t{\n\t\t// \u5C4F\u5E55\u9707\u52A8\uFF08v0.7.7.0\uFF09\uFF1A\u6BCF\u6B21\u6446\u4F4D\u53E0\u52A0\u4E00\u4E2A\u968F\u673A\u65B9\u5411\u504F\u79FB\uFF0C\u5E45\u5EA6\u6307\u6570\u8870\u51CF\u5230\u96F6\u2014\u2014\n\t\t// \u6446\u4F4D\u672C\u8EAB\u9010\u5E27\u91CD\u5199\uFF08\u4E0D\u542B\u6B8B\u91CF\uFF09\uFF0C\u9707\u5B8C\u81EA\u52A8\u5F52\u4F4D\uFF1B\u540D\u724C\u6295\u5F71\u7528\u540C\u4E00\u76F8\u673A\u4F4D\u7F6E\uFF0C\u6296\u52A8\u540C\u6B65\u4E00\u81F4\n\t\tvar shake = Vector3.Zero;\n\t\tif ( _shakeAmp \u003E 0.1f )\n\t\t{\n\t\t\tvar a = Game.Random.Float( 0f, MathF.PI * 2f );\n\t\t\tshake = new Vector3( MathF.Cos( a ), MathF.Sin( a ), 0f )\n\t\t\t\t* _shakeAmp * Game.Random.Float( 0.4f, 1f );\n\t\t\t_shakeAmp *= MathF.Exp( -6f * Time.Delta );\n\t\t}\n\n\t\tGameObject.WorldPosition = _focus \u002B Vector3.Up * GameConfig.CameraHeight \u002B shake;\n\t\tGameObject.WorldRotation = Rotation.LookAt( Vector3.Down, Vector3.Forward );\n\t}\n\n\t/// \u003Csummary\u003E \u76F8\u673A\u59FF\u6001\u521A\u5199\u5B8C\uFF0C\u7ACB\u523B\u8BA9 HUD \u7528\u540C\u4E00\u4EFD\u76F8\u673A\u6570\u636E\u91CD\u6295\u540D\u724C\uFF08\u96F6\u5E27\u95F4\u5DEE\uFF09 \u003C/summary\u003E\n\tvoid UpdateTagPositions( CircleroyaleGame game )\n\t{\n\t\tif ( game is null ) return;\n\t\tvar ortho = Cam.IsValid() ? Cam.OrthographicHeight : 0f;\n\t\tgame.Hud?.UpdateTagPositions( game, GameObject.WorldPosition, ortho );\n\t}\n\n\tstatic float ViewForMass( float mass )\n\t{\n\t\treturn Math.Clamp( GameConfig.CameraBaseView * MathF.Sqrt( mass / GameConfig.StartMass ),\n\t\t\tGameConfig.CameraViewMin, GameConfig.CameraViewMax );\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Rendering/NeonRenderer.cs","FileName":"NeonRenderer.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\n\n/// \u003Csummary\u003E\n/// \u50CF\u7D20\u6279\u6E32\u67D3\uFF08v0.7.8.36 \u6574\u4F53\u66FF\u6362\u9713\u8679\u7EBF\u6846\u98CE\uFF09\uFF1A\u6BCF\u5E27\u91CD\u5199\u5404\u6279\u6B21\u9876\u70B9\uFF0C\u5168\u573A\u5B9E\u4F53\u7EA6 7 \u4E2A DrawCall\u3002\n/// \u8D34\u56FE\u6279\u6B21\u8D70 line.shader \u7684 UV \u91C7\u6837 \u002B \u9876\u70B9\u8272 tint\uFF08D_BLEND=0 \u666E\u901A alpha\uFF09\uFF1A\n/// \u7403/\u5206\u8EAB/\u5B62\u5B50 = ball_body.png \u767D\u6A21 tint\uFF1B\u98DF\u7269 = food.png \u516D\u8272\u6761\u5E26\uFF1B\u91CE\u523A/\u5C16\u523A\u5206\u8EAB = spike.png\uFF1B\n/// \u523A\u7206\u5F39\u5E55 = spine.png\uFF08\u6309\u901F\u5EA6\u65CB\u8F6C\uFF09\uFF1B\u9053\u5177 = powerups.png \u56FE\u6807\u53E0\u7A0B\u5E8F\u5316\u83F1\u5F62\u5E95\u5EA7\u3002\n/// \u8868\u60C5/\u5934\u50CF\uFF08v0.7.8.40 \u4ECE SpriteRenderer \u8FC1\u5165\u2014\u2014sprite \u901A\u9053\u753B\u5728 OverlayWithDepth \u6279\u6B21\u4E4B\u524D\uFF0C\n/// \u4F1A\u88AB\u5B9E\u5FC3\u8EAB\u4F53 quad \u76D6\u4F4F\uFF09\uFF1Abot = face_0..5 \u6309 Mood \u9009\u6279\uFF0C\u771F\u4EBA = SetAvatar \u767B\u8BB0\u7684\u6BCF SteamId \u8D34\u56FE\u6279\u6B21\u3002\n/// \u7EBF/\u4E09\u89D2\u6279\u6B21\uFF08\u666E\u901A alpha\uFF09\uFF1A\u692D\u5706\u6295\u5F71\u3001CPU \u7C92\u5B50\u65B9\u5757\u3001\u7784\u51C6/\u8FB9\u7F18\u7BAD\u5934\u3002\n/// \u7279\u6548\u5165\u53E3\uFF08Pop/Spark/Puff/Swallow\uFF09\u7B7E\u540D\u4E0D\u53D8\uFF0C\u8C03\u7528\u65B9\u96F6\u6539\u52A8\u3002\n/// \u003C/summary\u003E\npublic sealed class NeonRenderer : Component\n{\n\tSceneDynamicObject _shapeLines;   // \u7EBF\u6BB5\uFF08\u666E\u901A alpha\uFF09\uFF1A\u9053\u5177\u5E95\u5EA7\u63CF\u8FB9/\u8FB9\u7F18\u7BAD\u5934\u5C3E\u6746\n\tSceneDynamicObject _shapeTris;    // \u4E09\u89D2\u5F62\uFF08\u666E\u901A alpha\uFF09\uFF1A\u6295\u5F71/\u7C92\u5B50/\u7784\u51C6\u7BAD\u5934/\u83F1\u5F62\u586B\u5145\n\tSceneDynamicObject _bodies;       // ball_body.png\uFF1A\u7403/\u5206\u8EAB/\u5B62\u5B50\u8EAB\u4F53\uFF08\u9876\u70B9\u8272 tint\uFF09\n\tSceneDynamicObject _food;         // food.png\uFF1A\u7CD6\u8C46\uFF08UV \u53D6\u8272\u683C\uFF09\n\tSceneDynamicObject _spikes;       // spike.png\uFF1A\u91CE\u523A \u002B \u5C16\u523A\u5206\u8EAB\n\tSceneDynamicObject _spines;       // spine.png\uFF1A\u523A\u7206\u5F39\u5E55\uFF08\u65CB\u8F6C\uFF09\n\tSceneDynamicObject _icons;        // powerups.png\uFF1A\u9053\u5177\u56FE\u6807\uFF08UV \u53D6\u683C\uFF09\n\tSceneDynamicObject _rings;        // shield_ring.png\uFF1A\u62A4\u76FE buff \u865A\u7EBF\u73AF\uFF08\u767D\u6A21 tint\uFF0C\u7ED5\u7403\u6162\u8F6C\uFF0Cv0.7.8.87\uFF09\n\tSceneDynamicObject[] _faces;      // face_0..5\uFF1Abot \u8868\u60C5\uFF08Mood \u9009\u6279\uFF0Cv0.7.8.40\uFF09\n\n\t/// \u003Csummary\u003E ball_body.png \u91CC\u7403\u4F53\u5185\u5BB9\u5360\u753B\u5E03\u6BD4\u4F8B\uFF08process.py fit margin 0.06\uFF09\u2014\u2014\u8D34\u56FE\u76F4\u5F84\u2192\u4E16\u754C\u76F4\u5F84\u6362\u7B97\u7528 \u003C/summary\u003E\n\tconst float BodyContentFraction = 0.9375f;\n\n\t/// \u003Csummary\u003E face_N.png \u91CC\u8138\u90E8\u5185\u5BB9\u5360\u753B\u5E03\u6BD4\u4F8B\u2014\u2014\u5934\u50CF\u76F4\u5F84\u2192\u4E16\u754C\u76F4\u5F84\u6362\u7B97\u7528 \u003C/summary\u003E\n\tconst float FaceContentFraction = 0.9f;\n\n\t/// \u003Csummary\u003E\n\t/// \u26A0\uFE0F powerups.png \u56FE\u96C6\u683C\u5B50\u987A\u5E8F\u662F\u7F8E\u672F\u5B9A\u7684\uFF1Aspeed/magnet/**shield/mass**/spike\uFF0C\n\t/// \u4E0E Kind \u679A\u4E3E\uFF08\u2026Mass=2, Shield=3\u2026\uFF09\u4E0D\u540C\u5E8F\u2014\u2014\u76F4\u63A5\u62FF Kind \u5F53\u683C\u5B50\u53F7\u4F1A\u628A\u76FE\u548C\u8D28\u91CF\u7F50\n\t/// \u753B\u53CD\uFF08v0.7.8.92 \u7528\u6237\u5B9E\u6D4B\u0022\u770B\u5230\u7684\u548C\u6361\u5230\u7684\u4E0D\u4E00\u6837\u0022\uFF09\u3002\u4E0B\u6807=Kind\uFF0C\u503C=\u56FE\u96C6\u683C\u5B50\u53F7\u3002\n\t/// \u003C/summary\u003E\n\tstatic readonly int[] PowerKindToCell = { 0, 1, 3, 2, 4 };\n\n\tsealed class AvatarBatch\n\t{\n\t\tpublic SceneDynamicObject Dyno;\n\t\tpublic Texture Tex;\n\t\tpublic Material Mat;   // \u5B58\u6863\u7528\uFF08\u8BCA\u65AD\u8BFB\uFF09\n\t}\n\n\t/// \u003Csummary\u003E \u771F\u4EBA Steam \u5934\u50CF\u6279\u6B21\uFF08\u6BCF SteamId \u4E00\u4E2A\u72EC\u7ACB\u8D34\u56FE\u6279\u6B21\uFF09 \u003C/summary\u003E\n\treadonly Dictionary\u003Clong, AvatarBatch\u003E _avatars = new();\n\n\t/// \u003Csummary\u003E \u5934\u50CF\u767B\u8BB0\u8868\uFF08Ball.SetAvatar \u5199\uFF0COnUpdate \u6D88\u8D39\uFF1B\u6E32\u67D3\u5668\u672A\u5C31\u7EEA/\u70ED\u91CD\u8F7D\u671F\u95F4\u5728\u6B64\u5F85\u547D\uFF09 \u003C/summary\u003E\n\tstatic readonly Dictionary\u003Clong, Texture\u003E _pendingAvatars = new();\n\n\t/// \u003Csummary\u003E \u5168\u5C40\u5F15\u7528\uFF08\u7279\u6548\u751F\u6210\u5165\u53E3 NeonRenderer.Pop/Spark/Puff \u8D70\u9759\u6001\u8C03\u7528\uFF09 \u003C/summary\u003E\n\tpublic static NeonRenderer Instance { get; private set; }\n\n\t/// \u003Csummary\u003E \u5E27\u7387\u8868\uFF08v0.7.8.104\uFF0Ccr_perf \u6E32\u67D3 A/B \u6D4B\u91CF\u7528\uFF09\uFF1A1 \u79D2\u7A97\u53E3\u5E73\u5747 \u003C/summary\u003E\n\tpublic static float DebugFps { get; private set; }\n\tstatic int _fpsFrames;\n\tstatic TimeSince _fpsWindow = 999f;\n\n\t/// \u003Csummary\u003E \u8C03\u8BD5 A/B\uFF08cr_perf \u6E32\u67D3\u51CF\u8D1F\u5B9A\u4F4D\uFF09\uFF1A\u6574\u4F53\u9690\u85CF/\u6062\u590D\u672C\u6E32\u67D3\u5668\u5168\u90E8\u6279\u6B21 \u003C/summary\u003E\n\tpublic void SetDebugVisible( bool visible )\n\t{\n\t\tforeach ( var d in new[] { _shapeLines, _shapeTris, _bodies, _food, _spikes, _spines, _icons, _rings } )\n\t\t\tif ( d is not null ) d.RenderingEnabled = visible;\n\t\tif ( _faces is not null )\n\t\t\tforeach ( var f in _faces )\n\t\t\t\tif ( f is not null ) f.RenderingEnabled = visible;\n\t\tforeach ( var a in _avatars.Values )\n\t\t\tif ( a.Dyno is not null ) a.Dyno.RenderingEnabled = visible;\n\t}\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\n\t\tInstance = this;\n\t\t_shapeLines = CreateShapeBatch();\n\t\t_shapeTris = CreateShapeBatch();\n\t\t_bodies = CreateTexBatch( \u0022ui/pixel/ball_body.png\u0022 );\n\t\t_food = CreateTexBatch( \u0022ui/pixel/food.png\u0022 );\n\t\t_spikes = CreateTexBatch( \u0022ui/pixel/spike.png\u0022 );\n\t\t_spines = CreateTexBatch( \u0022ui/pixel/spine.png\u0022 );\n\t\t_icons = CreateTexBatch( \u0022ui/pixel/powerups.png\u0022 );\n\t\t_rings = CreateTexBatch( \u0022ui/pixel/shield_ring.png\u0022 );\n\t\t_faces = new SceneDynamicObject[6];\n\t\tfor ( int i = 0; i \u003C 6; i\u002B\u002B )\n\t\t\t_faces[i] = CreateTexBatch( $\u0022ui/pixel/face_{i}.png\u0022, topmost: true );   // 0\u5F00\u5FC3 1\u5174\u594B 2\u60CA\u6050 3\u7729\u6655 4\u778C\u7761 5\u5F97\u610F\n\t}\n\n\tprotected override void OnDestroy()\n\t{\n\t\tbase.OnDestroy();\n\n\t\t// \u26A0\uFE0F \u4E16\u754C\u91CD\u5EFA\u53EA\u9500\u6BC1\u5B50 GameObject\uFF0C\u4E0D\u4F1A\u9500\u6BC1\u7EC4\u4EF6\u521B\u5EFA\u7684\u539F\u751F\u52A8\u6001\u6279\u6B21\u2014\u2014\u9057\u7559\u6279\u6B21\u4F1A\u5E26\u7740\n\t\t// \u6700\u540E\u4E00\u5E27\u51E0\u4F55\u7EE7\u7EED\u6E32\u67D3\uFF0C\u4E14\u65E7\u6750\u8D28\u88AB\u56DE\u6536\u540E\u5F15\u64CE\u56DE\u9000\u9519\u8BEF\u6750\u8D28\uFF1A\u6EE1\u5C4F\u7EA2\u51E0\u4F55 \u002B \u65B0\u7BA1\u7EBF\u521B\u5EFA\u5D29\u6E83\n\t\t// \uFF08\u5B9E\u6D4B\u7EA2\u7EBF/\u7EA2\u5757 \u002B rendersystemvulkan \u56FA\u5B9A\u504F\u79FB 0x471a0\uFF09\u3002\u7EC4\u4EF6\u9500\u6BC1\u65F6\u5FC5\u987B\u663E\u5F0F Delete\u3002\n\t\tDeleteBatch( _shapeLines );\n\t\tDeleteBatch( _shapeTris );\n\t\tDeleteBatch( _bodies );\n\t\tDeleteBatch( _food );\n\t\tDeleteBatch( _spikes );\n\t\tDeleteBatch( _spines );\n\t\tDeleteBatch( _icons );\n\t\tDeleteBatch( _rings );\n\t\tif ( _faces is not null )\n\t\t\tforeach ( var f in _faces ) DeleteBatch( f );\n\t\tforeach ( var av in _avatars.Values ) DeleteBatch( av.Dyno );\n\t\tforeach ( var sp in _avatarSpare ) DeleteBatch( sp.Dyno );\n\n\t\tif ( Instance == this ) Instance = null;\n\t}\n\n\tstatic void DeleteBatch( SceneDynamicObject dyno )\n\t{\n\t\tif ( dyno is null ) return;\n\t\ttry { dyno.Delete(); } catch { }\n\t}\n\n\t/// \u003Csummary\u003E \u6750\u8D28\u5F3A\u5F15\u7528\u6C60\uFF1A\u6750\u8D28\u82E5\u53EA\u88AB\u539F\u751F\u5BF9\u8C61\u5F15\u7528\uFF0C\u6258\u7BA1\u5305\u88C5\u4E00\u65E6\u88AB GC\uFF0C\u539F\u751F\u6750\u8D28\u968F\u4E4B\u56DE\u6536\uFF0C\n\t/// \u573A\u666F\u5BF9\u8C61\u5C31\u753B\u6210\u5F15\u64CE\u9519\u8BEF\u6750\u8D28\uFF08\u7EA2\u51E0\u4F55 = \u5B9E\u6D4B\u7684\u0022\u4F1A\u8BDD\u5185\u8DD1\u51E0\u5206\u949F\u53D8\u7EA2\u0022\u4E3B\u56E0\u4E4B\u4E00\uFF09\u3002\u521B\u5EFA\u7684\u6750\u8D28\u5168\u90E8\u7559\u6863\u3002 \u003C/summary\u003E\n\treadonly List\u003CMaterial\u003E _mats = new();\n\n\tMaterial TrackMat( Material m )\n\t{\n\t\tif ( m is not null ) _mats.Add( m );\n\t\treturn m;\n\t}\n\n\tSceneDynamicObject CreateShapeBatch( bool topmost = false )\n\t{\n\t\tvar dyno = new SceneDynamicObject( Scene.SceneWorld );\n\t\tdyno.Material = TrackMat( LineMaterial.Create() );\n\t\tdyno.Flags.CastShadows = false;\n\t\t// \u5934\u50CF/\u8868\u60C5\u6279\u6B21\u653E OverlayWithoutDepth\uFF08\u6BD4 OverlayWithDepth \u665A\u4E00\u6BB5\u7ED8\u5236\uFF0C\u65E0\u6DF1\u5EA6\u6D4B\u8BD5\uFF09\uFF1A\n\t\t// OverlayWithDepth \u5185\u90E8\u5BF9\u5404 dyno \u7684\u6392\u5E8F\u4E0D\u53D7\u9876\u70B9 z \u63A7\u5236\uFF08\u5B9E\u6D4B\u8868\u60C5\u88AB z=1.0 \u7684\u8EAB\u4F53\u76D6\u4F4F\uFF09\n\t\tdyno.RenderLayer = topmost ? SceneRenderLayer.OverlayWithoutDepth : SceneRenderLayer.OverlayWithDepth;\n\t\tdyno.Attributes.SetCombo( \u0022D_BLEND\u0022, 0 );   // \u666E\u901A alpha \u6DF7\u5408\uFF08\u50CF\u7D20\u98CE\u65E0\u52A0\u8272\u53D1\u5149\uFF09\n\t\treturn dyno;\n\t}\n\n\tsealed class TexBatch\n\t{\n\t\tpublic SceneDynamicObject Dyno;\n\t\tpublic string Path;\n\t\tpublic Texture Tex;\n\t\tpublic Material Mat;   // \u5B58\u6863\u7528\uFF08Dyno.Material \u53EA\u5199\u4E0D\u53EF\u8BFB\uFF09\uFF0CDumpTexState \u8BCA\u65AD\u8BFB\n\t}\n\n\t/// \u003Csummary\u003E \u8D34\u56FE\u6279\u6B21\u767B\u8BB0\uFF08\u8DEF\u5F84\u7559\u5E95\u2014\u2014\u70ED\u91CD\u8F7D\u91CD\u5EFA\u4E16\u754C\u65F6 Texture.Load \u53EF\u80FD\u77AC\u65F6\u5931\u8D25\uFF0C\n\t/// OnUpdate \u91CC 1s \u8282\u6D41\u8865\u8F7D\u81EA\u6108\uFF0C\u5426\u5219\u6574\u573A\u6E32\u67D3\u6210\u9519\u8BEF\u7EA2\u7EB9\u7406\uFF0C\u5B9E\u6D4B\uFF09 \u003C/summary\u003E\n\treadonly List\u003CTexBatch\u003E _texBatches = new();\n\n\tTimeSince _sinceTexHeal = 1f;\n\n\tSceneDynamicObject CreateTexBatch( string texturePath, bool topmost = false )\n\t{\n\t\tvar dyno = CreateShapeBatch( topmost );\n\n\t\tvar batch = new TexBatch { Dyno = dyno, Path = texturePath };\n\t\t_texBatches.Add( batch );\n\t\tLoadTex( batch );\n\n\t\treturn dyno;\n\t}\n\n\tvoid LoadTex( TexBatch b )\n\t{\n\t\tb.Tex = Texture.Load( b.Path, false );\n\t\tif ( b.Tex is null )\n\t\t\tLog.Warning( $\u0022[pixel] texture not ready: {b.Path} (1s \u540E\u81EA\u52A8\u91CD\u8BD5)\u0022 );\n\n\t\tb.Mat = TrackMat( MakeTexMaterial( b.Tex, pixel: true ) );   // SceneDynamicObject.Material \u53EA\u5199\uFF0C\u5148\u5907\u597D\u6750\u8D28\u518D\u8D4B\u503C\n\t\tb.Dyno.Material = b.Mat;\n\t}\n\n\t/// \u003Csummary\u003E \u8BCA\u65AD\uFF08\u4E34\u65F6\uFF0C\u7EA2\u5C4F\u6392\u67E5\u7528\uFF09\uFF1A\u5404\u8D34\u56FE\u6279\u6B21\u73B0\u5B58\u8D34\u56FE vs \u73B0\u573A\u91CD\u8F7D vs \u6750\u8D28\u7ED1\u5B9A\u5B9E\u51B5\u3002\n\t/// \u6392\u67E5\u5B8C\u53EF\u968F cr_tex \u5DE5\u5177\u4E00\u8D77\u5220\u3002 \u003C/summary\u003E\n\tpublic static string DumpTexState()\n\t{\n\t\tvar inst = Instance;\n\t\tif ( inst is null || !inst.IsValid() ) return \u0022no NeonRenderer instance\u0022;\n\n\t\tvar sb = new System.Text.StringBuilder();\n\t\tsb.AppendLine( $\u0022batches={inst._texBatches.Count} avatars={inst._avatars.Count} spare={inst._avatarSpare.Count}\u0022 );\n\n\t\tstatic string Tx( Texture t ) =\u003E t is null\n\t\t\t? \u0022NULL\u0022\n\t\t\t: $\u0022err={t.IsError} loaded={t.IsLoaded} {t.Width}x{t.Height} mips={t.Mips} fmt={t.ImageFormat}\u0022;\n\n\t\tforeach ( var b in inst._texBatches )\n\t\t{\n\t\t\tvar fresh = Texture.Load( b.Path, false );\n\t\t\tvar mat = b.Mat;\n\t\t\tvar matInfo = mat is null\n\t\t\t\t? \u0022mat=NULL\u0022\n\t\t\t\t: $\u0022mat={mat.Name} matValid={mat.IsValid()} shValid={( mat.Shader is null ? \u0022?\u0022 : mat.Shader.IsValid().ToString() )}\u0022;\n\t\t\tsb.AppendLine( $\u0022{b.Path}: now[{Tx( b.Tex )}] fresh[{Tx( fresh )}] {matInfo}\u0022 );\n\t\t}\n\n\t\tint n = 0;\n\t\tforeach ( var kv in inst._avatars )\n\t\t{\n\t\t\tsb.AppendLine( $\u0022avatar {kv.Key}: now[{Tx( kv.Value.Tex )}]\u0022 );\n\t\t\tif ( \u002B\u002Bn \u003E= 8 ) { sb.AppendLine( $\u0022\u2026 \u002B{inst._avatars.Count - n} more\u0022 ); break; }\n\t\t}\n\n\t\t// \u73B0\u573A\u63A2\u9488\uFF1A\u6B64\u523B\u65B0\u5EFA\u4E00\u4E2A line.shader \u6750\u8D28\uFF0C\u62A5\u544A\u6750\u8D28/\u7740\u8272\u5668/\u8D34\u56FE\u7ED1\u5B9A\u662F\u5426\u53EF\u7528\n\t\t// \uFF08\u7EA2\u5C4F = \u6240\u6709\u6279\u6B21\u56DE\u9000\u9519\u8BEF\u6750\u8D28\u65F6\uFF0C\u8FD9\u91CC\u80FD\u76F4\u63A5\u6307\u51FA\u662F\u6750\u8D28\u521B\u5EFA\u5931\u8D25\u8FD8\u662F\u7ED1\u5B9A\u5931\u8D25\uFF09\n\t\tvar probe = Material.Create( \u0022cr_probe\u0022, \u0022shaders/line.shader\u0022 );\n\t\tif ( probe is null )\n\t\t\tsb.AppendLine( \u0022probe: Material.Create -\u003E NULL\u0022 );\n\t\telse\n\t\t{\n\t\t\tvar sh = probe.Shader;\n\t\t\tsb.AppendLine( $\u0022probe: valid={probe.IsValid()} shaderName=\u0027{probe.ShaderName}\u0027 shader={( sh is null ? \u0022NULL\u0022 : sh.IsValid().ToString() )} first={( probe.FirstTexture is null ? \u0022NULL\u0022 : \u0022tex\u0022 )}\u0022 );\n\t\t\tvar probeTex = Texture.Load( \u0022ui/pixel/food.png\u0022, false );\n\t\t\tbool setOk = false;\n\t\t\ttry { setOk = probe.Set( \u0022g_tColor\u0022, probeTex ); } catch ( System.Exception e ) { sb.AppendLine( $\u0022probe set threw: {e.GetType().Name}\u0022 ); }\n\t\t\tsb.AppendLine( $\u0022probe set g_tColor -\u003E {setOk}\u0022 );\n\t\t}\n\t\treturn sb.ToString();\n\t}\n\n\t// \u2014\u2014 \u4E34\u65F6\u8BCA\u65AD\uFF08\u7EA2\u5C4F\u6392\u67E5\uFF09\uFF1A\u6BCF 30s \u81EA\u68C0\u4E00\u6B21\u0022\u6750\u8D28\u94FE\u0022\u5065\u5EB7\uFF08\u6750\u8D28/\u7740\u8272\u5668\u6709\u6548 \u002B \u8D34\u56FE\u53EF\u7ED1\u5B9A \u002B\n\t//    \u7EB9\u7406\u975E\u9519\u8BEF\u7EB9\u7406\uFF09\uFF0C\u53EA\u5728\u72B6\u6001\u7FFB\u8F6C\u65F6\u6253\u65E5\u5FD7\u2014\u2014\u6545\u969C\u53D1\u751F\u7684\u65F6\u523B\u4F1A\u81EA\u5DF1\u5199\u8FDB\u65E5\u5FD7\u6587\u4EF6\u3002\u6392\u67E5\u5B8C\u5220\u9664\u3002\u2014\u2014\n\tstatic Material _probeMat;\n\tstatic int _probeState = -1;   // -1 \u672A\u521D\u59CB\u5316\uFF1B0 OK\uFF1B1 FAIL\n\tTimeSince _sinceProbe = 30f;\n\n\tvoid ProbeMaterialHealth()\n\t{\n\t\tif ( _sinceProbe \u003C 30f ) return;\n\t\t_sinceProbe = 0;\n\n\t\tif ( _probeMat is null )\n\t\t\t_probeMat = Material.Create( \u0022cr_probe\u0022, \u0022shaders/line.shader\u0022 );\n\n\t\tvar tex = Texture.Load( \u0022ui/pixel/food.png\u0022, false );\n\t\tbool matOk = _probeMat is not null \u0026\u0026 _probeMat.IsValid();\n\t\tbool shOk = matOk \u0026\u0026 _probeMat.Shader is not null \u0026\u0026 _probeMat.Shader.IsValid();\n\t\tbool setOk = false;\n\t\tif ( matOk ) { try { setOk = _probeMat.Set( \u0022g_tColor\u0022, tex ); } catch { } }\n\t\tbool texOk = tex is not null \u0026\u0026 !tex.IsError;\n\t\tbool ok = matOk \u0026\u0026 shOk \u0026\u0026 setOk \u0026\u0026 texOk;\n\n\t\tint state = ok ? 0 : 1;\n\t\tif ( state != _probeState )\n\t\t{\n\t\t\t_probeState = state;\n\t\t\tGameLog.Info( $\u0022[pixel] material probe {( ok ? \u0022OK\u0022 : \u0022FAIL\u0022 )} (mat={matOk} shader={shOk} set={setOk} tex={texOk})\u0022 );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u8D34\u56FE\u6279\u6B21\u6750\u8D28\uFF08static\uFF1ATerritoryRenderer \u7684\u65D7/\u57CE\u5821\u8D34\u56FE\u6279\u6B21\u540C\u6B3E\u590D\u7528\uFF09\uFF1A\u72EC\u7ACB\u5B9E\u4F8B\u3002\n\t/// pixel=true \u6362 **Point/Nearest \u70B9\u91C7\u6837**\uFF08v0.7.8.93\uFF09\uFF1A\u50CF\u7D20\u8D34\u56FE\u653E\u5927\u5341\u51E0\u500D\u5FC5\u987B\u6700\u8FD1\u90BB\uFF0C\n\t/// \u53CC\u7EBF\u6027\u4F1A\u628A\u50CF\u7D20\u9897\u7C92\u7CCA\u6389\u2014\u2014\u65E7\u5B9E\u73B0\u610F\u56FE\u5C31\u662F point \u91C7\u6837\uFF0C\u4F46 SamplerIndex \u5199\u6B7B 0\uFF08\u9ED8\u8BA4\n\t/// bilinear \u69FD\u4F4D\uFF09\u4ECE\u672A\u751F\u6548\u3002\u8D70 line.shader \u7684 Bindless::GetSampler(SamplerIndex)\uFF0C\u69FD\u4F4D\u7531\n\t/// SamplerState.Index \u73B0\u53D6\uFF08\u5F15\u64CE\u5185\u90E8\u6709\u7F13\u5B58\uFF09\u3002\u5934\u50CF\u7B49\u7167\u7247\u4FDD\u6301\u9ED8\u8BA4 bilinear\uFF08\u70B9\u91C7\u6837\u4F1A\u952F\u9F7F\uFF09\u3002\n\t/// SamplerIndex \u8BBE\u4E0D\u8FDB\u53BB\u4E0D\u81F4\u547D\uFF08\u56DE\u9000\u9ED8\u8BA4\u91C7\u6837\uFF0C\u53EA\u662F\u7CCA\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tpublic static Material MakeTexMaterial( Texture tex, bool pixel = false )\n\t{\n\t\tvar mat = LineMaterial.Create();\n\t\ttry\n\t\t{\n\t\t\tif ( pixel )\n\t\t\t\tmat.Set( \u0022SamplerIndex\u0022, PixelSamplerIndex() );\n\t\t\telse\n\t\t\t\tmat.Set( \u0022SamplerIndex\u0022, 0 );\n\t\t}\n\t\tcatch { }\n\t\tif ( tex is not null ) mat.Set( \u0022g_tColor\u0022, tex );\n\t\treturn mat;\n\t}\n\n\tstatic int? _pointSamplerIndex;\n\n\t/// \u003Csummary\u003E Point \u91C7\u6837\u7684 bindless \u69FD\u4F4D\uFF08\u61D2\u53D6\uFF1A\u9759\u6001\u521D\u59CB\u5316\u4F1A\u5728\u6E32\u67D3\u8BBE\u5907\u5C31\u7EEA\u524D\u8DD1\uFF0C\u4E13\u7528\u670D\u52A1\u5668\u4E0A\u4F1A\u70B8\uFF09 \u003C/summary\u003E\n\tstatic int PixelSamplerIndex()\n\t{\n\t\t_pointSamplerIndex ??= new Sandbox.Rendering.SamplerState { Filter = Sandbox.Rendering.FilterMode.Point }.Index;\n\t\treturn _pointSamplerIndex.Value;\n\t}\n\n\t/// \u003Csummary\u003E \u771F\u4EBA\u5934\u50CF\u767B\u8BB0\uFF08Ball \u5404\u7AEF\u81EA\u53D6 Steam \u5934\u50CF\u540E\u8C03\uFF1B\u6BCF SteamId \u4E00\u4E2A\u72EC\u7ACB\u8D34\u56FE\u6279\u6B21\uFF09 \u003C/summary\u003E\n\tpublic static void SetAvatar( long steamId, Texture tex )\n\t{\n\t\tif ( steamId == 0 || tex is null ) return;\n\t\t_pendingAvatars[steamId] = tex;\n\t}\n\n\t/// \u003Csummary\u003E \u6D88\u5316\u5934\u50CF\u767B\u8BB0\uFF1A\u7F3A\u6279\u6B21\u8865\u5EFA\u3001\u8D34\u56FE\u53D8\u4E86\u6362\u6750\u8D28\uFF08\u6E32\u67D3\u5668\u672A\u5C31\u7EEA\u65F6\u7684\u767B\u8BB0\u7559\u5230\u4E0B\u4E00\u5E27\u751F\u6548\uFF09 \u003C/summary\u003E\n\tvoid SyncAvatars()\n\t{\n\t\tforeach ( var kv in _pendingAvatars )\n\t\t{\n\t\t\tif ( _avatars.TryGetValue( kv.Key, out var av ) \u0026\u0026 av.Tex == kv.Value ) continue;\n\n\t\t\tif ( av is null )\n\t\t\t{\n\t\t\t\tav = TakeAvatarBatch();\n\t\t\t\t_avatars[kv.Key] = av;\n\t\t\t}\n\t\t\tav.Mat = TrackMat( MakeTexMaterial( kv.Value ) );\n\t\t\tav.Dyno.Material = av.Mat;\n\t\t\tav.Tex = kv.Value;\n\t\t}\n\t}\n\n\treadonly HashSet\u003Clong\u003E _avatarSeen = new();\n\treadonly List\u003Clong\u003E _avatarDrop = new();\n\treadonly List\u003CAvatarBatch\u003E _avatarSpare = new();   // \u56DE\u6536\u6765\u590D\u7528\u7684\u7A7A\u6279\u6B21\uFF08\u907F\u514D\u73B0\u5220\u539F\u751F\u5BF9\u8C61\uFF09\n\tTimeSince _sinceAvatarPrune = 5f;\n\n\t/// \u003Csummary\u003E \u53D6\u4E00\u4E2A\u53EF\u7528\u6279\u6B21\uFF1A\u4F18\u5148\u590D\u7528\u56DE\u6536\u6C60\uFF0C\u6C60\u7A7A\u624D\u65B0\u5EFA\uFF08v0.7.8.87\uFF09 \u003C/summary\u003E\n\tAvatarBatch TakeAvatarBatch()\n\t{\n\t\tif ( _avatarSpare.Count \u003E 0 )\n\t\t{\n\t\t\tvar spare = _avatarSpare[^1];\n\t\t\t_avatarSpare.RemoveAt( _avatarSpare.Count - 1 );\n\t\t\treturn spare;\n\t\t}\n\t\treturn new AvatarBatch { Dyno = CreateShapeBatch( topmost: true ) };\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u56DE\u6536\u4E0D\u5728\u573A\u73A9\u5BB6\u7684\u5934\u50CF\u6279\u6B21\uFF08v0.7.8.87\uFF09\uFF1A\u0060_avatars\u0060 \u662F\u0022\u6BCF SteamId \u4E00\u4E2A\u52A8\u6001\u6279\u6B21 \u002B \u6750\u8D28\u0022\u7684\u91CD\u8D44\u6E90\uFF0C\n\t/// \u4E0D\u56DE\u6536\u4F1A\u968F\u0022\u8FD9\u53F0\u673A\u5668\u89C1\u8FC7\u7684\u73A9\u5BB6\u603B\u6570\u0022\u4E00\u76F4\u6DA8\u3002\u8D34\u56FE\u767B\u8BB0\u7559\u5728\u9759\u6001 \u0060_pendingAvatars\u0060\uFF08\u5F88\u8F7B\uFF09\uFF0C\n\t/// \u540C\u4E00\u73A9\u5BB6\u518D\u5165\u573A\u65F6 SyncAvatars \u4F1A\u590D\u7528\u6279\u6B21\u91CD\u5EFA\u3002\n\t/// \u26A0\uFE0F \u53EA\u6E05\u51E0\u4F55\u3001\u8FDB\u56DE\u6536\u6C60\uFF0C**\u4E0D\u8C03\u7528 SceneObject.Delete()**\uFF1A\u6279\u6B21\u662F\u0022\u6BCF\u5E27 Clear \u002B \u91CD\u65B0 Init\u0022\u9A71\u52A8\u7684\uFF0C\n\t/// \u5728\u6E38\u620F\u5FAA\u73AF\u91CC\u9500\u6BC1\u539F\u751F\u573A\u666F\u5BF9\u8C61\u98CE\u9669\u9AD8\uFF08\u6E05\u51E0\u4F55\u5373\u53EF\u8BA9\u8131\u7BA1\u6279\u6B21\u4E0D\u518D\u663E\u793A\uFF0C\u907F\u514D\u5E7D\u7075\u5934\u50CF\uFF09\n\t/// \u003C/summary\u003E\n\tvoid PruneAvatars( CircleroyaleGame game )\n\t{\n\t\tif ( _avatars.Count == 0 || _sinceAvatarPrune \u003C 5f ) return;\n\t\t_sinceAvatarPrune = 0;\n\n\t\t_avatarSeen.Clear();\n\t\tforeach ( var b in game.Balls )\n\t\t{\n\t\t\tif ( b.IsValid() \u0026\u0026 !b.IsBot ) _avatarSeen.Add( b.OwnerSteamId );   // \u6B7B\u4EA1\u5F85\u91CD\u751F\u7684\u7403\u4ECD\u7B97\u5728\u573A\n\t\t}\n\n\t\t_avatarDrop.Clear();\n\t\tforeach ( var kv in _avatars )\n\t\t{\n\t\t\tif ( !_avatarSeen.Contains( kv.Key ) ) _avatarDrop.Add( kv.Key );\n\t\t}\n\n\t\tforeach ( var k in _avatarDrop )\n\t\t{\n\t\t\tvar av = _avatars[k];\n\t\t\t_avatars.Remove( k );\n\t\t\tif ( av?.Dyno is null ) continue;\n\n\t\t\tav.Dyno.Clear();   // \u6E05\u6389\u6700\u540E\u4E00\u5E27\u7684\u51E0\u4F55\uFF1A\u8131\u7BA1\u540E\u6CA1\u4EBA\u518D\u6E05\uFF0C\u7559\u7740\u5C31\u662F\u573A\u666F\u91CC\u7684\u5E7D\u7075\u5934\u50CF\n\t\t\tav.Tex = null;\n\t\t\tif ( _avatarSpare.Count \u003C 24 ) _avatarSpare.Add( av );\n\t\t}\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\t\tif ( !Game.IsPlaying ) return;\n\n\t\t// \u5E27\u7387\u8868\uFF08v0.7.8.104\uFF0Ccr_perf A/B \u6D4B\u91CF\u7528\uFF09\n\t\t_fpsFrames\u002B\u002B;\n\t\tif ( _fpsWindow \u003E 1f )\n\t\t{\n\t\t\tDebugFps = _fpsFrames / _fpsWindow;\n\t\t\t_fpsFrames = 0;\n\t\t\t_fpsWindow = 0;\n\t\t}\n\n\t\tvar game = CircleroyaleGame.Current;\n\t\tif ( game is null || _shapeTris is null || _bodies is null || _faces is null ) return;\n\n\t\t_shapeLines.Clear();\n\t\t_shapeLines.Init( Graphics.PrimitiveType.Lines );\n\t\t_shapeTris.Clear();\n\t\t_shapeTris.Init( Graphics.PrimitiveType.Triangles );\n\t\t_bodies.Clear();\n\t\t_bodies.Init( Graphics.PrimitiveType.Triangles );\n\t\t_food.Clear();\n\t\t_food.Init( Graphics.PrimitiveType.Triangles );\n\t\t_spikes.Clear();\n\t\t_spikes.Init( Graphics.PrimitiveType.Triangles );\n\t\t_spines.Clear();\n\t\t_spines.Init( Graphics.PrimitiveType.Triangles );\n\t\t_icons.Clear();\n\t\t_icons.Init( Graphics.PrimitiveType.Triangles );\n\t\t_rings.Clear();\n\t\t_rings.Init( Graphics.PrimitiveType.Triangles );\n\n\t\tSyncAvatars();\n\t\tPruneAvatars( game );\n\n\t\t// \u8D34\u56FE\u81EA\u6108\uFF081s \u8282\u6D41\uFF09\uFF1A\u70ED\u91CD\u8F7D/\u8D44\u4EA7\u5237\u65B0\u671F\u95F4\u52A0\u8F7D\u5931\u8D25\u7684\u6279\u6B21\u8865\u8F7D\n\t\tif ( _sinceTexHeal \u003E 1f )\n\t\t{\n\t\t\t_sinceTexHeal = 0;\n\t\t\tforeach ( var b in _texBatches )\n\t\t\t{\n\t\t\t\tif ( b.Tex is not null ) continue;\n\t\t\t\tLoadTex( b );\n\t\t\t}\n\t\t}\n\n\t\tProbeMaterialHealth();\n\n\t\tforeach ( var f in _faces )\n\t\t{\n\t\t\tf.Clear();\n\t\t\tf.Init( Graphics.PrimitiveType.Triangles );\n\t\t}\n\t\tforeach ( var av in _avatars.Values )\n\t\t{\n\t\t\tav.Dyno.Clear();\n\t\t\tav.Dyno.Init( Graphics.PrimitiveType.Triangles );\n\t\t}\n\n\t\tUpdateParticles();\n\t\tUpdateWallPulses( Time.Delta );\n\n\t\t// ---- \u7403\uFF1A\u692D\u5706\u6295\u5F71 \u002B tint \u767D\u6A21 \u002B \u51FA\u751F\u4FDD\u62A4\u95EA\u70C1 ----\n\t\tforeach ( var ball in game.Balls )\n\t\t{\n\t\t\tif ( !ball.IsValid() || !ball.Alive ) continue;\n\n\t\t\tvar c = ball.WorldPosition;\n\t\t\tvar r = ball.Radius;\n\n\t\t\t// \u6295\u5F71\u753B\u5728\u811A\u4E0B\uFF1A\u5C4F\u5E55\u4E0B\u65B9 = \u4E16\u754C -X\uFF08\u76F8\u673A 90\u00B0 \u6EDA\u8F6C\uFF09\uFF0C\u6A2A\u957F\u7AD6\u6241\n\t\t\tAddShadowEllipse( c \u002B new Vector3( -r * 0.12f, 0f, 0.25f ), r * 0.32f, r * 0.72f );\n\n\t\t\t// \u51FA\u751F\u4FDD\u62A4\uFF1A\u6574\u7403\u65B9\u6CE2\u95EA\u70C1\uFF08\u6697\u76F8 35%\uFF09\uFF0C\u95EA\u70C1\u5728\u8EAB\u4F53 tint \u4E0A\uFF0C\u5934\u50CF\u4FDD\u6301\u53EF\u89C1\n\t\t\tvar blink = 1f;\n\t\t\tif ( ball.IsProtected )\n\t\t\t\tblink = MathF.Sin( Time.Now * GameConfig.SpawnBlinkHz * MathF.PI * 2f ) \u003E 0f ? 1f : 0.35f;\n\t\t\tvar tint = new Color( ball.NeonColor.r, ball.NeonColor.g, ball.NeonColor.b, blink );\n\n\t\t\tAddQuad( _bodies, c, r / BodyContentFraction, r / BodyContentFraction, 0f,\n\t\t\t\tnew Vector4( 0f, 0f, 1f, 1f ), tint, 1.0f );\n\n\t\t\t// \u62A4\u76FE buff\uFF08M6.5\uFF09\uFF1A\u767D\u6A21\u865A\u7EBF\u73AF\u67D3\u9053\u5177\u84DD\uFF0C\u7ED5\u7403\u6162\u8F6C\uFF08v0.7.8.87 \u63A5 shield_ring.png\uFF09\n\t\t\tif ( ball.HasBuff( PowerUpManager.Kind.Shield ) )\n\t\t\t\tAddQuad( _rings, c, r * 1.32f, r * 1.32f, Time.Now * 0.7f,\n\t\t\t\t\tnew Vector4( 0f, 0f, 1f, 1f ), PowerUpManager.ColorOf( (byte)PowerUpManager.Kind.Shield ), 1.05f );\n\n\t\t\t// \u5934\u50CF\uFF08v0.7.8.40\uFF09\uFF1Abot=\u8868\u60C5\u8138\uFF08Mood \u9009\u6279\uFF09\uFF0C\u771F\u4EBA=Steam \u5934\u50CF\uFF1B\u76F4\u5F84=\u534A\u5F84\uFF0C\u753B\u5728\u8EAB\u4F53\u4E4B\u4E0A\n\t\t\tDrawAvatar( c, r, ball.IsBot, ball.Mood, ball.OwnerSteamId, 1.1f );\n\n\t\t\t// \u7784\u51C6\u7BAD\u5934\uFF08v0.7.0.0\uFF09\uFF1A\u672C\u673A\u7403\u5916\u7F18\u767D\u8272\u5C0F\u4E09\u89D2\u6307\u5411\u5149\u6807\uFF0C\u4EC5\u5BF9\u5C40\u4E2D\u663E\u793A\n\t\t\tif ( game.IsMatchStarted \u0026\u0026 ball == game.LocalBall \u0026\u0026 ball.AimDir.Length \u003E 0.01f )\n\t\t\t\tWriteAimArrow( ball );\n\t\t}\n\n\t\t// ---- \u5206\u8EAB/\u5B62\u5B50/\u5C16\u523A\u5206\u8EAB\uFF08\u7EAF\u6570\u636E\u5B9E\u4F53\uFF09\uFF1Atint \u767D\u6A21 / \u523A\u8D34\u56FE\uFF0C\u51FA\u751F\u5F39\u8DF3\u4FDD\u7559 ----\n\t\tforeach ( var c in game.Cells )\n\t\t{\n\t\t\tvar color = GameConfig.Palette[Math.Clamp( c.ColorIndex, 0, GameConfig.Palette.Length - 1 )];\n\t\t\tvar pos = new Vector3( c.DrawPos.x, c.DrawPos.y, 0f );\n\t\t\tvar r = c.Radius;\n\n\t\t\t// \u51FA\u751F\u5F39\u8DF3\uFF08v0.7.8.28 juice\uFF09\uFF1A\u65B0 id \u9996\u5E27\u8BB0\u65F6\u523B\uFF0C0.3s \u5185\u7F29\u653E\u8109\u51B2\u3002\u5BA2\u6237\u7AEF\u955C\u50CF\u7528\u0022\u9996\u89C1 id\u0022\u5224\u5B9A\uFF0C\u53CC\u7AEF\u7EDF\u4E00\n\t\t\tif ( !_seenCells.Contains( c.Id ) )\n\t\t\t{\n\t\t\t\t_seenCells.Add( c.Id );\n\t\t\t\t_cellBorn[c.Id] = 0;\n\t\t\t\tif ( _seenCells.Count \u003E 512 ) PruneCellFx( game );\n\t\t\t}\n\t\t\tif ( _cellBorn.TryGetValue( c.Id, out var born ) \u0026\u0026 born \u003C 0.3f )\n\t\t\t{\n\t\t\t\tvar phase = born / 0.3f;\n\t\t\t\tr *= phase \u003C 0.3f\n\t\t\t\t\t? MathX.Lerp( 0.55f, 1.14f, phase / 0.3f )\n\t\t\t\t\t: MathX.Lerp( 1.14f, 1f, ( phase - 0.3f ) / 0.7f );\n\t\t\t}\n\n\t\t\tif ( c.PieceKind == CellPiece.Kind.SpikeMinion )\n\t\t\t{\n\t\t\t\t// \u5C16\u523A\u5206\u8EAB\uFF08v0.7.4.0\uFF09\uFF1A\u523A\u65CF\u8D34\u56FE\uFF08\u7D2B\u8272=\u662F\u523A\uFF09\uFF0C\u5C3A\u5BF8\u4E0D\u5C0F\u4E8E\u91CE\u523A\u89C2\u611F\n\t\t\t\tvar sr = MathF.Max( r, 40f );\n\t\t\t\tAddQuad( _spikes, pos, sr, sr, 0f, new Vector4( 0f, 0f, 1f, 1f ), Color.White, 0.65f );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t// \u5206\u8EAB/\u5B62\u5B50\uFF1A\u540C\u4E00\u5F20\u767D\u6A21 tint\uFF08\u5B62\u5B50\u7565\u7F29\uFF0C\u89C2\u611F=\u5C0F\u7CD6\u8C46\u56E2\uFF09\n\t\t\t\tvar scale = c.PieceKind == CellPiece.Kind.EjectedMass ? 0.85f : 1f;\n\t\t\t\tvar half = r * scale / BodyContentFraction;\n\t\t\t\tvar z = c.PieceKind == CellPiece.Kind.EjectedMass ? 0.8f : 0.9f;\n\t\t\t\tAddQuad( _bodies, pos, half, half, 0f, new Vector4( 0f, 0f, 1f, 1f ), color, z );\n\n\t\t\t\t// \u5206\u8EAB\u5934\u50CF\uFF08v0.7.8.40 \u81EA PieceAvatarLayer \u5E76\u5165\uFF09\uFF1A\u53EA\u7ED9 SplitPiece\uFF0C\u76F4\u5F84=\u5206\u8EAB\u534A\u5F84\n\t\t\t\tif ( c.PieceKind == CellPiece.Kind.SplitPiece )\n\t\t\t\t{\n\t\t\t\t\tvar owner = game.FindBallBySteamId( c.OwnerSteamId );\n\t\t\t\t\tDrawAvatar( pos, r, owner.IsValid() \u0026\u0026 owner.IsBot,\n\t\t\t\t\t\towner.IsValid() ? owner.Mood : (byte)0, c.OwnerSteamId, 0.95f );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// ---- \u91CE\u523A\uFF1A\u5582\u98DF\u957F\u5927\u540E\u76F4\u63A5\u7F29\u653E\u8D34\u56FE ----\n\t\tvar spikes = game.Spikes;\n\t\tif ( spikes is not null )\n\t\t{\n\t\t\tfor ( int i = 0; i \u003C spikes.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar grow = 1f \u002B GameConfig.SpikeGrowVisual * Math.Clamp( game.SpikeFedAt( i ) / GameConfig.SpikeGrowTarget, 0f, 1f );\n\t\t\t\tvar half = GameConfig.SpikeRadius * grow;\n\t\t\t\tAddQuad( _spikes, new Vector3( spikes[i].x, spikes[i].y, 0f ), half, half, 0f,\n\t\t\t\t\tnew Vector4( 0f, 0f, 1f, 1f ), Color.White, 0.6f );\n\t\t\t}\n\t\t}\n\n\t\t// ---- \u523A\u7206\u5F39\u5E55\uFF08v0.7.8.24\uFF09\uFF1A\u523A\u5F39\u8D34\u56FE\u6309\u901F\u5EA6\u65B9\u5411\u65CB\u8F6C\uFF0C\u968F\u5BFF\u547D\u6DE1\u51FA ----\n\t\tforeach ( var sp in game.Spines )\n\t\t{\n\t\t\tvar remain = 1f - Math.Clamp( sp.SinceSpawn / GameConfig.SpineLife, 0f, 1f );\n\t\t\tvar dir = sp.Vel;\n\t\t\tif ( dir.Length \u003C 0.001f ) continue;\n\t\t\tvar rot = MathF.Atan2( dir.y, dir.x );\n\t\t\tvar tint = new Color( 1f, 1f, 1f, 0.35f \u002B 0.65f * remain );\n\t\t\tAddQuad( _spines, new Vector3( sp.Pos.x, sp.Pos.y, 0f ), 17f, 17f, rot,\n\t\t\t\tnew Vector4( 0f, 0f, 1f, 1f ), tint, 1.2f );\n\t\t}\n\n\t\t// ---- \u9053\u5177\uFF08v0.7.3.0\uFF09\uFF1A\u65CB\u8F6C\u83F1\u5F62\u5E95\u5EA7\uFF08\u666E\u901A alpha\uFF09\u002B \u79CD\u7C7B\u56FE\u6807\u8D34\u56FE ----\n\t\tvar power = game.PowerUps;\n\t\tif ( power is not null \u0026\u0026 power.Ready )\n\t\t{\n\t\t\tvar spin = Time.Now * 1.6f;\n\t\t\tvar breathe = 1f \u002B MathF.Sin( Time.Now * 3f ) * 0.12f;\n\n\t\t\tforeach ( var s in power.Slots )\n\t\t\t{\n\t\t\t\tif ( !s.Alive ) continue;\n\n\t\t\t\tvar pos = new Vector3( s.Pos.x, s.Pos.y, 0f );\n\t\t\t\tvar kindColor = PowerUpManager.ColorOf( s.Kind );\n\t\t\t\tvar r = GameConfig.PowerUpRadius * breathe;\n\n\t\t\t\tWriteDiamond( pos, r, spin, kindColor );\n\n\t\t\t\tvar k = PowerKindToCell[Math.Clamp( s.Kind % 5, 0, 4 )];\n\t\t\t\tvar uv = new Vector4( k / 5f, 0f, ( k \u002B 1 ) / 5f, 1f );\n\t\t\t\tAddQuad( _icons, pos \u002B Vector3.Up * 1.3f, r * 1.35f, r * 1.35f, MathF.PI * 0.5f, uv, Color.White, 1.3f );\n\t\t\t}\n\t\t}\n\n\t\t// ---- \u98DF\u7269\uFF1A\u7CD6\u8C46\u8D34\u56FE\u6309\u8272\u53F7\u53D6 UV \u683C\uFF08\u89C6\u53E3\u5254\u9664\u4FDD\u7559\uFF09 ----\n\t\tvar foods = game.Food?.Foods;\n\t\tif ( foods is not null )\n\t\t{\n\t\t\t// \u89C6\u53E3\u5254\u9664\uFF08v0.6.3.0\uFF09\uFF1A4140 \u9897\u98DF\u7269\u53EA\u6709 ~20% \u5728\u753B\u9762\u91CC\u3002\n\t\t\t// \u26A0\uFE0F \u8F74\u5411\uFF08v0.7.8.87 \u4FEE\uFF09\uFF1A\u76F8\u673A\u56FA\u5B9A 90\u00B0 \u6EDA\u8F6C\u2014\u2014\u5C4F\u5E55\u6A2A\u5411=\u4E16\u754C Y\u3001\u5C4F\u5E55\u7EB5\u5411=\u4E16\u754C X\n\t\t\t// \uFF08\u89C1 GameHud.UpdateTagPositions \u540C\u4E00\u5957\u6295\u5F71\u5F0F\uFF09\uFF0C\u6240\u4EE5\u0022\u6309\u5BBD\u9AD8\u6BD4\u653E\u5F00\u7684\u6A2A\u5411\u4F59\u91CF\u0022\u5FC5\u987B\u7ED9 Y\u3001\n\t\t\t// \u7A84\u4F59\u91CF\u7ED9 X\u3002\u6B64\u524D\u4E24\u8F74\u5199\u53CD\uFF1A\u5C4F\u5E55\u5DE6\u53F3\u4E24\u4FA7\u4ECD\u53EF\u89C6\u8303\u56F4\u5185\u7684\u98DF\u7269\u88AB\u5254\u6389\uFF08\u89C2\u611F\u5C31\u662F\u7F3A\u98DF\u7269\uFF09\n\t\t\tvar fr = GameConfig.FoodRadius;\n\t\t\tvar cullCx = Scene.Camera.WorldPosition.x;\n\t\t\tvar cullCy = Scene.Camera.WorldPosition.y;\n\t\t\tvar cullAspect = MathF.Max( 2.4f, Screen.Height \u003E 0f ? Screen.Width / Screen.Height : 2.4f );\n\t\t\tvar cullHx = Scene.Camera.OrthographicHeight * 0.5f \u002B 64f;                 // \u4E16\u754C X = \u5C4F\u5E55\u7EB5\u5411\uFF08\u7A84\uFF09\n\t\t\tvar cullHy = Scene.Camera.OrthographicHeight * 0.5f * cullAspect \u002B 64f;    // \u4E16\u754C Y = \u5C4F\u5E55\u6A2A\u5411\uFF08\u5BBD\uFF0C\u8D85\u5BBD\u5C4F\u4E0D\u7F3A\u5DE6\u53F3\u98DF\u7269\uFF09\n\n\t\t\tfor ( int i = 0; i \u003C foods.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( !foods[i].Alive ) continue;\n\n\t\t\t\tvar fx = foods[i].Pos.x;\n\t\t\t\tvar fy = foods[i].Pos.y;\n\t\t\t\tif ( MathF.Abs( fx - cullCx ) \u003E cullHx || MathF.Abs( fy - cullCy ) \u003E cullHy ) continue;\n\n\t\t\t\tvar k = foods[i].ColorIndex % 6;\n\t\t\t\tvar uv = new Vector4( k / 6f, 0f, ( k \u002B 1 ) / 6f, 1f );\n\t\t\t\tAddQuad( _food, new Vector3( fx, fy, 0f ), fr / 0.95f, fr / 0.95f, 0f, uv, Color.White, 0.55f );\n\t\t\t}\n\t\t}\n\n\t\tDrawParticles();\n\t\tDrawWallPulses();\n\t\tDrawEdgeArrows();\n\t}\n\n\t// ---- \u5C4F\u5E55\u8FB9\u7F18\u961F\u53CB\u6307\u793A\u7BAD\u5934\uFF08v0.7.8.12\uFF09\uFF1AGameHud \u540D\u724C\u91CD\u6295\u65F6\u5199\u5165\u8BF7\u6C42\uFF0C\u672C\u7EC4\u4EF6 OnUpdate \u672B\u5C3E\u6D88\u8D39 ----\n\n\tsealed class EdgeArrowReq\n\t{\n\t\tpublic Vector3 Pos;      // \u8D34\u8FB9\u70B9\uFF08\u4E16\u754C\uFF09\n\t\tpublic Vector2 Dir;      // \u6307\u5411\u961F\u53CB\u771F\u5B9E\u65B9\u4F4D\uFF08\u4E16\u754C\uFF0C\u5355\u4F4D\u5411\u91CF\uFF09\n\t\tpublic Color Color;\n\t\tpublic float Size;       // \u4E16\u754C\u5355\u4F4D\uFF08\u6309\u6BCF\u50CF\u7D20\u4E16\u754C\u5355\u4F4D\u6298\u7B97\uFF0C\u4FDD\u6301\u50CF\u7D20\u5927\u5C0F\u6052\u5B9A\uFF09\n\t}\n\n\tstatic readonly List\u003CEdgeArrowReq\u003E _edgeArrows = new();\n\n\t/// \u003Csummary\u003E \u8BF7\u6C42\u4E00\u652F\u8FB9\u7F18\u7BAD\u5934\uFF08\u8D34\u8FB9\u4E16\u754C\u70B9 \u002B \u6307\u5411 \u002B \u989C\u8272\uFF09\u3002OnUpdate \u6D88\u8D39\u540E\u6E05\u7A7A\uFF0C\u665A\u4E00\u5E27\u65E0\u611F \u003C/summary\u003E\n\tpublic static void EdgeArrow( Vector3 pos, Vector2 dir, Color color, float size )\n\t{\n\t\t_edgeArrows.Add( new EdgeArrowReq { Pos = pos, Dir = dir, Color = color, Size = size } );\n\t}\n\n\t/// \u003Csummary\u003E \u5B9E\u5FC3\u4E09\u89D2\u7BAD\u5934 \u002B \u77ED\u5C3E\u6746\uFF0C\u671D\u5411 Dir \u003C/summary\u003E\n\tvoid DrawEdgeArrows()\n\t{\n\t\tfor ( int i = 0; i \u003C _edgeArrows.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar r = _edgeArrows[i];\n\t\t\tvar d = new Vector3( r.Dir.x, r.Dir.y, 0f );\n\t\t\tvar perp = new Vector3( -r.Dir.y, r.Dir.x, 0f );\n\t\t\tvar len = r.Size;\n\t\t\tvar halfW = len * 0.5f;\n\n\t\t\tvar tip = r.Pos \u002B d * len;\n\t\t\tvar b1 = r.Pos \u002B perp * halfW;\n\t\t\tvar b2 = r.Pos - perp * halfW;\n\n\t\t\tAddTri( _shapeTris, b1, b2, tip, r.Color * 0.9f );\n\n\t\t\t// \u5C3E\u6746\uFF1A\u4ECE\u7BAD\u5934\u5411\u5C4F\u5185\u5EF6\u4F38\u4E00\u5C0F\u6BB5\uFF0C\u63D0\u793A\u0022\u65B9\u5411\u0022\u800C\u975E\u0022\u4F4D\u7F6E\u70B9\u0022\n\t\t\tvar t0 = r.Pos - d * len * 0.6f;\n\t\t\t_shapeLines.AddVertex( new Vertex( t0, r.Color * 0.5f ) );\n\t\t\t_shapeLines.AddVertex( new Vertex( r.Pos, r.Color * 0.5f ) );\n\t\t}\n\t\t_edgeArrows.Clear();\n\t}\n\n\t// ---- CPU \u7C92\u5B50\uFF08M3 \u8868\u73B0\u529B\uFF09\uFF1A\u666E\u901A alpha \u6279\u6B21\u65B9\u5757\u7C92\u5B50 ----\n\n\t/// \u003Csummary\u003E \u7784\u51C6\u7BAD\u5934\uFF08v0.7.0.0\uFF09\uFF1A\u672C\u673A\u7403\u5916\u7F18\u7684\u767D\u8272\u5B9E\u5FC3\u5C0F\u4E09\u89D2\uFF0C\u6307\u5411\u5149\u6807\uFF08\u5206\u88C2/\u5410\u5B62\u5B50\u65B9\u5411\uFF09 \u003C/summary\u003E\n\tvoid WriteAimArrow( Ball ball )\n\t{\n\t\tvar dir = ball.AimDir;\n\t\tvar c = ball.WorldPosition;\n\t\tvar r = ball.Radius;\n\n\t\tvar len = Math.Clamp( r * 0.22f, 18f, 64f );   // \u7BAD\u5934\u957F\u5EA6\u968F\u7403\u5F84\u5FAE\u7F29\u653E\n\t\tvar halfW = len * 0.42f;\n\t\tvar gap = r * 1.04f;\n\n\t\tvar dir3 = new Vector3( dir.x, dir.y, 0f );\n\t\tvar perp = new Vector3( -dir.y, dir.x, 0f );\n\n\t\tvar baseC = c \u002B dir3 * gap;\n\t\tvar tip = c \u002B dir3 * ( gap \u002B len );\n\t\tvar p1 = baseC \u002B perp * halfW;\n\t\tvar p2 = baseC - perp * halfW;\n\n\t\tAddTri( _shapeTris, p1, p2, tip, new Color( 1f, 1f, 1f, 0.55f ) );\n\t}\n\n\t/// \u003Csummary\u003E \u5355\u4E2A\u5B9E\u5FC3\u4E09\u89D2 \u003C/summary\u003E\n\tvoid AddTri( SceneDynamicObject batch, Vector3 a, Vector3 b, Vector3 c, Color color )\n\t{\n\t\tbatch.AddVertex( new Vertex( a, color ) );\n\t\tbatch.AddVertex( new Vertex( b, color ) );\n\t\tbatch.AddVertex( new Vertex( c, color ) );\n\t}\n\n\t/// \u003Csummary\u003E \u8D34\u56FE\u56DB\u8FB9\u5F62\uFF08\u4E24\u4E2A\u4E09\u89D2\uFF09\uFF1Acenter \u4E2D\u5FC3\u3001half \u534A\u5BBD/\u534A\u9AD8\u3001rot \u5F27\u5EA6\u3001uv=(x0,y0,x1,y1) \u003C/summary\u003E\n\tstatic void AddQuad( SceneDynamicObject b, Vector3 center, float halfX, float halfY, float rot, Vector4 uv, Color color, float z )\n\t{\n\t\tvar cos = MathF.Cos( rot );\n\t\tvar sin = MathF.Sin( rot );\n\t\tvar rx = cos * halfX; var ry = sin * halfX;      // right \u8F74\n\t\tvar ux = -sin * halfY; var uy = cos * halfY;     // up \u8F74\n\n\t\tvar v0 = new Vertex( new Vector3( center.x - rx - ux, center.y - ry - uy, z ), new Vector4( uv.x, uv.y, 0f, 0f ), color );\n\t\tvar v1 = new Vertex( new Vector3( center.x \u002B rx - ux, center.y \u002B ry - uy, z ), new Vector4( uv.z, uv.y, 0f, 0f ), color );\n\t\tvar v2 = new Vertex( new Vector3( center.x \u002B rx \u002B ux, center.y \u002B ry \u002B uy, z ), new Vector4( uv.z, uv.w, 0f, 0f ), color );\n\t\tvar v3 = new Vertex( new Vector3( center.x - rx \u002B ux, center.y - ry \u002B uy, z ), new Vector4( uv.x, uv.w, 0f, 0f ), color );\n\n\t\tb.AddVertex( v0 ); b.AddVertex( v1 ); b.AddVertex( v2 );\n\t\tb.AddVertex( v0 ); b.AddVertex( v2 ); b.AddVertex( v3 );\n\t}\n\n\t/// \u003Csummary\u003E \u5934\u50CF/\u8868\u60C5\uFF1A\u76F4\u5F84=\u7403\u534A\u5F84\uFF08\u5360\u76F4\u5F84 50%\uFF0C\u7528\u6237\u6307\u5B9A\uFF09\uFF1Bbot \u4ECE face_N \u6279\u6B21\u53D6\uFF08Mood \u9009\u683C\uFF09\uFF0C\n\t/// \u771F\u4EBA\u4ECE Steam \u5934\u50CF\u6279\u6B21\u53D6\u3002\u6279\u6B21\u5728 OverlayWithoutDepth \u5C42\uFF0C\u5929\u7136\u753B\u5728\u8EAB\u4F53\uFF08OverlayWithDepth\uFF09\u4E4B\u4E0A\u3002\n\t/// rot=\u03C0/2\uFF1A\u76F8\u673A 90\u00B0 \u6EDA\u8F6C\u4E0B\u8D34\u56FE u \u8F74\u671D\u5C4F\u5E55\u7AD6\u76F4\uFF0C\u4E0D\u8F6C\u7684\u8BDD\u8138\u662F\u6A2A\u8EBA\u7684\uFF08\u98DF\u7269/\u7403\u4F53\u7B49\u5706\u5F62\u770B\u4E0D\u51FA\u6765\uFF09 \u003C/summary\u003E\n\tvoid DrawAvatar( Vector3 c, float radius, bool bot, byte mood, long steamId, float z )\n\t{\n\t\tvar half = radius * 0.5f / FaceContentFraction;\n\n\t\tif ( bot )\n\t\t{\n\t\t\tvar k = Math.Clamp( (int)mood, 0, 5 );\n\t\t\tAddQuad( _faces[k], c, half, half, MathF.PI * 0.5f, new Vector4( 0f, 0f, 1f, 1f ), Color.White, z );\n\t\t}\n\t\telse if ( _avatars.TryGetValue( steamId, out var av ) )\n\t\t{\n\t\t\tAddQuad( av.Dyno, c, half, half, MathF.PI * 0.5f, new Vector4( 0f, 0f, 1f, 1f ), Color.White, z );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u692D\u5706\u6295\u5F71\uFF08\u4E16\u754C XY \u5E73\u9762\uFF1Aax=\u4E16\u754C X \u534A\u8F74=\u5C4F\u5E55\u7AD6\u5411\uFF0Cay=\u4E16\u754C Y \u534A\u8F74=\u5C4F\u5E55\u6A2A\u5411\uFF09 \u003C/summary\u003E\n\tvoid AddShadowEllipse( Vector3 center, float ax, float ay, int segments = 14 )\n\t{\n\t\tsegments = Math.Max( 6, segments );\n\t\tvar prev = center \u002B new Vector3( ax, 0f, 0f );\n\t\tfor ( int i = 1; i \u003C= segments; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = i * MathF.PI * 2f / segments;\n\t\t\tvar p = center \u002B new Vector3( MathF.Cos( a ) * ax, MathF.Sin( a ) * ay, 0f );\n\t\t\tAddTri( _shapeTris, center, prev, p, new Color( 0.59f, 0.67f, 0.82f, 0.30f ) );\n\t\t\tprev = p;\n\t\t}\n\t}\n\n\tsealed class Particle\n\t{\n\t\tpublic Vector3 Pos;\n\t\tpublic Vector3 Vel;\n\t\tpublic float Life;\n\t\tpublic float MaxLife;\n\t\tpublic float Size;\n\t\tpublic Color Color;\n\t\tpublic Vector3 Target;   // Seek=true\uFF1A\u5438\u5F15\u76EE\u6807\uFF08\u541E\u7403\u6ED1\u8FDB\u5403\u8005\u5634\u91CC\uFF09\n\t\tpublic bool Seek;\n\t}\n\n\treadonly List\u003CParticle\u003E _particles = new();\n\treadonly HashSet\u003Cint\u003E _seenCells = new();                    // \u51FA\u751F\u5F39\u8DF3\uFF1A\u89C1\u8FC7\u7684\u5206\u8EAB/\u5B62\u5B50 id\uFF08\u5BA2\u6237\u7AEF\u955C\u50CF\u4E0D\u5E26 SinceSpawn\uFF0C\u7528\u0022\u9996\u89C1\u0022\u53CC\u7AEF\u7EDF\u4E00\u5224\u5B9A\uFF09\n\treadonly Dictionary\u003Cint, TimeSince\u003E _cellBorn = new();\n\n\t/// \u003Csummary\u003E \u7206\u88C2\uFF08\u6B7B\u4EA1/\u649E\u523A\uFF09\uFF1A\u4E2D\u5927\u7C92\u5B50\u5F84\u5411\u98DE\u6563 \u003C/summary\u003E\n\tpublic static void Pop( Vector3 pos, int colorIndex, float radius )\n\t{\n\t\tSpawn( pos, colorIndex, 26, 90f \u002B radius * 0.9f, 0.45f, 0.9f, 3f, 9f );\n\t}\n\n\t/// \u003Csummary\u003E \u706B\u82B1\uFF08\u5403\u98DF\u7269\uFF09\uFF1A\u51E0\u9897\u5C0F\u788E\u5C51\u4E00\u95EA \u003C/summary\u003E\n\tpublic static void Spark( Vector3 pos, int colorIndex )\n\t{\n\t\tSpawn( pos, colorIndex, 5, 100f, 0.2f, 0.4f, 2f, 4f );\n\t}\n\n\t/// \u003Csummary\u003E \u706B\u82B1\uFF08\u81EA\u5B9A\u4E49\u8272\uFF0C\u9053\u5177\u62FE\u53D6\u7528\uFF09\uFF1A\u79CD\u7C7B\u8272\u4E0D\u5728\u8C03\u8272\u677F\u91CC\uFF0C\u76F4\u63A5\u7ED9 Color \u003C/summary\u003E\n\tpublic static void Spark( Vector3 pos, Color color )\n\t{\n\t\tSpawnColored( pos, color, 10, 140f, 0.25f, 0.5f, 2f, 5f );\n\t}\n\n\t/// \u003Csummary\u003E \u55B7\u53D1\uFF08\u5206\u88C2/\u5410\u5B62\u5B50\u751F\u6210\u77AC\u95F4\uFF09 \u003C/summary\u003E\n\tpublic static void Puff( Vector3 pos, int colorIndex )\n\t{\n\t\tSpawn( pos, colorIndex, 8, 150f, 0.25f, 0.5f, 2f, 5f );\n\t}\n\n\t/// \u003Csummary\u003E \u541E\u7403\u6ED1\u5165\uFF08v0.7.8.28 juice\uFF09\uFF1A\u88AB\u5403\u8005\u7F29\u6210\u4E00\u56E2\u3001\u52A0\u901F\u6ED1\u8FDB\u5403\u8005\u5634\u91CC\uFF0C\u5C3A\u5BF8\u968F\u5BFF\u547D\u6536\u7F29\u5F52\u96F6 \u003C/summary\u003E\n\tpublic static void Swallow( Vector3 pos, int colorIndex, Vector3 target, float radius )\n\t{\n\t\tvar inst = Instance;\n\t\tif ( inst is null || !inst.IsValid() ) return;\n\n\t\tif ( inst._particles.Count \u003E= GameConfig.FxMaxParticles )\n\t\t\tinst._particles.RemoveAt( 0 );\n\n\t\tvar life = 0.3f;\n\t\tinst._particles.Add( new Particle\n\t\t{\n\t\t\tPos = pos,\n\t\t\tTarget = target,\n\t\t\tSeek = true,\n\t\t\tLife = life,\n\t\t\tMaxLife = life,\n\t\t\tSize = MathF.Max( 10f, radius * 0.9f ),\n\t\t\tColor = GameConfig.Palette[Math.Clamp( colorIndex, 0, GameConfig.Palette.Length - 1 )],\n\t\t} );\n\t\tSpawn( pos, colorIndex, 4, 120f, 0.15f, 0.3f, 2f, 4f );   // \u5C11\u91CF\u788E\u5C51\u70B9\u7F00\n\t}\n\n\tstatic void Spawn( Vector3 pos, int colorIndex, int count, float maxSpeed, float minLife, float maxLife, float minSize, float maxSize )\n\t{\n\t\tSpawnColored( pos, GameConfig.Palette[Math.Clamp( colorIndex, 0, GameConfig.Palette.Length - 1 )],\n\t\t\tcount, maxSpeed, minLife, maxLife, minSize, maxSize );\n\t}\n\n\tstatic void SpawnColored( Vector3 pos, Color color, int count, float maxSpeed, float minLife, float maxLife, float minSize, float maxSize )\n\t{\n\t\tvar inst = Instance;\n\t\tif ( inst is null || !inst.IsValid() ) return;\n\n\t\tfor ( int i = 0; i \u003C count; i\u002B\u002B )\n\t\t{\n\t\t\tif ( inst._particles.Count \u003E= GameConfig.FxMaxParticles )\n\t\t\t\tinst._particles.RemoveAt( 0 );   // \u6324\u6389\u6700\u8001\u7684\uFF0C\u4FDD\u5E27\u7387\u4F18\u5148\n\n\t\t\tvar a = Game.Random.Float( 0f, MathF.PI * 2f );\n\t\t\tvar sp = maxSpeed * Game.Random.Float( 0.35f, 1f );\n\t\t\tvar life = Game.Random.Float( minLife, maxLife );\n\t\t\tinst._particles.Add( new Particle\n\t\t\t{\n\t\t\t\tPos = pos \u002B new Vector3( Game.Random.Float( -6f, 6f ), Game.Random.Float( -6f, 6f ), 0f ),\n\t\t\t\tVel = new Vector3( MathF.Cos( a ) * sp, MathF.Sin( a ) * sp, 0f ),\n\t\t\t\tLife = life,\n\t\t\t\tMaxLife = life,\n\t\t\t\tSize = Game.Random.Float( minSize, maxSize ),\n\t\t\t\tColor = color,\n\t\t\t} );\n\t\t}\n\t}\n\n\tvoid UpdateParticles()\n\t{\n\t\tvar dt = Time.Delta;\n\t\tfor ( int i = _particles.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar p = _particles[i];\n\t\t\tp.Life -= dt;\n\t\t\tif ( p.Life \u003C= 0f )\n\t\t\t{\n\t\t\t\t_particles.RemoveAt( i );\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ( p.Seek )\n\t\t\t{\n\t\t\t\tp.Pos = Vector3.Lerp( p.Pos, p.Target, 1f - MathF.Exp( -11f * dt ) );   // \u6307\u6570\u5438\u5411\u5403\u8005\u5634\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tp.Pos \u002B= p.Vel * dt;\n\t\t\t\tp.Vel *= MathF.Exp( -3.5f * dt );   // \u963B\u5C3C\uFF0C\u98DE\u6563\u540E\u51CF\u901F\u60AC\u6D6E\u6D88\u6563\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u65B9\u5757\u7C92\u5B50\uFF08\u50CF\u7D20\u98CE\uFF09\uFF1A\u8F74\u5BF9\u9F50\u5C0F\u65B9\u5757\uFF0C\u5C3A\u5BF8/\u900F\u660E\u5EA6\u968F\u5BFF\u547D\u8870\u51CF \u003C/summary\u003E\n\tvoid DrawParticles()\n\t{\n\t\tforeach ( var p in _particles )\n\t\t{\n\t\t\tvar a = p.Life / p.MaxLife;\n\t\t\tvar size = p.Size * ( 0.5f \u002B a * 0.5f );\n\t\t\tvar col = new Color( p.Color.r, p.Color.g, p.Color.b, ( 0.25f \u002B a * 0.75f ) * 0.9f );\n\t\t\tAddQuad( _shapeTris, p.Pos, size * 0.5f, size * 0.5f, 0f, new Vector4( 0f, 0f, 1f, 1f ), col, 1.4f );\n\t\t}\n\t}\n\n\t// ---- \u649E\u5899\u8109\u51B2\uFF08v0.7.8.78\uFF09\uFF1A\u7403\u88AB\u56F4\u5899\u94B3\u4F4F\u65F6\uFF0C\u63A5\u89E6\u70B9\u4E00\u5708\u5C0F\u65B9\u5757\u5411\u5916\u6269\u6563\u6DE1\u51FA\u2014\u2014\u0022\u5899\u6321\u4F4F\u4E86\u0022\u7684\u53CD\u9988 ----\n\n\tconst float WallPulseLife = 0.4f;\n\n\tsealed class WallPulseFx\n\t{\n\t\tpublic Vector3 Pos;\n\t\tpublic float Age;\n\t\tpublic Color Color;\n\t}\n\n\treadonly List\u003CWallPulseFx\u003E _wallPulses = new();\n\n\t/// \u003Csummary\u003E \u56F4\u5899\u53D7\u51FB\u8109\u51B2\uFF1A\u63A5\u89E6\u70B9 \u002B \u649E\u7403\u989C\u8272\uFF08\u7A0B\u5E8F\u5316\u65B9\u5757\u73AF\uFF1B\u65E5\u540E\u53EF\u6362\u8D34\u56FE\u73AF\uFF09 \u003C/summary\u003E\n\tpublic static void WallPulse( Vector3 pos, Color color )\n\t{\n\t\tvar inst = Instance;\n\t\tif ( inst is null || !inst.IsValid() ) return;\n\n\t\tif ( inst._wallPulses.Count \u003E= 24 ) inst._wallPulses.RemoveAt( 0 );\n\t\tinst._wallPulses.Add( new WallPulseFx { Pos = pos, Age = 0f, Color = color } );\n\t}\n\n\tvoid UpdateWallPulses( float dt )\n\t{\n\t\tfor ( int i = _wallPulses.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\t_wallPulses[i].Age \u002B= dt;\n\t\t\tif ( _wallPulses[i].Age \u003E= WallPulseLife ) _wallPulses.RemoveAt( i );\n\t\t}\n\t}\n\n\tvoid DrawWallPulses()\n\t{\n\t\tforeach ( var w in _wallPulses )\n\t\t{\n\t\t\tvar t = w.Age / WallPulseLife;\n\t\t\tvar r = 8f \u002B t * 54f;                              // \u534A\u5F84\u5916\u6269\n\t\t\tvar alpha = ( 1f - t ) * 0.85f;                    // \u6E10\u9690\n\t\t\tvar size = 8f - t * 3f;                            // \u65B9\u5757\u968F\u6269\u6563\u7565\u7F29\n\t\t\tvar col = new Color( w.Color.r, w.Color.g, w.Color.b, alpha );\n\n\t\t\tfor ( int s = 0; s \u003C 14; s\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = s * MathF.PI * 2f / 14f;\n\t\t\t\tvar p = w.Pos \u002B new Vector3( MathF.Cos( a ) * r, MathF.Sin( a ) * r, 0f );\n\t\t\t\tAddQuad( _shapeTris, p, size * 0.5f, size * 0.5f, 0f, new Vector4( 0f, 0f, 1f, 1f ), col, 1.4f );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u51FA\u751F\u5F39\u8DF3\u6CE8\u518C\u8868\u8D85\u9650\u65F6\u6574\u4F53\u91CD\u5EFA\uFF08\u5F53\u524D\u6D3B\u7740\u7684 id \u91CD\u65B0\u7ACB\u518C\uFF0C\u91CD\u7F6E\u77AC\u95F4\u8F7B\u5FAE\u518D\u5F39\u4E00\u6B21\uFF0C\u7F55\u89C1\u53EF\u63A5\u53D7\uFF09 \u003C/summary\u003E\n\tvoid PruneCellFx( CircleroyaleGame game )\n\t{\n\t\t_seenCells.Clear();\n\t\t_cellBorn.Clear();\n\t\tforeach ( var c in game.Cells )\n\t\t\t_seenCells.Add( c.Id );\n\t}\n\n\t/// \u003Csummary\u003E \u9053\u5177\u83F1\u5F62\u5E95\u5EA7\uFF1A\u65CB\u8F6C\u7684\u6B63\u65B9\u5F62\uFF08\u534A\u900F\u660E\u586B\u5145 \u002B \u63CF\u8FB9\uFF09\uFF0C\u50CF\u7D20\u98CE\u4E0B\u4F5C\u56FE\u6807\u7684\u886C\u5E95 \u003C/summary\u003E\n\tvoid WriteDiamond( Vector3 c, float r, float spin, Color color )\n\t{\n\t\tvar pts = new Vector3[4];\n\t\tfor ( int i = 0; i \u003C 4; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = spin \u002B i * MathF.PI * 0.5f;\n\t\t\tpts[i] = c \u002B new Vector3( MathF.Cos( a ) * r, MathF.Sin( a ) * r, 0.7f );\n\t\t}\n\n\t\tfor ( int i = 0; i \u003C 4; i\u002B\u002B )\n\t\t{\n\t\t\tAddTri( _shapeTris, c \u002B Vector3.Up * 0.7f, pts[i], pts[( i \u002B 1 ) % 4], color * 0.35f );\n\t\t\t_shapeLines.AddVertex( new Vertex( pts[i], color ) );\n\t\t\t_shapeLines.AddVertex( new Vertex( pts[( i \u002B 1 ) % 4], color ) );\n\t\t}\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Rendering/TerritoryRenderer.cs","FileName":"TerritoryRenderer.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\n\n/// \u003Csummary\u003E\n/// \u9886\u571F\u6218\u4E89\u5730\u9762\u6E32\u67D3\uFF08M7\uFF0Cv0.7.8.79 \u50CF\u7D20\u98CE\u91CD\u5199\uFF09\uFF1A\u5360\u9886\u683C = \u961F\u8272\u5E95 \u002B \u7C97\u8FB9\u6846 \u002B \u65D7\u70B9\u65D7\u5B50\uFF08flag.png\n/// \u767D\u6A21\u67D3\u961F\u8272\uFF09\uFF0C\u5927\u672C\u8425 = \u57CE\u5821\uFF08castle.png \u767D\u6A21\u67D3\u961F\u8272\uFF09\uFF1B\u4E89\u593A\u4E2D\u7684\u4E2D\u7ACB\u683C = \u767D\u8FB9\u6846 \u002B \u4E89\u593A\u65B9\u8272\u8FDB\u5EA6\u6761\uFF1B\n/// \u5206\u6570\u8D8A\u9AD8\u5E95\u8272\u8D8A\u5B9E\u3002\u72B6\u6001\u7248\u672C\u53D8\u5316\u624D\u91CD\u5EFA\u51E0\u4F55\uFF0816 \u683C\u9876\u70B9\u91CF\u6781\u5C0F\uFF09\uFF1B\u8D34\u56FE\u6279\u6B21 1s \u81EA\u6108\u8865\u8F7D\n/// \uFF08NeonRenderer \u540C\u6B3E\uFF09\u3002CreateBackdrop \u5148\u4E8E Neon \u5EFA\u672C\u7EC4\u4EF6 \u2192 \u540C\u5C42\u7ED8\u5236\u5728\u7403/\u98DF\u7269\u4E4B\u4E0B\u3002\u975E\u9886\u571F\u6A21\u5F0F\u6E05\u7A7A\u3002\n/// \u26A0\uFE0F \u76F8\u673A 90\u00B0 \u6EDA\u8F6C\uFF1A\u65D7/\u57CE\u5821\u8FD9\u7C7B\u6709\u65B9\u5411\u7684\u8D34\u56FE\u5FC5\u987B rot=\u03C0/2 \u624D\u5C4F\u5E55\u6B63\u7ACB\uFF08\u8868\u60C5/\u5934\u50CF\u540C\u6B3E\u6559\u8BAD\uFF09\u3002\n/// \u003C/summary\u003E\npublic sealed class TerritoryRenderer : Component\n{\n\t/// \u003Csummary\u003E \u5168\u5C40\u5F15\u7528\uFF08cr_perf \u6E32\u67D3 A/B \u7528\uFF1BNeonRenderer.Instance \u540C\u6B3E\uFF09 \u003C/summary\u003E\n\tpublic static TerritoryRenderer Instance { get; private set; }\n\n\tSceneDynamicObject _shape;    // \u683C\u5E95/\u7C97\u8FB9\u6846/\u8FDB\u5EA6\u6761\uFF08\u666E\u901A alpha \u4E09\u89D2\u5F62\uFF09\n\tSceneDynamicObject _flags;    // flag.png\uFF08\u767D\u6A21 tint\uFF09\n\tSceneDynamicObject _castles;  // castle.png\uFF08\u767D\u6A21 tint\uFF09\n\tint _builtVersion = -1;\n\tfloat _builtHalf = -1f;\n\tTimeSince _sinceRebuild = 999f;   // \u91CD\u5EFA\u8282\u6D41\uFF08\u5582\u65D7\u9AD8\u5CF0 host 5Hz \u5FEB\u7167\uFF0C\u5168\u6E05\u91CD\u5199\u4E09\u6279\u51E0\u4F55\u8FC7\u9891\u4F1A\u95EA\uFF09\n\n\tsealed class TexBatch\n\t{\n\t\tpublic SceneDynamicObject Dyno;\n\t\tpublic string Path;\n\t\tpublic Texture Tex;\n\t\tpublic Material Mat;   // \u6750\u8D28\u5F3A\u5F15\u7528\uFF08\u6258\u7BA1\u5305\u88C5\u88AB GC \u4F1A\u8FDE\u5E26\u56DE\u6536\u539F\u751F\u6750\u8D28 \u2192 \u7EA2\u51E0\u4F55\uFF0C\u5B9E\u6D4B\uFF09\n\t}\n\n\t/// \u003Csummary\u003E \u6750\u8D28\u5F3A\u5F15\u7528\u6C60\uFF1A\u6750\u8D28\u53EA\u88AB\u539F\u751F\u5BF9\u8C61\u5F15\u7528\u65F6\uFF0C\u6258\u7BA1\u5305\u88C5\u88AB GC \u540E\u539F\u751F\u6750\u8D28\u968F\u4E4B\u56DE\u6536\uFF0C\n\t/// \u573A\u666F\u5BF9\u8C61\u753B\u6210\u5F15\u64CE\u9519\u8BEF\u6750\u8D28\uFF08\u7EA2\u51E0\u4F55\uFF09\u3002\u6240\u6709\u521B\u5EFA\u7684\u6750\u8D28\u5728\u6B64\u7559\u6863\u3002 \u003C/summary\u003E\n\treadonly List\u003CMaterial\u003E _mats = new();\n\n\tMaterial TrackMat( Material m )\n\t{\n\t\tif ( m is not null ) _mats.Add( m );\n\t\treturn m;\n\t}\n\n\treadonly List\u003CTexBatch\u003E _texBatches = new();\n\tTimeSince _sinceTexHeal = 1f;\n\n\t/// \u003Csummary\u003E \u8C03\u8BD5 A/B\uFF08v0.7.8.104\uFF0Ccr_perf \u6E32\u67D3\u51CF\u8D1F\u5B9A\u4F4D\uFF09\uFF1A\u9690\u85CF/\u6062\u590D\u5168\u90E8\u9886\u571F\u6279\u6B21\n\t/// \uFF08\u7EC4\u4EF6 Enabled \u5173\u4E0D\u6389\u5DF2\u6784\u5EFA\u7684\u539F\u751F\u6279\u6B21\u2014\u2014SceneObject.RenderingEnabled \u624D\u662F\u771F\u5F00\u5173\uFF09 \u003C/summary\u003E\n\tpublic void SetDebugVisible( bool visible )\n\t{\n\t\tif ( _shape is not null ) _shape.RenderingEnabled = visible;\n\t\tif ( _flags is not null ) _flags.RenderingEnabled = visible;\n\t\tif ( _castles is not null ) _castles.RenderingEnabled = visible;\n\t\tforeach ( var b in _texBatches )\n\t\t\tif ( b.Dyno is not null ) b.Dyno.RenderingEnabled = visible;\n\t}\n\n\tconst float BorderThickness = 18f;         // \u683C\u8FB9\u6846\u7C97\u7EC6\uFF08\u4E16\u754C\u5355\u4F4D\uFF0C\u50CF\u7D20\u7C97\u8FB9\u89C2\u611F\uFF09\n\tconst float FlagWorldHeight = 400f;        // \u65D7\u5B50\u4E16\u754C\u9AD8\u5EA6\uFF08\u7EA6 1/7.5 \u683C\u5BBD\uFF09\n\tconst float CastleWorldFraction = 0.30f;   // \u57CE\u5821\u5360\u683C\u5BBD\u6BD4\u4F8B\n\tconst float BarWidth = 380f;               // \u8FDB\u5EA6\u6761\u5BBD\n\tconst float BarHeight = 36f;               // \u8FDB\u5EA6\u6761\u9AD8\n\tconst float FlagTexAspect = 32f / 52f;     // flag.png \u5BBD\u9AD8\u6BD4\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\t\tInstance = this;\n\t\t_shape = CreateShapeBatch();\n\t\t_flags = CreateTexBatch( \u0022ui/pixel/flag.png\u0022 );\n\t\t_castles = CreateTexBatch( \u0022ui/pixel/castle.png\u0022 );\n\t}\n\n\tprotected override void OnDestroy()\n\t{\n\t\tbase.OnDestroy();\n\n\t\t// \u7EC4\u4EF6\u9500\u6BC1\u65F6\u5FC5\u987B\u663E\u5F0F\u5220\u9664\u539F\u751F\u6279\u6B21\uFF1A\u4E16\u754C\u91CD\u5EFA\u53EA\u9500\u6BC1\u5B50 GameObject\uFF0C\u9057\u7559\u6279\u6B21\u4F1A\u5E26\u6700\u540E\u4E00\u5E27\n\t\t// \u51E0\u4F55\u7EE7\u7EED\u6E32\u67D3\uFF0C\u65E7\u6750\u8D28\u56DE\u6536\u540E\u56DE\u9000\u9519\u8BEF\u6750\u8D28\u2014\u2014\u6EE1\u5C4F\u7EA2\u51E0\u4F55 \u002B Vulkan \u7BA1\u7EBF\u5D29\u6E83\u7684\u5143\u51F6\uFF08\u5B9E\u6D4B\uFF09\n\t\tDeleteBatch( _shape );\n\t\tDeleteBatch( _flags );\n\t\tDeleteBatch( _castles );\n\t}\n\n\tstatic void DeleteBatch( SceneDynamicObject dyno )\n\t{\n\t\tif ( dyno is null ) return;\n\t\ttry { dyno.Delete(); } catch { }\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\t\tif ( !Game.IsPlaying ) return;\n\n\t\tvar half = GameConfig.ArenaHalfSize;\n\n\t\tif ( TerritoryManager.Active )\n\t\t{\n\t\t\t// \u91CD\u5EFA\u8282\u6D41\uFF08\u22650.25s\uFF09\uFF1A\u5582\u65D7\u9AD8\u5CF0 host 5Hz \u5FEB\u7167\uFF0C\u5168\u6E05\u91CD\u5199\u4E09\u6279\u51E0\u4F55\u8FC7\u9891\u4F1A\u95EA\uFF1B\n\t\t\t// \u9519\u8FC7\u7684\u7248\u672C\u4E0D\u8FFD\uFF0C\u4E0B\u4E00\u62CD\u81EA\u7136\u753B\u4E0A\uFF08\u8FDB\u5EA6\u5DEE 1/5 \u683C\u5206\u4E0D\u6E05\uFF09\n\t\t\tif ( ( _builtVersion != TerritoryManager.StateVersion || MathF.Abs( _builtHalf - half ) \u003E 0.5f )\n\t\t\t\t\u0026\u0026 _sinceRebuild \u003E 0.25f )\n\t\t\t\tRebuild( half );\n\t\t}\n\t\telse if ( _builtVersion \u003E= 0 )\n\t\t{\n\t\t\tClear();\n\t\t}\n\n\t\t// \u8D34\u56FE\u81EA\u6108\uFF081s \u8282\u6D41\uFF09\uFF1A\u70ED\u91CD\u8F7D/\u8D44\u4EA7\u5237\u65B0\u671F\u95F4\u52A0\u8F7D\u5931\u8D25\u8865\u8F7D\uFF0C\u5426\u5219\u6574\u573A\u9519\u8BEF\u7EA2\u7EB9\u7406\uFF08NeonRenderer \u5B9E\u6D4B\uFF09\n\t\tif ( _sinceTexHeal \u003E 1f )\n\t\t{\n\t\t\t_sinceTexHeal = 0;\n\t\t\tforeach ( var b in _texBatches )\n\t\t\t{\n\t\t\t\tif ( b.Tex is not null ) continue;\n\t\t\t\tLoadTex( b );\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid Clear()\n\t{\n\t\t_builtVersion = -1;\n\t\tif ( _shape.IsValid() ) _shape.Clear();\n\t\tif ( _flags.IsValid() ) _flags.Clear();\n\t\tif ( _castles.IsValid() ) _castles.Clear();\n\t}\n\n\tSceneDynamicObject CreateShapeBatch()\n\t{\n\t\tvar dyno = new SceneDynamicObject( Scene.SceneWorld );\n\t\tdyno.Material = TrackMat( LineMaterial.Create() );\n\t\tdyno.Flags.CastShadows = false;\n\t\tdyno.RenderLayer = SceneRenderLayer.OverlayWithDepth;\n\t\tdyno.Attributes.SetCombo( \u0022D_BLEND\u0022, 0 );   // \u50CF\u7D20\u98CE\u666E\u901A alpha\uFF08\u52A0\u8272\u53D1\u5149\u5DF2\u968F\u9713\u8679\u98CE\u5E9F\u5F03\uFF09\n\t\treturn dyno;\n\t}\n\n\tSceneDynamicObject CreateTexBatch( string path )\n\t{\n\t\tvar b = new TexBatch { Dyno = CreateShapeBatch(), Path = path };\n\t\t_texBatches.Add( b );\n\t\tLoadTex( b );\n\t\treturn b.Dyno;\n\t}\n\n\tvoid LoadTex( TexBatch b )\n\t{\n\t\tb.Tex = Texture.Load( b.Path, false );\n\t\tif ( b.Tex is null )\n\t\t\tLog.Warning( $\u0022[pixel] texture not ready: {b.Path} (1s \u540E\u81EA\u52A8\u91CD\u8BD5)\u0022 );\n\t\tb.Mat = TrackMat( NeonRenderer.MakeTexMaterial( b.Tex, pixel: true ) );   // Material \u53EA\u5199\uFF1A\u5148\u5907\u597D\u518D\u8D4B\n\t\tb.Dyno.Material = b.Mat;\n\t}\n\n\tvoid Rebuild( float half )\n\t{\n\t\t_builtVersion = TerritoryManager.StateVersion;\n\t\t_builtHalf = half;\n\t\t_sinceRebuild = 0;\n\n\t\t// \u8BCA\u65AD\uFF08\u4E34\u65F6\uFF0C\u5BA2\u6237\u7AEF HUD \u7206\u95EA\u6392\u67E5\uFF09\uFF1A\u8BB0\u5F55\u6BCF\u6B21\u91CD\u5EFA\u7684\u65F6\u523B\u4E0E\u7248\u672C\uFF0C\u7528 Log\uFF08\u5BA2\u6237\u7AEF\u65E5\u5FD7\u4E5F\u6253\uFF09\n\t\tLog.Info( $\u0022[terr] rebuild v={TerritoryManager.StateVersion} half={half:0}\u0022 );\n\n\t\t_shape.Clear();\n\t\t_shape.Init( Graphics.PrimitiveType.Triangles );\n\t\t_flags.Clear();\n\t\t_flags.Init( Graphics.PrimitiveType.Triangles );\n\t\t_castles.Clear();\n\t\t_castles.Init( Graphics.PrimitiveType.Triangles );\n\n\t\tvar grid = GameConfig.TerritoryGrid;\n\t\tvar s = TerritoryManager.CellSize;\n\t\tvar flagHalfH = FlagWorldHeight * 0.5f;\n\t\tvar flagHalfW = flagHalfH * FlagTexAspect;\n\t\tvar castleHalf = s * CastleWorldFraction * 0.5f;\n\n\t\tfor ( int cy = 0; cy \u003C grid; cy\u002B\u002B )\n\t\t{\n\t\t\tfor ( int cx = 0; cx \u003C grid; cx\u002B\u002B )\n\t\t\t{\n\t\t\t\tTerritoryManager.CellState( cx, cy, out var owner, out var scoreTeam, out var score );\n\t\t\t\tvar frac = Math.Clamp( score / GameConfig.TerritoryCaptureScore, 0f, 1f );\n\t\t\t\tvar center = TerritoryManager.CellCenter( cx, cy );\n\n\t\t\t\tvar x0 = -half \u002B cx * s;\n\t\t\t\tvar x1 = -half \u002B ( cx \u002B 1 ) * s;\n\t\t\t\tvar y0 = -half \u002B cy * s;\n\t\t\t\tvar y1 = -half \u002B ( cy \u002B 1 ) * s;\n\t\t\t\tvar isHq = IsHq( cx, cy );\n\n\t\t\t\t// \u4E3B\u8272\uFF1A\u5F52\u5C5E\u65B9\u961F\u8272\uFF1B\u4E89\u593A\u4E2D\u4E2D\u7ACB=\u767D\u8FB9\u6846\u002B\u4E89\u593A\u65B9\u8FDB\u5EA6\uFF1B\u65E0\u4EBA\u95EE\u6D25=\u7070\u65D7\n\t\t\t\tColor teamColor;\n\t\t\t\tbool contested = false;\n\t\t\t\tif ( owner \u003E= 0 )\n\t\t\t\t\tteamColor = MatchState.TeamColor( owner );\n\t\t\t\telse if ( scoreTeam \u003E= 0 \u0026\u0026 score \u003E 0f )\n\t\t\t\t{\n\t\t\t\t\tteamColor = MatchState.TeamColor( scoreTeam );\n\t\t\t\t\tcontested = true;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\tteamColor = new Color( 0.62f, 0.65f, 0.73f );\n\n\t\t\t\tif ( owner \u003E= 0 || contested )\n\t\t\t\t{\n\t\t\t\t\t// \u683C\u5E95\uFF1A\u8D8A\u6EE1\u8D8A\u5B9E\uFF08\u4E89\u593A\u4E2D\u66F4\u6DE1\u2014\u2014\u8FD8\u6CA1\u5230\u624B\uFF09\n\t\t\t\t\tvar fillA = owner \u003E= 0 ? 0.16f \u002B 0.18f * frac : 0.07f \u002B 0.10f * frac;\n\t\t\t\t\tvar fill = new Color( teamColor.r, teamColor.g, teamColor.b, fillA );\n\t\t\t\t\tAddQuadShape( x0 \u002B BorderThickness, y0 \u002B BorderThickness, x1 - BorderThickness, y1 - BorderThickness, 0.20f, fill );\n\n\t\t\t\t\t// \u7C97\u8FB9\u6846\uFF08\u50CF\u7D20\u9886\u5730\u611F\uFF1B\u4E89\u593A\u4E2D=\u767D\u8272\u865A\u4F4D\u4EE5\u5F85\uFF09\n\t\t\t\t\tvar lineC = owner \u003E= 0\n\t\t\t\t\t\t? new Color( teamColor.r, teamColor.g, teamColor.b, 0.85f )\n\t\t\t\t\t\t: new Color( 1f, 1f, 1f, 0.55f );\n\t\t\t\t\tAddBorder( x0, y0, x1, y1, 0.30f, lineC );\n\n\t\t\t\t\t// \u8FDB\u5EA6\u6761\uFF08\u9A7B\u519B/\u5360\u9886\u8FDB\u5EA6\uFF0C\u65D7/\u57CE\u5821\u4E0B\u65B9\uFF09\n\t\t\t\t\tif ( frac \u003E 0.01f )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar barTop = center.y \u002B FlagWorldHeight * 0.62f \u002B BarHeight;\n\t\t\t\t\t\tAddQuadShape( center.x - BarWidth * 0.5f, barTop - BarHeight, center.x \u002B BarWidth * 0.5f, barTop, 0.35f,\n\t\t\t\t\t\t\tnew Color( 0.16f, 0.15f, 0.22f, 0.5f ) );\n\t\t\t\t\t\tAddQuadShape( center.x - BarWidth * 0.5f, barTop - BarHeight, center.x - BarWidth * 0.5f \u002B BarWidth * frac, barTop, 0.35f,\n\t\t\t\t\t\t\tnew Color( teamColor.r, teamColor.g, teamColor.b, 0.95f ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// \u65D7/\u57CE\u5821\uFF1A\u5927\u672C\u8425=\u57CE\u5821\uFF0C\u666E\u901A\u683C=\u65D7\u5B50\uFF08rot=\u03C0/2 \u5BF9\u9F50\u5C4F\u5E55\u6B63\u7ACB\uFF09\n\t\t\t\tColor spriteTint = owner \u003E= 0 ? new Color( teamColor.r, teamColor.g, teamColor.b, 1f )\n\t\t\t\t\t: contested ? new Color( 1f, 1f, 1f, 1f )\n\t\t\t\t\t: new Color( 0.72f, 0.74f, 0.82f, 0.45f );\n\n\t\t\t\tif ( isHq )\n\t\t\t\t\tAddTexQuad( _castles, center, castleHalf, castleHalf, MathF.PI * 0.5f, spriteTint, 0.52f );\n\t\t\t\telse\n\t\t\t\t\tAddTexQuad( _flags, center, flagHalfW, flagHalfH, MathF.PI * 0.5f, spriteTint, 0.52f );\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic bool IsHq( int cx, int cy )\n\t{\n\t\tfor ( var t = 0; t \u003C 4; t\u002B\u002B )\n\t\t{\n\t\t\tvar hq = TerritoryManager.HqCell( t );\n\t\t\tif ( hq.Cx == cx \u0026\u0026 hq.Cy == cy ) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// ---- \u9876\u70B9\u5199\u5165\uFF08shape=\u7EAF\u8272 quad\uFF1Btex=UV quad\uFF0C\u5199\u6CD5\u540C NeonRenderer.AddQuad\uFF09----\n\n\tvoid AddQuadShape( float x0, float y0, float x1, float y1, float z, Color color )\n\t{\n\t\tvar a = new Vertex( new Vector3( x0, y0, z ), color );\n\t\tvar b = new Vertex( new Vector3( x1, y0, z ), color );\n\t\tvar c = new Vertex( new Vector3( x1, y1, z ), color );\n\t\tvar d = new Vertex( new Vector3( x0, y1, z ), color );\n\t\t_shape.AddVertex( a ); _shape.AddVertex( b ); _shape.AddVertex( c );\n\t\t_shape.AddVertex( a ); _shape.AddVertex( c ); _shape.AddVertex( d );\n\t}\n\n\tvoid AddBorder( float x0, float y0, float x1, float y1, float z, Color color )\n\t{\n\t\t// \u56DB\u6761\u7C97\u8FB9\u5411\u5185\u94FA\uFF0C\u4E0D\u8D8A\u8FC7\u683C\u754C\n\t\tAddQuadShape( x0, y0, x1, y0 \u002B BorderThickness, z, color );\n\t\tAddQuadShape( x0, y1 - BorderThickness, x1, y1, z, color );\n\t\tAddQuadShape( x0, y0 \u002B BorderThickness, x0 \u002B BorderThickness, y1 - BorderThickness, z, color );\n\t\tAddQuadShape( x1 - BorderThickness, y0 \u002B BorderThickness, x1, y1 - BorderThickness, z, color );\n\t}\n\n\tvoid AddTexQuad( SceneDynamicObject batch, Vector3 center, float halfX, float halfY, float rot, Color color, float z )\n\t{\n\t\tvar cos = MathF.Cos( rot );\n\t\tvar sin = MathF.Sin( rot );\n\t\tvar rx = cos * halfX; var ry = sin * halfX;      // right \u8F74\n\t\tvar ux = -sin * halfY; var uy = cos * halfY;     // up \u8F74\n\n\t\tvar v0 = new Vertex( new Vector3( center.x - rx - ux, center.y - ry - uy, z ), new Vector4( 0f, 0f, 0f, 0f ), color );\n\t\tvar v1 = new Vertex( new Vector3( center.x \u002B rx - ux, center.y \u002B ry - uy, z ), new Vector4( 1f, 0f, 0f, 0f ), color );\n\t\tvar v2 = new Vertex( new Vector3( center.x \u002B rx \u002B ux, center.y \u002B ry \u002B uy, z ), new Vector4( 1f, 1f, 0f, 0f ), color );\n\t\tvar v3 = new Vertex( new Vector3( center.x - rx \u002B ux, center.y - ry \u002B uy, z ), new Vector4( 0f, 1f, 0f, 0f ), color );\n\n\t\tbatch.AddVertex( v0 ); batch.AddVertex( v1 ); batch.AddVertex( v2 );\n\t\tbatch.AddVertex( v0 ); batch.AddVertex( v2 ); batch.AddVertex( v3 );\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Bot/BotStats.cs","FileName":"BotStats.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Text;\n\n/// \u003Csummary\u003E\n/// bot \u884C\u4E3A\u7EDF\u8BA1\uFF08v0.7.8.95\uFF0C\u8C03\u53C2\u7528\uFF09\uFF1Ahost \u4FA7\u6309\u7403 Id \u8BB0\u5404\u884C\u4E3A\u7684\u89E6\u53D1\u8BA1\u6570\uFF0C\n/// \u7F16\u8F91\u5668 MCP \u0060cr_botstats\u0060 \u8BFB\u53D6\u3002\u7EAF\u5185\u5B58\u3001\u4E0D\u8054\u7F51\uFF1B\u70ED\u91CD\u8F7D\u4F1A\u6E05\u96F6\uFF08\u53EF\u63A5\u53D7\uFF09\u3002\n/// \u76EE\u7684\uFF1A\u8C03 bot \u6570\u503C\u6709\u6570\u636E\u53EF\u770B\uFF08\u9003\u8DD1/\u8FFD\u730E/\u5582\u4EBA/\u5582\u65D7/\u6280\u80FD\u7684\u5206\u5E03\uFF09\uFF0C\u4E0D\u9760\u8089\u773C\u731C\u3002\n/// \u003C/summary\u003E\npublic static class BotStats\n{\n\t/// \u003Csummary\u003E \u4E00\u53EA bot \u7684\u7D2F\u8BA1\u8BA1\u6570\uFF08Difficulty/Archetype \u5B58 byte \u907F\u514D\u6E38\u620F\u7A0B\u5E8F\u96C6\u7C7B\u578B\u51FA\u73B0\u5728\u952E\u4E0A\uFF09 \u003C/summary\u003E\n\tpublic sealed class Entry\n\t{\n\t\tpublic string Name = \u0022\u0022;\n\t\tpublic int Team = -1;\n\t\tpublic byte Difficulty;\n\t\tpublic byte Archetype;\n\t\tpublic TimeSince Seen;   // \u6700\u540E\u4E00\u6B21\u51B3\u7B56\u65F6\u523B\uFF0830s \u4E0D\u6D3B\u8DC3\u7684\u6761\u76EE\u4E0D\u518D\u8F93\u51FA\uFF09\n\t\tpublic long Decisions, Flees, Kites, Bequests, Hunts, SplitEscapes, SplitHunts, FeedMates, FeedFlags, SpikeBombs, Skills;\n\t}\n\n\tstatic readonly Dictionary\u003CGuid, Entry\u003E _byId = new();\n\n\t/// \u003Csummary\u003E \u6761\u76EE\u6570\u4E0A\u9650\uFF1A\u7403\u5BF9\u8C61\u4F1A\u88AB\u9500\u6BC1\u91CD\u5EFA\uFF08\u83DC\u5355\u6F14\u793A\u8D5B\u6536\u573A\u3001\u56DE\u83DC\u5355\u6E05\u573A\uFF09\uFF0C\u800C _byId \u6309 Ball.Id\n\t/// \u7D2F\u79EF\u3001\u6761\u76EE\u6C38\u4E0D\u5931\u6548\u2014\u2014\u957F\u4F1A\u8BDD\u4F1A\u628A\u0022\u8FD9\u53F0\u673A\u5668\u89C1\u8FC7\u7684\u6240\u6709 bot \u7403\u0022\u90FD\u7559\u5728\u5B57\u5178\u91CC\u3002\u8D85\u8FC7\u4E0A\u9650\u65F6\n\t/// \u6E05\u6389\u4E45\u672A\u51B3\u7B56\uFF08Seen \u003E PruneAfterSeconds\uFF09\u7684\u6761\u76EE\uFF08v0.7.8.105\uFF09\u3002 \u003C/summary\u003E\n\tconst int MaxEntries = 256;\n\tconst float PruneAfterSeconds = 60f;\n\n\t/// \u003Csummary\u003E \u56DE\u6536\u4E45\u672A\u51B3\u7B56\u7684\u6761\u76EE\uFF08Track \u5185\u6309\u9700\u89E6\u53D1\uFF0C\u4E0D\u505A\u6BCF\u5E27\u626B\u63CF\uFF09 \u003C/summary\u003E\n\tstatic readonly List\u003CGuid\u003E _pruneScratch = new();\n\n\tstatic void PruneStale()\n\t{\n\t\tif ( _byId.Count \u003C= MaxEntries ) return;\n\n\t\t_pruneScratch.Clear();\n\t\tforeach ( var kv in _byId )\n\t\t{\n\t\t\tif ( kv.Value.Seen \u003E PruneAfterSeconds ) _pruneScratch.Add( kv.Key );\n\t\t}\n\n\t\tforeach ( var k in _pruneScratch ) _byId.Remove( k );\n\t}\n\n\t/// \u003Csummary\u003E \u53D6/\u5EFA\u4E00\u53EA bot \u7684\u8BA1\u6570\u6761\u76EE\uFF08BotBrain \u6BCF\u6B21\u51B3\u7B56\u8C03\u4E00\u6B21\uFF0C\u987A\u5E26\u5237\u65B0\u540D\u5B57/\u961F\u4F0D/\u6D3B\u8DC3\u65F6\u523B\uFF09 \u003C/summary\u003E\n\tpublic static Entry Track( Ball ball, byte difficulty, byte archetype )\n\t{\n\t\tif ( ball is null || !ball.IsValid() ) return null;\n\t\tPruneStale();   // \u8D85\u4E0A\u9650\u65F6\u6E05\u4E45\u672A\u51B3\u7B56\u7684\u6761\u76EE\uFF0C\u9632\u5B57\u5178\u968F\u0022\u89C1\u8FC7\u7684 bot \u7403\u603B\u6570\u0022\u65E0\u754C\u589E\u957F\n\t\tif ( !_byId.TryGetValue( ball.Id, out var e ) )\n\t\t{\n\t\t\te = new Entry();\n\t\t\t_byId[ball.Id] = e;\n\t\t}\n\t\te.Name = ball.PlayerName;\n\t\te.Team = ball.TeamIndex;\n\t\te.Difficulty = difficulty;\n\t\te.Archetype = archetype;\n\t\te.Seen = 0;\n\t\treturn e;\n\t}\n\n\tpublic static string Dump()\n\t{\n\t\t// \u53EA\u8F93\u51FA 30 \u79D2\u5185\u8FD8\u6D3B\u7740\u7684 bot\u2014\u2014\u8DE8\u5C40\u6B8B\u7559\u7684\u65E7\u7403\u6761\u76EE\u4E0D\u518D\u5237\u5C4F\n\t\tif ( _byId.Count == 0 ) return \u0022no bot stats yet\u0022;\n\t\tvar sb = new StringBuilder();\n\t\tint shown = 0;\n\t\tsb.AppendLine( \u0022name | team | diff | archetype | dec | flee | kite | bequest | hunt | spEsc | spHunt | feed | feedFlag | bomb | skill\u0022 );\n\t\tforeach ( var e in _byId.Values )\n\t\t{\n\t\t\tif ( e.Seen \u003E 30f ) continue;\n\t\t\tshown\u002B\u002B;\n\t\t\tsb.AppendLine(\n\t\t\t\t$\u0022{e.Name} | T{e.Team} | {(BotDifficulty)e.Difficulty} | {(BotArchetype)e.Archetype} | \u0022 \u002B\n\t\t\t\t$\u0022{e.Decisions} | {e.Flees} | {e.Kites} | {e.Bequests} | {e.Hunts} | {e.SplitEscapes} | {e.SplitHunts} | \u0022 \u002B\n\t\t\t\t$\u0022{e.FeedMates} | {e.FeedFlags} | {e.SpikeBombs} | {e.Skills}\u0022 );\n\t\t}\n\t\tsb.Insert( 0, $\u0022bots={shown} (of {_byId.Count} tracked)\\n\u0022 );\n\t\treturn sb.ToString();\n\t}\n}\n"},{"Ident":"slfy.circleroyale","Path":"UI/EndBoard.cs","FileName":"EndBoard.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Sandbox.UI;\n\n/// \u003Csummary\u003E\n/// \u7ED3\u7B97\u9762\u677F\uFF08M5\uFF09\uFF1A\u6BD4\u8D5B\u7ED3\u675F\u5168\u5C4F\u7ED3\u7B97\u2014\u2014\u672C\u5C40\u6392\u884C\uFF08\u9010\u884C\u6ED1\u5165\u52A8\u753B\uFF09\u002B \u5168\u670D\u6392\u884C\n/// \uFF08\u4E91\u7AEF\u4F18\u5148 ScoreUploader\uFF0C\u7A7A\u4E86\u964D\u7EA7 LocalBoard \u672C\u5730\u699C\uFF09\u002B \u56DE\u83DC\u5355\u6309\u94AE\u3002\n/// \u7EAF C# Panel \u002B \u540C\u76EE\u5F55 EndBoard.cs.scss\uFF1B\u663E\u793A\u671F\u95F4\u5168\u573A\u6A21\u62DF\u51BB\u7ED3\uFF08MatchState.MatchOver\uFF09\u3002\n/// \u003C/summary\u003E\npublic sealed class EndBoard : PanelComponent\n{\n\tLabel _title;\n\tLabel _winner;\n\tImage _crown;    // \u51A0\u519B\u7687\u51A0\uFF08v0.7.8.38 \u50CF\u7D20\u98CE\uFF09\uFF1A\u60AC\u5728 winner \u884C\u4E0A\u65B9\u5C45\u4E2D\n\tLabel _matchHead;\n\tLabel[] _matchRows;\n\tLabel _selfRow;\n\tLabel _globalHead;\n\tLabel[] _globalRows;\n\tLabel _globalNote;\n\tLabel _back;\n\n\tbool _built;\n\tbool _shown;\n\tTimeSince _sinceShow;\n\tint _backSecs = -1;   // \u8FD4\u56DE\u6309\u94AE\u4E0A\u6B21\u663E\u793A\u7684\u79D2\u6570\uFF08\u53D8\u4E86\u624D\u91CD\u62FC\u6587\u672C\uFF0Cv0.7.8.87\uFF09\n\n\tScoreWire[] _standings;\n\tlong _mySteamId;\n\n\t// \u9010\u884C reveal \u52A8\u753B\uFF1A\u884C\u7D22\u5F15 \u2192 \u51FA\u573A\u65F6\u523B\uFF08\u79D2\uFF09\uFF0C\u900F\u660E\u5EA6/\u4F4D\u79FB\u5728 0.25s \u5185\u6E10\u5165\n\tconst float MatchRowDelay = 0.45f;\n\tconst float MatchRowStagger = 0.22f;\n\tconst float GlobalDelay = 3.4f;\n\tconst float GlobalStagger = 0.16f;\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\t\tBuildUi();\n\t\tHide();\n\t}\n\n\tvoid BuildUi()\n\t{\n\t\tvar root = Panel;\n\t\tif ( root is null ) return;\n\t\tif ( _built \u0026\u0026 _title.IsValid() \u0026\u0026 root.ChildrenCount \u003E 0 ) return;\n\n\t\troot.DeleteChildren( true );\n\n\t\troot.Style.Position = PositionMode.Absolute;\n\t\troot.Style.Left = 0f;\n\t\troot.Style.Top = 0f;\n\t\troot.Style.Right = 0f;\n\t\troot.Style.Bottom = 0f;\n\t\troot.Style.BackgroundColor = new Color( 0.008f, 0.012f, 0.05f, 0.88f );\n\t\troot.Style.PointerEvents = PointerEvents.All;\n\n\t\t// \u6807\u9898/\u51A0\u519B/\u56DE\u83DC\u5355\u5C45\u4E2D\uFF1B\u672C\u5C40\u6392\u884C\uFF08\u5DE6\u5217\uFF09\u4E0E\u5168\u670D\u6392\u884C\uFF08\u53F3\u5217\uFF09\u5E76\u6392\uFF08v0.6.3.0 \u7528\u6237\u5B9A\u7A3F\uFF09\n\t\t_title = NewRow( root, 88f, 360f, 1200f, \u0022eb-title\u0022 );\n\t\t_title.Text = \u0022MATCH OVER\u0022;\n\n\t\t_winner = NewRow( root, 166f, 360f, 1200f, \u0022eb-winner\u0022 );\n\n\t\t_crown = new Image();\n\t\t_crown.Texture = Texture.Load( \u0022ui/pixel/crown.png\u0022 );\n\t\t_crown.Style.Position = PositionMode.Absolute;\n\t\t_crown.Style.Width = 22f;\n\t\t_crown.Style.Height = 22f;\n\t\t_crown.Style.Display = DisplayMode.None;\n\t\troot.AddChild( _crown );\n\n\t\t_matchHead = NewRow( root, 244f, 260f, 580f, \u0022eb-head\u0022 );\n\t\t_matchHead.Text = \u0022MATCH RANKING\u0022;\n\n\t\t// \u672C\u5C40\u6392\u884C\uFF1A10 \u884C \u002B \u699C\u5916\u81EA\u8EAB\u884C\uFF08\u5DE6\u5217\uFF09\n\t\t_matchRows = new Label[GameConfig.LeaderboardRows];\n\t\tfor ( int i = 0; i \u003C _matchRows.Length; i\u002B\u002B )\n\t\t{\n\t\t\t_matchRows[i] = NewRow( root, 276f \u002B i * 28f, 260f, 580f, \u0022eb-row\u0022 );\n\t\t}\n\n\t\t_selfRow = NewRow( root, 276f \u002B _matchRows.Length * 28f, 260f, 580f, \u0022eb-row self\u0022 );\n\t\t_selfRow.Style.Display = DisplayMode.None;\n\n\t\t_globalHead = NewRow( root, 244f, 1060f, 580f, \u0022eb-head global\u0022 );\n\t\t_globalHead.Text = \u0022GLOBAL TOP 10\u0022;\n\n\t\t_globalRows = new Label[10];\n\t\tfor ( int i = 0; i \u003C _globalRows.Length; i\u002B\u002B )\n\t\t{\n\t\t\t_globalRows[i] = NewRow( root, 276f \u002B i * 28f, 1060f, 580f, \u0022eb-row small\u0022 );\n\t\t}\n\n\t\t_globalNote = NewRow( root, 276f \u002B _globalRows.Length * 28f, 1060f, 580f, \u0022eb-note\u0022 );\n\n\t\t_back = NewRow( root, 620f, 360f, 1200f, \u0022eb-back\u0022 );\n\t\t_back.Text = Networking.IsActive ? \u0022RETURN TO LOBBY \u2014 [SPACE]\u0022 : \u0022BACK TO MENU \u2014 [SPACE]\u0022;\n\t\t_back.AddEventListener( \u0022onclick\u0022, () =\u003E OnBackClicked() );\n\n\t\t_built = true;\n\t}\n\n\t/// \u003Csummary\u003E \u5EFA\u4E00\u884C Label\uFF1Aleft/width \u662F 1920 \u8BBE\u8BA1\u7A3F\u5750\u6807\uFF0C\u6362\u7B97\u6210\u0022\u4E2D\u5FC3\u951A\u5B9A \u002B \u76F8\u5BF9\u504F\u79FB\u0022\u2014\u2014\n\t/// \u4EFB\u610F\u5206\u8FA8\u7387\u4E0B\u6574\u4F53\u4FDD\u6301\u5C45\u4E2D\uFF08left:960 \u5B9A\u4F4D\u5728\u7A84\u7A97\u53E3\u4F1A\u6574\u4F53\u504F\u53F3\uFF0C\u5B9E\u6D4B\uFF09 \u003C/summary\u003E\n\tLabel NewRow( Panel root, float top, float left, float width, string classes )\n\t{\n\t\tvar l = new Label() { Classes = classes };\n\t\tl.Style.Position = PositionMode.Absolute;\n\t\tl.Style.Left = Length.Percent( 50f );\n\t\tl.Style.MarginLeft = left - 960f;\n\t\tl.Style.Width = width;\n\t\tl.Style.Top = top;\n\t\troot.AddChild( l );\n\t\treturn l;\n\t}\n\n\t/// \u003Csummary\u003E \u6BD4\u8D5B\u7ED3\u675F\u5C55\u793A\u7ED3\u7B97\uFF08CircleroyaleGame.OnMatchOverRemote \u8C03\u7528\uFF0C\u53CC\u7AEF\uFF09 \u003C/summary\u003E\n\tpublic void Show( ScoreWire[] standings, long mySteamId, string myName )\n\t{\n\t\t_standings = standings;\n\t\t_mySteamId = mySteamId;\n\n\t\tBuildUi();\n\t\tif ( !_built ) return;\n\n\t\t_sinceShow = 0;\n\t\t_backSecs = -1;   // \u5F3A\u5236\u91CD\u62FC\u8FD4\u56DE\u6309\u94AE\u6587\u672C\uFF08v0.7.8.87\uFF09\n\t\t_shown = true;\n\t\tPanel.Style.Display = DisplayMode.Flex;\n\n\t\t_winner.Text = WinnerText( standings );\n\n\t\tFillMatchRows();\n\t\tFillGlobalPlaceholder();\n\t\tScoreUploader.FetchGlobal( ApplyGlobal );\n\t}\n\n\t/// \u003Csummary\u003E \u56DE\u5927\u5385/\u4E3B\u83DC\u5355\uFF08\u5B9E\u4F8B\u65B9\u6CD5\uFF1A\u70ED\u91CD\u8F7D\u53EF\u91CD\u6620\u5C04\uFF09 \u003C/summary\u003E\n\tvoid OnBackClicked() =\u003E CircleroyaleGame.Current?.AfterSettlement();\n\n\t/// \u003Csummary\u003E \u56DE\u5927\u5385\u65F6\u6536\u8D77\uFF08\u9762\u677F\u968F GameObject \u590D\u7528\uFF0C\u4E0B\u6B21\u5F00\u5C40\u518D\u5C55\u793A\uFF09 \u003C/summary\u003E\n\tpublic void Hide()\n\t{\n\t\t_shown = false;\n\t\tif ( Panel.IsValid() ) Panel.Style.Display = DisplayMode.None;\n\t}\n\n\t// ---- \u5185\u5BB9\u586B\u5145 ----\n\n\tstring WinnerText( ScoreWire[] rows )\n\t{\n\t\tif ( rows is null || rows.Length == 0 ) return \u0022\u0022;\n\n\t\tif ( !MatchState.IsTeam )\n\t\t\treturn $\u0022WINNER \u2014 {rows[0].Name}   ({rows[0].Mass:0})\u0022;\n\n\t\t// \u9886\u571F\u6218\u4E89\uFF08M7.4 \u79EF\u5206\u5236\uFF09\uFF1A\u80DC\u8D1F\u6309\u5360\u65D7\u79EF\u5206\uFF08host \u6392\u5E8F\u540C\u6E90\uFF0C\u8FD9\u91CC\u53EA\u53D6\u6587\u6848\uFF09\n\t\tif ( MatchState.IsTerritory )\n\t\t{\n\t\t\tint bestT = -1;\n\t\t\tfloat bestPts = -1f;\n\t\t\tfor ( var t = 0; t \u003C 4; t\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar pts = TerritoryManager.Points( t );\n\t\t\t\tif ( pts \u003E bestPts )\n\t\t\t\t{\n\t\t\t\t\tbestPts = pts;\n\t\t\t\t\tbestT = t;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn $\u0022TEAM {bestT \u002B 1} WINS   ({bestPts:0} PTS)\u0022;\n\t\t}\n\n\t\t// \u56E2\u961F\u8D5B\uFF1A\u80DC\u8D1F\u6309\u961F\u4F0D 3 \u4EBA\u603B\u5206\n\t\tvar totals = new Dictionary\u003Cint, float\u003E();\n\t\tforeach ( var r in rows )\n\t\t{\n\t\t\ttotals.TryGetValue( r.Team, out var m );\n\t\t\ttotals[r.Team] = m \u002B r.Mass;\n\t\t}\n\n\t\tint best = -1;\n\t\tfloat bestMass = -1f;\n\t\tforeach ( var kv in totals )\n\t\t{\n\t\t\tif ( kv.Value \u003E bestMass )\n\t\t\t{\n\t\t\t\tbestMass = kv.Value;\n\t\t\t\tbest = kv.Key;\n\t\t\t}\n\t\t}\n\t\treturn $\u0022TEAM {best \u002B 1} WINS   ({bestMass:0})\u0022;\n\t}\n\n\tvoid FillMatchRows()\n\t{\n\t\tvar rows = _standings;\n\t\tif ( rows is null || rows.Length == 0 )\n\t\t{\n\t\t\t// \u65E0\u540D\u5355\uFF08\u53D6\u6D88\u5C40/\u7A7A\u7ED3\u7B97\uFF09\uFF1A\u6E05\u884C\u800C\u4E0D\u662F NRE\uFF08v0.7.8.87\uFF1BWinnerText \u65E9\u5DF2\u6309 null \u9632\u8FC7\uFF09\n\t\t\tfor ( int i = 0; i \u003C _matchRows.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( _matchRows[i].IsValid() \u0026\u0026 _matchRows[i].Text.Length \u003E 0 ) _matchRows[i].Text = \u0022\u0022;\n\t\t\t}\n\t\t\tif ( _selfRow.IsValid() \u0026\u0026 _selfRow.Style.Display != DisplayMode.None ) _selfRow.Style.Display = DisplayMode.None;\n\t\t\treturn;\n\t\t}\n\n\t\tint selfRank = -1;\n\t\tfor ( int i = 0; i \u003C rows.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( rows[i].SteamId == _mySteamId \u0026\u0026 !rows[i].Bot )\n\t\t\t{\n\t\t\t\tselfRank = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tfor ( int i = 0; i \u003C _matchRows.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar row = _matchRows[i];\n\t\t\tif ( row is null || !row.IsValid() ) continue;   // \u8FC7\u671F\u5F15\u7528\u4E0D\u53EF\u5199\uFF08v0.7.8.87\uFF09\n\n\t\t\tif ( i \u003E= rows.Length )\n\t\t\t{\n\t\t\t\trow.Text = \u0022\u0022;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar r = rows[i];\n\t\t\trow.Text = RowText( $\u0022{i \u002B 1}.\u0022, r.Name, $\u0022{r.Mass:0}\u0022, r.Team );\n\n\t\t\tbool isSelf = r.SteamId == _mySteamId \u0026\u0026 !r.Bot;\n\t\t\trow.Style.FontColor = isSelf ? new Color( 0.898f, 0.282f, 0.553f ) : new Color( 0.29f, 0.23f, 0.39f, 0.85f );\n\t\t}\n\n\t\t// \u699C\u5916\u81EA\u8EAB\u884C\n\t\tbool showSelf = selfRank \u003E= _matchRows.Length;\n\t\tif ( _selfRow.IsValid() )\n\t\t{\n\t\t\t_selfRow.Style.Display = showSelf ? DisplayMode.Flex : DisplayMode.None;\n\t\t\tif ( showSelf )\n\t\t\t{\n\t\t\t\tvar r = rows[selfRank];\n\t\t\t\t_selfRow.Text = RowText( $\u0022#{selfRank \u002B 1}.\u0022, r.Name, $\u0022{r.Mass:0}\u0022, r.Team );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u884C\u6587\u672C\uFF08\u56E2\u961F\u8D5B\u5E26 T# \u524D\u7F00\uFF1B\u666E\u901A\u8D5B\u53EA\u6709\u540D\u6B21/\u540D\u5B57/\u5206\u6570\uFF09 \u003C/summary\u003E\n\tstring RowText( string rank, string name, string mass, byte team )\n\t{\n\t\treturn MatchState.IsTeam ? $\u0022{rank} [T{team \u002B 1}] {name}   {mass}\u0022 : $\u0022{rank}  {name}   {mass}\u0022;\n\t}\n\n\t/// \u003Csummary\u003E \u5168\u670D\u699C\u5148\u5360\u4F4D\u0022\u52A0\u8F7D\u4E2D\u0022\uFF0C\u4E91\u7AEF\u56DE\u6765\u540E\u586B\u5145\uFF08\u7A7A\u4E86\u964D\u7EA7\u672C\u5730\u699C\uFF09 \u003C/summary\u003E\n\tvoid FillGlobalPlaceholder()\n\t{\n\t\t_globalRows[0].Text = \u0022LOADING ...\u0022;\n\t\tfor ( int i = 1; i \u003C _globalRows.Length; i\u002B\u002B )\n\t\t\t_globalRows[i].Text = \u0022\u0022;\n\t\t_globalNote.Text = \u0022\u0022;\n\t}\n\n\tvoid ApplyGlobal( List\u003C( string Name, float Mass )\u003E entries )\n\t{\n\t\tif ( !_shown ) return;\n\t\tif ( _globalRows is null || _globalRows[0] is null || !_globalRows[0].IsValid() ) return;\n\n\t\tif ( entries is null )\n\t\t{\n\t\t\t// \u4E91\u7AEF\u4E0D\u53EF\u7528\uFF08\u672A\u53D1\u5E03\u5305\u662F\u5E38\u6001\uFF09\uFF1A\u964D\u7EA7\u672C\u673A\u6301\u4E45\u5316\u699C\n\t\t\tentries = LocalBoard.Top( _globalRows.Length );\n\t\t\t_globalNote.Text = entries is not null ? \u0022OFFLINE BOARD (THIS MACHINE)\u0022 : \u0022NO RECORDS YET\u0022;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_globalNote.Text = \u0022BEST SINGLE-MATCH SCORE\u0022;\n\t\t}\n\n\t\tfor ( int i = 0; i \u003C _globalRows.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( entries is null || i \u003E= entries.Count )\n\t\t\t{\n\t\t\t\t_globalRows[i].Text = \u0022\u0022;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t_globalRows[i].Text = $\u0022{i \u002B 1}.  {entries[i].Name}   {entries[i].Mass:0}\u0022;\n\t\t}\n\t}\n\n\t// ---- \u9010\u5E27\uFF1Areveal \u52A8\u753B \u002B \u5149\u6807 \u002B \u7A7A\u683C\u56DE\u83DC\u5355 ----\n\n\tprotected override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\n\t\tBuildUi();\n\t\tif ( !_shown ) return;\n\n\t\tif ( Panel.IsValid() \u0026\u0026 Panel.Style.Display != DisplayMode.Flex ) Panel.Style.Display = DisplayMode.Flex;   // \u540C\u503C\u5B88\u536B\uFF08v0.7.8.87\uFF09\n\t\tMouse.Visibility = MouseVisibility.Visible;   // \u7ED3\u7B97\u671F\u6309\u94AE\u8981\u80FD\u70B9\n\n\t\t// \u7A7A\u683C\u63D0\u524D\u8FD4\u56DE\uFF08\u6309\u94AE\u6587\u6848\u5199\u7740 [SPACE]\uFF09\uFF1A\u653E\u5728\u5165\u573A\u52A8\u753B\u4E4B\u524D\uFF0C\u6309\u4E0B\u5373\u54CD\u5E94\u3002\n\t\t// MatchOver \u540E Tick/Ball.OnUpdate \u90FD\u4E0D\u518D\u8DD1\uFF0C\u8FD9\u91CC\u4E0D\u63A5\u5C31\u6CA1\u4EBA\u63A5\u4E86\u3002\n\t\t// BallInput.Typing \u5B88\u536B\uFF1A\u804A\u5929\u6846\u805A\u7126\u65F6\u6309\u952E\u4F1A\u65E0\u5DEE\u522B\u8FDB InputContext\uFF0C\u6253\u5B57\u6253\u7A7A\u683C\u4E0D\u80FD\u9000\u5C40\n\t\tif ( !BallInput.Typing \u0026\u0026 Input.Pressed( \u0022Jump\u0022 ) )\n\t\t{\n\t\t\tCircleroyaleGame.Current?.AfterSettlement();\n\t\t\treturn;\n\t\t}\n\n\t\tfloat t = _sinceShow;\n\t\tAnimate( _title, t, 0.05f );\n\t\tAnimate( _winner, t, 0.2f );\n\t\tAnimate( _matchHead, t, MatchRowDelay - 0.2f );\n\n\t\tfor ( int i = 0; i \u003C _matchRows.Length; i\u002B\u002B )\n\t\t\tAnimate( _matchRows[i], t, MatchRowDelay \u002B i * MatchRowStagger );\n\n\t\tAnimate( _selfRow, t, MatchRowDelay \u002B _matchRows.Length * MatchRowStagger );\n\t\tAnimate( _globalHead, t, GlobalDelay - 0.2f );\n\n\t\tfor ( int i = 0; i \u003C _globalRows.Length; i\u002B\u002B )\n\t\t\tAnimate( _globalRows[i], t, GlobalDelay \u002B i * GlobalStagger );\n\n\t\tAnimate( _globalNote, t, GlobalDelay \u002B _globalRows.Length * GlobalStagger );\n\t\tAnimate( _back, t, GlobalDelay \u002B _globalRows.Length * GlobalStagger \u002B 0.3f );\n\n\t\t// \u7687\u51A0\u60AC\u5728\u51A0\u519B\u884C\u6B63\u4E0A\u65B9\uFF08winner \u662F\u5B9A\u5BBD\u5C45\u4E2D\u884C\uFF0C\u6587\u5B57\u4E2D\u5FC3\u6052 960\uFF1B\u900F\u660E\u5EA6\u968F\u5176\u5165\u573A\u52A8\u753B\uFF09\n\t\tif ( _crown.IsValid() )\n\t\t{\n\t\t\tvar show = _winner.IsValid() \u0026\u0026 !string.IsNullOrEmpty( _winner.Text );\n\t\t\tvar crownMode = show ? DisplayMode.Flex : DisplayMode.None;\n\t\t\tif ( _crown.Style.Display != crownMode ) _crown.Style.Display = crownMode;\n\t\t\tif ( show )\n\t\t\t{\n\t\t\t\t// \u4F4D\u7F6E/\u951A\u70B9\u5E38\u91CF\u53EA\u5728\u9996\u5E27\u5199\u4E00\u6B21\uFF08\u6BCF\u5E27\u540C\u503C\u5199=\u5E03\u5C40\u91CD\u7B97\uFF0Cv0.7.8.87\uFF09\uFF1BTop \u662F\u6D6E\u52A8\u52A8\u753B\uFF0C\u6309\u9608\u503C\u5199\n\t\t\t\tif ( !SamePx( _crown.Style.MarginLeft, -11f ) )\n\t\t\t\t{\n\t\t\t\t\t_crown.Style.Left = Length.Percent( 50f );   // \u51A0\u519B\u884C\u4E2D\u5FC3\u951A\u5B9A\u540E\u7687\u51A0\u8DDF\u968F\u9762\u677F\u4E2D\u5FC3\n\t\t\t\t\t_crown.Style.MarginLeft = -11f;             // 22px \u5BBD\u7684\u534A\u4E2A\n\t\t\t\t}\n\n\t\t\t\tvar top = 138f \u002B MathF.Sin( t * 3f ) * 4f;\n\t\t\t\tif ( !SamePx( _crown.Style.Top, top ) ) _crown.Style.Top = top;\n\t\t\t\tif ( !SameFloat( _crown.Style.Opacity, _winner.Style.Opacity ?? 1f ) ) _crown.Style.Opacity = _winner.Style.Opacity;\n\t\t\t}\n\t\t}\n\n\t\t// \u81EA\u52A8\u8FD4\u56DE\uFF08v0.6.4.2\uFF09\uFF1A\u7ED3\u7B97\u663E\u793A 10 \u79D2\u540E\u56DE\u5927\u5385\uFF08\u591A\u4EBA\uFF09/\u4E3B\u83DC\u5355\uFF08\u5355\u673A\uFF09\uFF0C\u7A7A\u683C\u53EF\u63D0\u524D\n\t\tvar remain = GameConfig.SettlementAutoReturnSeconds - _sinceShow;\n\t\tif ( remain \u003C= 0f )\n\t\t{\n\t\t\tCircleroyaleGame.Current?.AfterSettlement();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _back.IsValid() )\n\t\t{\n\t\t\t// \u79D2\u6570\u53D8\u4E86\u624D\u91CD\u62FC\u6587\u672C\uFF08\u6BCF\u5E27\u63D2\u503C=\u6BCF\u5E27\u4E00\u6B21\u5B57\u7B26\u4E32\u5206\u914D \u002B \u5E03\u5C40\u5199\uFF0Cv0.7.8.87\uFF09\n\t\t\tvar secs = (int)MathF.Ceiling( remain );\n\t\t\tif ( secs != _backSecs )\n\t\t\t{\n\t\t\t\t_backSecs = secs;\n\t\t\t\t_back.Text = Networking.IsActive\n\t\t\t\t\t? $\u0022RETURN TO LOBBY IN {secs} \u2014 [SPACE NOW]\u0022\n\t\t\t\t\t: $\u0022BACK TO MENU IN {secs} \u2014 [SPACE NOW]\u0022;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u5355\u884C\u6E10\u5165\uFF1A\u51FA\u573A\u524D Opacity 0\uFF1B0.25s \u5185\u900F\u660E\u5EA6 0\u21921\u3001PaddingLeft 36px\u21920\uFF08\u89C6\u89C9\u4E0A\u4ECE\u53F3\u6ED1\u5230\u4F4D\uFF09 \u003C/summary\u003E\n\tvoid Animate( Label row, float now, float revealAt )\n\t{\n\t\tif ( row is null || !row.IsValid() ) return;\n\n\t\tfloat k = Math.Clamp( ( now - revealAt ) / 0.25f, 0f, 1f );\n\n\t\t// \u52A8\u753B\u8DD1\u5B8C\uFF08k=1\uFF09\u540E\u4E0D\u518D\u5199\u6837\u5F0F\uFF1A\u7ED3\u7B97\u8981\u6302 10 \u79D2\uFF0C\u540C\u503C\u5199\u4F1A\u4E00\u76F4\u5F3A\u8FEB\u5E03\u5C40\uFF08v0.7.8.87\uFF09\n\t\tif ( k \u003E= 1f )\n\t\t{\n\t\t\tif ( !SameFloat( row.Style.Opacity, 1f ) ) row.Style.Opacity = 1f;\n\t\t\tif ( !SamePx( row.Style.PaddingLeft, 0f ) ) row.Style.PaddingLeft = 0f;\n\t\t\treturn;\n\t\t}\n\n\t\tif ( k \u003C= 0f )\n\t\t{\n\t\t\tif ( !SameFloat( row.Style.Opacity, 0f ) ) row.Style.Opacity = 0f;\n\t\t\treturn;\n\t\t}\n\n\t\trow.Style.Opacity = k;   // \u52A8\u753B\u8FDB\u884C\u4E2D\u9010\u5E27\u53D8\u5316\uFF0C\u5FC5\u987B\u5199\n\t\trow.Style.PaddingLeft = ( 1f - k ) * 36f;\n\t}\n\n\t// ---- \u6837\u5F0F\u5C5E\u6027\u90FD\u662F nullable\uFF08Length?/float?\uFF09\uFF1A\u540C\u503C\u5B88\u536B\u7684 null \u5B89\u5168\u6BD4\u8F83\uFF08\u0022\u672A\u8BBE\u7F6E\u0022\u7B97\u4E0D\u540C \u2192 \u9700\u8981\u5199\uFF09 ----\n\n\t/// \u003Csummary\u003E \u50CF\u7D20\u957F\u5EA6\u662F\u5426\u5DF2\u7B49\u4E8E px\uFF08\u672A\u8BBE\u7F6E/\u5355\u4F4D\u4E0D\u662F px \u90FD\u7B97\u4E0D\u540C\uFF09 \u003C/summary\u003E\n\tstatic bool SamePx( Length? len, float px ) =\u003E\n\t\tlen.HasValue \u0026\u0026 len.Value.Unit == LengthUnit.Pixels \u0026\u0026 MathF.Abs( len.Value.Value - px ) \u003C 0.5f;\n\n\t/// \u003Csummary\u003E \u6D6E\u70B9\u6837\u5F0F\uFF08Opacity \u7B49\uFF09\u662F\u5426\u5DF2\u7B49\u4E8E\u76EE\u6807\u503C \u003C/summary\u003E\n\tstatic bool SameFloat( float? v, float target, float eps = 0.01f ) =\u003E\n\t\tv.HasValue \u0026\u0026 MathF.Abs( v.Value - target ) \u003C eps;\n}\n"},{"Ident":"slfy.circleroyale","Path":"Game/ClassSkillManager.cs","FileName":"ClassSkillManager.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// \u804C\u4E1A\u6280\u80FD\u7BA1\u7406\u5668\uFF08M7.4\uFF0C\u9886\u571F\u6A21\u5F0F\u4E13\u5C5E\uFF09\uFF1AQ \u952E\u65BD\u653E\u7684\u56DB\u804C\u4E1A\u51B7\u5374\u6280\u80FD\uFF0Chost \u6743\u5A01\u3002\n/// \u6307\u6325\u5B98=\u4F20\u9001\u5230\u6700\u5927\u961F\u53CB\u8EAB\u8FB9\uFF08\u8FDC\u7A0B\u73A9\u5BB6\u7ECF ClassTeleport \u5E7F\u64AD\u77AC\u79FB\uFF09\uFF1B\u5DE5\u5175=6 \u79D2\u5927\u52A0\u901F\uFF08\u590D\u7528 Speed buff\uFF09\uFF1B\n/// \u62A4\u536B=\u539F\u5730\u951A\u5B9A\u5C16\u523A 8 \u79D2\uFF08\u590D\u7528\u5C16\u523A\u5206\u8EAB\u78B0\u649E\u5224\u5B9A\uFF09\uFF1B\u5766\u514B=\u002B40% \u5F53\u524D\u8D28\u91CF 10 \u79D2\uFF0C\u5230\u671F\u56DE\u6536\u3001\u671F\u95F4\u7981\u5206\u88C2\u3002\n/// \u51B7\u5374\u6309\u7403 Id \u8BB0\uFF08\u771F\u4EBA=\u672C\u7403\uFF1Bbot \u5404\u7403\u72EC\u7ACB\uFF09\u3002\u5BA2\u6237\u7AEF\u51B7\u5374\u4E3A\u672C\u5730\u4F30\u7B97\uFF0C\n/// host \u51B7\u5374\u672A\u597D\u65F6\u9759\u9ED8\u62D2\u7EDD\uFF08\u65E0\u526F\u4F5C\u7528\uFF09\u3002\u5766\u514B\u56DE\u6536\u8868\u6301\u6709 Ball \u5F15\u7528\uFF0C\u70ED\u91CD\u8F7D\u6362\u7A0B\u5E8F\u96C6\u5BF9\u8C61\u4F1A\u5931\u6548\u2014\u2014\n/// \u5C4A\u65F6\u8BE5\u6B21\u56DE\u6536\u9759\u9ED8\u6D88\u5931\uFF08\u5BF9\u5C40\u672C\u8EAB\u4E5F\u4F1A\u88AB\u70ED\u91CD\u8F7D\u6253\u65AD\uFF09\uFF0C\u53EF\u63A5\u53D7\u7684\u8FB9\u754C\u3002\n/// \u003C/summary\u003E\npublic static class ClassSkillManager\n{\n\tsealed class Surge\n\t{\n\t\tpublic Ball Ball;\n\t\tpublic float Added;\n\t\tpublic TimeSince Since;\n\t}\n\n\tstatic readonly List\u003CSurge\u003E _surges = new();\n\tstatic readonly Dictionary\u003CGuid, TimeSince\u003E _cd = new();   // \u7403 Id \u2192 \u4E0A\u6B21\u65BD\u653E\uFF08\u6309\u7403\u4E0D\u6309 SteamId\u2014\u2014bot \u7684 SteamId \u5168 0 \u4F1A\u5168\u961F\u5171\u4EAB\u4E00\u4E2A\u51B7\u5374\uFF09\n\n\tpublic static string NameOf( byte cls ) =\u003E cls switch\n\t{\n\t\t0 =\u003E \u0022COMMANDER\u0022,\n\t\t1 =\u003E \u0022ENGINEER\u0022,\n\t\t2 =\u003E \u0022GUARD\u0022,\n\t\t3 =\u003E \u0022TANK\u0022,\n\t\t_ =\u003E \u0022\u0022,\n\t};\n\n\t/// \u003Csummary\u003E HUD \u77ED\u6807\uFF08Q \u94AE\u5C31\u7EEA\u6001\u663E\u793A\uFF09 \u003C/summary\u003E\n\tpublic static string TagOf( byte cls ) =\u003E cls switch\n\t{\n\t\t0 =\u003E \u0022CMD\u0022,\n\t\t1 =\u003E \u0022ENG\u0022,\n\t\t2 =\u003E \u0022GRD\u0022,\n\t\t3 =\u003E \u0022TNK\u0022,\n\t\t_ =\u003E \u0022\u0022,\n\t};\n\n\tpublic static float CooldownOf( byte cls ) =\u003E cls switch\n\t{\n\t\t0 =\u003E GameConfig.ClassCdCommander,\n\t\t1 =\u003E GameConfig.ClassCdEngineer,\n\t\t2 =\u003E GameConfig.ClassCdGuard,\n\t\t3 =\u003E GameConfig.ClassCdTank,\n\t\t_ =\u003E 999f,\n\t};\n\n\t/// \u003Csummary\u003E \u7403\u590D\u6D3B\u65F6\u6E05\u51B7\u5374\uFF08v0.7.8.87\uFF09\uFF1A\u590D\u6D3B\u4F1A\u91CD\u63B7\u804C\u4E1A\uFF0C\u4F46\u51B7\u5374\u6309\u7403 Id \u8BB0\u3001\u7403\u5BF9\u8C61\u53C8\u8DE8\u5C40\u590D\u7528\u2014\u2014\n\t/// \u4E0D\u6E05\u7684\u8BDD\u521A\u590D\u6D3B Q \u663E\u793A\u7740\u65B0\u804C\u4E1A\u5374\u6309\u4E0D\u52A8\uFF08\u4E0A\u4E00\u6761\u547D/\u4E0A\u4E00\u5C40\u7684 45~50s \u51B7\u5374\u8FD8\u6302\u7740\uFF09 \u003C/summary\u003E\n\tpublic static void ResetBall( Ball ball )\n\t{\n\t\tif ( ball.IsValid() ) _cd.Remove( ball.Id );\n\t}\n\n\t/// \u003Csummary\u003E \u5C40\u95F4\u6E05\u8D26\uFF08v0.7.8.87\uFF09\uFF1A\u6E05\u5168\u90E8\u51B7\u5374 \u002B \u672A\u56DE\u6536\u7684\u5766\u514B\u4E34\u65F6\u8D28\u91CF\u8BB0\u8D26\n\t/// \uFF08\u8DE8\u5C40\u6B8B\u7559\u4F1A\u5728\u65B0\u5C40\u628A\u91CD\u751F\u7403\u7684\u8D28\u91CF\u53C8\u6263\u4E00\u904D\uFF09 \u003C/summary\u003E\n\tpublic static void ResetAll()\n\t{\n\t\t_cd.Clear();\n\t\t_surges.Clear();\n\t}\n\n\t/// \u003Csummary\u003E \u5766\u514B\u7206\u53D1\u671F\u5185\u7981\u5206\u88C2\uFF08\u4E34\u65F6\u8D28\u91CF\u5206\u88C2=\u5957\u73B0\u6C38\u4E45\u8D28\u91CF\uFF09 \u003C/summary\u003E\n\tpublic static bool IsTankSurging( Ball ball )\n\t{\n\t\tif ( ball is null || !ball.IsValid() ) return false;\n\t\tforeach ( var s in _surges )\n\t\t{\n\t\t\tif ( s.Ball == ball ) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/// \u003Csummary\u003E host \u6743\u5A01\u65BD\u653E\uFF1A\u51B7\u5374/\u5B58\u6D3B/\u6A21\u5F0F\u6821\u9A8C\u540E\u6267\u884C\uFF1B\u8FD4\u56DE\u662F\u5426\u771F\u7684\u653E\u4E86\uFF08\u6CA1\u961F\u53CB\u7684\u6307\u6325\u5B98\u4E0D\u8FDB\u51B7\u5374\uFF09 \u003C/summary\u003E\n\tpublic static bool Cast( Ball ball )\n\t{\n\t\tif ( !MatchState.IsTerritory ) return false;\n\t\tif ( ball is null || !ball.IsValid() || !ball.Alive ) return false;\n\t\tif ( ball.TerritoryClass \u003E 3 ) return false;\n\n\t\tvar sid = ball.Id;   // \u51B7\u5374\u6309\u7403\u8EAB\u8BB0\uFF08\u771F\u4EBA=\u672C\u7403\uFF1Bbot \u5404\u7403\u72EC\u7ACB\uFF0C\u4E0D\u53D7 SteamId \u5168 0 \u5F71\u54CD\uFF09\n\t\tif ( _cd.TryGetValue( sid, out var since ) \u0026\u0026 since \u003C CooldownOf( ball.TerritoryClass ) ) return false;\n\n\t\tswitch ( ball.TerritoryClass )\n\t\t{\n\t\t\tcase 0:\n\t\t\t\tif ( !CastCommander( ball ) ) return false;\n\t\t\t\tbreak;\n\t\t\tcase 1:\n\t\t\t\tball.ApplyBuff( (byte)PowerUpManager.Kind.Speed, GameConfig.ClassEngineerSeconds );\n\t\t\t\tbreak;\n\t\t\tcase 2:\n\t\t\t\tCircleroyaleGame.Current?.SpawnGuardSpike( ball );\n\t\t\t\tbreak;\n\t\t\tcase 3:\n\t\t\t\tCastTank( ball );\n\t\t\t\tbreak;\n\t\t}\n\n\t\t_cd[sid] = 0;\n\t\tGameLog.Info( $\u0022[class] {NameOf( ball.TerritoryClass )} cast by \u0027{ball.PlayerName}\u0027\u0022 );\n\t\treturn true;\n\t}\n\n\tstatic bool CastCommander( Ball ball )\n\t{\n\t\tvar game = CircleroyaleGame.Current;\n\t\tif ( game is null ) return false;\n\n\t\tBall best = null;\n\t\tforeach ( var b in game.Balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.Alive || b == ball ) continue;\n\t\t\tif ( b.TeamIndex \u003C 0 || b.TeamIndex != ball.TeamIndex ) continue;\n\t\t\tif ( best is null || b.Mass \u003E best.Mass ) best = b;\n\t\t}\n\t\tif ( best is null ) return false;   // \u5B64\u8EAB\u65E0\u961F\u53CB\uFF1A\u6280\u80FD\u4E0D\u653E\u3001\u4E0D\u8FDB\u51B7\u5374\n\n\t\tvar pos = best.WorldPosition;\n\t\tif ( ball.IsBot || ball.OwnerSteamId == Game.SteamId.Value )\n\t\t{\n\t\t\tball.TeleportTo( pos );           // host \u672C\u673A\u7403/bot\uFF1A\u76F4\u63A5\u77AC\u79FB\n\t\t\tgame.MarkTeleport( ball, pos );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tgame.MarkTeleport( ball, pos );   // \u8FDC\u7A0B\u7403\uFF1Ahost \u5148\u951A\u5B9A\u6821\u9A8C\u4F4D\uFF0Cowner \u7AEF\u6536\u5E7F\u64AD\u81EA\u884C\u77AC\u79FB\n\t\t\tNetworkManager.ClassTeleport( ball.OwnerSteamId, pos.x, pos.y );\n\t\t}\n\t\treturn true;\n\t}\n\n\tstatic void CastTank( Ball ball )\n\t{\n\t\tvar added = MathF.Max( GameConfig.ClassTankMassMin, ball.Mass * GameConfig.ClassTankMassFraction );\n\t\tball.Mass = MathF.Min( GameConfig.MaxMass, ball.Mass \u002B added );\n\t\t_surges.Add( new Surge { Ball = ball, Added = added, Since = 0 } );\n\t}\n\n\t/// \u003Csummary\u003E host \u6BCF\u5E27\uFF08TickTerritory \u8C03\uFF09\uFF1A\u5766\u514B\u4E34\u65F6\u8D28\u91CF\u5230\u671F\u56DE\u6536\uFF08\u4E0B\u9650\u51FA\u751F\u8D28\u91CF\uFF1B\u7403\u6CA1\u4E86\u5C31\u4E0D\u8865\uFF09 \u003C/summary\u003E\n\tpublic static void Tick()\n\t{\n\t\tfor ( int i = _surges.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar s = _surges[i];\n\t\t\tif ( s.Since \u003C GameConfig.ClassTankSeconds ) continue;\n\n\t\t\tvar b = s.Ball;\n\t\t\tif ( b.IsValid() \u0026\u0026 b.Alive )\n\t\t\t\tb.Mass = MathF.Max( GameConfig.StartMass, b.Mass - s.Added );\n\t\t\t_surges.RemoveAt( i );\n\t\t}\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Game/PowerUpManager.cs","FileName":"PowerUpManager.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// \u9053\u5177\u7BA1\u7406\uFF08v0.7.3.0\uFF09\uFF1A**host \u6743\u5A01 \u002B \u9759\u6001 RPC \u540C\u6B65**\uFF0C\u4E0E FoodManager \u540C\u6B3E\u6A21\u5F0F\n/// \uFF08\u7EAF\u6570\u636E\u5B9E\u4F53\u4E0D\u8D70\u7F51\u7EDC\u5BF9\u8C61\u2014\u2014\u8FD0\u884C\u65F6 live create \u4E0D\u590D\u5236\u5230\u5BA2\u6237\u7AEF\uFF0C\u5B9E\u6D4B\uFF09\u3002\n/// \u56FA\u5B9A\u69FD\u4F4D\u5236\uFF1A\u573A\u4E0A\u6052 PowerUpOnField \u4E2A\u69FD\uFF0C\u88AB\u6361\u540E\u540C\u69FD\u5EF6\u65F6\u91CD\u5237\uFF08\u6362\u79CD\u7C7B\u6362\u4F4D\u7F6E\uFF09\uFF0C\n/// \u5168\u91CF\u540C\u6B65\u53EA\u6709\u4E00\u4E2A\u5C0F\u6570\u7EC4\uFF0C\u4E2D\u9014\u52A0\u5165/\u70ED\u91CD\u8F7D\u6062\u590D\u90FD\u4FBF\u5B9C\u3002\n/// \u62FE\u53D6\u4E3B\u7403\u4E0E\u5206\u8EAB\u90FD\u7B97\uFF08v0.7.8.6\uFF0C\u5B58\u4E3B\u4EBA\u80CC\u5305\uFF09\uFF1B\u6548\u679C\u5728\u62FE\u53D6\u77AC\u95F4\u5E94\u7528\u5230 Ball\uFF08buff \u6302\u73A9\u5BB6\uFF0C\u8D28\u91CF\u7F50\u5373\u65F6\u52A0\u8D28\u91CF\uFF09\u3002\n/// Init/Tick \u4EC5\u6743\u5A01\u7AEF\u8C03\u7528\uFF1BApply* \u4EC5\u4F9B\u5BA2\u6237\u7AEF\u5E94\u7528\u8FDC\u7AEF\u4E8B\u4EF6\u3002\n/// \u003C/summary\u003E\npublic sealed class PowerUpManager\n{\n\t/// \u003Csummary\u003E \u9053\u5177\u79CD\u7C7B\uFF08HUD/\u6E32\u67D3/\u6548\u679C switch \u5171\u7528\uFF1BNone=255 \u4F5C Ball.BuffKind \u7684\u0022\u65E0 buff\u0022\u54E8\u5175\uFF09 \u003C/summary\u003E\n\tpublic enum Kind : byte\n\t{\n\t\tSpeed = 0,    // \u26A1 \u52A0\u901F\uFF1A8s \u79FB\u901F \u00D71.4\uFF08\u9EC4\uFF09\n\t\tMagnet = 1,   // \uD83E\uDDF2 \u78C1\u94C1\uFF1A8s \u5438\u98DF\u534A\u5F84\u7FFB\u500D\uFF08\u9752\uFF09\n\t\tMass = 2,     // \uD83D\uDC8A \u8D28\u91CF\u7F50\uFF1A\u002B300 \u5373\u65F6\uFF08\u54C1\u7EA2\uFF09\n\t\tShield = 3,   // \uD83D\uDEE1 \u62A4\u76FE\uFF1A5s \u4E0D\u53EF\u88AB\u5927\u7403\u5403\uFF08\u84DD\uFF09\n\t\tSpike = 4,    // \uD83D\uDCA5 \u5C16\u523A\u5206\u8EAB\uFF1A\u751F\u6210\u8DDF\u968F\u4E3B\u4EBA 20s \u7684\u5C16\u523A\uFF08\u7D2B\u7EFF\uFF09\uFF0C\u649E\u7834\u6BD4\u81EA\u5DF1\u5927\u7684\u7403\u5956 3s \u65E0\u654C\n\t}\n\n\tpublic const byte None = 255;\n\n\t/// \u003Csummary\u003E \u69FD\u4F4D\u6E32\u67D3\u8272\uFF08\u4E0E\u9713\u8679\u8C03\u8272\u677F\u72EC\u7ACB\u2014\u2014\u9053\u5177\u8981\u4E00\u773C\u548C\u98DF\u7269\u533A\u5206\u5F00\uFF09 \u003C/summary\u003E\n\tpublic static Color ColorOf( byte kind )\n\t{\n\t\tswitch ( kind )\n\t\t{\n\t\t\tcase (byte)Kind.Speed: return new Color( 1.00f, 0.85f, 0.25f );\n\t\t\tcase (byte)Kind.Magnet: return new Color( 0.30f, 0.90f, 1.00f );\n\t\t\tcase (byte)Kind.Mass: return new Color( 1.00f, 0.40f, 0.70f );\n\t\t\tcase (byte)Kind.Shield: return new Color( 0.40f, 0.55f, 1.00f );\n\t\t\tcase (byte)Kind.Spike: return new Color( 0.55f, 1.00f, 0.45f );   // \u4EAE\u7EFF\uFF08\u91CE\u523A\u7D2B=\u969C\u788D\uFF0C\u7EFF\u8272=\u53EF\u64CD\u7EB5\u7684\u6B66\u5668\uFF09\n\t\t\tdefault: return new Color( 0.6f, 0.6f, 0.6f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E HUD/\u56FE\u4F8B\u7528\u7684\u9053\u5177\u540D \u003C/summary\u003E\n\tpublic static string NameOf( byte kind )\n\t{\n\t\tswitch ( kind )\n\t\t{\n\t\t\tcase (byte)Kind.Speed: return \u0022SPEED\u0022;\n\t\t\tcase (byte)Kind.Magnet: return \u0022MAGNET\u0022;\n\t\t\tcase (byte)Kind.Shield: return \u0022SHIELD\u0022;\n\t\t\tcase (byte)Kind.Spike: return \u0022SPIKE\u0022;\n\t\t\tcase (byte)Kind.Mass: return \u0022\u002BMASS\u0022;\n\t\t\tdefault: return \u0022\u0022;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u9053\u5177\u69FD\uFF08\u6E32\u67D3\u4E0E HUD \u8BFB\uFF1Bhost \u6743\u5A01\uFF0C\u5BA2\u6237\u7AEF\u4E3A\u4E8B\u4EF6\u955C\u50CF\uFF09 \u003C/summary\u003E\n\tpublic sealed class Slot\n\t{\n\t\tpublic bool Alive;\n\t\tpublic byte Kind;\n\t\tpublic Vector2 Pos;\n\t\tpublic TimeSince SincePicked;   // \u4EC5 host\uFF1A\u91CD\u5237\u8BA1\u65F6\n\t}\n\n\treadonly List\u003CSlot\u003E _slots = new();\n\n\t/// \u003Csummary\u003E \u5582\u98DF\u5171\u4EAB\u79EF\u5206\u6C60\uFF08\u6309\u69FD\uFF0C\u4EC5 host\uFF09\uFF1A\u8C01\u5410\u4E1D\u90FD\u5F80\u91CC\u5145\uFF0C\u79EF\u5206 \u2265 \u672C\u6B21\u5582\u98DF\u8005\u4F53\u91CD\u00D7PowerUpFeedRatio \u5373\u89E6\u53D1\u7ED9\u5582\u98DF\u8005\u2014\u2014\n\t/// \u5927\u7403\u5582\u5230\u4E00\u534A\u4F1A\u88AB\u5C0F\u7403\u51E0\u53E3\u62A2\u8D70\uFF08\u864E\u53E3\u593A\u98DF\uFF0Cv0.7.4.0 \u7528\u6237\u5B9A\u7A3F\uFF09 \u003C/summary\u003E\n\tfloat[] _feedScores = Array.Empty\u003Cfloat\u003E();\n\n\t/// \u003Csummary\u003E Q \u6280\u80FD\u5145\u80FD\u8BA1\u65F6\uFF08\u6309 SteamId\uFF0C\u4EC5 host\uFF1Bv0.7.8.30 Q \u6539\u968F\u65F6\u95F4\u5145\u80FD\uFF0CE \u4ECD\u62FE\u53D6\uFF09 \u003C/summary\u003E\n\treadonly Dictionary\u003Clong, TimeSince\u003E _qCharge = new();\n\n\t/// \u003Csummary\u003E Q \u69FD\u5F53\u524D\u6301\u6709\u6280\u80FD\u7684\u7403\uFF08\u51B7\u5374\u6682\u505C\u6807\u8BB0\uFF0C\u68C0\u6D4B\u0022\u653E\u8D70\u0022\u7684\u8F6C\u53D8\uFF09 \u003C/summary\u003E\n\treadonly HashSet\u003Clong\u003E _qHeld = new();\n\n\t/// \u003Csummary\u003E \u5168\u90E8\u69FD\u4F4D\uFF08NeonRenderer \u6BCF\u5E27\u8BFB\uFF0C\u5404\u7AEF\u5185\u5BB9\u4E00\u81F4\uFF09 \u003C/summary\u003E\n\tpublic IReadOnlyList\u003CSlot\u003E Slots =\u003E _slots;\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF/\u70ED\u91CD\u8F7D\u540E\u672C\u5730\u5217\u8868\u672A\u5C31\u7EEA\uFF08\u4E0D\u6E32\u67D3\u4E5F\u4E0D\u8BEF\u5224\uFF09 \u003C/summary\u003E\n\tpublic bool Ready =\u003E _slots.Count \u003E 0;\n\n\t/// \u003Csummary\u003E host\uFF1A\u751F\u6210\u5168\u90E8\u69FD\u4F4D\u5E76\u5E7F\u64AD\u5168\u91CF\uFF08\u5F00\u5C40/\u6F14\u793A\u8D5B\u8C03\u7528\uFF09 \u003C/summary\u003E\n\tpublic void Init()\n\t{\n\t\t_slots.Clear();\n\t\tClearCharge();   // \u5C40\u95F4\u6E05\u8D26\uFF08v0.7.8.87\uFF09\uFF1A\u89C1 ClearCharge \u6CE8\u91CA\n\t\tfor ( int i = 0; i \u003C GameConfig.PowerUpOnField; i\u002B\u002B )\n\t\t{\n\t\t\t_slots.Add( new Slot\n\t\t\t{\n\t\t\t\tAlive = true,\n\t\t\t\tKind = RandomKind(),\n\t\t\t\tPos = RandomPos(),\n\t\t\t} );\n\t\t}\n\n\t\tif ( Networking.IsActive ) NetworkManager.PowerUpFull( ToWire() );\n\t\t_feedScores = new float[_slots.Count];\n\t}\n\n\t/// \u003Csummary\u003E \u6E05\u7A7A\uFF08\u56DE\u4E3B\u83DC\u5355\uFF1B\u5BA2\u6237\u7AEF\u7ECF PowerUpFull \u7A7A\u6570\u7EC4\u540C\u6B65\uFF09 \u003C/summary\u003E\n\tpublic void Reset()\n\t{\n\t\t_slots.Clear();\n\t\t_feedScores = Array.Empty\u003Cfloat\u003E();\n\t\tClearCharge();\n\t}\n\n\t/// \u003Csummary\u003E \u6E05 Q \u5145\u80FD/\u6301\u6709\u65F6\u957F\u8BB0\u8D26\uFF08v0.7.8.87\uFF09\uFF1A\u0060_qCharge\u0060 \u6309 SteamId \u8BB0\u800C \u0060TimeSince\u0060 \u4E00\u76F4\u5728\u8D70\uFF0C\n\t/// \u4E0D\u968F\u5C40\u6E05\u7A7A\u7684\u8BDD\uFF0C\u4E0A\u4E00\u5C40\u5145\u80FD\u5230\u4E00\u534A\u7684\u73A9\u5BB6\u5728\u65B0\u5C40\u5F00\u5C40\u51E0\u4E4E\u7ACB\u523B\u767D\u62FF\u4E00\u4E2A\u6280\u80FD \u003C/summary\u003E\n\tvoid ClearCharge()\n\t{\n\t\t_qCharge.Clear();\n\t\t_qHeld.Clear();\n\t}\n\n\t/// \u003Csummary\u003E host\uFF1A\u62FE\u53D6\u5224\u5B9A\uFF08\u4E3B\u7403 \u002B \u5206\u8EAB\u5171\u7528\uFF09\u002B \u5230\u671F\u69FD\u4F4D\u91CD\u5237\u3002\u4EC5\u6743\u5A01\u7AEF\u6BCF\u5E27\u8C03\u7528\u3002\n\t/// v0.7.5.0 \u8D77\u62FE\u53D6**\u5B58\u5165\u80CC\u5305**\uFF08bot \u4F8B\u5916\uFF1A\u62FE\u53D6\u5373\u7528\uFF09\u2014\u2014\u80CC\u5305\u6EE1\u5219\u9053\u5177\u7559\u5728\u573A\u4E0A\u4E0D\u88AB\u6361\uFF1B\n\t/// v0.7.8.6 \u8D77\u5206\u8EAB\uFF08SplitPiece\uFF09\u4E5F\u8D70\u540C\u4E00\u5224\u5B9A\uFF0C\u6361\u5230\u5B58\u4E3B\u4EBA\u80CC\u5305 \u003C/summary\u003E\n\tpublic void Tick( List\u003CBall\u003E balls, List\u003CCellPiece\u003E cells = null )\n\t{\n\t\tif ( _slots.Count == 0 ) return;\n\n\t\tforeach ( var ball in balls )\n\t\t{\n\t\t\tif ( !ball.IsValid() || !ball.Alive ) continue;\n\t\t\tPickupPass( ball, ball.WorldPosition, ball.Radius );\n\t\t}\n\n\t\t// \u5206\u8EAB\u62FE\u53D6\uFF08v0.7.8.6\uFF09\uFF1A\u6361\u5230\u5B58\u4E3B\u4EBA\u80CC\u5305\uFF08\u540C\u4E00 Q/E \u80CC\u5305\uFF09\uFF1B\u5B62\u5B50\u4E0D\u6361\uFF08\u4E2D\u6027\u98DF\u7269\u56E2\uFF09\uFF0C\n\t\t// \u5C16\u523A\u5206\u8EAB\u4E0D\u6361\uFF08\u6B66\u5668\u4E0D\u517C\u804C\u53D6\u8D27\uFF09\u3002\u4E3B\u4EBA\u6B7B\u4EA1\u7684\u5206\u8EAB\u4E0B\u4E00\u5E27 TickCells \u624D\u6E05\uFF0C\u8FD9\u91CC\u987A\u5E26\u8FC7\u6EE4\u3002\n\t\t// \u26A0\uFE0F \u5012\u5E8F for\uFF1Abot \u62FF\u7740\uFF0820s \u540E\u8F6C\u6B63\u7684\uFF09\u5C16\u523A\u5206\u8EAB\u8E29\u5230\u5C16\u523A\u9053\u5177 \u2192 \u62FE\u53D6\u5373\u7528 \u2192\n\t\t// SpawnSpikeMinion \u5F80 _cells \u8FFD\u52A0\u2014\u2014foreach \u679A\u4E3E\u4E2D\u6539\u96C6\u5408\u4F1A\u70B8\uFF08v0.7.8.20 \u4FEE\uFF09\n\t\tif ( cells is not null )\n\t\t{\n\t\t\tfor ( int i = cells.Count - 1; i \u003E= 0; i-- )\n\t\t\t{\n\t\t\t\tvar c = cells[i];\n\t\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\t\tif ( !c.OwnerBall.IsValid() || !c.OwnerBall.Alive ) continue;\n\t\t\t\tPickupPass( c.OwnerBall, c.Pos, c.Radius );\n\t\t\t}\n\t\t}\n\n\t\t// \u7A7A\u69FD\u91CD\u5237\n\t\tfor ( int i = 0; i \u003C _slots.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar s = _slots[i];\n\t\t\tif ( s.Alive || s.SincePicked \u003C GameConfig.PowerUpRefillSeconds ) continue;\n\n\t\t\ts.Alive = true;\n\t\t\ts.Kind = RandomKind();\n\t\t\ts.Pos = RandomPos();\n\t\t\tif ( i \u003C _feedScores.Length ) _feedScores[i] = 0f;   // \u91CD\u5237\u79EF\u5206\u6E05\u96F6\uFF08\u57F9\u517B\u8FDB\u5EA6\u4E0D\u4F5C\u6570\uFF09\n\t\t\tif ( Networking.IsActive ) NetworkManager.PowerUpRespawned( i, s.Kind, s.Pos.x, s.Pos.y );\n\t\t}\n\n\t\t// Q \u6280\u80FD\u968F\u65F6\u95F4\u5145\u80FD\uFF08v0.7.8.30 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u771F\u4EBA\u7403 Q \u69FD\u7A7A\u65F6\u6309**\u5F53\u524D\u8D28\u91CF**\u8BA1\u65F6\u5145\u80FD\n\t\t// \uFF0815~60s\uFF0C\u8D8A\u8F7B\u8D8A\u5FEB\uFF09\uFF0C\u5230\u70B9\u81EA\u52A8\u53D1\u4E00\u4E2A\u968F\u673A\u6280\u80FD\u3002bot \u62FE\u53D6\u5373\u7528\u65E0\u80CC\u5305\uFF0C\u4E0D\u53C2\u4E0E\uFF1B\n\t\t// \u6301\u6709\u672A\u653E\u65F6\u51B7\u5374\u6682\u505C\uFF08\u6280\u80FD\u8EBA\u5728\u69FD\u91CC\u7B49\u6309\u952E\uFF09\u3002\u8BA1\u65F6\u6309 SteamId \u8BB0\uFF08\u8DE8\u70ED\u91CD\u8F7D\u7A33\u5B9A\uFF09\u3002\n\t\t// \u9886\u571F\u6A21\u5F0F\uFF08M7.4\uFF09\uFF1AQ \u952E\u8BA9\u7ED9\u804C\u4E1A\u6280\u80FD\u2014\u2014\u5145\u80FD pass \u51BB\u7ED3\u6E05\u96F6\uFF0CPowerA \u4FDD\u6301\u7A7A\u69FD\n\t\tif ( MatchState.IsTerritory )\n\t\t{\n\t\t\t_qCharge.Clear();\n\t\t\t_qHeld.Clear();\n\t\t\treturn;\n\t\t}\n\t\tforeach ( var b in balls )\n\t\t{\n\t\t\tif ( !b.IsValid() || !b.Alive || b.IsBot ) continue;\n\n\t\t\tvar sid = b.OwnerSteamId;\n\t\t\tif ( b.PowerA != None )\n\t\t\t{\n\t\t\t\t_qHeld.Add( sid );   // \u6280\u80FD\u5F85\u653E\uFF1A\u51B7\u5374\u6682\u505C\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ( _qHeld.Remove( sid ) )\n\t\t\t{\n\t\t\t\t_qCharge[sid] = 0;   // \u521A\u653E\u8D70/\u590D\u6D3B\u6E05\u7A7A\uFF1A\u51B7\u5374\u4ECE\u73B0\u5728\u8D77\u7B97\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ( !_qCharge.TryGetValue( sid, out var ts ) )\n\t\t\t{\n\t\t\t\t_qCharge[sid] = 0;   // \u9996\u89C1\uFF08\u5165\u5C40\uFF09\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ( ts \u003C GameConfig.QSkillCooldownFor( b.Mass ) ) continue;\n\n\t\t\t_qCharge[sid] = 0;\n\t\t\tb.PowerA = RandomKind();\n\t\t\tif ( GameSfx.IsMine( b.OwnerSteamId ) )\n\t\t\t{\n\t\t\t\tGameSfx.Pickup();\n\t\t\t\tCircleroyaleGame.Current?.Hud?.AddBanner( $\u0022{NameOf( b.PowerA )} READY (Q)\u0022, ColorOf( b.PowerA ) );\n\t\t\t}\n\t\t\t// \u5BA2\u6237\u7AEF\u73A9\u5BB6\u7684 Q \u5230\u70B9\uFF1A\u6A2A\u5E45\u002B\u97F3\u6548\u8D70\u5E7F\u64AD\uFF08mode2\uFF09\uFF0Chost \u56DE\u58F0\u7531 OnPowerBannerRemote \u7684 IsAuthority \u6321\u6389\uFF08v0.7.8.31\uFF09\n\t\t\tif ( Networking.IsActive ) NetworkManager.PowerBanner( b.PowerA, b.OwnerSteamId, 2 );\n\t\t\tGameLog.Info( $\u0022[game] q skill granted: {NameOf( b.PowerA )} -\u003E {b.PlayerName}\u0022 );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u4E00\u4E2A\u0022\u62FE\u53D6\u4F53\u0022\uFF08\u4E3B\u7403\u6216\u5206\u8EAB\uFF09\u626B\u4E00\u904D\u69FD\u4F4D\u3002bot \u62FE\u53D6\u5373\u7528\uFF1B\u771F\u4EBA\u5B58\u5165 Q/E \u80CC\u5305\n\t/// \uFF08\u6548\u679C\u7B49\u6309\u952E\u91CA\u653E\uFF09\uFF0C\u80CC\u5305\u6EE1\u5219\u9053\u5177\u7559\u5728\u573A\u4E0A\u3002\u5224\u5B9A\u5708\u5C01\u9876\u53CD\u9A6C\u592A\uFF08\u5927\u7403\u8981\u523B\u610F\u78BE\u4E0A\u53BB\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tvoid PickupPass( Ball receiver, Vector3 pos, float radius )\n\t{\n\t\t// \u62FE\u53D6\u5224\u5B9A\u5C01\u9876\uFF08v0.7.4.0 \u53CD\u9A6C\u592A\uFF09\uFF1A\u5927\u7403\u4E0D\u518D\u0022\u8DEF\u8FC7\u987A\u8D70\u0022\u2014\u2014\u5224\u5B9A\u5708\u6309\u5C01\u9876\u534A\u5F84\u7B97\uFF0C\n\t\t// \u5FC5\u987B\u523B\u610F\u78BE\u5230\u9053\u5177\u6B63\u4E0A\u65B9\u624D\u5403\u5F97\u5230\uFF1B\u5C0F\u7403\u53CD\u800C\u8F7B\u677E\n\t\tvar rr = MathF.Min( radius, GameConfig.PowerUpPickRadiusCap ) \u002B GameConfig.PowerUpRadius;\n\n\t\tfor ( int i = 0; i \u003C _slots.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar s = _slots[i];\n\t\t\tif ( !s.Alive ) continue;\n\n\t\t\tvar dx = s.Pos.x - pos.x;\n\t\t\tvar dy = s.Pos.y - pos.y;\n\t\t\tif ( dx * dx \u002B dy * dy \u003E rr * rr ) continue;\n\n\t\t\tif ( receiver.IsBot )\n\t\t\t{\n\t\t\t\tConsume( i, receiver );   // bot \u4E0D\u56E4\uFF1A\u62FE\u53D6\u5373\u89E6\u53D1\u6548\u679C\uFF08banner \u7531 Consume \u5185 IsMine \u6321\u6389\uFF09\n\t\t\t}\n\t\t\telse if ( receiver.StorePower( s.Kind ) \u003E= 0 )\n\t\t\t{\n\t\t\t\t// \u771F\u4EBA\uFF1A\u5B58\u5165 Q/E \u80CC\u5305\u69FD\uFF08\u6548\u679C\u7B49\u73A9\u5BB6\u6309\u952E\u91CA\u653E\uFF09\uFF0C\u9053\u5177\u69FD\u91CD\u5237\n\t\t\t\ts.Alive = false;\n\t\t\t\ts.SincePicked = 0;\n\n\t\t\t\tNeonRenderer.Spark( new Vector3( s.Pos.x, s.Pos.y, 0f ), ColorOf( s.Kind ) );\n\t\t\t\tif ( GameSfx.IsMine( receiver.OwnerSteamId ) )\n\t\t\t\t{\n\t\t\t\t\tGameSfx.EatFood( new Vector3( s.Pos.x, s.Pos.y, 0f ) );\n\t\t\t\t\tGameAchievements.PowerStored();   // \u6210\u5C31\uFF1A\u7D2F\u8BA1\u6361 5 \u4E2A\u5165\u5305\uFF08host \u672C\u673A\u4FA7\uFF09\n\t\t\t\t}\n\t\t\t\tif ( Networking.IsActive ) NetworkManager.PowerUpPicked( i, s.Kind, receiver.OwnerSteamId );\n\n\t\t\t\tShowBanner( receiver, s.Kind, stored: true );\n\t\t\t\tGameLog.Info( $\u0022[game] powerup stored: {NameOf( s.Kind )} -\u003E {receiver.PlayerName}\u0022 );\n\t\t\t}\n\t\t\t// \u80CC\u5305\u6EE1\uFF1A\u4E0D\u6D88\u8017\uFF0C\u9053\u5177\u7559\u5728\u573A\u4E0A\uFF08\u4E0B\u4E00\u9897\u7403/\u4E0B\u4E2A\u5E27\u518D\u5224\uFF09\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u6D88\u8017\u9053\u5177\u5E76\u89E6\u53D1\u6548\u679C\uFF08bot \u62FE\u53D6\u5373\u7528 / \u5582\u98DF\u89E6\u53D1\u5171\u7528\u8DEF\u5F84\uFF1B\u69FD\u91CD\u5237\uFF09 \u003C/summary\u003E\n\tvoid Consume( int index, Ball receiver )\n\t{\n\t\tif ( (uint)index \u003E= (uint)_slots.Count ) return;\n\t\tvar s = _slots[index];\n\t\tif ( !s.Alive ) return;\n\n\t\ts.Alive = false;\n\t\ts.SincePicked = 0;\n\t\tif ( index \u003C _feedScores.Length ) _feedScores[index] = 0f;\n\n\t\tApplyEffect( receiver, s.Kind );\n\t\tNeonRenderer.Spark( new Vector3( s.Pos.x, s.Pos.y, 0f ), ColorOf( s.Kind ) );\n\t\tif ( GameSfx.IsMine( receiver.OwnerSteamId ) )\n\t\t\tGameSfx.EatFood( new Vector3( s.Pos.x, s.Pos.y, 0f ) );\n\n\t\tShowBanner( receiver, s.Kind, stored: false );\n\t\tif ( Networking.IsActive ) NetworkManager.PowerUpPicked( index, s.Kind, receiver.OwnerSteamId );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u4E2A\u4EBA\u9AD8\u5149\u6A2A\u5E45\uFF08v0.7.6.0\uFF09\uFF1Ahost \u672C\u5730\u64AD\u81EA\u5DF1\u7684 \u002B \u5E7F\u64AD\u7ED9\u5176\u4ED6\u7AEF\uFF08\u5BA2\u6237\u7AEF\u6309 IsMine \u518D\u663E\u793A\uFF09\u3002\n\t/// stored=true \u0022XXX STORED\u0022\uFF08\u6361\u5230\u5165\u80CC\u5305\uFF09/ false \u6FC0\u6D3B\u6587\u6848\uFF08\u5582\u517B\u89E6\u53D1/bot \u62FE\u53D6\u5373\u7528\uFF09\u3002\n\t/// v0.7.7.0\uFF1A\u6A2A\u5E45\u914D\u5957\u0022\u53EE\u549A\u0022\u97F3\uFF08\u672C\u673A\u8033\u6735\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tvoid ShowBanner( Ball receiver, byte kind, bool stored )\n\t{\n\t\tvar text = stored ? $\u0022{NameOf( kind )} STORED\u0022 : GameHud.ActivationText( kind );\n\t\tif ( GameSfx.IsMine( receiver.OwnerSteamId ) )\n\t\t{\n\t\t\tCircleroyaleGame.Current?.Hud?.AddBanner( text, ColorOf( kind ) );\n\t\t\tGameSfx.Pickup();\n\t\t}\n\n\t\tif ( Networking.IsActive ) NetworkManager.PowerBanner( kind, receiver.OwnerSteamId, (byte)( stored ? 0 : 1 ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// host\uFF1A\u5B62\u5B50\u5582\u9053\u5177\uFF08\u0022\u8840\u91CF\u6C60\u0022\uFF09\u2014\u2014\u5171\u4EAB\u79EF\u5206\u6C60\u6309\u69FD\u7D2F\u8BA1\uFF08\u8C01\u5410\u4E1D\u90FD\u5F80\u91CC\u5145\uFF09\uFF0C\n\t/// \u79EF\u5206 \u2265 \u672C\u6B21\u5582\u98DF\u8005\u5F53\u524D\u4F53\u91CD \u00D7 PowerUpFeedRatio \u5373\u89E6\u53D1\u6548\u679C\u7ED9\u5582\u98DF\u8005\uFF08**\u7ACB\u5373\u751F\u6548\uFF0C\u4E0D\u5165\u80CC\u5305**\uFF09\u3002\n\t/// \u5927\u7403\u8981\u5582\u5230 5%\u00D7\u5927\u4F53\u91CD\u624D\u8F6E\u5230\u81EA\u5DF1\u8FBE\u6807\uFF0C\u5C0F\u7403\u51E0\u53E3\u5C31\u591F\u2014\u2014\u4E2D\u9014\u622A\u80E1\u5373\u0022\u864E\u53E3\u593A\u98DF\u0022\u3002\n\t/// feeder \u65E0\u6548\uFF08\u65AD\u7EBF/\u6B7B\u4EA1\u77AC\u95F4\uFF09\u53EA\u5145\u80FD\u4E0D\u5224\u5B9A\u3002\n\t/// \u003C/summary\u003E\n\tpublic void Feed( Ball feeder, int index, float blobMass )\n\t{\n\t\tif ( (uint)index \u003E= (uint)_slots.Count || !_slots[index].Alive ) return;\n\t\tif ( index \u003E= _feedScores.Length ) Array.Resize( ref _feedScores, _slots.Count );\n\n\t\t_feedScores[index] \u002B= blobMass;\n\n\t\tif ( feeder is null || !feeder.IsValid() || !feeder.Alive ) return;\n\t\tif ( _feedScores[index] \u003C feeder.Mass * GameConfig.PowerUpFeedRatio ) return;\n\n\t\tvar kind = _slots[index].Kind;\n\t\tConsume( index, feeder );\n\t\tGameAchievements.FeedTriggered( feeder.OwnerSteamId );   // \u6210\u5C31\uFF1A\u9996\u6B21\u5582\u98DF\u89E6\u53D1\uFF08host \u672C\u673A\u4FA7\uFF09\n\t\tGameLog.Info( $\u0022[game] powerup fed to trigger: {NameOf( kind )} -\u003E {feeder.PlayerName}\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u9053\u5177\u6548\u679C\uFF08host \u6743\u5A01\uFF1B\u8D28\u91CF\u7F50\u5373\u65F6\u5230\u8D26\uFF0C\u5176\u4F59\u6302 buff\uFF0C\u5C16\u523A\u5206\u8EAB\u5728\u6E38\u620F\u7CFB\u7EDF\u751F\u6210\uFF09\u3002\n\t/// \u62FE\u53D6\u5B58\u80CC\u5305\uFF08v0.7.5.0\uFF09\uFF1A\u771F\u4EBA\u6309\u952E UsePower \u65F6\u624D\u8D70\u5230\u8FD9\u91CC \u003C/summary\u003E\n\tpublic static void ApplyEffect( Ball ball, byte kind )\n\t{\n\t\tswitch ( kind )\n\t\t{\n\t\t\tcase (byte)Kind.Speed:\n\t\t\t\tball.ApplyBuff( kind, GameConfig.PowerUpSpeedSeconds );\n\t\t\t\tbreak;\n\t\t\tcase (byte)Kind.Magnet:\n\t\t\t\tball.ApplyBuff( kind, GameConfig.PowerUpMagnetSeconds );\n\t\t\t\tbreak;\n\t\t\tcase (byte)Kind.Shield:\n\t\t\t\tball.ApplyBuff( kind, GameConfig.PowerUpShieldSeconds );\n\t\t\t\tbreak;\n\t\t\tcase (byte)Kind.Mass:\n\t\t\t\tball.Mass = MathF.Min( GameConfig.MaxMass, ball.Mass \u002B GameConfig.PowerUpMassBonus );\n\t\t\t\tbreak;\n\t\t\tcase (byte)Kind.Spike:\n\t\t\t\tCircleroyaleGame.Current?.SpawnSpikeMinion( ball );\n\t\t\t\tbreak;\n\t\t}\n\n\t\tGameLog.Info( $\u0022[game] powerup picked: {NameOf( kind )} -\u003E {ball.PlayerName}\u0022 );\n\t}\n\n\tbyte RandomKind() =\u003E (byte)Game.Random.Float( 0f, 4.999f );\n\n\tVector2 RandomPos()\n\t{\n\t\tvar half = GameConfig.ArenaHalfSize - GameConfig.PowerUpMargin;\n\t\treturn new Vector2( Game.Random.Float( -half, half ), Game.Random.Float( -half, half ) );\n\t}\n\n\t/// \u003Csummary\u003E \u5168\u91CF\u5FEB\u7167\uFF08\u5F00\u5C40\u8865\u53D1/\u65B0\u8FDE\u63A5\u5B9A\u5411\u7528\uFF1Bhost \u4FA7\u8C03\u7528\uFF09 \u003C/summary\u003E\n\tpublic PowerUpWire[] ToWireForSync() =\u003E ToWire();\n\n\tPowerUpWire[] ToWire()\n\t{\n\t\tvar wires = new PowerUpWire[_slots.Count];\n\t\tfor ( int i = 0; i \u003C _slots.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar s = _slots[i];\n\t\t\twires[i] = new PowerUpWire\n\t\t\t{\n\t\t\t\tKind = s.Alive ? s.Kind : None,\n\t\t\t\tX = s.Pos.x,\n\t\t\t\tY = s.Pos.y,\n\t\t\t};\n\t\t}\n\t\treturn wires;\n\t}\n\n\t// ---- \u5BA2\u6237\u7AEF\u5E94\u7528\uFF08\u8FDC\u7AEF\u4E8B\u4EF6 \u2192 \u672C\u5730\u69FD\u4F4D\uFF09----\n\n\tpublic void ApplyFull( PowerUpWire[] wires )\n\t{\n\t\t_slots.Clear();\n\t\tif ( wires is null ) return;\n\n\t\tforeach ( var w in wires )\n\t\t{\n\t\t\t_slots.Add( new Slot\n\t\t\t{\n\t\t\t\tAlive = w.Kind != None,\n\t\t\t\tKind = w.Kind,\n\t\t\t\tPos = new Vector2( w.X, w.Y ),\n\t\t\t} );\n\t\t}\n\t}\n\n\tpublic void ApplyPicked( int index )\n\t{\n\t\tif ( (uint)index \u003E= (uint)_slots.Count ) return;   // \u70ED\u91CD\u8F7D\u540E\u672A\u540C\u6B65\u5168\u91CF\u524D\uFF0C\u5FFD\u7565\u589E\u91CF\n\t\t_slots[index].Alive = false;\n\t}\n\n\tpublic void ApplyRespawned( int index, byte kind, Vector2 pos )\n\t{\n\t\tif ( (uint)index \u003E= (uint)_slots.Count ) return;\n\n\t\tvar s = _slots[index];\n\t\ts.Alive = true;\n\t\ts.Kind = kind;\n\t\ts.Pos = pos;\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"UI/MainMenu.cs","FileName":"MainMenu.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using Sandbox.UI;\n\n/// \u003Csummary\u003E\n/// \u4E3B\u83DC\u5355\uFF08M4\uFF1B2026-09-09 \u53C2\u8003\u624B\u6E38\u5E03\u5C40\u91CD\u6392\uFF0C\u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u4E2D\u592E\u5927 logo \u002B \u4E09\u6309\u94AE\u6A2A\u6392\n/// \uFF08CREATE GAME / JOIN GAME / VS BOTS\uFF09\uFF0C\u5DE6\u4E0A\u73A9\u5BB6\u8D44\u6599\u5361\uFF08\u5934\u50CF/\u540D\u5B57/\u7B49\u7EA7\uFF09\uFF0C\n/// \u53F3\u4E0A\u8BBE\u7F6E/\u6392\u884C\u699C\u5C0F\u6309\u94AE\uFF1BQUIT \u6309\u9700\u6C42\u79FB\u9664\uFF08\u7F16\u8F91\u5668\u505C Play \u5373\u9000\u51FA\uFF0Cstandalone \u518D\u52A0\u56DE\uFF09\u3002\n/// \u70B9\u51FB \u002B \u952E\u76D8\u5FEB\u6377\u952E\u53CC\u901A\u9053\uFF08\u7F16\u8F91\u5668\u5185\u5D4C\u89C6\u53E3\u53EF\u80FD\u6536\u4E0D\u5230\u9F20\u6807\u70B9\u51FB\uFF0C\u952E\u76D8\u4FDD\u5E95\uFF09\uFF1A\n/// 1=\u5EFA\u670D 2=\u52A0\u5165 3=\u4EBA\u673A 4=\u8BBE\u7F6E\u9875 5(\u6570\u5B57\u952E)=\u6392\u884C\u699C\u3002\n/// \u6837\u5F0F\u5728\u540C\u76EE\u5F55 MainMenu.cs.scss\uFF08\u6309\u0022\u7C7B\u540D.cs.scss\u0022\u81EA\u52A8\u52A0\u8F7D\uFF09\u3002\n/// \u003C/summary\u003E\npublic sealed class MainMenu : PanelComponent\n{\n\tLabel _status;\n\tLabel _level;\n\tImage _avatar;\n\tTimeSince _sinceAvatarTry = 1f;\n\tbool _built;\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\t\tBuildUi();\n\t}\n\n\tvoid BuildUi()\n\t{\n\t\tvar root = Panel;\n\t\tif ( root is null ) return;\n\n\t\t// \u5DF2\u5EFA\u597D\u4E14\u5F15\u7528\u6709\u6548 \u2192 \u4E0D\u91CD\u590D\u5EFA\uFF08\u70ED\u91CD\u8F7D\u534A\u6B8B\u65F6\u6574\u4F53\u6E05\u7A7A\u91CD\u5EFA\uFF09\n\t\tif ( _built \u0026\u0026 _status.IsValid() \u0026\u0026 root.ChildrenCount \u003E 0 ) return;\n\n\t\troot.DeleteChildren( true );\n\n\t\t// \u5DE6\u4E0A\uFF1A\u73A9\u5BB6\u8D44\u6599\u5361\uFF08\u5934\u50CF / \u540D\u5B57 / \u7B49\u7EA7\uFF09\n\t\tvar profile = new Panel() { Classes = \u0022profile\u0022 };\n\t\troot.AddChild( profile );\n\n\t\t_avatar = new Image() { Classes = \u0022profile-avatar\u0022 };\n\t\tprofile.AddChild( _avatar );\n\n\t\tvar info = new Panel() { Classes = \u0022profile-info\u0022 };\n\t\tprofile.AddChild( info );\n\n\t\tvar name = new Label() { Classes = \u0022profile-name\u0022 };\n\t\tname.Text = Sandbox.Utility.Steam.PersonaName;\n\t\tinfo.AddChild( name );\n\n\t\t_level = new Label() { Classes = \u0022profile-level\u0022 };\n\t\tinfo.AddChild( _level );\n\n\t\t// \u53F3\u4E0A\uFF1A\u8BBE\u7F6E / \u6392\u884C\u699C\uFF08\u56FE\u6807\u5C0F\u6309\u94AE\uFF1AGear / Trophy\uFF09\n\t\tvar actions = new Panel() { Classes = \u0022top-actions\u0022 };\n\t\troot.AddChild( actions );\n\t\tAddIconButton( actions, \u0022ui/skin/ic_gear.png\u0022, \u0022settings\u0022 );\n\t\tAddIconButton( actions, \u0022ui/skin/ic_trophy.png\u0022, \u0022rankings\u0022 );\n\n\t\t// \u4E2D\u592E\u5927 logo\uFF08\u526F\u6807\u9898\u5DF2\u6309\u9700\u6C42\u79FB\u9664\uFF09\n\t\tvar title = new Label() { Classes = \u0022title\u0022 };\n\t\ttitle.Text = \u0022CIRCLEROYALE\u0022;\n\t\troot.AddChild( title );\n\n\t\t// \u4E09\u4E2A\u4E3B\u6309\u94AE\u6A2A\u6392\uFF08v0.7.8.43 \u53C2\u8003\u624B\u6E38\u5E03\u5C40\uFF09\n\t\tvar row = new Panel() { Classes = \u0022menu-row\u0022 };\n\t\troot.AddChild( row );\n\t\tAddButton( row, \u0022CREATE GAME\u0022, \u0022host\u0022 );\n\t\tAddButton( row, \u0022JOIN GAME\u0022, \u0022join\u0022 );\n\t\tAddButton( row, \u0022VS BOTS\u0022, \u0022bots\u0022 );\n\n\t\t_status = new Label() { Classes = \u0022status\u0022 };\n\t\troot.AddChild( _status );\n\n\t\tvar ver = new Label() { Classes = \u0022version\u0022 };\n\t\tver.Text = GameConfig.Version;\n\t\troot.AddChild( ver );\n\n\t\tvar hint = new Label() { Classes = \u0022hint\u0022 };\n\t\thint.Text = \u0022WASD MOVE \u2014 SPACE SPLIT \u2014 R FEED\u0022;\n\t\troot.AddChild( hint );\n\n\t\tTryLoadAvatar();\n\t\t_built = true;\n\t}\n\n\tvoid AddButton( Panel parent, string text, string id )\n\t{\n\t\tvar b = new Label() { Classes = $\u0022btn {id}\u0022 };\n\t\tb.Text = text;\n\t\t// lambda \u5FC5\u987B\u6355\u83B7 this\uFF08\u8C03\u5B9E\u4F8B\u65B9\u6CD5\uFF09\u2014\u2014\u65E0\u6355\u83B7\u7684\u9759\u6001 lambda \u5728\u70ED\u91CD\u8F7D\u540E\u65E0\u6CD5\u91CD\u6620\u5C04\uFF0C\n\t\t// \u70B9\u51FB\u4F1A\u629B NoMatchLambda \u5F02\u5E38\uFF08\u5B9E\u6D4B\uFF1A\u6309\u94AE\u5168\u53D8\u0022\u70B9\u4E86\u6CA1\u53CD\u5E94\u0022\uFF09\n\t\tb.AddEventListener( \u0022onclick\u0022, () =\u003E OnMenuButton( id ) );\n\t\tparent.AddChild( b );\n\t}\n\n\t/// \u003Csummary\u003E \u56FE\u6807\u5C0F\u6309\u94AE\uFF08\u53F3\u4E0A\u89D2\u8BBE\u7F6E/\u6392\u884C\u699C\uFF09\uFF1Adobou \u69FD\u5E95\u56FE \u002B \u50CF\u7D20\u56FE\u6807\u3002\n\t/// \u56FE\u6807\u4E0D\u5B9A\u4F4D\u4E0D\u8BBE\u5C3A\u5BF8\uFF0C\u5168\u4EA4\u7ED9 scss\uFF08.icon-btn \u7684 flex \u5C45\u4E2D \u002B .icon-btn-img \u7684\u5BBD\u9AD8\uFF09\u2014\u2014\n\t/// \u4E4B\u524D\u56FE\u6807\u7EDD\u5BF9\u5B9A\u4F4D\uFF0C\u53C2\u7167\u7269\u662F\u6574\u4E2A\u53F3\u4E0A\u89D2\u5BB9\u5668\u800C\u4E0D\u662F\u6309\u94AE\uFF0C\u4E24\u4E2A\u56FE\u6807\u53E0\u5728\u4E00\u8D77\uFF08\u5B9E\u6D4B\uFF09\uFF1B\n\t/// C# \u5185\u8054\u6837\u5F0F\u4F1A\u538B\u8FC7\u6837\u5F0F\u8868\uFF0C\u8FD9\u91CC\u8BBE\u4E86\u5C3A\u5BF8\u7528\u6237\u5728 scss \u91CC\u5C31\u6539\u4E0D\u52A8\u4E86 \u003C/summary\u003E\n\tvoid AddIconButton( Panel parent, string iconPath, string id )\n\t{\n\t\tvar b = new Panel() { Classes = \u0022icon-btn\u0022 };\n\t\tvar icon = new Image() { Classes = \u0022icon-btn-img\u0022 };\n\t\ticon.Texture = Texture.Load( iconPath );\n\t\ticon.Style.PointerEvents = PointerEvents.None;   // \u70B9\u51FB\u7A7F\u900F\u5230\u6309\u94AE\n\t\tb.AddChild( icon );\n\t\tb.AddEventListener( \u0022onclick\u0022, () =\u003E OnMenuButton( id ) );   // \u6355\u83B7 this\uFF1A\u70ED\u91CD\u8F7D\u53EF\u91CD\u6620\u5C04\n\t\tparent.AddChild( b );\n\t}\n\n\t/// \u003Csummary\u003E \u672C\u673A\u73A9\u5BB6\u5934\u50CF\uFF1ASteam \u62C9\u53D6\uFF08\u4E0B\u8F7D\u5F02\u6B65\uFF0COnUpdate 1s \u8282\u6D41\u91CD\u8BD5\uFF09\uFF0C\u5931\u8D25\u843D\u5360\u4F4D\u56FE \u003C/summary\u003E\n\tvoid TryLoadAvatar()\n\t{\n\t\tif ( _avatar is null || !IsValid ) return;\n\n\t\tvar sid = (long)(ulong)Sandbox.Utility.Steam.SteamId;\n\t\tvar tex = sid != 0 ? Texture.LoadAvatar( sid, 128 ) : null;\n\t\t_avatar.Texture = tex ?? Texture.Load( GameConfig.AvatarPlaceholderPath, false );\n\t}\n\n\t/// \u003Csummary\u003E \u83DC\u5355\u6309\u94AE\u70B9\u51FB\uFF08\u5B9E\u4F8B\u65B9\u6CD5\uFF1A\u70ED\u91CD\u8F7D\u53EF\u91CD\u6620\u5C04\uFF09 \u003C/summary\u003E\n\tvoid OnMenuButton( string id )\n\t{\n\t\tGameSfx.UiClick();\n\t\tCircleroyaleGame.Current?.MenuAction( id switch { \u0022host\u0022 =\u003E 0, \u0022join\u0022 =\u003E 1, \u0022bots\u0022 =\u003E 2, \u0022settings\u0022 =\u003E 4, \u0022rankings\u0022 =\u003E 5, _ =\u003E 3 } );\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\n\t\t// \u5F3A\u5236\u5149\u6807\u53EF\u89C1\u53EF\u4EA4\u4E92\uFF08\u9762\u677F\u9ED8\u8BA4 PointerEvents=None\uFF0CAuto \u6A21\u5F0F\u4E0B\u6CA1\u4EBA\u0022\u60F3\u8981\u9F20\u6807\u0022\u5149\u6807\u5C31\u6CA1\u4E86\uFF09\uFF1B\n\t\t// Hide() \u65F6\u6062\u590D Auto\u3002\u653E\u5728 OnUpdate \u6BCF\u5E27\u65AD\u8A00\uFF0C\u4EFB\u4F55\u6837\u5F0F\u7EA7\u8054\u610F\u5916\u90FD\u515C\u5F97\u4F4F\n\t\tMouse.Visibility = MouseVisibility.Visible;\n\n\t\tBuildUi();\n\n\t\t// \u952E\u76D8\u4FDD\u5E95\uFF08\u7F16\u8F91\u5668\u5185\u5D4C\u89C6\u53E3\u9F20\u6807\u4E8B\u4EF6\u4E0D\u53EF\u9760\uFF0C\u952E\u76D8\u4E00\u5B9A\u80FD\u8FDB\u6765\uFF09\uFF1A\n\t\t// [4] \u8BBE\u7F6E\u9875\uFF1B\u6570\u5B57 5 \u76F4\u8BFB\uFF08Slot5 \u672A\u5728 Input \u914D\u7F6E\u91CC\uFF09= \u6392\u884C\u699C\uFF1BQUIT \u5DF2\u79FB\u9664\n\t\tif ( Input.Pressed( \u0022Slot1\u0022 ) ) CircleroyaleGame.Current?.MenuAction( 0 );\n\t\tif ( Input.Pressed( \u0022Slot2\u0022 ) ) CircleroyaleGame.Current?.MenuAction( 1 );\n\t\tif ( Input.Pressed( \u0022Slot3\u0022 ) ) CircleroyaleGame.Current?.MenuAction( 2 );\n\t\tif ( Input.Pressed( \u0022Slot4\u0022 ) ) CircleroyaleGame.Current?.MenuAction( 4 );\n\t\tif ( Input.Keyboard.Pressed( \u00225\u0022 ) ) CircleroyaleGame.Current?.MenuAction( 5 );\n\n\t\t// \u7B49\u7EA7\u89D2\u6807\uFF08\u4E91\u7AEF XP \u5F02\u6B65\u52A0\u8F7D\uFF0CGameXp \u5185\u90E8\u8282\u6D41\u81EA\u6108\u91CD\u8BD5\uFF09\n\t\tGameXp.EnsureLocalXp();\n\t\tSetText( _level, GameXp.LevelText() );\n\n\t\t// \u5934\u50CF\u6CA1\u62C9\u5230\uFF08Steam \u4E0B\u8F7D\u5F02\u6B65\uFF09\u6BCF\u79D2\u91CD\u8BD5\u4E00\u6B21\uFF0C\u62C9\u5230\u5373\u505C\n\t\tif ( _avatar.IsValid() \u0026\u0026 _avatar.Texture is null \u0026\u0026 _sinceAvatarTry \u003E 1f )\n\t\t{\n\t\t\t_sinceAvatarTry = 0;\n\t\t\tTryLoadAvatar();\n\t\t}\n\t}\n\n\tstatic void SetText( Label l, string text )\n\t{\n\t\tif ( l is null || !l.IsValid() || l.Text == text ) return;\n\t\tl.Text = text;\n\t}\n\n\t/// \u003Csummary\u003E \u72B6\u6001\u884C\uFF08\u8FDE\u63A5\u4E2D / \u5931\u8D25\u91CD\u8BD5\u63D0\u793A\uFF09 \u003C/summary\u003E\n\tpublic void SetStatus( string text )\n\t{\n\t\tif ( _status.IsValid() ) _status.Text = text;\n\t}\n\n\t/// \u003Csummary\u003E \u5F00\u5C40\u540E\u9690\u85CF\u83DC\u5355\uFF08Panel \u65E0 Enabled\uFF0C\u76F4\u63A5\u7981\u6389\u6574\u4E2A GameObject\uFF09\uFF0C\u5E76\u628A\u5149\u6807\u8FD8\u7ED9\u6E38\u620F \u003C/summary\u003E\n\tpublic void Hide()\n\t{\n\t\tif ( !GameObject.Enabled ) return;   // \u5DF2\u9690\u85CF\uFF1A\u522B\u52A8\u5149\u6807\uFF08ReconcileUi \u6BCF\u5E27\u8C03\uFF0C\u623F\u95F4\u9875\u8981\u5149\u6807\u53EF\u70B9\uFF09\n\t\tMouse.Visibility = MouseVisibility.Auto;\n\t\tGameObject.Enabled = false;\n\t}\n\n\t/// \u003Csummary\u003E \u4ECE\u623F\u95F4/\u7ED3\u7B97\u56DE\u83DC\u5355\uFF08M5\uFF09\uFF1A\u91CD\u65B0\u663E\u793A\uFF08OnUpdate \u6062\u590D\u5149\u6807\u65AD\u8A00\uFF09 \u003C/summary\u003E\n\tpublic void Show()\n\t{\n\t\tGameObject.Enabled = true;\n\t\tBuildUi();\n\t\tif ( _status.IsValid() ) _status.Text = \u0022\u0022;\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"ui/settingspanel.cs.scss","FileName":"settingspanel.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"SettingsPanel {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\tfont-family: PixelFusion;\n\tcolor: #4a3b63;\n\tpointer-events: all;\n\tbackground-color: rgba( 234, 244, 252, 0.88 );   // \u6D45\u84DD\u78E8\u7802\uFF08\u50CF\u7D20\u98CE\uFF09\n\n\t.sp-title {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 88px;\n\t\tfont-size: 48px;\n\t\tfont-weight: 800;\n\t\tletter-spacing: 10px;\n\t\ttext-align: center;\n\t\tcolor: #f272a7;\n\t\ttext-shadow: 0 4px 0 #4a3b63;\n\t}\n\n\t.sp-hint {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 178px;\n\t\tfont-size: 13px;\n\t\tletter-spacing: 3px;\n\t\ttext-align: center;\n\t\tcolor: rgba( 74, 59, 99, 0.6 );\n\t}\n\n\t// \u8BBE\u7F6E\u884C\uFF08v0.7.8.42\uFF09\uFF1Adobou \u5976\u6CB9\u6309\u94AE\u4E5D\u5BAB\u683C\uFF0C600 \u5BBD\u5C45\u4E2D\uFF08\u4F4D\u7F6E\u7531 C# \u8BBE left:center\uFF09\n\t.sp-row {\n\t\theight: 54px;\n\t\tpadding-top: 8px;   // \u7528\u6237\u8981\u6C42\uFF1A\u6587\u5B57\u4E0B\u79FB 8 \u50CF\u7D20\n\t\tfont-size: 20px;\n\t\tfont-weight: 700;\n\t\tletter-spacing: 3px;\n\t\ttext-align: center;\n\t\tcolor: #4a3b63;\n\t\tborder-image: url( ui/skin/btn_white.png ) 52 fill / 26px stretch;\n\t\tborder-image-tint: #ceecee;   // \u6D45\u7EFF\u5E95\uFF08\u4E0E UiKit \u836F\u4E38\u4E00\u81F4\uFF09\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t}\n\n\t.sp-row:hover {\n\t\tborder-image-tint: #e4f7dd;\n\t\tcolor: #277e4b;\n\t}\n\n\t// \u8FD4\u56DE\u6309\u94AE\uFF08v0.7.8.49\uFF09\uFF1AUiKit \u836F\u4E38\u6309\u94AE\uFF08\u5916\u89C2\u5728 UiKit.cs.scss\uFF09\uFF0C\u8FD9\u91CC\u53EA\u7BA1\u6446\u653E\n\t.sp-back {\n\t\tposition: absolute;\n\t\tleft: 50%;\n\t\tmargin-left: -110px;   // \u81EA\u5DF1\u5BBD\u5EA6 220 \u7684\u4E00\u534A\uFF0C\u4EFB\u610F\u5206\u8FA8\u7387\u6C34\u5E73\u5C45\u4E2D\n\t\ttop: 848px;\n\t\twidth: 220px;\n\t\tpadding-top: 8px;\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Game/GameAchievements.cs","FileName":"GameAchievements.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// \u6210\u5C31\u5BF9\u63A5\uFF08v0.7.8.14\uFF0C\u5171 25 \u6761\uFF09\uFF1A\u6210\u5C31\u672C\u4F53\u5728 sbox.game \u6E38\u620F\u5305\u9875\u9762 Services \u003E Achievements \u914D\u7F6E\n/// \uFF08ident/\u6807\u9898/\u63CF\u8FF0/\u56FE\u6807/\u5206\u503C/\u89E3\u9501\u6A21\u5F0F\uFF09\uFF0C\u4EE3\u7801\u53EA\u6309 ident \u89E6\u53D1 Manual \u6210\u5C31\u3002\n/// \u5F15\u64CE\u884C\u4E3A\uFF08\u6E90\u7801 AchievementCollection.ManualUnlock\uFF09\uFF1Aident \u672A\u914D\u7F6E/\u5DF2\u89E3\u9501/\u975E Manual \u65F6\n/// \u9759\u9ED8\u8DF3\u8FC7\u2014\u2014\u5148\u5199\u4EE3\u7801\u540E\u914D\u9875\u9762\uFF0C\u914D\u597D\u5373\u751F\u6548\uFF1B\u6BCF\u6B21\u4E8B\u4EF6\u90FD\u8C03 Unlock\uFF0C\u7531\u5F15\u64CE\u53BB\u91CD\u3002\n/// Unlock \u53EA\u5BF9**\u672C\u673A\u73A9\u5BB6**\u751F\u6548\uFF08\u4E13\u7528\u670D\u52A1\u5668\u81EA\u52A8 no-op\uFF09\u2014\u2014\u6240\u6709\u89E6\u53D1\u70B9\u5FC5\u987B\u5148 GameSfx.IsMine\n/// \u5224\u5B9A\u0022\u662F\u4E0D\u662F\u672C\u673A\u8D5A\u5230\u7684\u0022\uFF0Chost \u4E0D\u66FF\u5BA2\u6237\u7AEF\u89E3\u9501\u3002\n/// \u7D2F\u8BA1\u578B\u6210\u5C31\uFF08\u5403\u98DF\u7269\u603B\u6570\u7B49\uFF09\u8D70\u5305\u9875\u9762 Stat \u7ED1\u5B9A\uFF08Stats.Increment \u5373\u53EF\uFF09\uFF0C\u4E0D\u8D70\u8FD9\u91CC\u3002\n/// \u8FDB\u7A0B\u5185\u8BA1\u6570\u5668\uFF08\u51FB\u6740\u6570/\u6361\u5305\u6570\uFF09\u91CD\u542F\u4F1A\u6E05\u96F6\u2014\u2014\u4E00\u6B21\u6027\u6210\u5C31\u89E3\u9501\u7531\u4E91\u7AEF\u53BB\u91CD\uFF0C\u53EA\u5F71\u54CD\u0022\u665A\u51E0\u5C40\u89E3\u9501\u0022\u3002\n/// \u003C/summary\u003E\npublic static class GameAchievements\n{\n\t// ---- \u9996\u6279 5 \u6761\uFF08v0.7.8.13\uFF09----\n\tpublic const string FirstBlood = \u0022first_blood\u0022;     // \u9996\u6B21\u5403\u6389\u4E00\u540D\u73A9\u5BB6\n\tpublic const string Godlike = \u0022godlike\u0022;            // \u5355\u5C40\u4E94\u8FDE\u6740\n\tpublic const string SpikeBlast = \u0022spike_blast\u0022;     // \u5C16\u523A\u5206\u8EAB\u70B8\u6389\u6BD4\u81EA\u5DF1\u5927\u7684\u7403\n\tpublic const string TeamWinner = \u0022team_winner\u0022;     // \u56E2\u961F\u8D5B\u6240\u5728\u961F\u593A\u51A0\n\tpublic const string Champion = \u0022champion\u0022;          // \u4E2A\u4EBA\u7B2C\u4E00\u540D\u7ED3\u7B97\n\n\t// ---- \u7B2C\u4E8C\u6279 20 \u6761\uFF08v0.7.8.14\uFF09----\n\tpublic const string DoubleKill = \u0022double_kill\u0022;     // \u5355\u5C40\u53CC\u8FDE\u6740\n\tpublic const string TripleKill = \u0022triple_kill\u0022;     // \u5355\u5C40\u4E09\u8FDE\u6740\n\tpublic const string Rampage = \u0022rampage\u0022;            // \u5355\u5C40\u56DB\u8FDE\u6740\n\tpublic const string BigPrey = \u0022big_prey\u0022;           // \u5355\u53E3\u541E\u6389\u5927\u7403\n\tpublic const string Revenge = \u0022revenge\u0022;            // \u5403\u6389\u4E0A\u6B21\u5403\u6389\u81EA\u5DF1\u7684\u73A9\u5BB6\n\tpublic const string Hunter10 = \u0022hunter_10\u0022;         // \u7D2F\u8BA1\u5403 10 \u540D\u73A9\u5BB6\n\tpublic const string Hunter50 = \u0022hunter_50\u0022;         // \u7D2F\u8BA1\u5403 50 \u540D\u73A9\u5BB6\n\tpublic const string HalfTon = \u0022half_ton\u0022;           // \u4E00\u6761\u547D\u603B\u8D28\u91CF 500\n\tpublic const string OneTon = \u0022one_ton\u0022;             // \u4E00\u6761\u547D\u603B\u8D28\u91CF 1000\n\tpublic const string TwoTon = \u0022two_ton\u0022;             // \u4E00\u6761\u547D\u603B\u8D28\u91CF 2000\n\tpublic const string FiveTon = \u0022five_ton\u0022;           // \u4E00\u6761\u547D\u603B\u8D28\u91CF 5000\n\tpublic const string FirstTool = \u0022first_tool\u0022;       // \u9996\u6B21\u4F7F\u7528\u9053\u5177\n\tpublic const string FullBag = \u0022full_bag\u0022;           // \u4E24\u683C\u80CC\u5305\u540C\u65F6\u6709\u8D27\n\tpublic const string PowerHoarder = \u0022power_hoarder\u0022; // \u7D2F\u8BA1\u6361 5 \u4E2A\u9053\u5177\u5165\u5305\n\tpublic const string FirstFeed = \u0022first_feed\u0022;       // \u9996\u6B21\u5582\u98DF\u89E6\u53D1\u9053\u5177\n\tpublic const string SpikeOp = \u0022spike_op\u0022;           // \u9996\u6B21\u653E\u51FA\u5C16\u523A\u5206\u8EAB\n\tpublic const string OctoSplit = \u0022octo_split\u0022;       // \u540C\u65F6\u62E5\u6709 8 \u9897\u8EAB\u4F53\n\tpublic const string Untouchable = \u0022untouchable\u0022;    // \u4E00\u6761\u547D\u5B58\u6D3B 5 \u5206\u949F\n\tpublic const string FirstDeath = \u0022first_death\u0022;     // \u7B2C\u4E00\u6B21\u88AB\u5403\n\tpublic const string FirstWin = \u0022first_win\u0022;         // \u9996\u6B21\u83B7\u80DC\n\n\tconst float BigPreyMass = 800f;   // \u5355\u53E3\u541E\u5165\u8D28\u91CF\u95E8\u69DB\uFF08\u8FD4\u8FD8\u524D\u7684\u53E3\u5F84\uFF0C\u5BF9\u5E94 ~1000 \u8D28\u91CF\u7684\u7403\uFF09\n\n\t/// \u003Csummary\u003E \u4E00\u6761\u547D\u5185\u7684\u8D28\u91CF\u91CC\u7A0B\u7891\uFF08\u4E0E MassSteps \u4E00\u4E00\u5BF9\u5E94\u89E3\u9501 HalfTon/OneTon/TwoTon/FiveTon\uFF09 \u003C/summary\u003E\n\tstatic readonly float[] MassSteps = { 500f, 1000f, 2000f, 5000f };\n\n\tstatic int _lifetimeKills;     // \u8FDB\u7A0B\u5185\u7D2F\u8BA1\u51FB\u6740\n\tstatic int _lifetimePickups;   // \u8FDB\u7A0B\u5185\u7D2F\u8BA1\u9053\u5177\u5165\u5305\n\tstatic long _lastKiller;       // \u6700\u8FD1\u5403\u6389\u672C\u673A\u7684\u73A9\u5BB6\uFF08\u590D\u4EC7\u540D\u5355\uFF09\n\n\t/// \u003Csummary\u003E \u672C\u673A\u51FB\u6740\uFF08\u6BCF\u6B21\u5403\u7403\u90FD\u8C03\uFF0C\u5F15\u64CE\u5BF9\u5DF2\u89E3\u9501\u9759\u9ED8\u53BB\u91CD\uFF09\uFF1B\n\t/// combo = \u672C\u5C40\u8FDE\u6740\u6570\uFF08GameSfx 10s \u7A97\u53E3\uFF09\uFF0CmassGained = \u672C\u53E3\u541E\u5165\u8D28\u91CF \u003C/summary\u003E\n\tpublic static void LocalKill( int combo, float massGained, long eatenSteamId )\n\t{\n\t\tGameXp.OnLocalKill();   // \u7ECF\u9A8C\u8BB0\u8D26\uFF1A\u672C\u5C40\u51FB\u6740\uFF08v0.7.8.19\uFF09\n\n\t\tUnlock( FirstBlood );\n\t\tif ( combo \u003E= 2 ) Unlock( DoubleKill );\n\t\tif ( combo \u003E= 3 ) Unlock( TripleKill );\n\t\tif ( combo \u003E= 4 ) Unlock( Rampage );\n\t\tif ( combo \u003E= 5 ) Unlock( Godlike );\n\n\t\tif ( massGained \u003E= BigPreyMass ) Unlock( BigPrey );\n\t\tif ( eatenSteamId != 0 \u0026\u0026 eatenSteamId == _lastKiller ) Unlock( Revenge );\n\n\t\t_lifetimeKills\u002B\u002B;\n\t\tif ( _lifetimeKills \u003E= 10 ) Unlock( Hunter10 );\n\t\tif ( _lifetimeKills \u003E= 50 ) Unlock( Hunter50 );\n\t}\n\n\t/// \u003Csummary\u003E \u672C\u673A\u88AB\u5403\uFF1A\u53C2\u4E0E\u5956 \u002B \u8BB0\u4EC7\uFF08\u4E4B\u540E\u5403\u6389\u4ED6\u89E3\u9501 Revenge\uFF09 \u003C/summary\u003E\n\tpublic static void LocalDeath( long killerSteamId )\n\t{\n\t\t_lastKiller = killerSteamId;\n\t\tUnlock( FirstDeath );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u6BCF\u5E27\u4F53\u68C0\uFF08GameHud.OnUpdate \u8C03\uFF0C\u5168\u7AEF\uFF09\uFF1A\u4E00\u6761\u547D\u8D28\u91CF\u91CC\u7A0B\u7891 / 8 \u9897\u8EAB\u4F53 / \u5B58\u6D3B 5 \u5206\u949F / \u80CC\u5305\u53CC\u6EE1\u3002\n\t/// \u95E8\u69DB\u5224\u5B9A\u6BCF\u5E27\u91CD\u590D\u89E6\u53D1\u7531\u5F15\u64CE\u53BB\u91CD\uFF0C\u65E0\u9700\u672C\u5730\u72B6\u6001\u3002\n\t/// \u003C/summary\u003E\n\tpublic static void MatchTick( CircleroyaleGame game )\n\t{\n\t\tif ( !game.IsMatchStarted || MatchState.MatchOver ) return;\n\n\t\tvar local = game.LocalBall;\n\t\tif ( !local.IsValid() || !local.Alive ) return;\n\n\t\tvar mass = game.TotalMassFor( local );\n\t\tif ( mass \u003E= MassSteps[0] ) Unlock( HalfTon );\n\t\tif ( mass \u003E= MassSteps[1] ) Unlock( OneTon );\n\t\tif ( mass \u003E= MassSteps[2] ) Unlock( TwoTon );\n\t\tif ( mass \u003E= MassSteps[3] ) Unlock( FiveTon );\n\n\t\tint pieces = 0;\n\t\tforeach ( var c in game.Cells )\n\t\t{\n\t\t\tif ( c.PieceKind == CellPiece.Kind.SplitPiece \u0026\u0026 c.OwnerSteamId == local.OwnerSteamId ) pieces\u002B\u002B;\n\t\t}\n\t\tif ( pieces \u003E= 8 ) Unlock( OctoSplit );\n\n\t\tif ( local.LifeSeconds \u003E 300f ) Unlock( Untouchable );\n\n\t\t// \u80CC\u5305\u53CC\u6EE1\uFF08PowerA/B \u662F [Sync]\uFF0C255=\u7A7A\uFF09\u2014\u2014\u540C\u6B65\u5230\u4F4D\u624D\u89E6\u53D1\uFF0C\u65E0\u65F6\u5E8F\u7ADE\u6001\n\t\tif ( local.PowerA != 255 \u0026\u0026 local.PowerB != 255 ) Unlock( FullBag );\n\t}\n\n\t/// \u003Csummary\u003E \u672C\u673A\u6309 Q/E \u91CA\u653E\u9053\u5177\uFF08UsePowerSlot \u6A2A\u5E45\u5904\u8C03\uFF09\uFF1A\u9996\u6B21\u7528\u9053\u5177 / \u9996\u6B21\u653E\u5C16\u523A \u003C/summary\u003E\n\tpublic static void PowerUsed( byte kind )\n\t{\n\t\tUnlock( FirstTool );\n\t\tif ( kind == (byte)PowerUpManager.Kind.Spike ) Unlock( SpikeOp );\n\t}\n\n\t/// \u003Csummary\u003E \u672C\u673A\u6361\u5230\u9053\u5177\u5165\u5305\uFF08host \u672C\u673A\u62FE\u53D6\u4E0E\u5BA2\u6237\u7AEF\u5E7F\u64AD\u843D\u5730\u4E24\u6761\u8DEF\u90FD\u4F1A\u8C03\uFF09\uFF1A\u7D2F\u8BA1 5 \u4E2A \u003C/summary\u003E\n\tpublic static void PowerStored()\n\t{\n\t\tif ( \u002B\u002B_lifetimePickups \u003E= 5 ) Unlock( PowerHoarder );\n\t}\n\n\t/// \u003Csummary\u003E \u5582\u98DF\u89E6\u53D1\u9053\u5177\uFF08host \u7684 Feed \u5224\u5B9A \u002B \u5BA2\u6237\u7AEF PowerBanner \u843D\u5730\uFF0C\u4E24\u8DEF\u90FD\u8FC7 IsMine\uFF09 \u003C/summary\u003E\n\tpublic static void FeedTriggered( long feederSteamId )\n\t{\n\t\tif ( GameSfx.IsMine( feederSteamId ) )\n\t\t\tUnlock( FirstFeed );\n\t}\n\n\t/// \u003Csummary\u003E \u5C16\u523A\u5206\u8EAB\u70B8\u6389\u6BD4\u4E3B\u4EBA\u66F4\u5927\u7684\u7403\uFF08host \u5224\u5B9A\uFF1Bowner=\u5C16\u523A\u4E3B\u4EBA\uFF0C\u5E7F\u64AD\u5230\u5404\u7AEF\u540E\u672C\u673A\u5224\u5B9A\uFF09 \u003C/summary\u003E\n\tpublic static void SpikeMinionBlast( long ownerSteamId )\n\t{\n\t\tif ( GameSfx.IsMine( ownerSteamId ) )\n\t\t\tUnlock( SpikeBlast );\n\t\tif ( Networking.IsActive )\n\t\t\tNetworkManager.SpikeReward( ownerSteamId );\n\t}\n\n\t/// \u003Csummary\u003E \u5C16\u523A\u5956\u52B1\u5E7F\u64AD\u843D\u5730\uFF08\u5BA2\u6237\u7AEF\uFF1Bhost \u5DF2\u5728 SpikeMinionBlast \u5224\u8FC7\uFF0C\u6321\u56DE\u58F0\uFF09 \u003C/summary\u003E\n\tpublic static void OnSpikeRewardRemote( long ownerSteamId )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\tif ( GameSfx.IsMine( ownerSteamId ) )\n\t\t\tUnlock( SpikeBlast );\n\t}\n\n\t/// \u003Csummary\u003E \u6BD4\u8D5B\u7ED3\u7B97\uFF08\u5168\u7AEF\u5404\u81EA\u5224\u5B9A\uFF09\uFF1A\u56E2\u961F\u8D5B=\u672C\u961F\u603B\u8D28\u91CF\u7B2C\u4E00\uFF08\u5E76\u5217\u4E5F\u7B97\uFF09\uFF1B\u666E\u901A\u8D5B=\u4E2A\u4EBA\u7B2C\u4E00\u540D\u3002\n\t/// \u8FD4\u56DE\u662F\u5426\u83B7\u80DC\uFF08GameXp \u7ED3\u7B97\u7ECF\u9A8C\u7528\uFF09 \u003C/summary\u003E\n\tpublic static bool Settlement( ScoreWire[] standings, long localSteamId )\n\t{\n\t\tif ( standings is null || standings.Length == 0 ) return false;\n\n\t\tbool won = false;\n\n\t\tif ( MatchState.IsTeam )\n\t\t{\n\t\t\tvar local = CircleroyaleGame.Current?.LocalBall;\n\t\t\tif ( local.IsValid() \u0026\u0026 local.TeamIndex \u003E= 0 )\n\t\t\t{\n\t\t\t\tvar totals = new Dictionary\u003Cint, float\u003E();\n\t\t\t\tforeach ( var row in standings )\n\t\t\t\t{\n\t\t\t\t\tvar t = totals.TryGetValue( row.Team, out var m ) ? m : 0f;\n\t\t\t\t\ttotals[row.Team] = t \u002B row.Mass;\n\t\t\t\t}\n\n\t\t\t\tvar mine = totals.GetValueOrDefault( local.TeamIndex );\n\t\t\t\tvar best = -1f;\n\t\t\t\tforeach ( var kv in totals )\n\t\t\t\t{\n\t\t\t\t\tif ( kv.Value \u003E best ) best = kv.Value;\n\t\t\t\t}\n\n\t\t\t\tif ( mine \u003E= best )\n\t\t\t\t{\n\t\t\t\t\tUnlock( TeamWinner );\n\t\t\t\t\twon = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse if ( standings[0].SteamId == localSteamId )\n\t\t{\n\t\t\tUnlock( Champion );\n\t\t\twon = true;\n\t\t}\n\n\t\tif ( won ) Unlock( FirstWin );\n\t\treturn won;\n\t}\n\n\tstatic void Unlock( string ident ) =\u003E Sandbox.Services.Achievements.Unlock( ident );\n}"},{"Ident":"slfy.circleroyale","Path":"Game/ScoreUploader.cs","FileName":"ScoreUploader.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\n/// \u003Csummary\u003E\n/// \u6210\u7EE9\u4E0A\u4F20\u4E0E\u5168\u670D\u699C\u8BFB\u53D6\uFF08M5\uFF09\u3002\u4E91\u7AEF\u8D70 Sandbox.Services\uFF1A\n/// \u4E0A\u4F20 = Stats.SetValue(\u0022cr_match_mass\u0022, \u672C\u5C40\u603B\u8D28\u91CF) \u002B Flush\uFF1B\n/// \u8BFB\u53D6 = Leaderboards.GetFromStat \u805A\u5408 Max\uFF08\u5168\u670D\u6700\u9AD8\u5355\u5C40\uFF09\u3002\n/// \u26A0\uFE0F \u672A\u53D1\u5E03\u7684\u672C\u5730\u5305\u540E\u7AEF\u6CA1\u6709\u5305\u8EAB\u4EFD\uFF0CRefresh \u5185\u90E8\u541E 404\uFF08Entries \u4FDD\u6301\u7A7A\uFF09\u2014\u2014\n/// \u5C55\u793A\u5C42\u636E\u6B64\u81EA\u52A8\u964D\u7EA7\u5230 LocalBoard \u7684\u672C\u5730\u6301\u4E45\u5316\u699C\u3002\n/// \u003C/summary\u003E\npublic static class ScoreUploader\n{\n\t/// \u003Csummary\u003E \u6392\u884C\u699C\u7EDF\u8BA1\u540D\uFF08\u805A\u5408 Max = \u5355\u5C40\u6700\u9AD8\u603B\u8D28\u91CF\uFF09 \u003C/summary\u003E\n\tpublic const string StatName = \u0022cr_match_mass\u0022;\n\n\t/// \u003Csummary\u003E \u672C\u673A\u6210\u7EE9\u4E0A\u4E91\uFF08\u6309\u767B\u5F55\u7528\u6237\u8BB0\u8D26\uFF1B\u62FF\u4E0D\u5230\u672C\u673A\u7403\u5C31\u9759\u9ED8\u8DF3\u8FC7\uFF09 \u003C/summary\u003E\n\tpublic static void SubmitOwn( CircleroyaleGame game )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar local = game?.LocalBall;\n\t\t\tif ( !local.IsValid() ) return;\n\n\t\t\tvar mass = game.TotalMassFor( local );\n\t\t\tSandbox.Services.Stats.SetValue( StatName, mass );\n\t\t\tSandbox.Services.Stats.Flush();\n\t\t\tGameLog.Info( $\u0022[score] uploaded {mass:0} ({StatName})\u0022 );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tGameLog.Info( $\u0022[score] upload failed: {e.Message}\u0022 );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u62C9\u5168\u670D\u699C Top10\uFF08\u5F02\u6B65\uFF1B\u7A7A/\u5931\u8D25\u56DE\u8C03 null\u2014\u2014\u5C55\u793A\u5C42\u964D\u7EA7\u672C\u5730\u699C\uFF09 \u003C/summary\u003E\n\tpublic static async void FetchGlobal( Action\u003CList\u003C( string Name, float Mass )\u003E\u003E done )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar board = Sandbox.Services.Leaderboards.GetFromStat( StatName );\n\t\t\tboard.SetAggregationMax();\n\t\t\tboard.SetSortDescending();\n\t\t\tboard.FilterByNone();\n\t\t\tboard.MaxEntries = 10;\n\t\t\tawait board.Refresh();\n\n\t\t\tvar list = new List\u003C( string Name, float Mass )\u003E();\n\t\t\tforeach ( var e in board.Entries )\n\t\t\t\tlist.Add( ( e.DisplayName, (float)e.Value ) );\n\n\t\t\tdone( list.Count \u003E 0 ? list : null );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tGameLog.Info( $\u0022[score] global board fetch failed: {e.Message}\u0022 );\n\t\t\tdone( null );\n\t\t}\n\t}\n}\n"},{"Ident":"slfy.circleroyale","Path":"Player/Ball.cs","FileName":"Ball.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\n\n/// \u003Csummary\u003E\n/// \u7403\uFF08\u771F\u4EBA/bot \u5171\u7528\uFF09\uFF1A**owner \u672C\u5730\u6A21\u62DF\u79FB\u52A8**\uFF08\u771F\u4EBA=\u952E\u76D8\u8F93\u5165\uFF0Cbot=host \u7684 BotBrain\uFF09\uFF0C\n/// \u6743\u5A01\u6570\u503C\uFF08Mass/IsBot/ColorIndex/PlayerName/OwnerSteamId\uFF09\u7531 host \u7ECF [Sync(FromHost)] \u4E0B\u53D1\uFF0C\n/// \u4F4D\u7F6E\u968F\u7F51\u7EDC transform \u81EA\u52A8\u540C\u6B65\uFF08owner \u2192 \u5176\u4ED6\u7AEF\uFF09\u3002\n/// \u89C6\u89C9\uFF1A\u7403\u4F53/\u8868\u60C5/\u5934\u50CF\u5168\u7531 NeonRenderer \u6279\u91CF\u7ED8\u5236\uFF08\u8868\u60C5\u6309 Mood \u9009 face_N \u6279\u6B21\uFF1B\n/// \u771F\u4EBA Steam \u5934\u50CF\u5404\u7AEF\u6309 OwnerSteamId \u81EA\u53D6\u540E\u7ECF NeonRenderer.SetAvatar \u767B\u8BB0\uFF09\u3002\n/// \u003C/summary\u003E\npublic sealed class Ball : Component\n{\n\t[Sync( SyncFlags.FromHost )] public float Mass { get; set; } = GameConfig.StartMass;\n\n\t[Sync( SyncFlags.FromHost )] public bool IsBot { get; set; }\n\n\t[Sync( SyncFlags.FromHost )] public int ColorIndex { get; set; }\n\n\t[Sync( SyncFlags.FromHost )] public string PlayerName { get; set; } = \u0022Player\u0022;\n\n\t/// \u003Csummary\u003E \u62E5\u6709\u8005 SteamId\uFF08bot \u4E3A 0\uFF09\uFF1Bhost \u5199\u5165\uFF0C\u5404\u7AEF\u636E\u6B64\u52A0\u8F7D\u5BF9\u5E94\u5934\u50CF \u003C/summary\u003E\n\t[Sync( SyncFlags.FromHost )] public long OwnerSteamId { get; set; }\n\n\t/// \u003Csummary\u003E \u961F\u4F0D\u53F7\uFF08\u56E2\u961F\u8D5B 0..N-1\uFF0C\u666E\u901A\u8D5B -1\uFF09\uFF1Bhost \u5F00\u5C40\u8F6E\u8F6C\u5206\u914D\uFF0C\u91CD\u751F\u4E0D\u53D8 \u003C/summary\u003E\n\t[Sync( SyncFlags.FromHost )] public int TeamIndex { get; set; } = -1;\n\n\t/// \u003Csummary\u003E \u86F0\u4F0F\u5907\u7528\u7403\uFF08v0.7.8.25\uFF09\uFF1A\u623F\u95F4\u9884\u7559/\u672C\u5C40\u591A\u4F59\u7684 bot\u3002Alive=false \u4E0E\u0022\u6B7B\u4EA1\u0022\u65E0\u6CD5\u533A\u5206\uFF0C\n\t/// TickBots \u66FE\u628A 16 \u9897\u86F0\u4F0F\u7403\u5F53\u5C38\u4F53\u539F\u5730\u590D\u6D3B\u6210\u65E0\u961F FFA bot \u704C\u8FDB T1\u2014\u2014\u5FC5\u987B\u6709\u72EC\u7ACB\u6807\u8BB0 \u003C/summary\u003E\n\t[Sync( SyncFlags.FromHost )] public bool Dormant { get; set; }\n\n\t/// \u003Csummary\u003E \u4E3B\u4EBA\u8D26\u53F7\u7B49\u7EA7\uFF08v0.7.8.19\uFF09\uFF1A\u5404\u7AEF\u7B97\u597D\u5199\u8FDB**\u81EA\u5DF1\u7684\u7403**\uFF08\u666E\u901A [Sync]=owner \u53EF\u5199\uFF09\uFF0C\n\t/// \u5168\u573A\u540D\u724C\u53EF\u89C1\uFF1Bbot \u6052 0 \u4E0D\u663E\u793A \u003C/summary\u003E\n\t[Sync] public int AccountLevel { get; set; }\n\n\t// ---- \u9053\u5177 buff\uFF08v0.7.3.0\uFF09\uFF1Ahost \u6743\u5A01\u5199\u5165\uFF0C\u5404\u7AEF\u5C55\u793A/\u6548\u679C\u5171\u7528\u3002\n\t// Kind \u6B8B\u7559\u65E7\u503C\u6CA1\u5173\u7CFB\uFF0CHasBuff \u53EA\u770B _sinceBuff \u003C BuffDuration\uFF1B\n\t// serial \u81EA\u589E\u4FDD\u8BC1\u0022\u540C\u7C7B buff \u518D\u6361\u4E00\u6B21\u0022\u4E5F\u89E6\u53D1 [Change]\uFF08\u5BA2\u6237\u7AEF\u91CD\u7F6E\u672C\u5730\u8BA1\u65F6\uFF09 ----\n\n\t[Sync( SyncFlags.FromHost )] public byte BuffKind { get; set; } = 255;\n\n\t[Sync( SyncFlags.FromHost )] public float BuffDuration { get; set; }\n\n\t[Sync( SyncFlags.FromHost ), Change( nameof( OnBuffSynced ) )] public byte BuffSerial { get; set; }\n\n\t/// \u003Csummary\u003E bot \u8868\u60C5\u6863\uFF08v0.7.8.36 \u50CF\u7D20\u98CE\uFF09\uFF1A0=\u5F00\u5FC3 1=\u5174\u594B(\u8FFD\u730E) 2=\u60CA\u6050(\u9003\u8DD1) 3=\u7729\u6655 4=\u778C\u7761 5=\u5F97\u610F(\u738B\u724C\u8FFD\u730E)\u3002\n\t/// host \u7684 BotBrain \u5199\u5165\uFF08FromHost\uFF09\uFF0CNeonRenderer \u6BCF\u5E27\u6309 Mood \u73B0\u753B\u8868\u60C5\u8138\uFF1B\u771F\u4EBA\u7403\u4E0D\u53C2\u4E0E \u003C/summary\u003E\n\t[Sync( SyncFlags.FromHost )] public byte Mood { get; set; }\n\n\tTimeSince _sinceBuff = 999f;\n\n\t// ---- \u9053\u5177\u80CC\u5305\uFF08v0.7.5.0 \u4E3B\u52A8\u4F7F\u7528\uFF09\uFF1A\u62FE\u53D6\u5B58\u5165 Q/E \u4E24\u683C\uFF0C\u6309\u952E\u91CA\u653E\u6548\u679C\u3002\n\t// 255 = \u7A7A\u69FD\uFF1B\u6B7B\u4EA1/\u91CD\u751F\u6E05\u7A7A\uFF08\u673A\u4F1A\u4E0D\u8DE8\u547D\u643A\u5E26\uFF09 ----\n\n\t[Sync( SyncFlags.FromHost )] public byte PowerA { get; set; } = 255;\n\n\t[Sync( SyncFlags.FromHost )] public byte PowerB { get; set; } = 255;\n\n\t/// \u003Csummary\u003E \u5404\u7AEF\u6536\u5230 buff \u53D8\u5316\uFF08\u542B host \u672C\u5730\u5199\u5165\uFF09\u5373\u91CD\u7F6E\u672C\u5730\u8BA1\u65F6\u2014\u2014\u5BA2\u6237\u7AEF\u636E\u6B64\u5012\u7B97\u5269\u4F59\u65F6\u95F4 \u003C/summary\u003E\n\tvoid OnBuffSynced() =\u003E _sinceBuff = 0;\n\n\t/// \u003Csummary\u003E host\uFF1A\u7ED9\u8FD9\u9897\u7403\u4E0A buff\uFF08\u8D28\u91CF\u7F50\u7B49\u5373\u65F6\u6548\u679C\u4E0D\u8D70\u8FD9\u91CC\uFF09 \u003C/summary\u003E\n\tpublic void ApplyBuff( byte kind, float duration )\n\t{\n\t\tBuffKind = kind;\n\t\tBuffDuration = duration;\n\t\tBuffSerial\u002B\u002B;\n\t\t_sinceBuff = 0;\n\t}\n\n\t/// \u003Csummary\u003E buff \u662F\u5426\u751F\u6548\u4E2D\uFF08host \u6743\u5A01\u5224\u5B9A\u4E0E\u5BA2\u6237\u7AEF\u5C55\u793A\u540C\u4E00\u516C\u5F0F\uFF0C\u5BA2\u6237\u7AEF\u81EA\u7136\u8FC7\u671F\u65E0\u9700\u901A\u77E5\uFF09 \u003C/summary\u003E\n\tpublic bool HasBuff( PowerUpManager.Kind kind ) =\u003E\n\t\tBuffKind == (byte)kind \u0026\u0026 _sinceBuff \u003C BuffDuration;\n\n\t/// \u003Csummary\u003E \u5F53\u524D buff \u5269\u4F59\u79D2\u6570\uFF08HUD \u5012\u8BA1\u65F6\u7528\uFF1B\u65E0 buff \u4E3A 0\uFF09 \u003C/summary\u003E\n\tpublic float BuffLeft =\u003E BuffKind == 255 ? 0f : MathF.Max( 0f, BuffDuration - _sinceBuff );\n\n\t/// \u003Csummary\u003E \u80CC\u5305\u7B2C slot \u683C\uFF080=Q \u69FD 1=E \u69FD\uFF09\u7684\u9053\u5177\u79CD\u7C7B\uFF1B255=\u7A7A \u003C/summary\u003E\n\tpublic byte PowerSlot( int slot ) =\u003E slot == 0 ? PowerA : PowerB;\n\n\t/// \u003Csummary\u003E \u62FE\u53D6\u5165\u5305\uFF08v0.7.8.30 \u8D77**\u53EA\u8FDB E \u69FD**\u2014\u2014Q \u6539\u4E3A\u968F\u65F6\u95F4\u5145\u80FD\u7684\u6280\u80FD\u4F4D\uFF0C\u4E0D\u518D\u63A5\u6536\u62FE\u53D6\uFF09\uFF1B\n\t/// E \u6EE1\u8FD4\u56DE -1\uFF08\u9053\u5177\u7559\u5728\u573A\u4E0A\uFF09 \u003C/summary\u003E\n\tpublic int StorePower( byte kind )\n\t{\n\t\tif ( PowerB == 255 ) { PowerB = kind; return 1; }\n\t\treturn -1;\n\t}\n\n\t/// \u003Csummary\u003E host\uFF1A\u4F7F\u7528\u80CC\u5305\u9053\u5177\u2014\u2014\u6E05\u69FD\u5E76\u89E6\u53D1\u6548\u679C\uFF08PowerUpManager.ApplyEffect\uFF09 \u003C/summary\u003E\n\tpublic void UsePower( int slot )\n\t{\n\t\tvar kind = PowerSlot( slot );\n\t\tif ( kind == 255 ) return;\n\n\t\tif ( slot == 0 ) PowerA = 255;\n\t\telse PowerB = 255;\n\n\t\tPowerUpManager.ApplyEffect( this, kind );\n\t}\n\n\t/// \u003Csummary\u003E \u6309\u952E\u91CA\u653E\u80CC\u5305\u69FD\uFF08pressed \u4E3A true \u65F6\u5904\u7406\uFF09\uFF1A\u672C\u5730\u5148\u64AD\u6FC0\u6D3B\u6A2A\u5E45\u002B\u97F3\u6548\u002B\u8F7B\u9707\uFF0C\n\t/// \u518D\u8D70\u6743\u5A01/RPC \u8DEF\u5F84 \u003C/summary\u003E\n\tvoid UsePowerSlot( int slot, bool pressed )\n\t{\n\t\tif ( !pressed ) return;\n\n\t\tvar kind = PowerSlot( slot );\n\t\tif ( kind == 255 ) return;   // \u7A7A\u69FD\uFF1A\u6309\u952E\u65E0\u6548\uFF08\u4E0D\u64AD\u6A2A\u5E45\u4E0D\u53D1\u8BF7\u6C42\uFF09\n\n\t\tCircleroyaleGame.Current?.Hud?.AddBanner( GameHud.ActivationText( kind ), GameHud.KindColor( kind ) );\n\t\tGameSfx.Pickup();\n\t\tCircleroyaleGame.Current?.Camera?.Shake( 5f );   // \u653E\u6280\u80FD\u8F7B\u9707\uFF08\u51FB\u6740\u9707\u66F4\u731B\uFF0C\u968F combo \u52A0\u7801\uFF09\n\t\tGameAchievements.PowerUsed( kind );   // \u6210\u5C31\uFF1A\u9996\u6B21\u7528\u9053\u5177 / \u9996\u6B21\u653E\u5C16\u523A\uFF08v0.7.8.14\uFF09\n\n\t\tif ( NetworkManager.IsAuthority )\n\t\t\tUsePower( slot );\n\t\telse\n\t\t\tRequestUsePower( slot );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u662F\u5426\u5B58\u6D3B\u3002**\u7403\u5BF9\u8C61\u6C38\u4E0D\u9500\u6BC1**\uFF1A\u6B7B\u4EA1 = Alive=false\uFF08\u540C\u6B65\u5B57\u6BB5\uFF0C\u6E32\u67D3/AI/\u5224\u5B9A\u5168\u90E8\u8DF3\u8FC7\uFF09\uFF0C\n\t/// \u91CD\u751F = host \u7F6E\u56DE true\u3002\u7ED5\u5F00\u0022\u8FDB\u884C\u4E2D NetworkSpawn \u4E0D\u590D\u5236\u5230\u5DF2\u8FDE\u63A5\u5BA2\u6237\u7AEF\u0022\u7684\u5F15\u64CE\u884C\u4E3A\uFF08\u5B9E\u6D4B\u5927\u5751\uFF09\u3002\n\t/// \u003C/summary\u003E\n\t[Sync( SyncFlags.FromHost )]\n\tpublic bool Alive { get; set; } = true;\n\n\tpublic float Radius =\u003E GameConfig.StartRadius * MathF.Sqrt( Mass / GameConfig.StartMass );\n\n\t/// \u003Csummary\u003E \u5F53\u524D\u901F\u5EA6\uFF08v0.7.8.32\uFF1Abot \u62E6\u622A\u9884\u5224\u6309\u5B83\u6253\u63D0\u524D\u91CF\uFF1B\u53EA\u8BFB\uFF0CBallInput \u5185\u90E8\u7EF4\u62A4\uFF09 \u003C/summary\u003E\n\tpublic Vector3 Velocity =\u003E _velocity;\n\n\t/// \u003Csummary\u003E \u672C\u6761\u547D\u5DF2\u5B58\u6D3B\u79D2\u6570\uFF08\u6210\u5C31\u0022\u5B58\u6D3B 5 \u5206\u949F\u0022\u7528\uFF1B\u91CD\u751F\u65F6 _sinceSpawn \u5F52\u96F6\uFF09 \u003C/summary\u003E\n\tpublic float LifeSeconds =\u003E _sinceSpawn;\n\n\t/// \u003Csummary\u003E \u672C\u7403\u7684\u9713\u8679\u8272\uFF08\u7531 ColorIndex \u4ECE\u8C03\u8272\u677F\u53D6\uFF09 \u003C/summary\u003E\n\tpublic Color NeonColor =\u003E GameConfig.Palette[Math.Clamp( ColorIndex, 0, GameConfig.Palette.Length - 1 )];\n\n\t/// \u003Csummary\u003E \u79FB\u901F\uFF1A\u8D28\u91CF\u66F2\u7EBF \u00D7 \u52A0\u901F buff\uFF08\u26A1 \u65F6 \u00D71.4\uFF1BSpeed \u5C5E\u6027\u5404\u7AEF\u540C\u7B97\uFF0Cowner \u6A21\u62DF\u4E0E host \u6821\u9A8C\u4E00\u81F4\uFF09 \u003C/summary\u003E\n\tpublic float Speed\n\t{\n\t\tget\n\t\t{\n\t\t\tvar s = MathF.Max( GameConfig.MinSpeed,\n\t\t\t\tGameConfig.BaseSpeed * MathF.Pow( GameConfig.StartMass / Mass, GameConfig.SpeedCurve ) );\n\t\t\tif ( HasBuff( PowerUpManager.Kind.Speed ) ) s *= GameConfig.PowerUpSpeedBoost;\n\t\t\t// \u654C\u8425\u5927\u672C\u8425\u51CF\u901F\uFF08M7.2 \u5B89\u5168\u5C4B\uFF09\uFF1BInHostileHq \u5BF9\u65E0\u961F/\u975E\u9886\u571F\u6052 false\n\t\t\tif ( TerritoryManager.InHostileHq( TeamIndex, WorldPosition ) ) s *= GameConfig.TerritoryHqSlowFactor;\n\t\t\treturn s;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u662F\u5426\u51FA\u751F\u4FDD\u62A4\u4E2D\uFF08\u865A\u73AF\u62A4\u76FE\u8868\u73B0\uFF0C\u53CC\u5411\u514D\u6218\uFF09 \u003C/summary\u003E\n\tpublic bool IsProtected =\u003E _sinceSpawn \u003C GameConfig.SpawnProtectSeconds;\n\n\t/// \u003Csummary\u003E \u9886\u571F\u804C\u4E1A\uFF08M7.4\uFF0Chost \u968F\u673A\u5206\u914D\u968F\u590D\u6D3B\u91CD\u63B7\uFF1B255=\u65E0\u30020 \u6307\u6325\u5B98 1 \u5DE5\u5175 2 \u62A4\u536B 3 \u5766\u514B\uFF09 \u003C/summary\u003E\n\t[Sync( SyncFlags.FromHost )]\n\tpublic byte TerritoryClass { get; set; } = 255;\n\n\tTimeSince _sinceClassCast = 999f;   // \u672C\u5730\u51B7\u5374\u4F30\u7B97\uFF08host \u6743\u5A01\u51B7\u5374\u5728 ClassSkillManager\uFF0C\u672A\u597D\u65F6\u9759\u9ED8\u62D2\u7EDD\uFF09\n\n\t/// \u003Csummary\u003E \u672C\u5730\u4F30\u7B97\u7684\u804C\u4E1A\u51B7\u5374\u5269\u4F59\u79D2\uFF08HUD Q \u94AE\u5012 sweep \u7528\uFF09 \u003C/summary\u003E\n\tpublic float ClassCooldownRemaining =\u003E\n\t\tTerritoryClass \u003E 3 ? 0f : MathF.Max( 0f, ClassSkillManager.CooldownOf( TerritoryClass ) - _sinceClassCast );\n\n\t/// \u003Csummary\u003E Q \u65BD\u653E\u804C\u4E1A\u6280\u80FD\uFF08M7.4 \u9886\u571F\uFF09\uFF1A\u672C\u5730\u5373\u65F6\u53CD\u9988\u002B\u51B7\u5374\u4F30\u7B97\u8D77\u8868\uFF1Bhost \u76F4\u8C03 / \u5BA2\u6237\u7AEF\u53D1 RPC \u003C/summary\u003E\n\tvoid TryCastClassSkill()\n\t{\n\t\tif ( !Alive || TerritoryClass \u003E 3 ) return;\n\t\tif ( ClassCooldownRemaining \u003E 0f ) return;\n\n\t\tCircleroyaleGame.Current?.Hud?.AddBanner( ClassSkillManager.NameOf( TerritoryClass ) \u002B \u0022!\u0022, new Color( 0.98f, 0.83f, 0.35f ) );\n\t\tGameSfx.Pickup();\n\t\tCircleroyaleGame.Current?.Camera?.Shake( 5f );\n\t\t_sinceClassCast = 0;   // \u672C\u5730\u4F30\u7B97\u8D77\u8868\n\n\t\tif ( NetworkManager.IsAuthority )\n\t\t\tClassSkillManager.Cast( this );\n\t\telse\n\t\t\tNetworkManager.RequestClassSkill();\n\t}\n\n\t/// \u003Csummary\u003E \u7403\u4F53\u80CC\u666F\u586B\u5145\u8272\uFF1A\u771F\u4EBA=\u5934\u50CF\u4E3B\u8272\u53D6\u6837\uFF0Cbot=\u9713\u8679\u914D\u8272 \u003C/summary\u003E\n\tpublic Color BackgroundColor =\u003E _avatarColor ?? NeonColor;\n\n\tVector3 _velocity;\n\tTimeSince _sinceSpawn;\n\tTimeSince _sinceWallHit = 999f;    // \u649E\u5899\u8109\u51B2\u8282\u6D41\uFF08\u8D34\u5899\u6ED1\u884C\u4E0D\u8FDE\u7206\uFF09\n\tColor? _avatarColor;\n\tVector2 _botDir;\n\tVector2 _lastAimDir = Vector2.Zero;     // \u5149\u6807\u7784\u51C6\u65B9\u5411\uFF08\u5206\u88C2/\u5410\u5B62\u5B50\u7528\uFF0Cv0.7.0.0\uFF09\n\n\t/// \u003Csummary\u003E \u5F53\u524D\u7784\u51C6\u65B9\u5411\uFF08\u672C\u673A\u7403\uFF1A\u6E32\u67D3\u5916\u7F18\u7BAD\u5934\u7528\uFF1Bbot \u6052 Zero \u4E0D\u753B\uFF09 \u003C/summary\u003E\n\tpublic Vector2 AimDir =\u003E _lastAimDir;\n\tTimeSince _sinceEject;                  // \u5BA2\u6237\u7AEF\u6309\u4F4F\u8FDE\u5410\u7684\u8282\u6D41\uFF08host \u8FD8\u6709\u540C\u6B3E\u6821\u9A8C\uFF09\n\tTimeSince _sinceAvatarRetry = 1f;   // \u9996\u5E27\u7ACB\u5373\u5C1D\u8BD5\n\tint _avatarRetryCount;\n\tbool _avatarReady;\n\tbool _wasAlive = true;\n\n\t/// \u003Csummary\u003E \u6B7B\u4EA1\u65F6\u523B\uFF08host \u8BB0\u5F55\uFF0C\u7528\u4E8E bot \u539F\u5730\u590D\u6D3B\u7684\u5EF6\u65F6\uFF09 \u003C/summary\u003E\n\tpublic TimeSince SinceDeath { get; private set; }\n\n\t/// \u003Csummary\u003E\n\t/// host \u4FA7\u6807\u8BB0\uFF1A\u8FD9\u9897\u7403\u662F\u4E3A\u8FDC\u7A0B\u8FDE\u63A5\u751F\u6210\u7684\uFF08\u4EC5\u6743\u5A01\u7AEF\u6709\u610F\u4E49\uFF0C\u4E0D\u8054\u7F51\uFF09\u3002\n\t/// ScanBalls/SetLocalBall \u7528\u5B83\u628A\u8FDC\u7A0B\u7403\uFF08\u542B\u7ADE\u6001\u4E0B\u672A\u7F51\u7EDC\u5316\u7684\u5E7D\u7075\u7403\uFF09\u6392\u9664\u51FA\u672C\u673A\u7403\u5019\u9009\uFF0C\n\t/// \u5426\u5219\u540C\u8D26\u53F7\u53CC\u5F00\u65F6\u5E7D\u7075\u7403\u4F1A\u9876\u6389 host \u81EA\u5DF1\u7684\u7403\u3001\u76F8\u673A\u6765\u56DE\u8DF3\uFF08\u5B9E\u6D4B\uFF09\u3002\n\t/// \u003C/summary\u003E\n\tpublic bool IsRemoteOwned { get; set; }\n\n\t/// \u003Csummary\u003E host \u4FA7\u6807\u8BB0\u6B7B\u4EA1\uFF08\u5BF9\u8C61\u4FDD\u7559\uFF0C\u539F\u5730\u590D\u7528\uFF09 \u003C/summary\u003E\n\tpublic void MarkDead()\n\t{\n\t\tAlive = false;\n\t\tSinceDeath = 0;\n\t\t_velocity = Vector3.Zero;\n\t}\n\n\t/// \u003Csummary\u003E \u91CD\u7F6E\u51FA\u751F\u4FDD\u62A4\u8BA1\u65F6\uFF08host \u5728\u73A9\u5BB6\u771F\u6B63\u5165\u5C40\u65F6\u8C03\u7528\u2014\u2014pre-snapshot \u7403\u7684\n\t/// \u4FDD\u62A4\u671F\u5728\u63E1\u624B\u671F\u95F4\u5C31\u7A7A\u8DD1\u5B8C\u4E86\uFF0C\u5B9E\u6D4B\u52A0\u5165 1.6s \u5373\u88AB\u5403\uFF1BIsProtected \u6309\u5404\u7AEF\u672C\u5730\u8BA1\u65F6\u5224\u5B9A\uFF0C\n\t/// host \u91CD\u7F6E\u5373\u6062\u590D\u6743\u5A01\u5224\u5B9A\uFF0C\u5BA2\u6237\u7AEF\u81EA\u5DF1\u7684\u8BA1\u65F6\u968F\u5FEB\u7167\u5E94\u7528\u53E6\u7B97\uFF09 \u003C/summary\u003E\n\tpublic void RefreshSpawnProtection() =\u003E _sinceSpawn = 0;\n\n\t/// \u003Csummary\u003E \u77AC\u79FB\uFF08\u5206\u8EAB\u664B\u5347\u4E3B\u7403\u65F6\u7528\uFF09\uFF1A\u4F4D\u7F6E\u5F52 owner \u6A21\u62DF\uFF0Cowner \u7AEF\u5FC5\u987B\u81EA\u5DF1\u642C\u8FC7\u53BB\uFF1B\n\t/// \u4E0D\u89E6\u53D1\u4FDD\u62A4\u671F/\u91CD\u64AD\u97F3\u6548\u2014\u2014\u8FD9\u662F\u8EAB\u4EFD\u5EF6\u7EED\uFF0C\u4E0D\u662F\u91CD\u751F \u003C/summary\u003E\n\tpublic void TeleportTo( Vector3 pos )\n\t{\n\t\tWorldPosition = new Vector3( pos.x, pos.y, 0f );\n\t\t_velocity = Vector3.Zero;\n\t}\n\n\t/// \u003Csummary\u003E host \u4FA7\u521D\u59CB\u5316\uFF08\u4EC5\u6743\u5A01\u7AEF\u8C03\u7528\uFF1B\u5BA2\u6237\u7AEF\u5B57\u6BB5\u5168\u90E8\u8D70\u7F51\u7EDC\u540C\u6B65\uFF09\u3002\n\t/// teamIndex \u2265 0 \u65F6\u540C\u65F6\u5199\u5165\u961F\u4F0D\u53F7\uFF1B\u56E2\u961F\u8D5B\u989C\u8272\u9501\u961F\u4F0D\u8272\uFF0C\u666E\u901A\u8D5B\u968F\u673A\u9713\u8679\u8272 \u003C/summary\u003E\n\tpublic void Init( bool isBot, int teamIndex = -1 )\n\t{\n\t\tIsBot = isBot;\n\t\tif ( teamIndex \u003E= 0 ) TeamIndex = teamIndex;\n\n\t\tColorIndex = MatchState.IsTeam\n\t\t\t? ( ( TeamIndex % GameConfig.Palette.Length ) \u002B GameConfig.Palette.Length ) % GameConfig.Palette.Length\n\t\t\t: (int)Game.Random.Float( 0f, GameConfig.Palette.Length - 0.001f );\n\n\t\t_sinceSpawn = 0;\n\t\t_velocity = Vector3.Zero;\n\n\t\t// \u9053\u5177 buff \u4E0E\u80CC\u5305\u968F\u91CD\u751F\u6E05\u6389\uFF08\u673A\u4F1A\u4E0D\u8DE8\u547D\u643A\u5E26\uFF09\n\t\tBuffKind = 255;\n\t\tBuffDuration = 0f;\n\t\tPowerA = 255;\n\t\tPowerB = 255;\n\t}\n\n\t/// \u003Csummary\u003E bot \u7531 BotBrain \u6BCF\u5E27\u5199\u5165\u7684\u79FB\u52A8\u65B9\u5411\uFF08\u771F\u4EBA\u8D70 BallInput\uFF0C\u4E0D\u7528\u8FD9\u4E2A\uFF09 \u003C/summary\u003E\n\tpublic void SetBotDirection( Vector2 dir ) =\u003E _botDir = dir;\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\n\t\t// \u6240\u6709\u673A\u5668\u7EDF\u4E00\u4ECE\u7F51\u7EDC\u5BF9\u8C61\u7684 OnStart \u6CE8\u518C\uFF08host \u751F\u6210 / \u5BA2\u6237\u7AEF\u968F\u590D\u5236\u51FA\u73B0\uFF09\n\t\tCircleroyaleGame.Current?.RegisterBall( this );\n\t\tif ( !IsProxy \u0026\u0026 !IsBot )\n\t\t\tCircleroyaleGame.Current?.SetLocalBall( this );\n\n\t\tTryLoadAvatar();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u52A0\u8F7D\u5934\u50CF\uFF08\u771F\u4EBA\u6309 OwnerSteamId \u62C9 Steam \u5934\u50CF\uFF1A\u4E0B\u8F7D\u6709\u5EF6\u8FDF\uFF0C\u5931\u8D25\u6BCF\u79D2\u91CD\u8BD5\uFF0C15 \u6B21\u540E\u8F6C\u5360\u4F4D\u56FE\uFF1B\n\t/// \u62FF\u5230\u540E\u7ECF NeonRenderer.SetAvatar \u767B\u8BB0\u72EC\u7ACB\u8D34\u56FE\u6279\u6B21\uFF09\u3002bot \u8868\u60C5\u8138\u7531 NeonRenderer \u6309 Mood\n\t/// \u4ECE face_0..5 \u73B0\u753B\uFF0C\u4E0D\u5728\u8FD9\u91CC\u52A0\u8F7D\u3002\n\t/// \u003C/summary\u003E\n\tvoid TryLoadAvatar()\n\t{\n\t\tif ( IsBot )\n\t\t{\n\t\t\t_avatarReady = true;\n\t\t\treturn;\n\t\t}\n\n\t\tif ( OwnerSteamId == 0 ) return;   // \u540C\u6B65\u672A\u5230\uFF0C\u7B49\u4E0B\u4E00\u8F6E\u91CD\u8BD5\n\n\t\tvar avatar = Texture.LoadAvatar( OwnerSteamId, 128 );\n\t\tif ( avatar is null ) return;\n\n\t\tSetAvatarTexture( avatar );\n\t\t_avatarReady = true;\n\t}\n\n\t/// \u003Csummary\u003E \u5934\u50CF\u8D34\u56FE\u767B\u8BB0\u8FDB NeonRenderer\uFF08\u6E32\u67D3\u5668\u672A\u5C31\u7EEA\u65F6\u7531\u5176\u9759\u6001\u8868\u5F85\u547D\uFF0C\u4E0B\u4E00\u5E27\u751F\u6548\uFF09 \u003C/summary\u003E\n\tvoid SetAvatarTexture( Texture tex )\n\t{\n\t\tif ( tex is null )\n\t\t{\n\t\t\tLog.Warning( $\u0022avatar load failed: \u0027{PlayerName}\u0027 steam={OwnerSteamId}\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tNeonRenderer.SetAvatar( OwnerSteamId, tex );\n\t\t_avatarColor = SampleAverageColor( tex );\n\t}\n\n\t/// \u003Csummary\u003E \u5934\u50CF\u4E3B\u8272\uFF1A\u5168\u56FE\u4E0D\u900F\u660E\u50CF\u7D20\u7684\u5747\u503C\uFF08\u7528\u4F5C\u7403\u5E95\u586B\u5145\u8272\uFF09 \u003C/summary\u003E\n\tstatic Color? SampleAverageColor( Texture tex )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar pixels = tex.GetPixels( 0 );\n\t\t\tif ( pixels is null || pixels.Length == 0 ) return null;\n\n\t\t\tfloat r = 0f, g = 0f, b = 0f;\n\t\t\tint n = 0;\n\t\t\tfor ( int i = 0; i \u003C pixels.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar p = pixels[i];\n\t\t\t\tif ( p.a \u003C 32 ) continue;\n\t\t\t\tr \u002B= p.r;\n\t\t\t\tg \u002B= p.g;\n\t\t\t\tb \u002B= p.b;\n\t\t\t\tn\u002B\u002B;\n\t\t\t}\n\t\t\tif ( n == 0 ) return null;\n\n\t\t\treturn new Color( r / n / 255f, g / n / 255f, b / n / 255f );\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\n\t\t// \u5934\u50CF\u91CD\u8BD5\uFF1A\u6BCF\u79D2\u4E00\u6B21\uFF0C15 \u6B21\u672A\u62FF\u5230\u8F6C\u5360\u4F4D\u56FE\uFF08Steam \u5934\u50CF\u4E0B\u8F7D\u6162\u662F\u5E38\u6001\uFF09\n\t\tif ( !_avatarReady \u0026\u0026 _sinceAvatarRetry \u003E 1f )\n\t\t{\n\t\t\t_sinceAvatarRetry = 0;\n\t\t\tif ( _avatarRetryCount\u002B\u002B \u003E= 15 )\n\t\t\t{\n\t\t\t\tSetAvatarTexture( Texture.Load( GameConfig.AvatarPlaceholderPath, false ) );\n\t\t\t\t_avatarReady = true;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tTryLoadAvatar();\n\t\t\t}\n\t\t}\n\n\t\t// ---- \u5206\u88C2/\u5410\u5B62\u5B50\u8F93\u5165\uFF08M4.11\uFF1A\u6BCF\u5E27\u8DEF\u5F84\uFF0C\u4EC5\u672C\u673A\u771F\u4EBA\u3001\u6D3B\u7403\uFF09----\n\t\t// \u6743\u5A01\u7AEF\u76F4\u63A5\u6267\u884C\uFF1B\u5BA2\u6237\u7AEF\u53D1\u5B9E\u4F8B RPC\uFF08[Rpc.Host]\uFF0Chost \u4FA7\u6821\u9A8C\u540E\u6267\u884C\uFF0C\u5206\u8EAB\u662F host \u6743\u5A01\u6570\u636E\uFF09\u3002\n\t\t// \u97F3\u6548\u5728\u6309\u952E\u77AC\u95F4\u5373\u64AD\uFF08\u672C\u5730\u5373\u65F6\u53CD\u9988\uFF1Bhost \u6821\u9A8C\u4E0D\u8FC7\u9876\u591A\u7A7A\u54CD\u4E00\u6B21\uFF09\n\t\tif ( IsProxy || IsRemoteOwned || IsBot || !Alive ) return;\n\t\tif ( MatchState.MatchOver ) return;   // \u6BD4\u8D5B\u7ED3\u675F\uFF1A\u5168\u573A\u51BB\u7ED3\uFF0C\u8F93\u5165\u4E0D\u54CD\u5E94\n\n\t\t// \u505C\u5728\u7B49\u5F85\u9875\uFF08P \u9000\u5C40/\u6389\u7EBF\u6062\u590D\uFF09\u4E14\u4E0D\u5728\u5BF9\u5C40\u65F6\u505C\u6B62\u64CD\u63A7\uFF1A\u7403\u7559\u5728\u539F\u5730\uFF0C\u9632\u9694\u5C4F\u76F2\u5F00\u3002\n\t\t// \u26A0\uFE0F \u5FC5\u987B\u5E26 !IsMatchStarted\uFF1A\u53EA\u8981\u8FD8\u5728\u5BF9\u5C40\uFF08_gameStarted\uFF09\u5C31\u7EDD\u4E0D\u7981\u8F93\u5165\u2014\u2014\n\t\t// _lobbyWaiting \u82E5\u56E0\u5F02\u5E38\u6B8B\u7559\u4E3A true\uFF0C\u4F1A\u628A\u5BF9\u5C40\u4E2D\u7684\u79FB\u52A8/\u5206\u88C2/\u5410\u5B62\u5B50/\u7784\u51C6\u5168\u9501\u6B7B\n\t\t// \uFF08\u5B9E\u6D4B\uFF1A\u4E0D\u80FD\u5206\u8EAB/\u5410\u4E1D\u3001\u7784\u51C6\u7BAD\u5934\u51BB\u5728\u65E7\u65B9\u5411\uFF09\n\t\t{\n\t\t\tvar g = CircleroyaleGame.Current;\n\t\t\tif ( g is not null \u0026\u0026 g.IsClientWaiting \u0026\u0026 !g.IsMatchStarted )\n\t\t\t{\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// \u7784\u51C6\uFF08v0.7.0.0 \u7528\u6237\u9700\u6C42\uFF09\uFF1A_lastAimDir \u4F9B\u6E32\u67D3\u7784\u51C6\u7EBF\u7528\uFF1B\u52A8\u4F5C\u65B9\u5411\u7531 host \u6309\u8EAB\u4F53\u9010\u4E2A\u7B97\n\t\tvar aimDir = BallInput.GetAimDir( Scene.Camera, WorldPosition );\n\t\tif ( aimDir.Length \u003E 0.001f ) _lastAimDir = aimDir;\n\n\t\t// \u9F20\u6807\u952E\u5E76\u884C\uFF08v0.7.4.1 \u7528\u6237\u9700\u6C42\uFF09\uFF1A\u53F3\u952E\u5206\u88C2\u3001\u5DE6\u952E\u5410\u4E1D\uFF08\u0022Attack1\u0022/\u0022Attack2\u0022 \u4E3A\u5F15\u64CE\u5185\u5EFA\u6309\u94AE\uFF0C\n\t\t// \u5B98\u65B9\u9879\u76EE\u540C\u6B3E\u7528\u6CD5\uFF09\uFF1B\u7A7A\u683C/R \u4FDD\u7559\uFF0C\u4E24\u5957\u952E\u4F4D\u5171\u5B58\u3002\n\t\t// v0.7.8.7 \u624B\u611F\uFF1A\u4F20**\u5149\u6807\u4E16\u754C\u5750\u6807**\u800C\u975E\u65B9\u5411\u2014\u2014host \u5BF9\u6BCF\u4E2A\u8EAB\u4F53\u5355\u72EC\u7B97\u6307\u5411\uFF08\u5206\u8EAB\u4E5F\u671D\u5149\u6807\uFF09\n\t\tvar aimPoint = BallInput.GetAimPoint( Scene.Camera );\n\n\t\tif ( GameConfig.EnableSplit \u0026\u0026 ( Input.Pressed( \u0022Jump\u0022 ) || Input.Pressed( \u0022Attack2\u0022 ) ) )\n\t\t{\n\t\t\tif ( Mass \u003E= GameConfig.SplitMinMass ) GameSfx.Split( WorldPosition );\n\n\t\t\tif ( NetworkManager.IsAuthority )\n\t\t\t\tCircleroyaleGame.Current?.DoSplit( this, aimPoint );\n\t\t\telse\n\t\t\t\tRequestSplit( aimPoint );\n\t\t}\n\n\t\tif ( GameConfig.EnableEject\n\t\t\t\u0026\u0026 ( Input.Down( \u0022Reload\u0022 ) || Input.Down( \u0022Attack1\u0022 ) )\n\t\t\t\u0026\u0026 _sinceEject \u003E GameConfig.EjectCooldownSeconds )\n\t\t{\n\t\t\t_sinceEject = 0;\n\t\t\tif ( Mass \u003E= 2f ) GameSfx.Eject( WorldPosition );   // \u81F3\u5C11\u80FD\u5410\u6700\u5C0F\u4E00\u9897\uFF08\u4E0B\u9650 1 \u002B \u6D88\u8017 1\uFF09\n\n\t\t\tif ( NetworkManager.IsAuthority )\n\t\t\t\tCircleroyaleGame.Current?.DoEject( this, aimPoint );\n\t\t\telse\n\t\t\t\tRequestEject( aimPoint );\n\t\t}\n\n\t\t// \u9053\u5177\u4E3B\u52A8\u4F7F\u7528\uFF08v0.7.5.0\uFF09\uFF1AQ/E \u91CA\u653E\u80CC\u5305\u91CC\u7684\u9053\u5177\u2014\u2014\u6743\u5A01\u7AEF\u76F4\u63A5\u89E6\u53D1\uFF0C\u5BA2\u6237\u7AEF\u53D1 RPC\u3002\n\t\t// banner \u6309\u952E\u5373\u64AD\uFF08\u672C\u5730\u5373\u65F6\u53CD\u9988\uFF0C\u4E0D\u7B49 host \u5F80\u8FD4\uFF09\uFF1B\u69FD\u4F4D\u7A7A\u5219\u4EC0\u4E48\u90FD\u4E0D\u53D1\u751F\n\t\t\t// Q \u952E\uFF08M7.4\uFF09\uFF1A\u9886\u571F\u6A21\u5F0F=\u804C\u4E1A\u6280\u80FD\uFF08\u51B7\u5374\u5236\uFF0CPowerA \u65F6\u95F4\u5145\u80FD pass \u5728\u9886\u571F\u4E0B\u51BB\u7ED3\uFF09\uFF1B\n\t\t\t// \u5176\u4ED6\u6A21\u5F0F=\u5145\u80FD\u9053\u5177\u69FD\uFF08\u65E2\u6709\u884C\u4E3A\uFF09\u3002E \u952E\u9053\u5177\u7167\u65E7\n\t\t\t// \u6587\u672C\u8F93\u5165\uFF08\u804A\u5929\u6846\uFF09\u6301\u952E\u76D8\u7126\u70B9\u65F6\u4E00\u5F8B\u4E0D\u8BFB\u6309\u952E\uFF1A\u5F15\u64CE\u628A\u6309\u952E\u65E0\u5DEE\u522B\u4E22\u8FDB\u6240\u6709 InputContext\uFF0C\n\t\t\t// \u4E0D\u6321\u7684\u8BDD\u804A\u5929\u91CC\u6253 Q/E \u4F1A\u76F4\u63A5\u653E\u804C\u4E1A\u6280/\u9053\u5177\n\t\t\tif ( !BallInput.Typing )\n\t\t\t{\n\t\t\t\tif ( MatchState.IsTerritory )\n\t\t\t\t{\n\t\t\t\t\tif ( Input.Keyboard.Pressed( \u0022Q\u0022 ) ) TryCastClassSkill();\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tUsePowerSlot( 0, Input.Keyboard.Pressed( \u0022Q\u0022 ) );\n\t\t\t\t}\n\t\t\t\tUsePowerSlot( 1, Input.Keyboard.Pressed( \u0022E\u0022 ) );\n\t\t\t}\n\t}\n\n\tprotected override void OnFixedUpdate()\n\t{\n\t\tif ( IsProxy ) return;   // \u53EA\u6709\u62E5\u6709\u8005\uFF08\u771F\u4EBA\uFF09\u6216 host\uFF08bot\uFF09\u6A21\u62DF\n\t\tif ( IsRemoteOwned ) return;   // host \u4FA7\u8FDC\u7A0B\u7403\u7531\u5176\u4E3B\u4EBA\u6A21\u62DF\uFF1B\u672A\u7F51\u7EDC\u5316\u7684\u5E7D\u7075\u7403\u7EDD\u4E0D\u672C\u5730\u6A21\u62DF\n\n\t\t// \u6B7B\u4EA1\u72B6\u6001\uFF1A\u505C\u4F4F\u4E0D\u52A8\uFF08\u5BF9\u8C61\u4FDD\u7559\uFF09\uFF1BAlive \u7FFB\u56DE true \u65F6\u672C\u5730\u91CD\u751F\uFF08\u968F\u673A\u6362\u4F4D\u002B\u4FDD\u62A4\u671F\uFF09\n\t\tif ( !Alive )\n\t\t{\n\t\t\t_velocity = Vector3.Zero;\n\t\t\t_wasAlive = false;\n\t\t\treturn;\n\t\t}\n\t\tif ( !_wasAlive )\n\t\t{\n\t\t\t_wasAlive = true;\n\t\t\t_sinceSpawn = 0;          // \u590D\u6D3B\u4FDD\u62A4\u671F\uFF08\u672C\u5730\u8868\u73B0\uFF1B\u6743\u5A01\u4FDD\u62A4\u671F\u5728 host \u7684 Init \u91CC\uFF09\n\t\t\t_velocity = Vector3.Zero;\n\t\t\t// owner \u81EA\u5DF1\u968F\u673A\u6362\u4F4D\uFF08\u4F4D\u7F6E\u5F52 owner \u6A21\u62DF\uFF09\uFF1B\u9886\u571F\u6A21\u5F0F\u9ED8\u8BA4\u56DE\u5927\u672C\u8425\uFF0C\n\t\t\t// \u6B7B\u4EA1\u9762\u677F\u70B9 FRONT\uFF08M7.2\uFF09\u5219\u6362\u5230\u6700\u8FD1\u5DF1\u65B9\u5360\u9886\u683C\u2014\u2014\u504F\u597D\u968F\u70B9\u51FB\u5B58\u672C\u673A\uFF0C\u6B64\u5904\u6D88\u8D39\n\t\t\tWorldPosition = RandomSpawnPos( TeamIndex, CircleroyaleGame.Current?.ConsumeFrontRespawn() ?? false, WorldPosition );\n\t\t\tif ( GameSfx.IsMine( OwnerSteamId ) )   // \u53EA\u64AD\u672C\u673A\u7403\uFF1A\u5F00\u5C40 32 \u4E2A bot \u540C\u5E27\u751F\u6210\u66FE\u53E0 33 \u5C42\u97F3\uFF08v0.7.8.10 \u4FEE\uFF09\n\t\t\t\tGameSfx.Respawn( WorldPosition );\n\t\t\tGameSfx.ResetEatCount();   // \u4E0B\u4E00\u6761\u547D\u5403\u98DF\u7269\u97F3\u4ECE 0 \u91CD\u65B0\u8BA1\n\t\t\tGameSfx.ResetCombo();      // \u8FDE\u51FB\u6E05\u96F6\uFF08\u65B0\u7684\u4E00\u6761\u547D\u91CD\u65B0\u8D77\u7B97\uFF09\n\t\t\tGameLog.Info( $\u0022[ball] local respawn \u0027{PlayerName}\u0027\u0022 );\n\t\t}\n\n\t\t// \u6BD4\u8D5B\u7ED3\u675F\uFF1A\u5168\u573A\u51BB\u7ED3\uFF08\u7ED3\u7B97\u9762\u677F\u63A5\u7BA1\uFF0C\u8C01\u4E5F\u4E0D\u8BB8\u518D\u52A8\uFF09\n\t\tif ( MatchState.MatchOver )\n\t\t{\n\t\t\t_velocity = Vector3.Zero;\n\t\t\treturn;\n\t\t}\n\n\t\t// \u771F\u4EBA = \u672C\u5730\u8F93\u5165\uFF08owner \u6A21\u62DF\uFF09\uFF1Bbot = BotBrain \u5199\u5165\u7684\u65B9\u5411\u3002\n\t\t// \u5206\u88C2/\u5410\u5B62\u5B50\u7684\u8F93\u5165\u5224\u5B9A\u5DF2\u79FB\u5230 OnUpdate\uFF08\u6BCF\u5E27\u8DEF\u5F84\uFF09\u2014\u2014Input \u6309\u952E\u8FB9\u6CBF\u6309\u6E32\u67D3\u5E27\u5237\u65B0\uFF0C\n\t\t// \u56FA\u5B9A\u6B65\u8FDB\u7684 FixedUpdate \u53EF\u80FD\u9519\u8FC7 Pressed \u8FB9\u6CBF\uFF08\u6309\u4E86\u6CA1\u53CD\u5E94\u7684\u9690\u60A3\uFF09\n\t\tvar dir = IsBot ? _botDir : BallInput.GetMoveDir( Scene.Camera );\n\n\t\t// \u901F\u5EA6\u4E0E\u8F6C\u5411\u90FD\u968F\u8D28\u91CF\u8870\u51CF\uFF08\u627918\u2460\uFF09\uFF1A\u4F53\u79EF\u8D8A\u5927\u6781\u901F\u8D8A\u4F4E\u3001\u60EF\u6027\u8D8A\u5F3A\u8F6C\u5411\u8D8A\u7B28\u62D9\uFF0C\n\t\t// \u5927\u7403\u5FC5\u987B\u63D0\u524D\u9884\u5224\u8D70\u4F4D\u3002\u54CD\u5E94\u65F6\u95F4\u5E38\u6570 = BaseTurnSpeed \u00D7 (StartMass/Mass)^TurnCurve\n\t\tvar turn = MathF.Max( GameConfig.MinTurnSpeed,\n\t\t\tGameConfig.BaseTurnSpeed * MathF.Pow( GameConfig.StartMass / Mass, GameConfig.TurnCurve ) );\n\t\tvar t = 1f - MathF.Exp( -turn * Time.Delta );\n\t\t_velocity = Vector3.Lerp( _velocity, dir * Speed, t );\n\n\t\tvar pos = WorldPosition \u002B _velocity * Time.Delta;\n\n\t\t// \u573A\u5730\u8FB9\u754C\u94B3\u5236\uFF08\u534A\u5F84\u5F02\u5E38\u6DA8\u7834\u573A\u5730\u65F6 half \u515C\u5E95\u4E3A 0\uFF0C\u7EDD\u4E0D\u8BA9 Clamp \u7684 min\u003Emax \u629B\u5F02\u5E38\uFF09\n\t\tvar half = MathF.Max( 0f, GameConfig.ArenaHalfSize - Radius );\n\t\tvar preX = pos.x;\n\t\tvar preY = pos.y;\n\t\tpos.x = Math.Clamp( pos.x, -half, half );\n\t\tpos.y = Math.Clamp( pos.y, -half, half );\n\t\tpos.z = 0f;\n\n\t\tWorldPosition = pos;\n\n\t\t// \u649E\u5899\u8109\u51B2\uFF08v0.7.8.78\uFF09\uFF1A\u94B3\u5236\u771F\u628A\u4F4D\u7F6E\u6309\u4F4F \u002B \u649E\u51FB\u901F\u5EA6\u591F \u002B \u8282\u6D41\uFF0C\u63A5\u89E6\u70B9\u6765\u4E00\u5708\u6269\u6563\u73AF\u3002\n\t\t// \u5404\u7AEF\u672C\u5730\u89E6\u53D1\uFF08\u771F\u4EBA owner \u673A / bot host \u673A\uFF09\uFF0C\u7279\u6548\u4E0D\u505A\u7F51\u7EDC\u5E7F\u64AD\n\t\tif ( _sinceWallHit \u003E GameConfig.WallHitFxCooldown )\n\t\t{\n\t\t\tVector3 contact;\n\t\t\tvar hit = false;\n\n\t\t\tif ( preX != pos.x \u0026\u0026 MathF.Abs( _velocity.x ) \u003E GameConfig.WallHitFxMinSpeed )\n\t\t\t{\n\t\t\t\thit = true;\n\t\t\t\tcontact = new Vector3( MathF.Sign( pos.x ) * ( GameConfig.ArenaHalfSize - 2f ), pos.y, 0f );\n\t\t\t}\n\t\t\telse if ( preY != pos.y \u0026\u0026 MathF.Abs( _velocity.y ) \u003E GameConfig.WallHitFxMinSpeed )\n\t\t\t{\n\t\t\t\thit = true;\n\t\t\t\tcontact = new Vector3( pos.x, MathF.Sign( pos.y ) * ( GameConfig.ArenaHalfSize - 2f ), 0f );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tcontact = Vector3.Zero;\n\t\t\t}\n\n\t\t\tif ( hit )\n\t\t\t{\n\t\t\t\t_sinceWallHit = 0;\n\t\t\t\tNeonRenderer.WallPulse( contact, NeonColor );\n\t\t\t}\n\t\t}\n\n\t\t// \u7403\u4F53\u63A8\u6324\uFF08v0.7.8.24 \u7403\u7403\u5927\u4F5C\u6218\u624B\u611F\uFF09\uFF1A\u5403\u4E0D\u52A8\u5BF9\u65B9=\u5B9E\u5FC3\u2014\u2014\u63A8\u51FA\u4E0E\u654C\u65B9\u7684\u91CD\u53E0\n\t\tApplyPush( half );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u7403\u4F53\u63A8\u6324\uFF08v0.7.8.24 \u7403\u7403\u5927\u4F5C\u6218\u624B\u611F\uFF09\uFF1A**\u5404\u81EA\u628A\u81EA\u5DF1\u63A8\u51FA\u91CD\u53E0**\u2014\u2014\u5BF9\u65B9 bot/host \u7403\u8D70\u540C\u4EE3\u7801\u8DEF\u5F84\u3001\n\t/// \u771F\u4EBA\u7403\u7531\u5176\u4E3B\u4EBA\u63A8\uFF0C\u53CC\u65B9\u6309\u8D28\u91CF\u6743\u91CD\u8BA9\u4F4D\uFF0C\u4E00\u5E27\u5185\u6536\u655B\u5230\u5206\u79BB\uFF0C\u4E0D\u9700\u8981\u4F4D\u7F6E\u6743\u5A01\u4EF2\u88C1\u3002\n\t/// \u961F\u53CB\u4E0E\u81EA\u5DF1\u7684\u8EAB\u4F53\u4E0D\u63A8\uFF08BoB \u60EF\u4F8B\u53EF\u91CD\u53E0\uFF09\uFF1B\u80FD\u5403/\u88AB\u5403\u65F6\u4E0D\u63A8\uFF08\u5403\u5F52 host \u6743\u5A01\uFF0C\u5403\u4E86\u5C31\u4E0D\u5B58\u5728\u91CD\u53E0\uFF09\uFF1B\n\t/// \u5C16\u523A\u5206\u8EAB\u662F\u6B66\u5668\u4E0D\u63A8\uFF08\u78B0\u5230\u8D70\u6B66\u5668\u5224\u5B9A\uFF09\uFF1B\u5B62\u5B50\u4E0D\u63A8\u3002\u4F4D\u7F6E\u662F owner \u6743\u5A01\uFF0C\u63A8\u5B8C\u81EA\u52A8\u540C\u6B65\n\t/// \u003C/summary\u003E\n\tvoid ApplyPush( float half )\n\t{\n\t\tvar game = CircleroyaleGame.Current;\n\t\tif ( game is null ) return;\n\n\t\tforeach ( var b in game.Balls )\n\t\t{\n\t\t\tif ( b is null || !b.IsValid() || !b.Alive || b == this ) continue;\n\t\t\tif ( b.OwnerSteamId == OwnerSteamId ) continue;\n\t\t\tif ( game.SameTeamSteam( OwnerSteamId, b.OwnerSteamId ) ) continue;\n\n\t\t\tvar hi = MathF.Max( Mass, b.Mass );\n\t\t\tvar lo = MathF.Min( Mass, b.Mass );\n\t\t\tif ( hi \u003E= lo * GameConfig.EatRatio ) continue;   // \u80FD\u5403\u5C31\u5403\uFF08host \u6743\u5A01\uFF09\uFF0C\u4E0D\u63A8\n\n\t\t\tvar dx = WorldPosition.x - b.WorldPosition.x;\n\t\t\tvar dy = WorldPosition.y - b.WorldPosition.y;\n\t\t\tvar rr = Radius \u002B b.Radius;\n\t\t\tvar d2 = dx * dx \u002B dy * dy;\n\t\t\tif ( d2 \u003E= rr * rr ) continue;\n\t\t\tvar d = MathF.Sqrt( MathF.Max( d2, 0.0001f ) );\n\t\t\tvar nx = d \u003E 0.001f ? dx / d : 1f;\n\t\t\tvar ny = d \u003E 0.001f ? dy / d : 0f;\n\t\t\t// \u5B8C\u5168\u91CD\u5408\u515C\u5E95\uFF1A\u63A8\u6EE1 rr \u7684\u4E00\u534A\uFF1B\u6B63\u5E38\u60C5\u51B5\u63A8\u0022\u91CD\u53E0 \u00D7 \u5BF9\u65B9\u8D28\u91CF\u5360\u6BD4\u0022\uFF08\u8D8A\u91CD\u8BA9\u5F97\u8D8A\u5C11\uFF09\n\t\t\tvar push = d \u003E 0.001f ? MathF.Min( rr - d, GameConfig.PushMaxStep ) * ( b.Mass / ( Mass \u002B b.Mass ) ) : rr * 0.5f;\n\n\t\t\tWorldPosition = new Vector3(\n\t\t\t\tMath.Clamp( WorldPosition.x \u002B nx * push, -half, half ),\n\t\t\t\tMath.Clamp( WorldPosition.y \u002B ny * push, -half, half ), 0f );\n\t\t}\n\n\t\t// \u654C\u65B9\u5206\u8EAB=\u5B9E\u5FC3\u969C\u788D\uFF08\u81EA\u5DF1\u4E0D\u52A8\uFF0C\u6211\u8BA9\u8DEF\u5168\u989D\u63A8\uFF1B\u80FD\u5403\u65F6\u4E0D\u63A8\u2014\u2014host \u4F1A\u5403\u6389\u5B83\uFF09\n\t\tforeach ( var c in game.Cells )\n\t\t{\n\t\t\tif ( c.PieceKind != CellPiece.Kind.SplitPiece ) continue;\n\t\t\tif ( c.OwnerSteamId == OwnerSteamId ) continue;\n\t\t\tif ( game.SameTeamSteam( OwnerSteamId, c.OwnerSteamId ) ) continue;\n\t\t\tif ( Mass \u003E= c.Mass * GameConfig.EatRatio ) continue;\n\n\t\t\tvar dx = WorldPosition.x - c.Pos.x;\n\t\t\tvar dy = WorldPosition.y - c.Pos.y;\n\t\t\tvar rr = Radius \u002B c.Radius;\n\t\t\tvar d2 = dx * dx \u002B dy * dy;\n\t\t\tif ( d2 \u003E= rr * rr ) continue;\n\t\t\tvar d = MathF.Sqrt( MathF.Max( d2, 0.0001f ) );\n\t\t\tvar nx = d \u003E 0.001f ? dx / d : 1f;\n\t\t\tvar ny = d \u003E 0.001f ? dy / d : 0f;\n\t\t\tvar push = d \u003E 0.001f ? MathF.Min( rr - d, GameConfig.PushMaxStep ) : rr * 0.5f;\n\n\t\t\tWorldPosition = new Vector3(\n\t\t\t\tMath.Clamp( WorldPosition.x \u002B nx * push, -half, half ),\n\t\t\t\tMath.Clamp( WorldPosition.y \u002B ny * push, -half, half ), 0f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u573A\u5730\u5185\u51FA\u751F\u70B9\uFF08owner \u590D\u6D3B\u65F6\u672C\u5730\u9009\u4F4D\uFF09\uFF1A\u8D70\u6E38\u620F\u7CFB\u7EDF\u7684\u5B89\u5168\u91C7\u6837\uFF0C\u65E0\u7CFB\u7EDF\u65F6\u7EAF\u968F\u673A\u515C\u5E95\u3002\n\t/// \u9886\u571F\u6A21\u5F0F\uFF08M7\uFF09\uFF1A\u6709\u961F\u53F7\u5C31\u56DE\u5DF1\u65B9\u5927\u672C\u8425\u91C7\u6837\uFF1BfrontLine=true\uFF08M7.2 \u4E8C\u9009\u4E00\uFF09\u6362\u6700\u8FD1\u5DF1\u65B9\u5360\u9886\u683C \u003C/summary\u003E\n\tstatic Vector3 RandomSpawnPos( int teamIndex = -1, bool frontLine = false, Vector3 nearPos = default )\n\t{\n\t\tvar game = CircleroyaleGame.Current;\n\t\tif ( game is not null ) return game.RandomSpawnPos( teamIndex, frontLine, nearPos );\n\n\t\tif ( MatchState.IsTerritory \u0026\u0026 teamIndex \u003E= 0 )\n\t\t{\n\t\t\tif ( frontLine \u0026\u0026 TerritoryManager.TryFrontSpawn( teamIndex, nearPos, out var front ) )\n\t\t\t\treturn front;\n\t\t\tTerritoryManager.HqBounds( teamIndex, out var minX, out var maxX, out var minY, out var maxY );\n\t\t\treturn new Vector3( Game.Random.Float( minX, maxX ), Game.Random.Float( minY, maxY ), 0f );\n\t\t}\n\n\t\tvar half = GameConfig.ArenaHalfSize - 128f;\n\t\treturn new Vector3( Game.Random.Float( -half, half ), Game.Random.Float( -half, half ), 0f );\n\t}\n\n\t// ---- \u5206\u88C2/\u5410\u5B62\u5B50 RPC\uFF08M3\uFF09\uFF1A\u5BA2\u6237\u7AEF \u2192 host\uFF0Chost \u6821\u9A8C\u5F52\u5C5E\u540E\u6267\u884C\u3002\n\t// v0.7.8.7\uFF1A\u53C2\u6570\u8BED\u4E49\u6539\u4E3A**\u5149\u6807\u4E16\u754C\u5750\u6807**\uFF08aimPoint\uFF09\uFF0Chost \u9010\u8EAB\u4F53\u7B97\u6307\u5411 ----\n\n\t[Rpc.Host]\n\tpublic void RequestSplit( Vector2 aimPoint )\n\t{\n\t\tvar caller = Rpc.Caller;\n\t\tif ( caller is not null \u0026\u0026 caller.SteamId.Value != OwnerSteamId ) return;   // \u53EA\u80FD\u64CD\u4F5C\u81EA\u5DF1\u7684\u7403\n\n\t\tCircleroyaleGame.Current?.DoSplit( this, aimPoint );\n\t}\n\n\t[Rpc.Host]\n\tpublic void RequestEject( Vector2 aimPoint )\n\t{\n\t\tvar caller = Rpc.Caller;\n\t\tif ( caller is not null \u0026\u0026 caller.SteamId.Value != OwnerSteamId ) return;\n\n\t\tCircleroyaleGame.Current?.DoEject( this, aimPoint );\n\t}\n\n\t[Rpc.Host]\n\tpublic void RequestUsePower( int slot )\n\t{\n\t\tvar caller = Rpc.Caller;\n\t\tif ( caller is not null \u0026\u0026 caller.SteamId.Value != OwnerSteamId ) return;   // \u53EA\u80FD\u64CD\u4F5C\u81EA\u5DF1\u7684\u7403\n\n\t\tUsePower( slot );\n\t}\n\n\tprotected override void OnDestroy()\n\t{\n\t\tbase.OnDestroy();\n\t\tCircleroyaleGame.Current?.RemoveBall( this );\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Player/CellPiece.cs","FileName":"CellPiece.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\n\n/// \u003Csummary\u003E\n/// \u5206\u88C2\u7EC6\u80DE / \u5410\u51FA\u7684\u5B62\u5B50\uFF08**\u7EAF\u6570\u636E\u5B9E\u4F53\uFF0C\u4E0D\u662F GameObject**\uFF09\u3002\n/// \u4E3A\u4EC0\u4E48\u4E0D\u8D70\u7F51\u7EDC\u5BF9\u8C61\uFF1A\u8FD0\u884C\u65F6 live create \u5E7F\u64AD\u5230\u5DF2\u8FDE\u63A5\u5BA2\u6237\u7AEF\u7684\u53EF\u9760\u6027\u672A\u9A8C\u8BC1\uFF08M2 \u9057\u7559\uFF09\uFF0C\n/// \u800C\u98DF\u7269\u540C\u6B3E\u0022\u9759\u6001 RPC \u5FEB\u7167\u0022\u662F\u672C\u9879\u76EE\u5B9E\u6D4B 100% \u53EF\u9760\u7684\u552F\u4E00\u540C\u6B65\u901A\u9053\u3002\n/// host \u6743\u5A01\u6A21\u62DF\uFF08\u51B2\u91CF\u8870\u51CF \u002B \u8DDF\u968F\u4E3B\u4EBA\u8F6C\u5411\uFF09\uFF0C\u72B6\u6001\u7ECF CellsState \u9759\u6001 RPC \u6309 CellSyncInterval\n/// \u5E7F\u64AD\uFF1B\u5404\u7AEF\u672C\u5730\u955C\u50CF\u5217\u8868\u53EA\u8D1F\u8D23\u6E32\u67D3\u4E0E\u5E73\u6ED1\uFF08DrawPos \u8FFD Pos\uFF0C\u5BA2\u6237\u7AEF\u628A 15Hz \u5FEB\u7167\u63D2\u987A\uFF09\u3002\n/// \u003C/summary\u003E\npublic sealed class CellPiece\n{\n\tpublic enum Kind : byte\n\t{\n\t\tSplitPiece = 0,   // \u5206\u88C2\u51FA\u7684\u7EC6\u80DE\uFF08\u8DDF\u968F\u4E3B\u4EBA\u65B9\u5411\u79FB\u52A8\uFF0C\u51B7\u5374\u540E\u5E76\u56DE\uFF09\n\t\tEjectedMass = 1,  // \u5410\u51FA\u7684\u5B62\u5B50\uFF08\u4E2D\u6027\u98DF\u7269\u56E2\uFF0C\u8C01\u90FD\u80FD\u5403\uFF09\n\t\tSpikeMinion = 2,  // \u5C16\u523A\u5206\u8EAB\uFF08v0.7.4.0 \u9053\u5177\u751F\u6210\uFF09\uFF1A\u8DDF\u968F\u4E3B\u4EBA 20s\uFF0C\u5230\u671F\u53D8\u56DE\u666E\u901A\u5206\u8EAB\uFF1B\u654C\u65B9\u6309\u5C16\u523A\u89C4\u5219\u5224\u5B9A\n\t}\n\n\tpublic int Id;\n\n\tpublic Kind PieceKind;\n\n\t/// \u003Csummary\u003E \u4E3B\u4EBA SteamId\uFF08bot \u4E3A 0\uFF1Bbot \u4E0D\u5206\u88C2\uFF0C\u6B64\u5B57\u6BB5\u4EC5\u771F\u4EBA\u6709\u503C\uFF09 \u003C/summary\u003E\n\tpublic long OwnerSteamId;\n\n\t/// \u003Csummary\u003E \u4E3B\u4EBA\u961F\u4F0D\u53F7\uFF08-1 \u65E0\u961F\uFF1Bhost \u51FA\u751F\u65F6\u8BB0\u4E00\u6B21\uFF0C\u9886\u571F\u5582\u65D7\u8BA1\u5206\u7528\uFF0C\u514D\u9010\u5E27\u67E5\u8868\uFF09 \u003C/summary\u003E\n\tpublic int TeamIndex = -1;\n\n\t/// \u003Csummary\u003E \u951A\u5B9A\u4E0D\u52A8\uFF08M7.4 \u62A4\u536B\u56FA\u5B88\u5C16\u523A\uFF09\uFF1A\u4E0D\u8DDF\u968F\u4E3B\u4EBA\u3001\u65E0\u8F6C\u5411 \u003C/summary\u003E\n\tpublic bool Anchored;\n\n\t/// \u003Csummary\u003E \u5B58\u5728\u65F6\u957F\u8986\u76D6\uFF08\u79D2\uFF1B\u5C0F\u4E8E 0 = \u7528 Kind \u9ED8\u8BA4\u5BFF\u547D\uFF09\u2014\u2014\u62A4\u536B\u5C16\u523A 8s / \u9053\u5177\u5C16\u523A 20s\uFF08M7.4\uFF09 \u003C/summary\u003E\n\tpublic float MaxLife = -1f;\n\n\tpublic int ColorIndex;\n\n\tpublic float Mass;\n\n\t/// \u003Csummary\u003E \u6743\u5A01\u4F4D\u7F6E\uFF08host \u6BCF\u5E27\u63A8\u8FDB\uFF1B\u5BA2\u6237\u7AEF = \u6700\u8FD1\u4E00\u6B21\u5FEB\u7167\u503C\uFF0C15Hz \u8DF3\u53D8\uFF09 \u003C/summary\u003E\n\tpublic Vector3 Pos;\n\n\t/// \u003Csummary\u003E \u6E32\u67D3\u4F4D\u7F6E\uFF1A\u5411 Pos \u6307\u6570\u5E73\u6ED1\uFF08host \u9010\u5E27\u6743\u5A01\u76F4\u63A5\u8D34\u9F50\uFF1B\u5BA2\u6237\u7AEF\u63D2\u503C\uFF09 \u003C/summary\u003E\n\tpublic Vector3 DrawPos;\n\n\t/// \u003Csummary\u003E \u5F39\u5C04\u51B2\u91CF\uFF08\u5206\u88C2/\u5410\u51FA\u77AC\u95F4\uFF09\uFF0C\u968F\u65F6\u95F4\u6307\u6570\u8870\u51CF \u003C/summary\u003E\n\tpublic Vector3 Impulse;\n\n\t/// \u003Csummary\u003E \u8DDF\u968F\u4E3B\u4EBA\u79FB\u52A8\u65B9\u5411\u7684\u901F\u5EA6\u5206\u91CF\uFF08\u4EC5 host \u6A21\u62DF\uFF09 \u003C/summary\u003E\n\tpublic Vector3 SteerVel;\n\n\t/// \u003Csummary\u003E \u51FA\u751F\u65F6\u523B\uFF1A\u5408\u5E76\u51B7\u5374\uFF08\u5206\u8EAB\uFF09\u4E0E\u53EF\u98DF\u5EF6\u8FDF\uFF08\u5B62\u5B50\uFF09\u90FD\u770B\u5B83 \u003C/summary\u003E\n\tpublic TimeSince SinceSpawn;\n\n\t/// \u003Csummary\u003E \u4EC5 host\uFF1A\u4E3B\u4EBA\u7403\u5F15\u7528\uFF08\u5206\u8EAB\u6E05\u7406\u3001\u8F6C\u5411\u65B9\u5411\u6765\u6E90\uFF09\uFF1B\u5B62\u5B50\u6052\u4E3A null\uFF08\u4E2D\u6027\u5B58\u6D3B\uFF09 \u003C/summary\u003E\n\tpublic Ball OwnerBall;\n\n\t/// \u003Csummary\u003E \u534A\u5F84\u6CBF\u7528\u7403\u7684\u8D28\u91CF\u5B9A\u5F8B\uFF1Ar = StartRadius \u00D7 \u221A(mass/StartMass) \u003C/summary\u003E\n\tpublic float Radius =\u003E GameConfig.StartRadius * MathF.Sqrt( MathF.Max( 1f, Mass ) / GameConfig.StartMass );\n\n\t/// \u003Csummary\u003E \u79FB\u52A8\u901F\u5EA6\u6CBF\u7528\u7403\u7684\u901F\u5EA6\u66F2\u7EBF\uFF1A\u8D8A\u5927\u8D8A\u6162 \u003C/summary\u003E\n\tpublic float Speed =\u003E MathF.Max( GameConfig.MinSpeed,\n\t\tGameConfig.BaseSpeed * MathF.Pow( GameConfig.StartMass / MathF.Max( 1f, Mass ), GameConfig.SpeedCurve ) );\n\n\t/// \u003Csummary\u003E \u5408\u5E76\u51B7\u5374\u65F6\u957F\uFF08\u79D2\uFF09\uFF1A\u4E3B\u52A8\u5206\u88C2\u6309\u8D28\u91CF\u66F2\u7EBF\uFF08MergeCooldownFor\uFF09\uFF1B\u649E\u523A\u788E\u7247\u77ED\u51B7\u5374\uFF08SpikePieceMergeCooldown\uFF09 \u003C/summary\u003E\n\tpublic float MergeCooldown = GameConfig.MergeCooldownBase;\n\n\t/// \u003Csummary\u003E \u5408\u5E76\u51B7\u5374\u662F\u5426\u5DF2\u8FC7\uFF08\u4EC5\u5206\u8EAB\u6709\u610F\u4E49\uFF09 \u003C/summary\u003E\n\tpublic bool IsMergeReady =\u003E SinceSpawn \u003E MergeCooldown;\n}\n\n/// \u003Csummary\u003E\n/// \u523A\u7206\u5F39\u5E55\uFF08v0.7.8.24\uFF09\uFF1A\u5582\u523A\u5582\u6EE1\u540E\u4ECE\u523A\u5C04\u51FA\u7684\u5C16\u523A\u5F39\u3002host \u6743\u5A01\u6A21\u62DF\uFF08\u542B\u547D\u4E2D\u5224\u5B9A\uFF09\uFF0C\n/// \u5BA2\u6237\u7AEF\u4E3A\u7EAF\u89C6\u89C9\u5F39\u9053\u955C\u50CF\uFF08SpineDir \u516C\u5F0F\u53CC\u7AEF\u4E00\u81F4\uFF0C\u89C6\u89C9\u5F39\u9053=\u6743\u5A01\u5F39\u9053\uFF09\u3002\n/// \u003C/summary\u003E\npublic sealed class SpikeSpine\n{\n\tpublic Vector2 Pos;\n\n\tpublic Vector2 Vel;\n\n\t/// \u003Csummary\u003E \u51FA\u751F\u65F6\u523B\uFF08SpineLife \u5230\u671F\u6D88\u6563\uFF09 \u003C/summary\u003E\n\tpublic TimeSince SinceSpawn;\n}\n\n/// \u003Csummary\u003E CellsState RPC \u7684\u7EBF\u683C\u5F0F\uFF08\u5168 blittable \u5B57\u6BB5\uFF0C\u540C FoodData \u6A21\u5F0F\uFF09 \u003C/summary\u003E\npublic struct CellWire\n{\n\tpublic int Id;\n\tpublic byte Kind;        // CellPiece.Kind\n\tpublic byte ColorIndex;\n\tpublic long Owner;       // OwnerSteamId\n\tpublic float X;\n\tpublic float Y;\n\tpublic float Mass;\n}\n"},{"Ident":"slfy.circleroyale","Path":"ui/lobbypanel.cs.scss","FileName":"lobbypanel.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"LobbyPanel {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\tfont-family: PixelFusion;\n\tcolor: #4a3b63;\n\tpointer-events: all;\n\tbackground-color: rgba( 234, 244, 252, 0.88 );   // \u6D45\u84DD\u78E8\u7802\uFF08\u50CF\u7D20\u98CE\uFF09\n\n\t// \u7248\u5F0F\uFF08\u53C2\u8003\u56FE\uFF0C2026-09-09 \u7528\u6237\u5B9A\u7A3F\uFF09\uFF1A\u5DE6\u680F\u73A9\u5BB6\u5217\u8868\uFF08\u951A\u5C4F\u5E55\u5DE6\uFF09/ \u53F3\u680F\u53C2\u6570\u8BBE\u7F6E \u002B \u5927\u5F00\u59CB\u6309\u94AE\u3002\n\t// \u53F3\u680F\u4E00\u5F8B right \u951A\u5B9A\uFF08\u7528\u6237\u7A97\u53E3\u8BBE\u8BA1\u5BBD ~1800 \u2260 1920\uFF0C\u56FA\u5B9A left \u5B9A\u4F4D\u4F1A\u9876\u51FA\u53F3\u7F18\uFF09\n\n\t.lp-title {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 80px;\n\t\tfont-size: 100px;\n\t\tfont-weight: 800;\n\t\tletter-spacing: 10px;\n\t\ttext-align: center;\n\t\tcolor: #f272a7;\n\t\ttext-shadow: 0 4px 0 #4a3b63;\n\t}\n\n\t.lp-status {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 215px;\n\t\tfont-size: 32px;\n\t\tfont-weight: 700;\n\t\tletter-spacing: 4px;\n\t\ttext-align: center;\n\t\tcolor: #d98a00;\n\t\ttext-shadow: 0 3px 0 rgba( 74, 59, 99, 0.25 );\n\t}\n\n\t// \u4E24\u680F\u5C0F\u8282\u6807\u9898\uFF08players \u5DE6\u680F / settings \u53F3\u680F\uFF0C\u4F4D\u7F6E\u5206\u5199\uFF09\n\t.lp-head {\n\t\tposition: absolute;\n\t\ttop: 278px;\n\t\tfont-size: 13px;\n\t\tletter-spacing: 4px;\n\t\ttext-align: center;\n\t\tcolor: rgba( 74, 59, 99, 0.45 );\n\t}\n\n\t.lp-head.players {\n\t\tleft: 80px;\n\t\twidth: 600px;\n\t}\n\n\t.lp-head.settings {\n\t\tright: 60px;\n\t\twidth: 1040px;\n\t}\n\n\t// \u53C2\u6570\u8BBE\u7F6E\u724C\uFF08\u53F3\u680F\u7EB5\u6392\u4E09\u5F20\uFF1BTop \u7531\u4EE3\u7801\u6309\u884C\u7ED9\uFF09\n\t.lp-setting {\n\t\tposition: absolute;\n\t\tright: 60px;\n\t\twidth: 1040px;\n\t\tfont-size: 30px;\n\t\tletter-spacing: 2px;\n\t\ttext-align: center;\n\t\tcolor: #4a3b63;\n\t\tborder-image: url( ui/skin/note.png ) 40 fill / 5px stretch;\n\t\tpadding: 14px 10px;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t}\n\n\t.lp-setting:hover {\n\t\tcolor: #e5487e;\n\t\tborder-image-tint: #ffe4f0;\n\t}\n\n\t// \u8BBE\u7F6E\u53EF\u70B9\u63D0\u793A\uFF08\u4EC5 host\uFF09\uFF1A\u724C\u5806\u4E0B\u65B9\u91D1\u8272\u5F15\u5BFC\u5C0F\u5B57\n\t.lp-setting-hint {\n\t\tposition: absolute;\n\t\tright: 60px;\n\t\twidth: 1040px;\n\t\ttop: 658px;\n\t\tfont-size: 24px;\n\t\tletter-spacing: 4px;\n\t\ttext-align: center;\n\t\tcolor: rgba( 217, 138, 0, 0.75 );\n\t}\n\n\t// \u73A9\u5BB6\u5217\u8868\u6EDA\u52A8\u5BB9\u5668\uFF08\u539F\u751F\u7F51\u9875\u5F0F\u6EDA\u52A8\uFF1A\u6EDA\u8F6E \u002B \u6EDA\u52A8\u6761\uFF0C\u6EA2\u51FA\u624D\u51FA\u73B0\uFF09\n\t.lp-players-scroll {\n\t\tposition: absolute;\n\t\tleft: 92px;\n\t\ttop: 322px;\n\t\twidth: 576px;\n\t\theight: 616px;\n\t\tflex-direction: column;   // \u26A0\uFE0F s\u0026box \u9ED8\u8BA4 row\uFF0C\u4E0D\u52A0\u5C31\u662F\u6A2A\u6392\n\t\toverflow-y: scroll;\n\t\tscrollbar-width: 10px;   // \u2605 \u6EDA\u52A8\u6761\u7C97\u7EC6\uFF08\u7CBE\u4FEE\u6539\u8FD9\u91CC\uFF09\n\t\tscrollbar-color: rgba( 74, 59, 99, 0.45 ) transparent;   // \u2605 \u6ED1\u5757\u8272 \u8F68\u9053\u8272\n\t\tpadding-right: 4px;\n\t\tpointer-events: all;   // \u9762\u677F\u9ED8\u8BA4 None\uFF1A\u4E0D\u6253\u5F00\u6536\u4E0D\u5230\u6EDA\u8F6E\n\t}\n\n\t// \u73A9\u5BB6\u884C\u724C\uFF08\u6D41\u5185\u5355\u5217\uFF0Cnote.png \u5C0F\u724C\uFF09\n\t.lp-player {\n\t\tflex-shrink: 0;   // \u4E0D\u8BB8\u538B\u7F29\uFF1A\u538B\u4E86\u5C31\u4E0D\u6EA2\u51FA\u3001\u6EDA\u52A8\u6761\u6C38\u8FDC\u51FA\u4E0D\u6765\n\t\tmargin: 0 0 8px 0;\n\t\tfont-size: 30px;\n\t\tletter-spacing: 1px;\n\t\ttext-align: left;\n\t\tcolor: #4a3b63;\n\t\tborder-image: url( ui/skin/note.png ) 28 fill / 5px stretch;\n\t\tpadding: 8px 50px;\n\t}\n\n\t// \u73A9\u5BB6\u5217\u8868\u5BB9\u5668\u5E95\uFF08dobou InventoryHotbar \u4E5D\u5BAB\u683C\uFF0C\u7528\u6237\u6307\u5B9A\uFF09\uFF1A\n\t// \u7F69\u4F4F\u5DE6\u680F\u4E24\u5B50\u5217\uFF08\u884C\u724C x80..680 / y320..925\uFF09\uFF0C\u56DB\u5468\u7559 ~20px \u5185\u886C\n\t.lp-players-bg {\n\t\tposition: absolute;\n\t\tleft: 60px;\n\t\ttop: 300px;\n\t\twidth: 640px;\n\t\theight: 660px;\n\t\tborder-image: url( ui/skin/hotbar.png ) 60 fill / 30px stretch;\n\t}\n\n\t// BOTS \u8865\u4F4D\u63D0\u793A\uFF1A\u8BBE\u7F6E\u724C\u5806\u4E0B\u3001\u6309\u94AE\u4E0A\n\t.lp-note {\n\t\tposition: absolute;\n\t\tright: 60px;\n\t\twidth: 1040px;\n\t\ttop: 700px;\n\t\tfont-size: 20px;\n\t\tletter-spacing: 3px;\n\t\ttext-align: center;\n\t\tcolor: rgba(21, 9, 54, 0.4);\n\t}\n\n\t// \u9053\u5177\u56FE\u4F8B\u5DF2\u6309\u7528\u6237\u8981\u6C42\u6574\u4E2A\u79FB\u9664\uFF082026-09-09\uFF09\u2014\u2014Q/E \u7528\u6CD5\u5F00\u5C40\u540E HUD \u91CC\u8FD8\u6709\u63D0\u793A\n\n\t.lp-countdown {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 600px;\n\t\tfont-family: PixelFusion;\n\t\tfont-size: 40px;\n\t\tfont-weight: 800;\n\t\tletter-spacing: 8px;\n\t\ttext-align: center;\n\t\tcolor: #f272a7;\n\t\ttext-shadow: 0 4px 0 #4a3b63;\n\t}\n\n\t// \u4E0B\u9762\u4E09\u9897\u5168\u662F UiKit \u836F\u4E38\uFF08\u5916\u89C2\u5728 Assets/ui/cruikit.scss \u7684 .cr-pill\uFF09\uFF0C\u8FD9\u91CC\u53EA\u7BA1\u6446\u653E\u3002\n\t// START/JOIN \u540C\u4E00\u69FD\u4F4D\uFF08\u540C\u4E00\u65F6\u523B\u53EA\u663E\u793A\u4E00\u4E2A\uFF09\uFF0C\u4E0E\u8BBE\u7F6E\u724C\u540C\u4E2D\u8F74\uFF08right:60\u002B1040 \u2192 \u4E2D\u8F74 = \u53F3\u7F18-580\uFF09\n\t.lp-start {\n\t\tposition: absolute;\n\t\tright: 400px;\n\t\ttop: 750px;\n\t\twidth: 360px;\n\t\tpadding-top: 8px;\n\t}\n\n\t.lp-join {\n\t\tposition: absolute;\n\t\tright: 400px;\n\t\ttop: 850px;\n\t\twidth: 360px;\n\t\tpadding-top: 8px;\n\t}\n\n\t.lp-leave {\n\t\tposition: absolute;\n\t\tright: 470px;\n\t\ttop: 950px;\n\t\twidth: 220px;\n\t\tpadding-top: 8px;\n\t}\n}\n"},{"Ident":"slfy.circleroyale","Path":"ui/rankingspanel.cs.scss","FileName":"rankingspanel.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"RankingsPanel {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\tfont-family: PixelFusion;\n\tcolor: #4a3b63;\n\tpointer-events: all;\n\tbackground-color: rgba( 234, 244, 252, 0.88 );   // \u6D45\u84DD\u78E8\u7802\uFF08\u50CF\u7D20\u98CE\uFF09\n\n\t.rk-title {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 80px;\n\t\tfont-size: 100px;\n\t\tfont-weight: 800;\n\t\tletter-spacing: 10px;\n\t\ttext-align: center;\n\t\tcolor: #f272a7;\n\t\ttext-shadow: 0 4px 0 #4a3b63;\n\t}\n\n\t.rk-status {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 460px;   // \u7A7A\u5361\u6B63\u4E2D\uFF1A\u8BFB\u53D6\u4E2D/\u65E0\u6570\u636E/\u5931\u8D25\u4E09\u79CD\u72B6\u6001\u90FD\u663E\u793A\u5728\u8FD9\uFF08\u884C\u724C\u4E4B\u540E\u521B\u5EFA\uFF0C\u538B\u5728\u7A7A\u724C\u4E0A\u5C42\uFF09\n\t\tfont-size: 32px;\n\t\tfont-weight: 700;\n\t\tletter-spacing: 4px;\n\t\ttext-align: center;\n\t\tcolor: #d98a00;\n\t\ttext-shadow: 0 3px 0 rgba( 74, 59, 99, 0.25 );\n\t}\n\n\t.rk-head {\n\t\tposition: absolute;\n\t\tleft: 50%;\n\t\tmargin-left: -550px;\n\t\twidth: 1100px;\n\t\ttop: 250px;\n\t\tfont-size: 12px;\n\t\tletter-spacing: 2px;\n\t\ttext-align: left;\n\t\tcolor: rgba( 74, 59, 99, 0.45 );\n\t}\n\n\t// \u6392\u884C\u699C\u6EDA\u52A8\u5BB9\u5668\uFF08\u539F\u751F\u7F51\u9875\u5F0F\u6EDA\u52A8\uFF1A\u6EDA\u8F6E \u002B \u6EDA\u52A8\u6761\uFF0C\u6EA2\u51FA\u624D\u51FA\u73B0\uFF09\n\t// \u884C\u724C\u6D41\u5185\u5355\u5217\uFF1B\u5BBD 1600 / \u8D77\u70B9 188 \u662F\u7528\u6237\u8C03\u6821\u503C\u7684\u8FC1\u79FB\n\t.rk-list {\n\t\tposition: absolute;\n\t\tleft: 50%;\n\t\tmargin-left: -800px;\n\t\twidth: 1600px;\n\t\ttop: 180px;\n\t\theight: 770px;   // \u2605 \u5217\u8868\u89C6\u53E3\u9AD8\u5EA6\uFF08\u7CBE\u4FEE\u6539\u8FD9\u91CC\uFF1B\u5E95\u5230 BACK \u4E0A\u65B9\u4E3A\u6B62\uFF09\n\t\tflex-direction: column;   // \u26A0\uFE0F s\u0026box \u9ED8\u8BA4 row\uFF0C\u4E0D\u52A0\u5C31\u662F\u6A2A\u6392\n\t\toverflow-y: scroll;\n\t\tscrollbar-width: 10px;   // \u2605 \u6EDA\u52A8\u6761\u7C97\u7EC6\n\t\tscrollbar-color: rgba( 74, 59, 99, 0.45 ) transparent;\n\t\tpointer-events: all;   // \u9762\u677F\u9ED8\u8BA4 None\uFF1A\u4E0D\u6253\u5F00\u6536\u4E0D\u5230\u6EDA\u8F6E\n\t}\n\n\t.rk-row {\n\t\tflex-shrink: 0;   // \u4E0D\u8BB8\u538B\u7F29\uFF1A\u538B\u4E86\u5C31\u4E0D\u6EA2\u51FA\u3001\u6EDA\u52A8\u6761\u6C38\u8FDC\u51FA\u4E0D\u6765\n\t\tmargin: 0 0 6px 0;   // \u2605 \u884C\u8DDD\uFF08\u539F top \u8D77\u70B9\u5F0F\u884C\u8DDD\u4F5C\u5E9F\uFF0C\u6539\u8FD9\u91CC\uFF09\n\t\tfont-size: 50px;\n\t\tletter-spacing: 10px;\n\t\ttext-align: left;\n\t\tcolor: #4a3b63;\n\t\tborder-image: url( ui/skin/note.png ) 50 fill / 5px stretch;\n\t\tpadding: 20px 100px;   // \u2605 \u884C\u5185\u6587\u5B57\u4E0E\u724C\u8FB9\u7F18\u7684\u8DDD\u79BB\uFF08\u7CBE\u4FEE\u6539\u8FD9\u91CC\uFF09\n\t}\n\n\t.rk-self {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\ttop: 860px;\n\t\tfont-size: 32px;\n\t\tfont-weight: 700;\n\t\tletter-spacing: 3px;\n\t\ttext-align: center;\n\t\tcolor: #e5488d;\n\t}\n\n\t// \u8FD4\u56DE\u6309\u94AE\uFF1AUiKit \u836F\u4E38\uFF08\u5916\u89C2\u5728 Assets/ui/cruikit.scss\uFF09\uFF0C\u8FD9\u91CC\u53EA\u7BA1\u6446\u653E\n\t.rk-back {\n\t\tposition: absolute;\n\t\tleft: 50%;\n\t\tmargin-left: -110px;   // \u81EA\u5DF1\u5BBD\u5EA6 220 \u7684\u4E00\u534A\uFF0C\u4EFB\u610F\u5206\u8FA8\u7387\u6C34\u5E73\u5C45\u4E2D\n\t\ttop: 980px;\n\t\twidth: 220px;\n\t\tpadding-top: 8px;\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"ui/mainmenu.cs.scss","FileName":"mainmenu.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"MainMenu {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\tfont-family: PixelFusion;\n\tcolor: #4a3b63;\n\tpointer-events: all;   // \u26A0\uFE0F \u53EA\u8BA4 auto/none/all\uFF1A\u5199 normal \u975E\u6CD5\u88AB\u5FFD\u7565\uFF08\u9762\u677F\u9ED8\u8BA4 None=\u9F20\u6807\u5168\u6B7B\uFF09\n\n\t// \u5B98\u65B9\u9879\u76EE\u624B\u6CD5\uFF1Aflex \u5217 \u002B align-items \u5C45\u4E2D\uFF08\u4EFB\u610F\u5206\u8FA8\u7387\u6C34\u5E73\u5C45\u4E2D\uFF09\u3002\n\t// \u26A0\uFE0F s\u0026box \u9ED8\u8BA4 flex-direction \u662F row\u2014\u2014\u4E0D\u663E\u5F0F\u5199 column \u7684\u8BDD\u5B50\u5143\u7D20\u5168\u90E8\u6A2A\u6392\uFF08\u5B9E\u6D4B\uFF09\n\tflex-direction: column;\n\talign-items: center;\n\n\t// ---- \u5DE6\u4E0A\uFF1A\u73A9\u5BB6\u8D44\u6599\u5361\uFF08\u5934\u50CF / \u540D\u5B57 / \u7B49\u7EA7\uFF09 ----\n\t.profile {\n\t\tposition: absolute;\n\t\tleft: 20px;\n\t\ttop: 16px;\n\t}\n\n\t.profile-avatar {\n\t\twidth: 72px;\n\t\theight: 72px;\n\t\tborder: 3px solid #4a3b63;\n\t\tborder-radius: 12px;\n\t\tbackground-color: rgba( 255, 255, 255, 0.75 );\n\t}\n\n\t.profile-info {\n\t\tposition: absolute;\n\t\tleft: 84px;\n\t\ttop: 0;\n\t\tbottom: 0;\n\t\tflex-direction: column;   // \u26A0\uFE0F \u9ED8\u8BA4 row\uFF1A\u540D\u5B57\u548C\u7B49\u7EA7\u4F1A\u6324\u6210\u4E00\u884C\n\t\tjustify-content: center;   // \u5782\u76F4\u5C45\u4E2D\u4E8E\u5934\u50CF\u9AD8\u5EA6\n\t}\n\n\t.profile-name {\n\t\tfont-size: 20px;\n\t\tfont-weight: 100;\n\t\tletter-spacing: 1px;\n\t\tcolor: #4a3b63;\n\t\ttext-shadow: 0 2px 0 rgba( 255, 255, 255, 0.9 );\n\t}\n\n\t.profile-level {\n\t\tmargin-top: 6px;\n\t\tfont-size: 12px;\n\t\tfont-weight: 700;\n\t\tletter-spacing: 2px;\n\t\tcolor: #d98a00;\n\t}\n\n\t// ---- \u53F3\u4E0A\uFF1A\u8BBE\u7F6E / \u6392\u884C\u699C\uFF08\u56FE\u6807\u5C0F\u6309\u94AE\uFF0C\u6A2A\u6392\uFF09 ----\n\t.top-actions {\n\t\tposition: absolute;\n\t\tright: 40px;\n\t\ttop: 40px;\n\t\tflex-direction: row;\n\t}\n\n\t.icon-btn {\n\t\twidth: 100px;\n\t\talign-items: center;      // \u56FE\u6807\u6C34\u5E73\u5C45\u4E2D\n\t\tjustify-content: center;  // \u56FE\u6807\u5782\u76F4\u5C45\u4E2D\n\t\theight: 100px;\n\t\tmargin-left: 24px;   // \u4E24\u9897\u6309\u94AE\u62C9\u5F00\u95F4\u8DDD\uFF0C\u522B\u7CCA\u6210\u4E00\u56E2\n\t\tborder-image: url( ui/skin/slot.png ) 30 fill / 10px stretch;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t}\n\n\t.icon-btn:hover {\n\t\tborder-image-tint: #ffd6e8;\n\t}\n\n\t.icon-btn-img {\n\t\t// \u26A0\uFE0F \u522B\u5199 position/left/top\u2014\u2014.icon-btn \u6CA1\u6709 position \u65F6\u767E\u5206\u6BD4\u89E3\u6790\u5230 .top-actions \u5BB9\u5668\uFF0C\n\t\t// \u4E24\u9897\u56FE\u6807\u4F1A\u53E0\u5230\u5BB9\u5668\u6B63\u4E2D\u3002\u5C45\u4E2D\u7531 .icon-btn \u7684 align/justify-center \u5B8C\u6210\uFF0C\u8FD9\u91CC\u53EA\u7BA1\u5C3A\u5BF8\n\t\twidth: 50px;\n\t\theight: 50px;\n\t}\n\n\t// ---- \u4E2D\u592E\u5927 logo ----\n\t.title {\n\t\twidth: 100%;\n\t\tmargin-top: 150px;\n\t\tfont-size: 196px;\n\t\tfont-weight: 800;\n\t\tletter-spacing: 8px;\n\t\tcolor: #f272a7;   // \u7CD6\u679C\u7C89 logo\n\t\ttext-shadow: 0 6px 0 #4a3b63, 0 11px 0 rgba( 74, 59, 99, 0.35 );\n\t\ttext-align: center;\n\t}\n\n\t// \u526F\u6807\u9898\u5DF2\u6309\u9700\u6C42\u79FB\u9664\uFF08v0.7.8.44\uFF09\n\n\t// ---- \u4E09\u4E2A\u4E3B\u6309\u94AE\u6A2A\u6392\uFF08\u53C2\u8003\u624B\u6E38\u5E03\u5C40\uFF1B\u7C89 / \u84DD / \u6A59\uFF09 ----\n\t.menu-row {\n\t\tmargin-top: 400px;\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t}\n\n\t.menu-row .btn {\n\t\twidth: 250px;\n\t\theight: 64px;\n\t\tmargin-top: 10px;\n\t\tmargin-left: 30px;\n\t\tfont-size: 25px;\n\t\tfont-weight: 300;\n\t\tletter-spacing: 5px;\n\t\tcolor: #4a3b63;\n\t\tpadding-top: 8px;\n\t\ttext-align: center;\n\t\tborder-image: url( ui/skin/btn_white.png ) 52 fill / 26px stretch;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t}\n\n\t.menu-row .btn.host { margin-left: 0; border-image-tint: #ffb9d0; }   // \u7C89=\u5EFA\u670D\n\t.menu-row .btn.join { border-image-tint: #b8d9ff; }                   // \u84DD=\u52A0\u5165\n\t.menu-row .btn.bots { border-image-tint: #ffcf9e; }                   // \u6A59=\u4EBA\u673A\n\n\t.menu-row .btn:hover {\n\t\tborder-image-tint: #ffffff;\n\t\tcolor: #e5487e;\n\t}\n\n\t// ---- \u89D2\u843D\u56FA\u5B9A\u4EF6 ----\n\t.status {\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 52px;\n\t\tfont-size: 120px;\n\t\tletter-spacing: 2px;\n\t\tcolor: #d98a00;\n\t\ttext-align: center;\n\t}\n\n\t.version {\n\t\tposition: absolute;\n\t\tright: 20px;\n\t\tbottom: 14px;\n\t\tfont-size: 12px;\n\t\tletter-spacing: 2px;\n\t\tcolor: rgba( 74, 59, 99, 0.4 );\n\t}\n\n\t.hint {\n\t\tposition: absolute;\n\t\tleft: 30px;\n\t\tbottom: 14px;\n\t\tfont-size: 12px;\n\t\tletter-spacing: 2px;\n\t\tcolor: rgba( 74, 59, 99, 0.5 );\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Game/FoodManager.cs","FileName":"FoodManager.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\n\n/// \u003Csummary\u003E\n/// \u98DF\u7269\u7BA1\u7406\uFF08**\u6BCF\u53F0\u673A\u5668\u4E00\u4EFD\u672C\u5730\u5B9E\u4F8B\uFF0C\u4E0D\u8D70\u5BF9\u8C61\u590D\u5236**\u2014\u2014NetList \u6302\u5BF9\u8C61\u65E0\u6CD5\u590D\u5236\u5230\u5BA2\u6237\u7AEF\uFF0C\u5B9E\u6D4B\uFF09\uFF1A\n/// host \u6743\u5A01\u751F\u6210/\u5403/\u91CD\u751F\uFF1B\u72B6\u6001\u53D8\u5316\u7ECF NetworkManager \u7684\u9759\u6001 RPC \u5E7F\u64AD\uFF0C\u5404\u7AEF\u5E94\u7528\u5230\u672C\u5730\u5217\u8868\u3002\n/// Init/TryEat/Tick \u4EC5\u6743\u5A01\u7AEF\u8C03\u7528\uFF1BApply* \u4EC5\u4F9B\u5BA2\u6237\u7AEF\u5E94\u7528\u8FDC\u7AEF\u4E8B\u4EF6\u3002\n/// \u7A7A\u95F4\u54C8\u5E0C\uFF08v0.6.3.0\uFF09\uFF1A3 \u500D\u573A\u5730 4140 \u9897\u98DF\u7269\u540E\uFF0C\u5403\u5224\u5B9A O(\u7403\u6570\u00D7\u98DF\u7269)\u224820 \u4E07\u6B21/\u5E27\u6210\u4E3A\u74F6\u9888\u2014\u2014\n/// 512 \u5355\u4F4D\u6876\u67E5\u8BE2\u628A\u6BCF\u7403\u68C0\u67E5\u91CF\u4ECE 4140 \u964D\u5230\u51E0\u5341\u3002\n/// \u003C/summary\u003E\npublic sealed class FoodManager\n{\n\tFoodData[] _foods;\n\tTimeSince[] _eatenAt;                           // \u4EC5 host\uFF08\u91CD\u751F\u8BA1\u65F6\uFF09\n\treadonly List\u003Cint\u003E _pendingRespawn = new();     // \u4EC5 host\n\n\t// ---- \u7A7A\u95F4\u54C8\u5E0C\uFF08\u4EC5 host \u5403\u5224\u5B9A\u7528\uFF1B\u5BA2\u6237\u7AEF\u65E0\u5403\u5224\u5B9A\u4E0D\u5EFA\u6876\uFF09----\n\tconst float BucketSize = 512f;\n\tDictionary\u003Cint, List\u003Cint\u003E\u003E _buckets;\n\tint _cols;\n\treadonly List\u003Cint\u003E _nearResults = new();\n\n\t/// \u003Csummary\u003E \u5B58\u6D3B\u98DF\u7269\u6570 \u003C/summary\u003E\n\tpublic int AliveCount { get; private set; }\n\n\t/// \u003Csummary\u003E \u5168\u91CF\u98DF\u7269\uFF08NeonRenderer \u6BCF\u5E27\u8BFB\uFF1B\u5404\u7AEF\u5185\u5BB9\u4E00\u81F4\uFF09 \u003C/summary\u003E\n\tpublic FoodData[] Foods =\u003E _foods;\n\n\tint BucketKey( Vector2 p )\n\t{\n\t\tvar half = GameConfig.ArenaHalfSize;\n\t\tint bx = Math.Clamp( (int) ( ( p.x \u002B half ) / BucketSize ), 0, _cols - 1 );\n\t\tint by = Math.Clamp( (int) ( ( p.y \u002B half ) / BucketSize ), 0, _cols - 1 );\n\t\treturn by * _cols \u002B bx;\n\t}\n\n\tvoid BucketAdd( int index ) =\u003E BucketAddAt( index, _foods[index].Pos );\n\n\tvoid BucketAddAt( int index, Vector2 p )\n\t{\n\t\tint key = BucketKey( p );\n\t\tif ( !_buckets.TryGetValue( key, out var list ) )\n\t\t{\n\t\t\tlist = new List\u003Cint\u003E( 16 );\n\t\t\t_buckets[key] = list;\n\t\t}\n\t\tlist.Add( index );\n\t}\n\n\tvoid BucketMove( int index, Vector2 from )\n\t{\n\t\tint oldKey = BucketKey( from );\n\t\tif ( _buckets.TryGetValue( oldKey, out var list ) )\n\t\t\tlist.Remove( index );\n\n\t\tBucketAdd( index );\n\t}\n\n\t/// \u003Csummary\u003E \u5706\u5F62\u8303\u56F4\u5185\u7684\u5B58\u6D3B\u98DF\u7269\u4E0B\u6807\uFF08\u8FD1\u4F3C\uFF1A\u6309\u6876\u7C97\u7B5B\uFF0C\u8C03\u7528\u65B9\u9700\u7CBE\u786E\u590D\u6838\u8DDD\u79BB\uFF09\u3002\n\t/// \u8FD4\u56DE\u5185\u90E8\u590D\u7528\u5217\u8868\uFF0C\u4E0B\u4E00\u6B21\u8C03\u7528\u5373\u5931\u6548\u3002\u4EC5 host\uFF08\u5403\u5224\u5B9A\u6743\u5A01\u7AEF\uFF09\u3002 \u003C/summary\u003E\n\tpublic List\u003Cint\u003E Nearby( Vector2 pos, float radius )\n\t{\n\t\t_nearResults.Clear();\n\t\tif ( _buckets is null || _foods is null ) return _nearResults;\n\n\t\tint minBx = Math.Clamp( (int) ( ( pos.x - radius \u002B GameConfig.ArenaHalfSize ) / BucketSize ), 0, _cols - 1 );\n\t\tint maxBx = Math.Clamp( (int) ( ( pos.x \u002B radius \u002B GameConfig.ArenaHalfSize ) / BucketSize ), 0, _cols - 1 );\n\t\tint minBy = Math.Clamp( (int) ( ( pos.y - radius \u002B GameConfig.ArenaHalfSize ) / BucketSize ), 0, _cols - 1 );\n\t\tint maxBy = Math.Clamp( (int) ( ( pos.y \u002B radius \u002B GameConfig.ArenaHalfSize ) / BucketSize ), 0, _cols - 1 );\n\n\t\tfor ( int by = minBy; by \u003C= maxBy; by\u002B\u002B )\n\t\t{\n\t\t\tfor ( int bx = minBx; bx \u003C= maxBx; bx\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( !_buckets.TryGetValue( by * _cols \u002B bx, out var list ) ) continue;\n\n\t\t\t\tforeach ( var idx in list )\n\t\t\t\t{\n\t\t\t\t\tif ( _foods[idx].Alive ) _nearResults.Add( idx );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn _nearResults;\n\t}\n\n\t/// \u003Csummary\u003E host \u6295\u98DF\uFF08\u6743\u5A01\u7AEF\uFF1B\u968F\u540E\u7ECF FoodFull RPC \u53D1\u7ED9\u65B0\u8FDE\u63A5\uFF09 \u003C/summary\u003E\n\tpublic void Init()\n\t{\n\t\t_foods = new FoodData[GameConfig.FoodCount];\n\t\t_eatenAt = new TimeSince[GameConfig.FoodCount];\n\t\t_pendingRespawn.Clear();\n\t\t_buckets = new Dictionary\u003Cint, List\u003Cint\u003E\u003E( 1024 );\n\t\t_cols = Math.Max( 1, (int) ( GameConfig.ArenaHalfSize * 2f / BucketSize ) \u002B 1 );\n\n\t\tfor ( int i = 0; i \u003C _foods.Length; i\u002B\u002B )\n\t\t{\n\t\t\t_foods[i] = MakeFood();\n\t\t\tBucketAdd( i );\n\t\t}\n\t\tAliveCount = _foods.Length;\n\t}\n\n\t/// \u003Csummary\u003E \u6BCF\u5E27\uFF1A\u628A\u5230\u671F\u7684\u91CD\u751F\u4F4D\u91CD\u65B0\u6295\u98DF\u5E76\u5E7F\u64AD\uFF08\u4EC5\u6743\u5A01\u7AEF\uFF09 \u003C/summary\u003E\n\tpublic void Tick()\n\t{\n\t\tfor ( int i = _pendingRespawn.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tint idx = _pendingRespawn[i];\n\t\t\tif ( _eatenAt[idx] \u003C GameConfig.FoodRespawnSeconds ) continue;\n\n\t\t\tvar oldPos = _foods[idx].Pos;\n\t\t\t_foods[idx] = MakeFood();\n\t\t\t_pendingRespawn.RemoveAt( i );\n\t\t\tAliveCount\u002B\u002B;\n\t\t\tBucketMove( idx, oldPos );\n\t\t\tif ( Networking.IsActive ) NetworkManager.FoodRespawned( idx, _foods[idx] );   // \u65E0\u4F1A\u8BDD\uFF08\u83DC\u5355\u6F14\u793A\u8D5B\uFF09\u4E0D\u5E7F\u64AD\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u5C1D\u8BD5\u5403\u6389\u6307\u5B9A\u98DF\u7269\uFF1B\u6210\u529F\u8FD4\u56DE true\uFF08\u8C03\u7528\u65B9\u636E\u6B64\u52A0\u8D28\u91CF\uFF09\u3002\u4EC5\u6743\u5A01\u7AEF\uFF0C\u5185\u90E8\u5E7F\u64AD\u3002\n\t/// eaterSteamId \u968F\u5E7F\u64AD\u4E0B\u53D1\uFF0C\u5BA2\u6237\u7AEF\u636E\u6B64\u5224\u65AD\u662F\u4E0D\u662F\u81EA\u5DF1\u7684\u5634\uFF08\u64AD\u5403\u97F3\uFF09 \u003C/summary\u003E\n\tpublic bool TryEat( int index, long eaterSteamId = 0 )\n\t{\n\t\tif ( (uint)index \u003E= (uint)_foods.Length || !_foods[index].Alive ) return false;\n\n\t\t_foods[index].Alive = false;\n\t\t_eatenAt[index] = 0;\n\t\t_pendingRespawn.Add( index );\n\t\tAliveCount--;\n\t\tif ( Networking.IsActive ) NetworkManager.FoodEaten( index, eaterSteamId );   // \u65E0\u4F1A\u8BDD\u4E0D\u5E7F\u64AD\n\t\treturn true;\n\t}\n\n\tFoodData MakeFood()\n\t{\n\t\tvar half = GameConfig.ArenaHalfSize - GameConfig.FoodMargin;\n\t\treturn new FoodData\n\t\t{\n\t\t\tPos = new Vector2( Game.Random.Float( -half, half ), Game.Random.Float( -half, half ) ),\n\t\t\tColorIndex = (byte)Game.Random.Float( 0f, GameConfig.Palette.Length - 0.001f ),\n\t\t\tAlive = true,\n\t\t\tType = 0,\n\t\t};\n\t}\n\n\t// ---- \u5BA2\u6237\u7AEF\u5E94\u7528\uFF08\u8FDC\u7AEF\u4E8B\u4EF6 \u2192 \u672C\u5730\u5217\u8868\uFF09----\n\n\tpublic void ApplyFull( FoodData[] foods )\n\t{\n\t\t_foods = foods;\n\t\tAliveCount = 0;\n\t\tforeach ( var f in _foods )\n\t\t{\n\t\t\tif ( f.Alive ) AliveCount\u002B\u002B;\n\t\t}\n\t}\n\n\tpublic void ApplyEaten( int index )\n\t{\n\t\tif ( _foods is null ) return;   // \u70ED\u91CD\u8F7D\u540E\u65B0\u5B9E\u4F8B\u672A\u540C\u6B65\u5168\u91CF\u524D\uFF0C\u5FFD\u7565\u589E\u91CF\n\t\tif ( (uint)index \u003E= (uint)_foods.Length || !_foods[index].Alive ) return;\n\n\t\tvar f = _foods[index];\n\t\tf.Alive = false;\n\t\t_foods[index] = f;\n\t\tAliveCount--;\n\t}\n\n\tpublic void ApplyRespawned( int index, FoodData food )\n\t{\n\t\tif ( _foods is null ) return;\n\t\tif ( (uint)index \u003E= (uint)_foods.Length ) return;\n\n\t\t// \u5DF2\u6D3B\u7740\uFF08\u91CD\u590D/\u4E71\u5E8F\u6295\u9012\uFF09\u4E5F\u66F4\u65B0\u6570\u636E\u4F46\u4E0D\u91CD\u590D\u8BA1\u6570\u2014\u2014AliveCount \u53EA\u5728\u771F\u590D\u71C3\u65F6 \u002B1\n\t\tif ( !_foods[index].Alive ) AliveCount\u002B\u002B;\n\t\t_foods[index] = food;\n\t}\n}"},{"Ident":"slfy.circleroyale","Path":"Game/TerritoryManager.cs","FileName":"TerritoryManager.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\n\n/// \u003Csummary\u003E\n/// \u9886\u571F\u6218\u4E89\u6A21\u5F0F\uFF08M7\uFF0C\u7528\u6237\u5B9A\u7A3F V2.1\uFF09\uFF1A4\u00D74 \u683C\uFF0C\u56DB\u89D2\u5927\u672C\u8425\u5F00\u5C40\u5C5E\u5DF1\u65B9\u4E14\u6C38\u4E0D\u6CA6\u9677\u3002\n/// \u9A7B\u519B\u6A21\u578B\u5360\u9886\uFF1A\u5410\u7403\u8FDB\u65D7\u5708\u8BA1\u5206\u2014\u2014\u4E2D\u7ACB\u683C\u5582\u6EE1 100 \u53D8\u8272\u5360\u9886\uFF08\u9A7B\u519B\u6EE1 100\uFF09\uFF1B\n/// \u654C\u65B9\u5410\u7403\u5148\u5543\u9A7B\u519B\uFF0C\u5543\u5149\u56DE\u4E2D\u7ACB\uFF08\u6EA2\u51FA\u8F6C\u4E3A\u81EA\u5DF1\u8FDB\u5EA6\uFF09\u3002\u65E0\u81EA\u52A8\u8870\u51CF\u3001\u65E0\u8865\u7ED9\u7EBF\uFF1B\n/// \u53EA\u80FD\u62A2\u4E0E\u5DF1\u65B9\u9886\u571F 4 \u90BB\u63A5\u7684\u683C\uFF08\u5927\u672C\u8425\u514D\u75AB\u654C\u65B9\u5582\u98DF\uFF09\u3002\n/// host \u6743\u5A01\u7ED3\u7B97\uFF08TickCells \u5410\u7403\u547D\u4E2D\u5373\u8BB0 \u002B 5Hz \u53D8\u5316\u5FEB\u7167\u5E7F\u64AD\uFF09\uFF1B\u5BA2\u6237\u7AEF ApplyRemote \u53EA\u8BFB\u3002\n/// \u683C\u5B50\u51E0\u4F55\u7531 ArenaHalfSize \u51B3\u5B9A\uFF08Replicated \u4E0B\u53D1\uFF0C\u53CC\u7AEF\u4E00\u81F4\uFF09\uFF0C\u65E0\u989D\u5916\u7F51\u7EDC\u72B6\u6001\u3002\n/// \u003C/summary\u003E\npublic static class TerritoryManager\n{\n\tconst int TeamCount = 4;\n\n\tstruct Cell\n\t{\n\t\tpublic int Owner;      // \u5360\u9886\u961F\uFF08-1 = \u4E2D\u7ACB\uFF09\n\t\tpublic int ScoreTeam;  // \u5206\u6570\u5F52\u5C5E\u961F\uFF08\u4E2D\u7ACB\u683C\u7684\u4E89\u593A\u8FDB\u5EA6\u65B9\uFF1B-1 = \u65E0\uFF09\n\t\tpublic float Score;    // \u4E2D\u7ACB\u683C = \u8BE5\u961F\u7684\u5360\u9886\u8FDB\u5EA6\uFF1B\u5DF1\u65B9\u683C = \u9A7B\u519B\u503C\n\t}\n\n\tstatic readonly Cell[] _cells = new Cell[GameConfig.TerritoryGrid * GameConfig.TerritoryGrid];\n\tstatic readonly int[] _owned = new int[TeamCount];\n\tstatic readonly float[] _points = new float[TeamCount];   // \u5360\u65D7\u79EF\u5206\uFF08\u6BCF\u65D7\u6BCF 3s \u002B1\uFF0C\u7528\u6237\u5B9A\u7A3F\uFF09\n\tstatic float _pointTimer;                                 // \u79EF\u5206\u53D1\u653E\u8282\u62CD\n\tstatic int _conquerTeam = -1;   // \u5148\u5230\u5F81\u670D\u80DC\u5229\u7684\u961F\uFF08TickTerritory \u8BFB\uFF0CEndMatch \u540E\u968F Reset \u6E05\uFF09\n\tstatic int _sentVersion;        // \u5DF2\u5E7F\u64AD\u8FC7\u7684\u7248\u672C\u53F7\uFF08host \u8282\u6D41\uFF1A\u53D8\u4E86\u624D\u53D1\uFF09\n\n\tpublic static bool Active =\u003E MatchState.IsTerritory;\n\n\t/// \u003Csummary\u003E \u72B6\u6001\u7248\u672C\u53F7\uFF1A\u4EFB\u4F55\u683C\u53D8\u5316 \u002B1\uFF08TerritoryRenderer \u636E\u6B64\u91CD\u5EFA\u51E0\u4F55\uFF09 \u003C/summary\u003E\n\tpublic static int StateVersion { get; private set; }\n\n\t/// \u003Csummary\u003E host\uFF1A\u81EA\u4E0A\u6B21\u5FEB\u7167\u540E\u72B6\u6001\u6709\u53D8\uFF08TickTerritory \u8282\u6D41\u5E7F\u64AD\u7528\uFF09 \u003C/summary\u003E\n\tpublic static bool NeedsSync =\u003E StateVersion != _sentVersion;\n\n\t/// \u003Csummary\u003E \u89C4\u5219\u751F\u6548\u70B9\u8C03\u7528\uFF08MatchState.Apply\uFF09\uFF1A\u5168\u7AEF\u590D\u4F4D\u4E3A\u5F00\u5C40\u6001\u2014\u2014\u56DB\u89D2\u5927\u672C\u8425\u5C5E\u5DF1\u65B9\u6EE1\u9A7B\u519B\uFF0C\u5176\u4F59\u5168\u4E2D\u7ACB \u003C/summary\u003E\n\tpublic static void Reset()\n\t{\n\t\tfor ( int i = 0; i \u003C _cells.Length; i\u002B\u002B )\n\t\t{\n\t\t\t_cells[i].Owner = -1;\n\t\t\t_cells[i].ScoreTeam = -1;\n\t\t\t_cells[i].Score = 0f;\n\t\t}\n\n\t\tfor ( var t = 0; t \u003C TeamCount; t\u002B\u002B )\n\t\t{\n\t\t\tvar ( cx, cy ) = HqCell( t );\n\t\t\tref var c = ref _cells[cy * GameConfig.TerritoryGrid \u002B cx];\n\t\t\tc.Owner = t;\n\t\t\tc.ScoreTeam = t;\n\t\t\tc.Score = GameConfig.TerritoryCaptureScore;\n\t\t}\n\n\t\t_conquerTeam = -1;\n\t\t_sentVersion = 0;\n\t\t_pointTimer = 0f;\n\t\tfor ( var t = 0; t \u003C TeamCount; t\u002B\u002B ) _points[t] = 0f;\n\t\tRecount();\n\t\tStateVersion\u002B\u002B;\n\t}\n\n\t/// \u003Csummary\u003E \u961F\u4F0D\u7684\u5927\u672C\u8425\u683C\uFF08\u56FA\u5B9A\u89D2\u6620\u5C04\uFF1A0 \u5DE6\u4E0A\u30011 \u53F3\u4E0A\u30012 \u53F3\u4E0B\u30013 \u5DE6\u4E0B\uFF09 \u003C/summary\u003E\n\tpublic static ( int Cx, int Cy ) HqCell( int team ) =\u003E team switch\n\t{\n\t\t0 =\u003E ( 0, 0 ),\n\t\t1 =\u003E ( GameConfig.TerritoryGrid - 1, 0 ),\n\t\t2 =\u003E ( GameConfig.TerritoryGrid - 1, GameConfig.TerritoryGrid - 1 ),\n\t\t_ =\u003E ( 0, GameConfig.TerritoryGrid - 1 ),\n\t};\n\n\tstatic bool IsHq( int cx, int cy )\n\t{\n\t\tfor ( var t = 0; t \u003C TeamCount; t\u002B\u002B )\n\t\t{\n\t\t\tvar hq = HqCell( t );\n\t\t\tif ( hq.Cx == cx \u0026\u0026 hq.Cy == cy ) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// ---- \u51E0\u4F55\uFF08\u53CC\u7AEF\u4E00\u81F4\uFF0C\u53EA\u4F9D\u8D56 ArenaHalfSize\uFF09----\n\n\tpublic static float CellSize =\u003E GameConfig.ArenaHalfSize * 2f / GameConfig.TerritoryGrid;\n\n\tpublic static Vector3 CellCenter( int cx, int cy )\n\t{\n\t\tvar half = GameConfig.ArenaHalfSize;\n\t\tvar s = CellSize;\n\t\treturn new Vector3( -half \u002B ( cx \u002B 0.5f ) * s, -half \u002B ( cy \u002B 0.5f ) * s, 0f );\n\t}\n\n\tpublic static void CellOf( Vector3 pos, out int cx, out int cy )\n\t{\n\t\tvar half = GameConfig.ArenaHalfSize;\n\t\tvar s = CellSize;\n\t\tcx = Math.Clamp( (int)MathF.Floor( ( pos.x \u002B half ) / s ), 0, GameConfig.TerritoryGrid - 1 );\n\t\tcy = Math.Clamp( (int)MathF.Floor( ( pos.y \u002B half ) / s ), 0, GameConfig.TerritoryGrid - 1 );\n\t}\n\n\t/// \u003Csummary\u003E \u961F\u4F0D\u5927\u672C\u8425\u5185\u7684\u51FA\u751F\u91C7\u6837\u8303\u56F4\uFF08\u8D34\u8FB9\u5185\u7F29\uFF0C\u522B\u51FA\u751F\u5230\u65D7\u5708\u8FB9\u4E0A\u6328\u6253\uFF09 \u003C/summary\u003E\n\tpublic static void HqBounds( int team, out float minX, out float maxX, out float minY, out float maxY )\n\t{\n\t\tvar ( cx, cy ) = HqCell( team );\n\t\tvar half = GameConfig.ArenaHalfSize;\n\t\tvar s = CellSize;\n\t\tvar inset = s * 0.28f;\n\t\tminX = -half \u002B cx * s \u002B inset;\n\t\tmaxX = -half \u002B ( cx \u002B 1 ) * s - inset;\n\t\tminY = -half \u002B cy * s \u002B inset;\n\t\tmaxY = -half \u002B ( cy \u002B 1 ) * s - inset;\n\t}\n\n\t/// \u003Csummary\u003E \u7403\u662F\u5426\u5728\u5DF1\u65B9\u5927\u672C\u8425\u5185\uFF08\u5927\u4F53\u578B\u8870\u51CF\u8C41\u514D\u5224\u5B9A\uFF0CBall \u8870\u51CF\u5904\u8C03\u7528\uFF09 \u003C/summary\u003E\n\tpublic static bool IsOwnHq( int team, Vector3 pos )\n\t{\n\t\tif ( !Active || team \u003C 0 ) return false;\n\t\tCellOf( pos, out var cx, out var cy );\n\t\tvar hq = HqCell( team );\n\t\treturn cx == hq.Cx \u0026\u0026 cy == hq.Cy;\n\t}\n\n\t/// \u003Csummary\u003E \u7403\u662F\u5426\u5728\u654C\u8425\u5927\u672C\u8425\u5185\uFF08M7.2 \u5B89\u5168\u5C4B\uFF1A\u51CF\u901F\u002B\u6389\u91CD\uFF1B\u65E0\u961F/\u975E\u9886\u571F\u6052 false\uFF09 \u003C/summary\u003E\n\tpublic static bool InHostileHq( int team, Vector3 pos )\n\t{\n\t\tif ( !Active || team \u003C 0 ) return false;\n\t\tCellOf( pos, out var cx, out var cy );\n\t\tref var c = ref _cells[cy * GameConfig.TerritoryGrid \u002B cx];\n\t\treturn IsHq( cx, cy ) \u0026\u0026 c.Owner \u003E= 0 \u0026\u0026 c.Owner != team;\n\t}\n\n\t/// \u003Csummary\u003E \u662F\u5426\u5B58\u5728\u524D\u7EBF\u683C\uFF08\u5927\u672C\u8425\u4EE5\u5916\u7684\u5DF1\u65B9\u5360\u9886\u683C\u2014\u2014\u6B7B\u4EA1\u9762\u677F FRONT \u6309\u94AE\u4EAE\u6697\u7528\uFF09 \u003C/summary\u003E\n\tpublic static bool HasFrontCell( int team ) =\u003E TryFrontSpawn( team, default, out _ );\n\n\t/// \u003Csummary\u003E \u8BE5\u4F4D\u7F6E\u7684\u65D7\u662F\u5426\u5141\u8BB8\u8BE5\u961F\u52A8\u4F5C\uFF08\u5DF1\u65B9\u683C\uFF0C\u6216\u90BB\u63A5\u5DF1\u65B9\u9886\u571F\u7684\u975E\u5927\u672C\u8425\u683C\uFF09\u2014\u2014UI \u5F15\u5BFC\u7528\uFF1B\n\t/// \u4E0E Feed \u7684\u5165\u4FB5\u95E8\u540C\u4E00\u89C4\u5219 \u003C/summary\u003E\n\tpublic static bool CanActOn( int team, Vector3 pos )\n\t{\n\t\tCellOf( pos, out var cx, out var cy );\n\t\treturn CanActOnCell( cx, cy, team );\n\t}\n\n\tpublic static bool CanActOnCell( int cx, int cy, int team )\n\t{\n\t\tif ( !Active || team \u003C 0 ) return false;\n\t\tref var c = ref _cells[cy * GameConfig.TerritoryGrid \u002B cx];\n\t\treturn c.Owner == team || ( !IsHq( cx, cy ) \u0026\u0026 AdjacentToOwned( cx, cy, team ) );\n\t}\n\n\t/// \u003Csummary\u003E \u524D\u7EBF\u590D\u6D3B\u70B9\uFF08M7.2 \u590D\u6D3B\u4E8C\u9009\u4E00\uFF09\uFF1A\u79BB nearPos \u6700\u8FD1\u7684\u5DF1\u65B9\u975E\u5927\u672C\u8425\u5360\u9886\u683C\u5185\u968F\u673A\u70B9\uFF1B\n\t/// \u6CA1\u6709\u524D\u7EBF\u683C\u8FD4\u56DE false\uFF08\u53EA\u5269\u5927\u672C\u8425\u53EF\u590D\u6D3B\uFF09 \u003C/summary\u003E\n\tpublic static bool TryFrontSpawn( int team, Vector3 nearPos, out Vector3 pos )\n\t{\n\t\tpos = default;\n\t\tif ( !Active || team \u003C 0 ) return false;\n\n\t\tvar best = float.MaxValue;\n\t\tvar found = false;\n\t\tvar grid = GameConfig.TerritoryGrid;\n\t\tfor ( int cy = 0; cy \u003C grid; cy\u002B\u002B )\n\t\t{\n\t\t\tfor ( int cx = 0; cx \u003C grid; cx\u002B\u002B )\n\t\t\t{\n\t\t\t\tref var c = ref _cells[cy * grid \u002B cx];\n\t\t\t\tif ( c.Owner != team || IsHq( cx, cy ) ) continue;\n\t\t\t\tvar d = CellCenter( cx, cy ).Distance( nearPos );\n\t\t\t\tif ( d \u003C best )\n\t\t\t\t{\n\t\t\t\t\tbest = d;\n\t\t\t\t\tpos = CellCenter( cx, cy );\n\t\t\t\t\tfound = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ( !found ) return false;\n\n\t\tvar s = CellSize;\n\t\tpos \u002B= new Vector3( Game.Random.Float( -s * 0.18f, s * 0.18f ), Game.Random.Float( -s * 0.18f, s * 0.18f ), 0f );\n\t\treturn true;\n\t}\n\n\t// ---- \u5582\u65D7\u7ED3\u7B97\uFF08host \u6743\u5A01\uFF09----\n\n\t/// \u003Csummary\u003E \u4E00\u9897\u5B62\u5B50\u843D\u8FDB\u65D7\u5708\uFF08TickCells \u5410\u7403\u547D\u4E2D\u8C03\u7528\uFF09\u3002\n\t/// \u8FD4\u56DE\u662F\u5426\u6D88\u8017\u8BE5\u5B62\u5B50\uFF1A\u8FDB\u65D7\u5708\u4E14\u89C4\u5219\u5141\u8BB8\u5C31\u6D88\u8017\u8BA1\u5206\uFF1B\u6CA1\u8FDB\u5708\u6216\u89C4\u5219\u4E0D\u5141\u8BB8\uFF08\u975E\u90BB\u63A5/\u5927\u672C\u8425\uFF09\n\t/// \u5219 false\uFF0C\u5B62\u5B50\u7EE7\u7EED\u5F53\u666E\u901A\u5B62\u5B50\u98DE\uFF0C\u5410\u51FA\u7684\u8D28\u91CF\u4E0D\u767D\u8D39 \u003C/summary\u003E\n\tpublic static bool Feed( int team, Vector3 pos, float blobMass )\n\t{\n\t\tif ( !Active || team \u003C 0 || blobMass \u003C= 0f ) return false;\n\n\t\tCellOf( pos, out var cx, out var cy );\n\t\tif ( CellCenter( cx, cy ).Distance( pos ) \u003E= GameConfig.TerritoryFlagRadius ) return false;   // \u6CA1\u8FDB\u65D7\u5708\n\n\t\tref var c = ref _cells[cy * GameConfig.TerritoryGrid \u002B cx];\n\n\t\t// \u5165\u4FB5\u89C4\u5219\uFF1A\u52A8\u522B\u4EBA/\u65E0\u4E3B\u7684\u683C\uFF0C\u5FC5\u987B\u4E0E\u5DF1\u65B9\u9886\u571F 4 \u90BB\u63A5\uFF08\u7A33\u624E\u7A33\u6253\uFF0C\u65E0\u98DE\u5730\uFF09\n\t\tif ( c.Owner != team \u0026\u0026 ( !AdjacentToOwned( cx, cy, team ) || IsHq( cx, cy ) ) )\n\t\t\treturn false;\n\n\t\tvar pts = MathF.Min( blobMass * GameConfig.TerritoryScorePerMass, GameConfig.TerritoryScoreCapPerBlob );\n\t\tif ( pts \u003C= 0f ) return false;\n\n\t\tvar changed = false;\n\n\t\tif ( c.Owner == team )\n\t\t{\n\t\t\t// \u5DF1\u65B9\u683C\uFF1A\u8865\u9A7B\u519B\uFF08\u65E0\u81EA\u52A8\u8870\u51CF\uFF0C\u53EA\u4F1A\u88AB\u654C\u65B9\u5543\u6389\u540E\u91CD\u5360\uFF09\n\t\t\tif ( c.Score \u003C GameConfig.TerritoryCaptureScore )\n\t\t\t{\n\t\t\t\tc.Score = MathF.Min( GameConfig.TerritoryCaptureScore, c.Score \u002B pts );\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\telse if ( c.Owner \u003C 0 )\n\t\t{\n\t\t\t// \u4E2D\u7ACB\u683C\uFF1A\u65E0\u4E3B\u8FDB\u5EA6\u5F52\u81EA\u5DF1\u7D2F\u52A0\uFF1B\u6709\u522B\u4EBA\u7684\u8FDB\u5EA6\u5148\u5543\uFF0C\u5543\u5149\u8F6C\u4E3A\u81EA\u5DF1\u7D2F\u52A0\uFF08\u6EA2\u51FA\u8BA1\u5165\uFF09\n\t\t\tif ( c.ScoreTeam \u003C 0 || c.ScoreTeam == team )\n\t\t\t{\n\t\t\t\tc.ScoreTeam = team;\n\t\t\t\tc.Score \u002B= pts;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tc.Score -= pts;\n\t\t\t\tif ( c.Score \u003C= 0f )\n\t\t\t\t{\n\t\t\t\t\tc.ScoreTeam = team;\n\t\t\t\t\tc.Score = -c.Score;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( c.Score \u003E= GameConfig.TerritoryCaptureScore )\n\t\t\t{\n\t\t\t\tc.Owner = team;\n\t\t\t\tc.Score = GameConfig.TerritoryCaptureScore;\n\t\t\t\t_owned[team]\u002B\u002B;\n\t\t\t\tchanged = true;\n\t\t\t\tNeonRenderer.Puff( CellCenter( cx, cy ), team );\n\t\t\t\tGameLog.Info( $\u0022[territory] team {team \u002B 1} captured cell ({cx},{cy}) \u2014 {_owned[team]}/{GameConfig.TerritoryWinCells}\u0022 );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// \u654C\u65B9\u683C\uFF1A\u5148\u5543\u9A7B\u519B\uFF0C\u5543\u5149\u56DE\u4E2D\u7ACB\uFF08\u6EA2\u51FA\u8F6C\u4E3A\u81EA\u5DF1\u8FDB\u5EA6\uFF0C\u63A5\u7740\u5582\u5C31\u80FD\u987A\u52BF\u5360\u9886\uFF09\n\t\t\tc.Score -= pts;\n\t\t\tif ( c.Score \u003C= 0f )\n\t\t\t{\n\t\t\t\t_owned[c.Owner]--;\n\t\t\t\tc.Owner = -1;\n\t\t\t\tc.ScoreTeam = team;\n\t\t\t\tc.Score = -c.Score;\n\t\t\t\tNeonRenderer.Puff( CellCenter( cx, cy ), team );\n\t\t\t\tGameLog.Info( $\u0022[territory] team {team \u002B 1} neutralized cell ({cx},{cy})\u0022 );\n\t\t\t}\n\t\t\tchanged = true;\n\t\t}\n\n\t\tif ( changed )\n\t\t{\n\t\t\tStateVersion\u002B\u002B;\n\t\t\tif ( _conquerTeam \u003C 0 \u0026\u0026 _owned[team] \u003E= GameConfig.TerritoryWinCells )\n\t\t\t{\n\t\t\t\t_conquerTeam = team;\n\t\t\t\tGameLog.Info( $\u0022[territory] team {team \u002B 1} reaches {GameConfig.TerritoryWinCells} cells \u2014 conquest!\u0022 );\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tstatic bool AdjacentToOwned( int cx, int cy, int team )\n\t{\n\t\treturn Owns( cx - 1, cy, team ) || Owns( cx \u002B 1, cy, team )\n\t\t\t|| Owns( cx, cy - 1, team ) || Owns( cx, cy \u002B 1, team );\n\t}\n\n\tstatic bool Owns( int cx, int cy, int team )\n\t{\n\t\tif ( cx \u003C 0 || cy \u003C 0 || cx \u003E= GameConfig.TerritoryGrid || cy \u003E= GameConfig.TerritoryGrid ) return false;\n\t\treturn _cells[cy * GameConfig.TerritoryGrid \u002B cx].Owner == team;\n\t}\n\n\tstatic void Recount()\n\t{\n\t\tfor ( var i = 0; i \u003C _owned.Length; i\u002B\u002B ) _owned[i] = 0;\n\t\tforeach ( var c in _cells )\n\t\t{\n\t\t\tif ( c.Owner \u003E= 0 \u0026\u0026 c.Owner \u003C _owned.Length ) _owned[c.Owner]\u002B\u002B;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u67D0\u961F\u5F53\u524D\u5360\u9886\u683C\u6570\uFF08HUD/\u7ED3\u7B97/\u80DC\u5229\u5224\u5B9A\u5171\u7528\uFF09 \u003C/summary\u003E\n\tpublic static int OwnedCount( int team ) =\u003E team \u003E= 0 \u0026\u0026 team \u003C TeamCount ? _owned[team] : 0;\n\n\t/// \u003Csummary\u003E host \u6BCF\u5E27\uFF1A\u5360\u65D7\u79EF\u5206\u53D1\u653E\uFF08\u7528\u6237\u5B9A\u7A3F\u2014\u2014\u6BCF\u5360\u9886\u4E00\u65D7\u6BCF 3 \u79D2 \u002B1 \u5206\uFF1B\u70B9\u6570\u53EA\u5728 host \u7D2F\u79EF\uFF0C\u968F\u5FEB\u7167\u4E0B\u53D1\uFF09 \u003C/summary\u003E\n\tpublic static void TickPoints( float dt )\n\t{\n\t\tif ( !Active ) return;\n\t\t_pointTimer \u002B= dt;\n\t\tvar step = GameConfig.TerritoryPointSeconds;\n\t\twhile ( _pointTimer \u003E= step )\n\t\t{\n\t\t\t_pointTimer -= step;\n\t\t\tfor ( var t = 0; t \u003C TeamCount; t\u002B\u002B ) _points[t] \u002B= _owned[t];\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u67D0\u961F\u5F53\u524D\u5360\u65D7\u79EF\u5206\uFF08\u80DC\u5229\u5224\u5B9A/HUD/\u7ED3\u7B97\u5171\u7528\uFF1B\u5BA2\u6237\u7AEF\u7ECF TerritoryState \u5FEB\u7167\u540C\u6B65\uFF09 \u003C/summary\u003E\n\tpublic static float Points( int team ) =\u003E team \u003E= 0 \u0026\u0026 team \u003C TeamCount ? _points[team] : 0f;\n\n\t/// \u003Csummary\u003E \u5355\u683C\u72B6\u6001\u8BFB\u53D6\uFF08TerritoryRenderer \u6BCF\u6B21\u91CD\u5EFA\u9010\u683C\u53D6\uFF09 \u003C/summary\u003E\n\tpublic static void CellState( int cx, int cy, out int owner, out int scoreTeam, out float score )\n\t{\n\t\tif ( cx \u003C 0 || cy \u003C 0 || cx \u003E= GameConfig.TerritoryGrid || cy \u003E= GameConfig.TerritoryGrid )\n\t\t{\n\t\t\towner = -1;\n\t\t\tscoreTeam = -1;\n\t\t\tscore = 0f;\n\t\t\treturn;\n\t\t}\n\t\tref var c = ref _cells[cy * GameConfig.TerritoryGrid \u002B cx];\n\t\towner = c.Owner;\n\t\tscoreTeam = c.ScoreTeam;\n\t\tscore = c.Score;\n\t}\n\n\t/// \u003Csummary\u003E \u8FBE\u6210\u5F81\u670D\u80DC\u5229\u7684\u961F\u4F0D\uFF08-1 = \u5C1A\u65E0\uFF09\uFF1Bhost \u68C0\u67E5\u5230\u5373 EndMatch \u003C/summary\u003E\n\tpublic static int ConquerWinner =\u003E _conquerTeam;\n\n\t// ---- \u7F51\u7EDC\u540C\u6B65 ----\n\n\t/// \u003Csummary\u003E host\uFF1A\u5F53\u524D\u56DB\u961F\u5360\u65D7\u79EF\u5206\uFF08\u4E0E\u683C\u5B50\u5FEB\u7167\u540C\u62CD\u4E0B\u53D1\uFF09 \u003C/summary\u003E\n\tpublic static float[] PointsWire()\n\t{\n\t\tvar arr = new float[TeamCount];\n\t\tfor ( var t = 0; t \u003C TeamCount; t\u002B\u002B ) arr[t] = _points[t];\n\t\treturn arr;\n\t}\n\n\t/// \u003Csummary\u003E host\uFF1A\u5F53\u524D 16 \u683C\u72B6\u6001\u5FEB\u7167\uFF08\u53D1\u51FA\u5373\u8BB0\u5F55\u7248\u672C\uFF0CNeedsSync \u5F52\u96F6\uFF09 \u003C/summary\u003E\n\tpublic static TerritoryWire[] Snapshot()\n\t{\n\t\tvar wires = new TerritoryWire[_cells.Length];\n\t\tfor ( int i = 0; i \u003C _cells.Length; i\u002B\u002B )\n\t\t{\n\t\t\twires[i] = new TerritoryWire\n\t\t\t{\n\t\t\t\tOwner = _cells[i].Owner \u003C 0 ? (byte)255 : (byte)_cells[i].Owner,\n\t\t\t\tScoreTeam = _cells[i].ScoreTeam \u003C 0 ? (byte)255 : (byte)_cells[i].ScoreTeam,\n\t\t\t\tScore = _cells[i].Score,\n\t\t\t};\n\t\t}\n\t\t_sentVersion = StateVersion;\n\t\treturn wires;\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u5E94\u7528 host \u5FEB\u7167\uFF08host \u56DE\u58F0\u7531 IsAuthority \u6321\u6389\uFF1B\u91CD\u590D\u5E94\u7528\u5E42\u7B49\uFF09\u3002\n\t/// points = \u56DB\u961F\u5360\u65D7\u79EF\u5206\uFF08null \u5BB9\u9519\uFF1A\u65E7\u5305/\u7A7A\u5305\u53EA\u5237\u683C\u5B50\uFF09 \u003C/summary\u003E\n\tpublic static void ApplyRemote( TerritoryWire[] wires, float[] points )\n\t{\n\t\tif ( NetworkManager.IsAuthority ) return;\n\t\tif ( wires is null || wires.Length != _cells.Length ) return;\n\n\t\t// \u9010\u9879\u6BD4\u5BF9\uFF0C\u5B8C\u5168\u6CA1\u53D8\u5C31\u8DF3\u8FC7\u2014\u2014host \u6BCF 2s \u4FDD\u6D3B\u5FEB\u7167\u5E26\u76F8\u540C\u6570\u636E\uFF0C\n\t\t// \u65E0\u8111 StateVersion\u002B\u002B \u4F1A\u8BA9\u5BA2\u6237\u7AEF\u9886\u571F\u51E0\u4F55\u9891\u7E41\u91CD\u5EFA\uFF08\u95EA\uFF0Cv0.7.8.86 \u4FEE\uFF09\n\t\tvar changed = false;\n\t\tfor ( int i = 0; i \u003C _cells.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar owner = wires[i].Owner == 255 ? -1 : wires[i].Owner;\n\t\t\tvar scoreTeam = wires[i].ScoreTeam == 255 ? -1 : wires[i].ScoreTeam;\n\t\t\tvar score = wires[i].Score;\n\t\t\tref var c = ref _cells[i];\n\t\t\tif ( c.Owner == owner \u0026\u0026 c.ScoreTeam == scoreTeam \u0026\u0026 c.Score == score ) continue;\n\t\t\tc.Owner = owner;\n\t\t\tc.ScoreTeam = scoreTeam;\n\t\t\tc.Score = score;\n\t\t\tchanged = true;\n\t\t}\n\t\tif ( points is not null )\n\t\t{\n\t\t\tfor ( var t = 0; t \u003C TeamCount \u0026\u0026 t \u003C points.Length; t\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( _points[t] != points[t] )\n\t\t\t\t{\n\t\t\t\t\t_points[t] = points[t];\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ( !changed ) return;\n\t\tRecount();\n\t\tStateVersion\u002B\u002B;\n\t}\n}\n\n/// \u003Csummary\u003E \u9886\u571F\u683C\u7F51\u7EDC\u5FEB\u7167\u7EBF\u683C\u5F0F\uFF08255 = \u4E2D\u7ACB/\u65E0\u5F52\u5C5E\uFF1B\u4E0E CellWire/ScoreWire \u540C\u9876\u5C42\u60EF\u4F8B\uFF09 \u003C/summary\u003E\npublic struct TerritoryWire\n{\n\tpublic byte Owner;\n\tpublic byte ScoreTeam;\n\tpublic float Score;\n}"},{"Ident":"slfy.circleroyale","Path":"UI/LobbyPanel.cs","FileName":"LobbyPanel.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380090,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Sandbox.UI;\n\n/// \u003Csummary\u003E\n/// \u6E38\u620F\u623F\u95F4/\u5927\u5385\uFF08M5 \u4E8C\u7EA7\u754C\u9762\uFF09\uFF1AHOST / VS BOTS \u70B9\u5F00\u540E\u8FDB\u5165\u2014\u2014host \u53EF\u8C03\u6BD4\u8D5B\u53C2\u6570\u3001\u770B\u73A9\u5BB6\u5217\u8868\u3001\n/// \u70B9 START \u5F00\u5C40\uFF1B\u5BA2\u6237\u7AEF JOIN \u8FDE\u4E0A\u540E\u8FDB\u540C\u4E00\u754C\u9762\uFF0C\u53C2\u6570\u4E0E\u73A9\u5BB6\u5217\u8868\u7531 host \u7ECF LobbyState RPC \u63A8\u9001\uFF0C\n/// \u663E\u793A\u0022\u7B49\u5F85\u623F\u4E3B\u5F00\u59CB\u0022\uFF0C\u623F\u4E3B\u5F00\u5C40\u540E\u7ECF MatchStarted RPC \u81EA\u52A8\u8FDB\u6E38\u620F\u3002\n/// \u4E3B\u83DC\u5355\u7684\u8BBE\u7F6E\u533A\u5DF2\u79FB\u5230\u8FD9\u91CC\uFF08\u623F\u95F4\u624D\u662F\u8BBE\u7F6E\u53C2\u6570\u7684\u5730\u65B9\uFF09\u3002\n/// \u7EAF C# Panel \u002B \u540C\u76EE\u5F55 LobbyPanel.cs.scss\u3002\n/// \u003C/summary\u003E\npublic sealed class LobbyPanel : PanelComponent\n{\n\tLabel _status;\n\tLabel[] _settings;       // 0=\u6A21\u5F0F 1=\u65F6\u957F 2=\u4EBA\u6570\uFF08host \u53EF\u70B9\u51FB\u5FAA\u73AF\uFF0Cclient \u53EA\u8BFB\u5C55\u793A\uFF09\n\tLabel _settingHint;      // \u0022CLICK A SETTING TO CYCLE IT\u0022\uFF08\u4EC5 host\uFF0Cv0.7.8.0\uFF09\n\tLabel[] _playerRows;     // \u73A9\u5BB6\u5217\u8868\uFF1A\u5355\u5217\uFF08\u6EDA\u52A8\u5BB9\u5668\u5185\uFF0C\u5BB9\u91CF = MaxPlayerRows\uFF09\n\tPanel _playersScroll;    // \u73A9\u5BB6\u5217\u8868\u539F\u751F\u6EDA\u52A8\u5BB9\u5668\uFF08overflow-y \u002B \u6EDA\u8F6E\uFF09\n\tLabel _botsNote;\n\tLabel _start;\n\tLabel _leave;\n\tLabel _join;             // \u5BA2\u6237\u7AEF\u0022JOIN GAME\u0022\u6309\u94AE\uFF1A\u5BF9\u5C40\u8FDB\u884C\u4E2D\u65F6\u663E\u793A\uFF0C\u70B9\u51FB\u540E 3-2-1 \u8FDB\u5165\uFF08v0.6.4.1\uFF09\n\tLabel _countdown;        // \u52A0\u5165\u5012\u8BA1\u65F6\uFF08\u5BA2\u6237\u7AEF\u6536\u5230 host \u5F00\u5C40\u6307\u4EE4\u540E 3-2-1\uFF0Cv0.6.4.0\uFF09\n\tbool _matchRunning;      // \u5BA2\u6237\u7AEF\u89C6\u89D2\uFF1Ahost \u7684\u5BF9\u5C40\u8FDB\u884C\u4E2D\uFF08JOIN GAME \u6309\u94AE\u4EAE\u8D77\uFF09\n\tbool _modeGlowActive;    // \u6A21\u5F0F\u884C\u547C\u5438\u8272\u6FC0\u6D3B\u4E2D\uFF08\u5207\u56DE\u5BA2\u6237\u7AEF\u8EAB\u4EFD\u65F6\u6062\u590D scss \u57FA\u8272\u4E00\u6B21\uFF09\n\n\tbool _built;\n\tbool _retryOpen;         // Panel \u672A\u5C31\u7EEA\u65F6\u7684\u5F00\u95E8\u91CD\u8BD5\uFF08EnsureWorld \u4F1A\u5148\u4E8E OnStart \u8C03\u7528 ShowClient/ShowHost\uFF09\n\tbool _isHost;\n\tbool _shown;\n\tbool _connectionLost;    // \u6389\u7EBF\u91CD\u8FDE\u4E2D\uFF1A\u72B6\u6001\u884C\u9501\u5B9A\u63D0\u793A\uFF08\u5426\u5219\u6BCF\u5E27 RefreshStatus \u4F1A\u8986\u76D6\u6389\uFF09\n\tTimeSince _sinceListRefresh;\n\n\t// \u5BA2\u6237\u7AEF\uFF1A\u623F\u4E3B\u63A8\u9001\u7684\u623F\u95F4\u4FE1\u606F\uFF08LobbyState RPC\uFF09\n\tbyte _clientMode;\n\tint _clientDuration;\n\tint _clientPlayers;\n\tList\u003Cstring\u003E _clientNames = new List\u003Cstring\u003E();\n\n\tconst int MaxPlayerRows = 64;   // = MaxPlayers\uFF08sbproj\uFF09\uFF0C\u6EDA\u52A8\u5217\u8868\u88C5\u5F97\u4E0B\u6EE1\u623F\n\n\t// \u6392\u7248\u9884\u89C8\uFF082026-09-09 \u7528\u6237\u8C03\u6837\u5F0F\u7528\uFF09\uFF1A\u5217\u8868\u663E\u793A\u4E00\u6279\u5047\u73A9\u5BB6\u540D\u3002\u5DF2\u8C03\u5B8C\u5173\u95ED\uFF1B\u4E0B\u6B21\u9884\u89C8\u518D\u6539 true\n\t// \uFF08static readonly \u4E0D\u7528 const\uFF1Aconst \u7684 true/false \u90FD\u4F1A\u628A\u5206\u652F\u5224\u6210\u4E0D\u53EF\u8FBE\u4EE3\u7801\uFF0C\u62A5 CS0162\uFF09\n\tstatic readonly bool ShowSamplePlayers = false;\n\tstatic readonly string[] SamplePlayers =\n\t{\n\t\t\u0022FLYBIRD\u0022, \u0022KITTEN_92\u0022, \u0022\u5C0F\uFFFD\uFFFD\uFFFD\u9E1F\u0022, \u0022ProEater\u0022, \u0022SNACKLORD\u0022, \u0022\u6C64\u5706\u0022, \u0022Wobble\u0022, \u0022XIAO_LONG\u0022,\n\t\t\u0022Mochi\u0022, \u0022\u5927\u7403\u738B\u0022, \u0022pixel_cat\u0022, \u0022Doodle\u0022, \u0022\u5E03\u4E01\u0022, \u0022GULP\u0022, \u0022\u6CE5\u9CC5\u0022, \u0022Bubbles\u0022,\n\t\t\u0022\u963F\u7FD4\u0022, \u0022Tofu\u0022, \u0022MUNCHER\u0022, \u0022\u4E91\u6735\u0022,\n\t};\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\t\tBuildUi();\n\t\tHide();\n\t}\n\n\tvoid BuildUi()\n\t{\n\t\tvar root = Panel;\n\t\tif ( root is null ) return;\n\t\tif ( _built \u0026\u0026 _status.IsValid() \u0026\u0026 root.ChildrenCount \u003E 0 ) return;\n\n\t\troot.DeleteChildren( true );\n\n\t\troot.Style.Position = PositionMode.Absolute;\n\t\troot.Style.Left = 0f;\n\t\troot.Style.Top = 0f;\n\t\troot.Style.Right = 0f;\n\t\troot.Style.Bottom = 0f;\n\t\troot.Style.BackgroundColor = new Color( 0.918f, 0.957f, 0.988f, 0.82f );   // \u6D45\u84DD\u78E8\u7802\uFF08\u50CF\u7D20\u98CE\u5B9A\u7A3F\uFF1B\u65E7\u6DF1\u7070\u906E\u7F69\u5728\u6D45\u8272\u4E16\u754C\u4E0A\u4E00\u5768\u810F\u7070\uFF0C\u7528\u6237\u5ACC\u4E11\uFF09\n\t\troot.Style.PointerEvents = PointerEvents.All;\n\n\t\tUiKit.AttachStyles( root );   // \u5171\u4EAB\u90E8\u4EF6\u6837\u5F0F\uFF08.cr-pill \u836F\u4E38\u6309\u94AE\uFF09\n\n\t\tvar title = new Label() { Classes = \u0022lp-title\u0022 };\n\t\ttitle.Text = \u0022GAME LOBBY\u0022;\n\t\troot.AddChild( title );\n\n\t\t_status = new Label() { Classes = \u0022lp-status\u0022 };\n\t\troot.AddChild( _status );\n\n\t\tvar settingsHead = new Label() { Classes = \u0022lp-head settings\u0022 };\n\t\tsettingsHead.Text = \u0022MATCH SETTINGS\u0022;\n\t\troot.AddChild( settingsHead );\n\n\t\t// \u8BBE\u7F6E\u724C\u7EB5\u6392\uFF08\u53C2\u8003\u56FE\uFF1A\u53F3\u680F\u4E09\u5F20\u6A2A\u724C\u4ECE\u4E0A\u5230\u4E0B\uFF09\uFF0Chost \u53EF\u70B9\u51FB\u5FAA\u73AF\uFF0Cclient \u53EA\u8BFB\u5C55\u793A\n\t\t_settings = new Label[3];\n\t\tfor ( int i = 0; i \u003C _settings.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar l = new Label() { Classes = \u0022lp-setting\u0022 };\n\t\t\tl.Style.Top = 330f \u002B i * 88f;   // \u2605 \u8BBE\u7F6E\u724C\u8D77\u70B9 \u002B \u884C\u8DDD\uFF08\u7CBE\u4FEE\u6539\u8FD9\u91CC\uFF09\n\t\t\tint index = i;\n\t\t\tl.AddEventListener( \u0022onclick\u0022, () =\u003E CycleSetting( index ) );\n\t\t\troot.AddChild( l );\n\t\t\t_settings[i] = l;\n\t\t}\n\n\t\t// \u8BBE\u7F6E\u53EF\u70B9\u63D0\u793A\uFF08v0.7.8.0\uFF09\uFF1A\u4EC5 host \u53EF\u89C1\u2014\u2014\u914D\u5408\u6A21\u5F0F\u884C\u547C\u5438\u91D1\u5149\uFF0C\u8BA9\u73A9\u5BB6\u77E5\u9053\u80FD\u6539\u6A21\u5F0F\n\t\t_settingHint = new Label() { Classes = \u0022lp-setting-hint\u0022 };\n\t\t_settingHint.Text = \u0022CLICK A SETTING TO CYCLE IT\u0022;\n\t\troot.AddChild( _settingHint );\n\n\t\t// \u9053\u5177\u56FE\u4F8B\u5DF2\u6309\u7528\u6237\u8981\u6C42\u6574\u4E2A\u79FB\u9664\uFF082026-09-09\uFF09\u2014\u2014Q/E \u7528\u6CD5\u5F00\u5C40\u540E HUD \u91CC\u8FD8\u6709\u63D0\u793A\n\n\t\tvar playersHead = new Label() { Classes = \u0022lp-head players\u0022 };\n\t\tplayersHead.Text = \u0022PLAYERS\u0022;\n\t\troot.AddChild( playersHead );\n\n\t\t// \u73A9\u5BB6\u5217\u8868\u5BB9\u5668\u5E95\uFF08dobou InventoryHotbar \u4E5D\u5BAB\u683C\uFF0C\u7528\u6237\u6307\u5B9A 2026-09-09\uFF09\uFF1A\n\t\t// \u5148\u4E8E\u884C\u724C\u521B\u5EFA\u538B\u5728\u5E95\u5C42\uFF1B\u7A7A\u5217\u8868\u65F6\u4E5F\u662F\u4E00\u4E2A\u6709\u6846\u7684\u6574\u6D01\u9762\u677F\uFF0C\u4E0D\u518D\u662F\u88F8\u7559\u767D\n\t\tvar playersBg = new Panel() { Classes = \u0022lp-players-bg\u0022 };\n\t\troot.AddChild( playersBg );\n\n\t\t// \u73A9\u5BB6\u5217\u8868\uFF1A\u5355\u5217\u6D41\u5185\u6392\u5217\u88C5\u8FDB\u539F\u751F\u6EDA\u52A8\u5BB9\u5668\uFF08overflow-y \u002B \u6EDA\u8F6E\uFF0C\u7F51\u9875\u5F0F\u6EDA\u52A8\u6761\uFF09\uFF0C\n\t\t// \u5BB9\u91CF\u63D0\u5230 64\uFF08= MaxPlayers\uFF09\uFF1Bhotbar \u5E95\u6846\u53EA\u662F\u88C5\u9970\uFF0C\u6EDA\u52A8\u53D1\u751F\u5728\u5B83\u4E0A\u9762\u7684\u900F\u660E\u5BB9\u5668\u91CC\n\t\t_playersScroll = new ScrollSoundPanel() { Classes = \u0022lp-players-scroll\u0022 };\n\t\troot.AddChild( _playersScroll );\n\n\t\t_playerRows = new Label[MaxPlayerRows];\n\t\tfor ( int i = 0; i \u003C _playerRows.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar row = new Label() { Classes = \u0022lp-player\u0022 };\n\t\t\t_playersScroll.AddChild( row );\n\t\t\t_playerRows[i] = row;\n\t\t}\n\n\t\t_botsNote = new Label() { Classes = \u0022lp-note\u0022 };\n\t\t_botsNote.Text = \u0022BOTS FILL REMAINING SLOTS AT START\u0022;\n\t\troot.AddChild( _botsNote );\n\n\t\t// \u52A0\u5165\u5012\u8BA1\u65F6\uFF08\u5BA2\u6237\u7AEF\uFF09\uFF1A\u6536\u5230 host \u5F00\u5C40\u6307\u4EE4\u540E\u663E\u793A 3-2-1\n\t\t_countdown = new Label() { Classes = \u0022lp-countdown\u0022 };\n\t\t_countdown.Style.Display = DisplayMode.None;\n\t\troot.AddChild( _countdown );\n\n\t\t// \u4E09\u4E2A\u6309\u94AE\u5168\u8D70 UiKit \u836F\u4E38\uFF08\u5916\u89C2\u5728 Assets/ui/cruikit.scss \u7684 .cr-pill\uFF09\uFF0C\u8FD9\u91CC\u53EA\u7BA1\u6446\u653E\u7C7B\n\t\t_join = UiKit.PillButton( root, \u0022JOIN GAME\u0022, \u0022lp-join\u0022, () =\u003E OnJoinClicked() );\n\n\t\t_start = UiKit.PillButton( root, \u0022START GAME\u0022, \u0022lp-start\u0022, () =\u003E OnStartClicked() );\n\n\t\t_leave = UiKit.PillButton( root, \u0022LEAVE\u0022, \u0022lp-leave\u0022, () =\u003E OnLeaveClicked() );\n\n\t\t_built = true;\n\t}\n\n\t/// \u003Csummary\u003E host \u8FDB\u5165\u623F\u95F4\uFF08HOST / VS BOTS \u5F00\u623F\u540E\u8C03\u7528\uFF09 \u003C/summary\u003E\n\tpublic void ShowHost()\n\t{\n\t\t_isHost = true;\n\t\tOpen();\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u8FDE\u5165\u540E\u8FDB\u5165\u623F\u95F4\uFF08\u7B49\u5F85\u623F\u4E3B\u5F00\u5C40\uFF1B\u5217\u8868\u7B49 LobbyState \u63A8\u9001\uFF09 \u003C/summary\u003E\n\tpublic void ShowClient()\n\t{\n\t\t_isHost = false;\n\t\tOpen();\n\t}\n\n\tvoid Open()\n\t{\n\t\tBuildUi();\n\t\tif ( !_built )\n\t\t{\n\t\t\t_retryOpen = true;   // Panel \u672A\u5C31\u7EEA\uFF08EnsureWorld \u7ACB\u5373\u8C03 ShowClient/ShowHost \u65F6\uFF09\uFF0COnUpdate \u91CD\u8BD5\n\t\t\treturn;\n\t\t}\n\n\t\t// \u9762\u677F\u88AB\u9500\u6BC1\u4F46 _built \u8FD8\u6B8B\u7559 true\uFF08\u4E16\u754C\u91CD\u5EFA/\u7EC4\u4EF6\u7981\u7528\u540E\uFF09\u2014\u2014\u91CD\u5EFA\uFF0C\u522B\u78B0 null Panel\uFF1A\n\t\t// \u6BCF\u5E27\u5F02\u5E38\u4F1A\u6F0F\u5F15\u64CE\u0022\u6BCF\u5E27\u5185\u5B58\u6808\u0022\uFF0C\u51E0\u5206\u949F\u540E\u5D29\u7F16\u8F91\u5668\uFF08\u5B9E\u6D4B\u6700\u8D35\u7684\u4E00\u8BFE\uFF09\n\t\tif ( Panel is null || !Panel.IsValid() )\n\t\t{\n\t\t\t_built = false;\n\t\t\t_retryOpen = true;\n\t\t\treturn;\n\t\t}\n\n\t\t_retryOpen = false;\n\t\tPanel.Style.Display = DisplayMode.Flex;\n\t\tif ( _shown )\n\t\t{\n\t\t\t// \u5DF2\u663E\u793A\uFF1A\u4E0D\u91CD\u7F6E\u72B6\u6001\uFF08ReconcileUi \u6BCF\u5E27\u8C03\uFF0CJOIN \u6309\u94AE/\u5012\u8BA1\u65F6\u4E0D\u80FD\u88AB\u6E05\u6389\uFF09\u3002\n\t\t\t// \u4F46\u72B6\u6001\u884C\u8981\u6062\u590D\u2014\u2014\u6389\u7EBF\u91CD\u8FDE\u6210\u529F\u56DE\u5230\u623F\u95F4\u65F6\uFF0C\u0022CONNECTION LOST\u0022 \u4E0D\u80FD\u4E00\u76F4\u6302\u7740\n\t\t\tif ( !_isHost ) RefreshStatus();\n\t\t\treturn;\n\t\t}\n\t\t_shown = true;\n\t\t_matchRunning = false;   // \u6BCF\u6B21\u8FDB\u623F\u95F4\u91CD\u7F6E\uFF08MatchStarted \u5230\u8FBE\u540E\u518D\u4EAE JOIN \u6309\u94AE\uFF09\n\t\tHideJoinCountdown();\n\t\t_sinceListRefresh = 1f;   // \u9996\u5E27\u7ACB\u5373\u5237\n\t\tRefreshAll();\n\t}\n\n\t/// \u003Csummary\u003E \u6536\u8D77\uFF08\u5F00\u5C40 / \u56DE\u83DC\u5355\uFF09 \u003C/summary\u003E\n\tpublic void Hide()\n\t{\n\t\t_shown = false;\n\t\t_retryOpen = false;\n\t\tif ( Panel.IsValid() ) Panel.Style.Display = DisplayMode.None;\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u5E94\u7528\u623F\u4E3B\u63A8\u9001\u7684\u623F\u95F4\u4FE1\u606F\uFF08CircleroyaleGame.OnLobbyStateRemote \u8F6C\u53D1\uFF09 \u003C/summary\u003E\n\tpublic void UpdateFromHost( byte mode, int duration, int players, string[] names )\n\t{\n\t\t_connectionLost = false;   // host \u6709\u6D88\u606F = \u8FDE\u63A5\u6D3B\u7740\uFF0C\u89E3\u9664\u6389\u7EBF\u63D0\u793A\n\t\t_clientMode = mode;\n\t\t_clientDuration = duration;\n\t\t_clientPlayers = players;\n\t\t_clientNames = names is not null ? new List\u003Cstring\u003E( names ) : new List\u003Cstring\u003E();\n\n\t\tif ( _shown \u0026\u0026 !_isHost )\n\t\t{\n\t\t\tRefreshSettings();\n\t\t\tRefreshPlayers();\n\t\t\tRefreshButtons();   // \u6389\u7EBF\u6062\u590D\u540E\u628A\u5E38\u9A7B\u6309\u94AE\u6446\u56DE\u6765\uFF08WAITING/JOIN \u6587\u6848\u4E5F\u9760\u5B83\u5237\uFF09\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E \u663E\u793A\u52A0\u5165\u5012\u8BA1\u65F6\uFF08CircleroyaleGame \u6BCF\u5E27\u8C03\u7528\uFF09 \u003C/summary\u003E\n\tpublic void ShowJoinCountdown( float secondsLeft )\n\t{\n\t\tif ( _countdown is null || !_countdown.IsValid() ) return;\n\n\t\t_countdown.Style.Display = DisplayMode.Flex;\n\t\t_countdown.Text = $\u0022JOINING MATCH IN {MathF.Ceiling( MathF.Max( 0f, secondsLeft ) ):0}\u0022;\n\n\t\t// \u5012\u8BA1\u65F6\u671F\u95F4\u6536\u8D77 JOIN \u6309\u94AE\n\t\tif ( _join.IsValid() ) _join.Style.Display = DisplayMode.None;\n\t}\n\n\t/// \u003Csummary\u003E \u6536\u8D77\u52A0\u5165\u5012\u8BA1\u65F6 \u003C/summary\u003E\n\tpublic void HideJoinCountdown()\n\t{\n\t\tif ( _countdown.IsValid() ) _countdown.Style.Display = DisplayMode.None;\n\t}\n\n\t/// \u003Csummary\u003E \u5BA2\u6237\u7AEF\u89C6\u89D2\uFF1Ahost \u7684\u5BF9\u5C40\u8FDB\u884C\u4E2D\uFF08\u623F\u95F4\u9875\u4EAE\u8D77 JOIN GAME \u6309\u94AE\uFF09 \u003C/summary\u003E\n\tpublic void SetMatchRunning()\n\t{\n\t\t_matchRunning = true;\n\t\tRefreshButtons();\n\n\t\tSetText( _status, \u0022MATCH IN PROGRESS \u2014 JOIN NOW\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E \u6587\u672C\u9632\u6296\uFF08v0.7.1.2\uFF09\uFF1A\u503C\u6CA1\u53D8\u5C31\u4E0D\u8D4B\u2014\u2014Label.Text \u6BCF\u5E27\u91CD\u8BBE\uFF08\u54EA\u6015\u540C\u503C\uFF09\u4F1A\u89E6\u53D1\u5E03\u5C40\n\t/// \u91CD\u7B97\uFF1BReconcileUi \u6BCF\u5E27\u8C03 ShowClient\u2192RefreshStatus \u628A\u72B6\u6001\u884C\u53CD\u590D\u91CD\u5237 = UI \u4E00\u95EA\u4E00\u95EA\uFF08\u5B9E\u6D4B\uFF09 \u003C/summary\u003E\n\tstatic void SetText( Label l, string text )\n\t{\n\t\tif ( !l.IsValid() || l.Text == text ) return;\n\t\tl.Text = text;\n\t}\n\n\t/// \u003Csummary\u003E \u6389\u7EBF\u81EA\u52A8\u91CD\u8FDE\u4E2D\uFF08CircleroyaleGame.OnDisconnectRecoveryStarted\uFF09\uFF1A\u72B6\u6001\u884C\u63D0\u793A \u002B \u6536\u6389 JOIN \u6309\u94AE \u003C/summary\u003E\n\tpublic void SetConnectionLost()\n\t{\n\t\t_connectionLost = true;\n\t\tSetText( _status, \u0022CONNECTION LOST \u2014 RECONNECTING ...\u0022 );\n\t\tif ( _join.IsValid() ) _join.Style.Display = DisplayMode.None;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tbase.OnUpdate();\n\n\t\tBuildUi();\n\n\t\t// EnsureWorld \u5148\u4E8E OnStart \u8C03\u8FC7 ShowClient/ShowHost \u4E14 Panel \u672A\u5C31\u7EEA \u2192 \u6B64\u523B\u8865\u5F00\n\t\tif ( _retryOpen \u0026\u0026 !_shown )\n\t\t{\n\t\t\tOpen();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !_shown ) return;\n\n\t\tif ( Panel.IsValid() \u0026\u0026 Panel.Style.Display != DisplayMode.Flex ) Panel.Style.Display = DisplayMode.Flex;   // \u540C\u503C\u5B88\u536B\uFF08v0.7.8.87\uFF09\n\t\tMouse.Visibility = MouseVisibility.Visible;   // \u6309\u94AE/\u8BBE\u7F6E\u9879\u8981\u80FD\u70B9\n\n\t\t// host\uFF1A\u73A9\u5BB6\u8FDB\u51FA\u3001\u8BBE\u7F6E\u53D8\u5316\u672C\u5730\u5C31\u80FD\u770B\u5230\uFF0C0.5s \u8F6E\u5237\uFF1Bclient \u5217\u8868\u9760 LobbyState \u63A8\u9001\n\t\tif ( _isHost \u0026\u0026 _sinceListRefresh \u003E 0.5f )\n\t\t{\n\t\t\t_sinceListRefresh = 0;\n\t\t\tRefreshPlayers();\n\t\t\tRefreshStatus();\n\t\t}\n\n\t\t// \u6A21\u5F0F\u884C\u547C\u5438\u91D1\u5149\uFF08v0.7.8.0\uFF0C\u4EC5 host\uFF09\uFF1A\u8BBE\u7F6E\u884C\u662F\u53EF\u70B9\u7684\uFF0C\u7528\u5149\u6548\u5F15\u5BFC\u73A9\u5BB6\u53BB\u6539\u6A21\u5F0F\u3002\n\t\t// \u989C\u8272\u52A8\u753B\u6BCF\u5E27\u5728\u53D8\uFF08Sin \u8FDE\u7EED\uFF09\uFF0C\u4E0D\u78B0 Text \u4E0D\u89E6\u53D1\u5E03\u5C40\u91CD\u7B97\uFF1B\u5BA2\u6237\u7AEF\u6062\u590D\u9759\u6001\u57FA\u8272\n\t\tif ( _settings is not null \u0026\u0026 _settings[0].IsValid() )\n\t\t{\n\t\t\tif ( _isHost )\n\t\t\t{\n\t\t\t\tvar glow = 0.72f \u002B 0.28f * MathF.Sin( Time.Now * 3.2f );\n\t\t\t\t_settings[0].Style.FontColor = new Color( 1f * glow, 0.76f * glow, 0.30f * glow );\n\t\t\t\t_modeGlowActive = true;\n\t\t\t}\n\t\t\telse if ( _modeGlowActive )\n\t\t\t{\n\t\t\t\t_settings[0].Style.FontColor = new Color( 0.29f, 0.23f, 0.39f );   // scss .lp-setting \u57FA\u8272\uFF08\u6885\u7D2B #4a3b63\uFF09\n\t\t\t\t_modeGlowActive = false;\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid RefreshAll()\n\t{\n\t\tRefreshSettings();\n\t\tRefreshPlayers();\n\t\tRefreshStatus();\n\t\tRefreshButtons();\n\t}\n\n\tvoid RefreshStatus()\n\t{\n\t\tif ( _isHost )\n\t\t{\n\t\t\tvar names = CircleroyaleGame.Current?.LobbyPlayerNames();\n\t\t\tint n = names?.Count ?? 1;\n\t\t\tSetText( _status, $\u0022ROOM OPEN \u2014 {n} PLAYER{( n \u003E 1 ? \u0022S\u0022 : \u0022\u0022 )} IN LOBBY\u0022 );\n\t\t}\n\t\telse if ( _connectionLost )\n\t\t{\n\t\t\tSetText( _status, \u0022CONNECTION LOST \u2014 RECONNECTING ...\u0022 );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tSetText( _status, _matchRunning ? \u0022MATCH IN PROGRESS \u2014 JOIN NOW\u0022 : \u0022WAITING FOR HOST TO START ...\u0022 );\n\t\t}\n\t}\n\n\tvoid RefreshSettings()\n\t{\n\t\tif ( _settings is null || _settings[0] is null || !_settings[0].IsValid() ) return;\n\n\t\tbyte mode;\n\t\tfloat duration;\n\t\tint players;\n\n\t\tif ( _isHost )\n\t\t{\n\t\t\tmode = (byte)MatchState.PendingMode;\n\t\t\tduration = MatchState.PendingDuration;\n\t\t\tplayers = MatchState.PendingPlayerTarget;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tmode = _clientMode;\n\t\t\tduration = _clientDuration;\n\t\t\tplayers = _clientPlayers;\n\t\t}\n\n\t\tSetText( _settings[0], (MatchState.Mode)mode switch\n\t\t{\n\t\t\tMatchState.Mode.Ffa =\u003E \u0022MODE \u2014 FREE FOR ALL\u0022,\n\t\t\tMatchState.Mode.Team2 =\u003E \u0022MODE \u2014 TEAM (2 / TEAM)\u0022,\n\t\t\tMatchState.Mode.Team3 =\u003E \u0022MODE \u2014 TEAM (3 / TEAM)\u0022,\n\t\t\tMatchState.Mode.Territory =\u003E \u0022MODE \u2014 TERRITORY WAR\u0022,\n\t\t\t_ =\u003E \u0022MODE \u2014 TEAM (4 / TEAM)\u0022,\n\t\t} );\n\n\t\tSetText( _settings[1], duration \u003C 60f\n\t\t\t? $\u0022TIME \u2014 {duration:0} SEC\u0022\n\t\t\t: $\u0022TIME \u2014 {duration / 60f:0} MIN\u0022 );\n\n\t\t// \u9886\u571F\u6A21\u5F0F\u4EBA\u6570\u56FA\u5B9A 4 \u961F \u00D7 4 \u4EBA\uFF08host Apply \u5F3A\u5236 16\uFF09\uFF0C\u83DC\u5355\u9009\u7684 PLAYERS \u65E0\u6548\u2014\u2014\u660E\u793A\u907F\u514D\u56F0\u60D1\n\t\tSetText( _settings[2], (MatchState.Mode)mode == MatchState.Mode.Territory ? \u0022PLAYERS \u2014 16 (4 TEAMS)\u0022 : $\u0022PLAYERS \u2014 {players}\u0022 );\n\t}\n\n\tvoid RefreshPlayers()\n\t{\n\t\tif ( _playerRows is null || _playerRows[0] is null || !_playerRows[0].IsValid() ) return;\n\n\t\tList\u003Cstring\u003E names;\n\t\tif ( _isHost )\n\t\t{\n\t\t\tnames = CircleroyaleGame.Current?.LobbyPlayerNames() ?? new List\u003Cstring\u003E();\n\t\t}\n\t\telse\n\t\t{\n\t\t\tnames = _clientNames;\n\t\t}\n\n\t\tif ( ShowSamplePlayers )\n\t\t\tnames = new List\u003Cstring\u003E( SamplePlayers );   // \u6392\u7248\u9884\u89C8\uFF1A\u5047\u540D\u66FF\u4EE3\u771F\u5B9E\u5217\u8868\uFF08\u770B\u5B8C\u5173\u6389\uFF09\n\n\t\tif ( names.Count == 0 ) names = new List\u003Cstring\u003E { \u0022...\u0022 };\n\n\t\tfor ( int i = 0; i \u003C _playerRows.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar row = _playerRows[i];\n\t\t\tif ( !row.IsValid() ) continue;\n\n\t\t\tif ( i \u003E= names.Count )\n\t\t\t{\n\t\t\t\tSetText( row, \u0022\u0022 );\n\t\t\t\tGameHud.SetDisplay( row, DisplayMode.None );   // \u7A7A\u4F4D\u4E0D\u753B\u836F\u7247\u6761\uFF08\u6EE1\u5C4F\u7A7A\u6761\u5F88\u96BE\u770B\uFF0C\u5B9E\u6D4B\u622A\u56FE\uFF09\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tGameHud.SetDisplay( row, DisplayMode.Flex );\n\t\t\tvar tag = i == 0 ? \u0022  (HOST)\u0022 : \u0022\u0022;\n\t\t\tSetText( row, $\u0022{i \u002B 1}.  {names[i]}{tag}\u0022 );\n\t\t\t// \u540C\u503C\u5B88\u536B\uFF1A\u5BA2\u6237\u7AEF\u6BCF\u6536\u5230\u4E00\u6B21 LobbyState \u5FC3\u8DF3\uFF083s\uFF09\u5C31\u5168\u91CF\u91CD\u5237\u672C\u5217\u8868\uFF0C\n\t\t\t// \u88F8\u5199 Display/FontColor = \u6BCF 3 \u79D2 64 \u884C\u5E03\u5C40\u91CD\u7B97 = \u5BA2\u6237\u7AEF\u5927\u5385\u5468\u671F\u6027\u7206\u95EA\uFF08\u5B9E\u6D4B\uFF09\n\t\t\tvar c = i == 0 ? new Color( 0.898f, 0.282f, 0.553f ) : new Color( 0.29f, 0.23f, 0.39f, 0.85f );\n\t\t\tif ( row.Style.FontColor != c ) row.Style.FontColor = c;\n\t\t}\n\t}\n\n\tvoid RefreshButtons()\n\t{\n\t\t// host \u6709 START\uFF1B\u5BA2\u6237\u7AEF\u7B2C\u4E8C\u9897\u6309\u94AE\u5E38\u9A7B\uFF08\u7528\u6237\u9700\u6C42 2026-09-09\uFF09\uFF1A\u672A\u5F00\u5C40\u663E\u793A WAITING FOR HOST\n\t\t// \uFF08\u70B9\u4E86\u4E0D\u54CD\u5E94\uFF09\uFF0C\u5F00\u5C40\u4EAE\u6210 JOIN GAME\u3002LEAVE \u4E24\u8FB9\u90FD\u6709\u3002Display \u540C\u503C\u5B88\u536B\uFF08\u968F RefreshAll \u6BCF 3s/0.5s \u5168\u91CF\u91CD\u5237\uFF09\n\t\tif ( _start.IsValid() )\n\t\t\tGameHud.SetDisplay( _start, _isHost ? DisplayMode.Flex : DisplayMode.None );\n\n\t\tif ( _join.IsValid() )\n\t\t{\n\t\t\tGameHud.SetDisplay( _join, !_isHost ? DisplayMode.Flex : DisplayMode.None );\n\t\t\tSetText( _join, _matchRunning ? \u0022JOIN GAME\u0022 : \u0022WAITING FOR HOST\u0022 );\n\t\t}\n\n\t\t// \u8BBE\u7F6E\u53EF\u70B9\u63D0\u793A\uFF08v0.7.8.0\uFF09\uFF1A\u53EA\u6709 host \u80FD\u6539\u8BBE\u7F6E\uFF0C\u63D0\u793A\u4E5F\u53EA\u7ED9 host\n\t\tif ( _settingHint.IsValid() )\n\t\t\tGameHud.SetDisplay( _settingHint, _isHost ? DisplayMode.Flex : DisplayMode.None );\n\t}\n\n\t/// \u003Csummary\u003E JOIN GAME \u70B9\u51FB\uFF08\u5B9E\u4F8B\u65B9\u6CD5\uFF1A\u70ED\u91CD\u8F7D\u53EF\u91CD\u6620\u5C04\uFF09\u3002WAITING FOR HOST \u72B6\u6001\u70B9\u4E86\u4E0D\u54CD\u5E94 \u003C/summary\u003E\n\tvoid OnJoinClicked()\n\t{\n\t\tif ( !_matchRunning ) return;\n\t\tCircleroyaleGame.Current?.ClientJoinMatch();\n\t}\n\n\t/// \u003Csummary\u003E START \u70B9\u51FB \u003C/summary\u003E\n\tvoid OnStartClicked() =\u003E CircleroyaleGame.Current?.StartMatchFromLobby();\n\n\t/// \u003Csummary\u003E LEAVE \u70B9\u51FB \u003C/summary\u003E\n\tvoid OnLeaveClicked() =\u003E CircleroyaleGame.Current?.ResetToMenu();\n\n\t// ---- host \u8C03\u53C2\uFF08\u4E0E\u4E3B\u83DC\u5355\u65F6\u4EE3\u7684\u5FAA\u73AF\u903B\u8F91\u4E00\u81F4\uFF09----\n\n\tvoid CycleSetting( int index )\n\t{\n\t\tif ( !_isHost ) return;\n\n\t\tGameSfx.UiClick();   // \u70B9\u4E86\u771F\u4F1A\u6539\u53C2\u6570\u624D\u54CD\uFF08client \u53EA\u8BFB\uFF0C\u70B9\u4E86\u4E0D\u54CD\uFF09\n\n\t\tswitch ( index )\n\t\t{\n\t\t\tcase 0:\n\t\t\t\t// \u6A21\u5F0F 5 \u6863\u5FAA\u73AF\uFF08v0.7.8.34\uFF09\uFF1A\u666E\u901A\u8D5B \u2192 2 \u4EBA\u961F \u2192 3 \u4EBA\u961F \u2192 4 \u4EBA\u961F \u2192 \u9886\u571F\u6218\u4E89\n\t\t\t\tMatchState.PendingMode = (MatchState.Mode) ( ( (int)MatchState.PendingMode \u002B 1 ) % 5 );\n\t\t\t\tbreak;\n\n\t\t\tcase 1:\n\t\t\t\tMatchState.PendingDuration = NextChoice( GameConfig.MatchDurationChoices, MatchState.PendingDuration );\n\t\t\t\tbreak;\n\n\t\t\tcase 2:\n\t\t\t\tMatchState.PendingPlayerTarget = NextIntChoice( GameConfig.PlayerTargetChoices, MatchState.PendingPlayerTarget );\n\t\t\t\tbreak;\n\t\t}\n\n\t\tRefreshSettings();\n\t\tCircleroyaleGame.Current?.MarkLobbyDirty();   // \u8BA9\u7B49\u5F85\u4E2D\u7684\u5BA2\u6237\u7AEF\u770B\u5230\u65B0\u53C2\u6570\n\t}\n\n\tstatic float NextChoice( float[] choices, float current )\n\t{\n\t\tfor ( int i = 0; i \u003C choices.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( MathF.Abs( choices[i] - current ) \u003C 0.5f )\n\t\t\t\treturn choices[( i \u002B 1 ) % choices.Length];\n\t\t}\n\t\treturn choices[0];\n\t}\n\n\tstatic int NextIntChoice( int[] choices, int current )\n\t{\n\t\tfor ( int i = 0; i \u003C choices.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( choices[i] == current )\n\t\t\t\treturn choices[( i \u002B 1 ) % choices.Length];\n\t\t}\n\t\treturn choices[0];\n\t}\n}\n"}]}