the browser-facing portion of osu!
at master 180 lines 6.3 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Models\Traits; 7 8trait Scoreable 9{ 10 protected $_enabledMods = null; 11 12 private float $_accuracy; 13 private int $_totalHits; 14 15 abstract public function getMode(): string; 16 17 public function getScoringType() 18 { 19 return 'score'; 20 } 21 22 public function getEnabledModsAttribute($value) 23 { 24 return $this->_enabledMods ??= app('mods')->bitsetToIds($value); 25 } 26 27 public function totalHits(): int 28 { 29 return $this->_totalHits ??= match ($this->getMode()) { 30 'osu' => 31 ($this->count50 + $this->count100 + $this->count300 + $this->countmiss) * 300, 32 'fruits' => 33 $this->count50 + $this->count100 + $this->count300 + $this->countmiss + $this->countkatu, 34 'mania' => ( 35 $this->getScoringType() === 'scorev2' 36 ? ($this->count50 + $this->count100 + $this->count300 + $this->countmiss + $this->countkatu + $this->countgeki) * 305 37 : ($this->count50 + $this->count100 + $this->count300 + $this->countmiss + $this->countkatu + $this->countgeki) * 300 38 ), 39 'taiko' => 40 ($this->count100 + $this->count300 + $this->countmiss) * 300, 41 }; 42 } 43 44 public function hits() 45 { 46 switch ($this->getMode()) { 47 case 'osu': 48 return $this->count50 * 50 + $this->count100 * 100 + $this->count300 * 300; 49 case 'fruits': 50 return $this->count50 + $this->count100 + $this->count300; 51 case 'mania': 52 if ($this->getScoringType() === 'scorev2') { 53 return $this->count50 * 50 + $this->count100 * 100 + $this->countkatu * 200 + $this->count300 * 300 + $this->countgeki * 305; 54 } else { 55 return $this->count50 * 50 + $this->count100 * 100 + $this->countkatu * 200 + ($this->count300 + $this->countgeki) * 300; 56 } 57 case 'taiko': 58 return $this->count100 * 150 + $this->count300 * 300; 59 } 60 } 61 62 public function accuracy(): float 63 { 64 if (!isset($this->_accuracy)) { 65 $totalHits = $this->totalHits(); 66 67 // in a rare case when the score row has zero hits 68 // (found it occuring in multiplayer scores) 69 $this->_accuracy = $totalHits === 0 ? 1 : $this->hits() / $totalHits; 70 } 71 72 return $this->_accuracy; 73 } 74 75 public function recalculateRank(): void 76 { 77 if (!$this->pass) { 78 $this->rank = 'F'; 79 80 return; 81 } 82 83 $totalHits = $this->totalHits(); 84 $accuracy = $this->accuracy(); 85 $countMiss = $this->countmiss; 86 87 switch ($this->getMode()) { 88 case 'osu': 89 $totalHitCount = $totalHits / 300; 90 $ratio300 = (float) ($totalHits === 0 ? 1 : $this->count300 / $totalHitCount); 91 $ratio50 = (float) ($totalHits === 0 ? 1 : $this->count50 / $totalHitCount); 92 93 $this->rank = match (true) { 94 $ratio300 === 1.0 => 95 $this->shouldHaveHiddenRank() ? 'XH' : 'X', 96 $ratio300 > 0.9 && $ratio50 <= 0.01 && $countMiss === 0 => 97 $this->shouldHaveHiddenRank() ? 'SH' : 'S', 98 ($ratio300 > 0.8 && $countMiss === 0) || $ratio300 > 0.9 => 99 'A', 100 ($ratio300 > 0.7 && $countMiss === 0) || $ratio300 > 0.8 => 101 'B', 102 $ratio300 > 0.6 => 103 'C', 104 default => 105 'D', 106 }; 107 108 break; 109 110 case 'taiko': 111 $totalHitCount = $totalHits / 300; 112 $ratio300 = (float) ($totalHits === 0 ? 1 : $this->count300 / $totalHitCount); 113 $ratio50 = (float) ($totalHits === 0 ? 1 : $this->count50 / $totalHitCount); 114 115 $this->rank = match (true) { 116 $ratio300 === 1.0 => 117 $this->shouldHaveHiddenRank() ? 'XH' : 'X', 118 $ratio300 > 0.9 && $ratio50 < 0.01 && $countMiss === 0 => 119 $this->shouldHaveHiddenRank() ? 'SH' : 'S', 120 ($ratio300 > 0.8 && $countMiss === 0) || $ratio300 > 0.9 => 121 'A', 122 ($ratio300 > 0.7 && $countMiss === 0) || $ratio300 > 0.8 => 123 'B', 124 $ratio300 > 0.6 => 125 'C', 126 default => 127 'D', 128 }; 129 130 break; 131 132 case 'fruits': 133 $this->rank = match (true) { 134 $accuracy === 1.0 => 135 $this->shouldHaveHiddenRank() ? 'XH' : 'X', 136 $accuracy > 0.98 => 137 $this->shouldHaveHiddenRank() ? 'SH' : 'S', 138 $accuracy > 0.94 => 139 'A', 140 $accuracy > 0.9 => 141 'B', 142 $accuracy > 0.85 => 143 'C', 144 default => 145 'D', 146 }; 147 148 break; 149 150 case 'mania': 151 $this->rank = match (true) { 152 $accuracy === 1.0 => 153 $this->shouldHaveHiddenRank() ? 'XH' : 'X', 154 $accuracy > 0.95 => 155 $this->shouldHaveHiddenRank() ? 'SH' : 'S', 156 $accuracy > 0.9 => 157 'A', 158 $accuracy > 0.8 => 159 'B', 160 $accuracy > 0.7 => 161 'C', 162 default => 163 'D', 164 }; 165 166 break; 167 } 168 } 169 170 private function shouldHaveHiddenRank(): bool 171 { 172 foreach ($this->enabled_mods as $mod) { 173 if ($mod === 'FI' || $mod === 'FL' || $mod === 'HD') { 174 return true; 175 } 176 } 177 178 return false; 179 } 180}