Lines
100.00%
140 / 140
Functions and Methods
100.00%
5 / 5
Classes and Traits
100.00%
1 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| MedalChecker | 100.00% | 140 / 140 | 100.00% | 5 / 5 | 16 | 100.00% | 1 / 1 | |||
| medals | 100.00% | 29 / 29 | 100.00% | 1 / 1 | 1 | |||||
| customMedals | 100.00% | 85 / 85 | 100.00% | 1 / 1 | 1 | |||||
| medalLevels | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 4 | |||||
| coreBadges | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 4 | |||||
| customMedalTiers | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 6 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace App\Medal; | |
| 6 | ||
| 7 | use App\Entity\AgentStat; | |
| 8 | ||
| 9 | /** | |
| 10 | * The medals of agent stats and the custom medals, as 4e-website's MedalChecker. Keep the lists in | |
| 11 | * sync with the app's data/Medals.kt and data/CustomMedals.kt; the images are the app's | |
| 12 | * (asset_mapper.yaml: badges/, custom_medals/). | |
| 13 | */ | |
| 14 | final class MedalChecker | |
| 15 | { | |
| 16 | /** The custom medal groups in 4e's order. */ | |
| 17 | public const array CUSTOM_MEDAL_GROUPS = ['anomaly', 'annual', 'event']; | |
| 18 | ||
| 19 | /** C.O.R.E subscription badges by the months they need (0: the plain C.O.R.E badge). */ | |
| 20 | public const array CORE_BADGES = [ | |
| 21 | 0 => 'badges/unique_badge_core.webp', | |
| 22 | 24 => 'badges/unique_badge_dual_core.webp', | |
| 23 | 36 => 'badges/unique_badge_core_year3.webp', | |
| 24 | 48 => 'badges/unique_badge_quad_core.webp', | |
| 25 | 60 => 'badges/unique_badge_penta_core.webp', | |
| 26 | ]; | |
| 27 | ||
| 28 | /** | |
| 29 | * The medals 4e-website shows for an agent, in its order. | |
| 30 | * | |
| 31 | * @return list<Medal> | |
| 32 | */ | |
| 33 | public function medals(): array | |
| 34 | { | |
| 35 | return [ | |
| 36 | new Medal('explorer', 'Explorer', 'Unique Portals Visited', [100, 1000, 2000, 10000, 30000]), | |
| 37 | new Medal('seer', 'Seer', 'Seer Points', [10, 50, 200, 500, 5000], discontinued: true), | |
| 38 | new Medal('recon', 'Recon', 'OPR Agreements', [100, 750, 2500, 5000, 10000]), | |
| 39 | new Medal('trekker', 'Trekker', 'Distance Walked', [10, 100, 300, 1000, 2500]), | |
| 40 | new Medal('builder', 'Builder', 'Resonators Deployed', [2000, 10000, 30000, 100000, 200000]), | |
| 41 | new Medal('connector', 'Connector', 'Links Created', [50, 1000, 5000, 25000, 100000]), | |
| 42 | new Medal('mind_controller', 'Mind Controller', 'Control Fields Created', [100, 500, 2000, 10000, 40000]), | |
| 43 | new Medal('engineer', 'Engineer', 'Mods Deployed', [150, 1500, 5000, 20000, 50000]), | |
| 44 | new Medal('illuminator', 'Illuminator', 'Mind Units Captured', [5000, 50000, 250000, 1000000, 4000000]), | |
| 45 | new Medal('recharger', 'Recharger', 'XM Recharged', [100000, 1000000, 3000000, 10000000, 25000000]), | |
| 46 | new Medal('liberator', 'Liberator', 'Portals Captured', [100, 1000, 5000, 15000, 40000]), | |
| 47 | new Medal('pioneer', 'Pioneer', 'Unique Portals Captured', [20, 200, 1000, 5000, 20000]), | |
| 48 | new Medal('purifier', 'Purifier', 'Resonators Destroyed', [2000, 10000, 30000, 100000, 300000]), | |
| 49 | new Medal('specops', 'SpecOps', 'Unique Missions Completed', [5, 25, 100, 200, 500]), | |
| 50 | new Medal('missionday', 'Mission Day', 'Mission Day(s) Attended', [1, 3, 6, 10, 20]), | |
| 51 | new Medal('nl1331_meetups', 'NL-1331 Meetups', 'NL-1331 Meetup(s) Attended', [1, 5, 10, 25, 50]), | |
| 52 | new Medal('hacker', 'Hacker', 'Hacks', [2000, 10000, 30000, 100000, 200000]), | |
| 53 | new Medal('translator', 'Translator', 'Glyph Hack Points', [200, 2000, 6000, 20000, 50000]), | |
| 54 | new Medal('sojourner', 'Sojourner', 'Longest Hacking Streak', [15, 30, 60, 180, 360]), | |
| 55 | new Medal('epoch', 'Epoch', 'Complete Hacking Streak', [2, 4, 8, 30, 60]), | |
| 56 | new Medal('recruiter', 'Recruiter', 'Agents successfully recruited', [2, 10, 25, 50, 100]), | |
| 57 | new Medal('ifs', 'First Saturday', 'First Saturday Events', [1, 6, 12, 24, 36]), | |
| 58 | new Medal('second_sunday', 'Second Sunday', 'Second Sunday Events', [1, 6, 12, 24, 36]), | |
| 59 | new Medal('scout', 'Scout', 'Scan portals', [50, 250, 1000, 3000, 6000]), | |
| 60 | new Medal('scout_controller', 'Scout Controller', 'Scan unique portals', [100, 500, 1000, 5000, 15000]), | |
| 61 | new Medal('maverick', 'Maverick', 'Drone hack portals', [250, 1000, 2000, 5000, 10000]), | |
| 62 | new Medal('reclaimer', 'Reclaimer', 'Reclaim portals from Machina ingression', [100, 1000, 5000, 15000, 40000]), | |
| 63 | ]; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * 4e-website's custom medals in its order, with the titles from its badgeinfos.json. | |
| 68 | * | |
| 69 | * @return list<CustomMedal> | |
| 70 | */ | |
| 71 | public function customMedals(): array | |
| 72 | { | |
| 73 | return [ | |
| 74 | new CustomMedal('anomaly', 'apollo_onsite', 'Apollo (On-site)'), | |
| 75 | new CustomMedal('anomaly', 'apollo', 'Apollo', ['bronze', 'silver', 'gold']), | |
| 76 | new CustomMedal('anomaly', 'orion_onsite', 'Orion (On-site)'), | |
| 77 | new CustomMedal('anomaly', 'orion', 'Orion', ['bronze', 'silver', 'gold', 'platinum']), | |
| 78 | new CustomMedal('anomaly', 'plus_gamma_onsite', '+Gamma (On-site)'), | |
| 79 | new CustomMedal('anomaly', 'plus_gamma', '+Gamma', ['bronze', 'silver', 'gold']), | |
| 80 | new CustomMedal('anomaly', 'plus_beta', '+Beta', ['bronze', 'silver', 'gold']), | |
| 81 | new CustomMedal('anomaly', 'plus_delta', '+Delta', ['bronze', 'silver', 'gold']), | |
| 82 | new CustomMedal('anomaly', 'plus_theta', '+Theta', ['bronze', 'silver', 'gold']), | |
| 83 | new CustomMedal('anomaly', 'plus_alpha', '+Alpha'), | |
| 84 | new CustomMedal('anomaly', 'erased_memories', 'Erased Memories'), | |
| 85 | new CustomMedal('anomaly', 'buried_memories', 'Buried Memories'), | |
| 86 | new CustomMedal('anomaly', 'cryptic_memories', 'Cryptic Memories'), | |
| 87 | new CustomMedal('anomaly', 'discoverie', 'Discoverie'), | |
| 88 | new CustomMedal('anomaly', 'ctrl', 'Ctrl'), | |
| 89 | new CustomMedal('anomaly', 'echo', 'Echo'), | |
| 90 | new CustomMedal('anomaly', 'mzfpk', 'MZFPK'), | |
| 91 | new CustomMedal('anomaly', 'epiphany_dawn', 'Epiphany Dawn'), | |
| 92 | new CustomMedal('anomaly', 'superposition', 'Superposition'), | |
| 93 | new CustomMedal('anomaly', 'kythera', 'Kythera'), | |
| 94 | new CustomMedal('anomaly', 'kureze_effect', 'Kureze Effect'), | |
| 95 | new CustomMedal('anomaly', 'umbra', 'Nemesis: Umbra'), | |
| 96 | new CustomMedal('anomaly', 'nemesis_myriad', 'Nemesis: Myriad'), | |
| 97 | new CustomMedal('anomaly', 'abaddon_prime', 'Abaddon Prime'), | |
| 98 | new CustomMedal('anomaly', 'darsana_prime', 'Darsana Prime'), | |
| 99 | new CustomMedal('anomaly', 'recursion_prime', 'Recursion Prime'), | |
| 100 | new CustomMedal('anomaly', 'cassandra_prime', 'Cassandra Prime'), | |
| 101 | new CustomMedal('anomaly', 'exo5', 'EXO5'), | |
| 102 | new CustomMedal('anomaly', '13_magnusreawakens', '13MAGNUS Reawakens'), | |
| 103 | new CustomMedal('anomaly', 'via_noir', 'Via Noir'), | |
| 104 | new CustomMedal('anomaly', 'via_lux', 'Via Lux'), | |
| 105 | new CustomMedal('anomaly', 'aegis_nova', 'Aegis Nova'), | |
| 106 | new CustomMedal('anomaly', 'obsidian', 'Obsidian'), | |
| 107 | new CustomMedal('anomaly', 'abaddon', 'Abaddon'), | |
| 108 | new CustomMedal('anomaly', 'persepolis', 'Persepolis'), | |
| 109 | new CustomMedal('anomaly', 'shonin', 'Shonin'), | |
| 110 | new CustomMedal('anomaly', 'darsana', 'Darsana'), | |
| 111 | new CustomMedal('anomaly', 'helios', 'Helios'), | |
| 112 | new CustomMedal('anomaly', 'initio', 'Initio'), | |
| 113 | new CustomMedal('anomaly', 'interitus', 'Interitus'), | |
| 114 | new CustomMedal('anomaly', 'recursion', 'Recursion'), | |
| 115 | new CustomMedal('annual', 'voyager', 'Voyager', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 116 | new CustomMedal('annual', 'catalyst', 'Catalyst', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 117 | new CustomMedal('annual', 'paragon', 'Paragon', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 118 | new CustomMedal('annual', 'pathfinder', 'Pathfinder', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 119 | new CustomMedal('annual', 'persistence', 'Persistence', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 120 | new CustomMedal('annual', 'resonance', 'Resonance', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 121 | new CustomMedal('annual', 'resurgence', 'Resurgence', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 122 | new CustomMedal('annual', 'ouroboros', 'Ouroboros', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 123 | new CustomMedal('annual', 'sage', 'Sage', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 124 | new CustomMedal('annual', 'luminary', 'Luminary', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 125 | new CustomMedal('annual', 'vanguard', 'Vanguard', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 126 | new CustomMedal('annual', 'innovator', 'Innovator', ['bronze', 'silver', 'gold', 'platinum', 'black']), | |
| 127 | new CustomMedal('event', '2026_summer_solstice', 'Summer Solstice 2026', ['bronze', 'silver']), | |
| 128 | new CustomMedal('event', 'orion', 'Orion: Global Op', ['bronze', 'silver', 'gold']), | |
| 129 | new CustomMedal('event', 'plus_gamma', '+Gamma: Global Op', ['bronze', 'silver', 'gold']), | |
| 130 | new CustomMedal('event', '2025_winter_solstice', 'Winter Solstice 2025 Dispatch Challenge', ['bronze', 'silver']), | |
| 131 | new CustomMedal('event', 'spider_web_2025', 'Spyder Web 2025'), | |
| 132 | new CustomMedal('event', 'plus_alpha', '+Alpha: Global Op', ['bronze', 'silver']), | |
| 133 | new CustomMedal('event', 'erased_memories', 'Erased Memories: Global Op', ['bronze', 'silver']), | |
| 134 | new CustomMedal('event', 'field_test_dispatch', 'Field Test: Dispatch', ['bronze', 'silver']), | |
| 135 | new CustomMedal('event', 'shared_memories', 'Shared Memories: Operation', ['bronze', 'silver']), | |
| 136 | new CustomMedal('event', 'buried_memories', 'Buried Memories: Global Op', ['bronze', 'silver']), | |
| 137 | new CustomMedal('event', 'cryptic_memories', 'Cryptic Memories: Global Op', ['bronze', 'silver']), | |
| 138 | new CustomMedal('event', 'chronos', 'Chronos', ['bronze', 'silver']), | |
| 139 | new CustomMedal('event', 'peace_day_2022', 'Peace Day 2022'), | |
| 140 | new CustomMedal('event', 'solstice_recharge', 'Solstice Recharge Challenge'), | |
| 141 | new CustomMedal('event', 'eosimprint', 'EOS Imprint', ['bronze', 'silver', 'gold']), | |
| 142 | new CustomMedal('event', 'csans', 'C. Sans', ['bronze', 'silver', 'gold']), | |
| 143 | new CustomMedal('event', 'courier_challenge', 'Courier Meet You Out There Challenge', ['bronze', 'silver', 'gold']), | |
| 144 | new CustomMedal('event', 'kinetic_challenge', 'Optima Kinetic Capsule Challenge', ['bronze', 'silver', 'gold']), | |
| 145 | new CustomMedal('event', 'knight_tessellation', 'Knight of Tessellation', ['silver', 'gold']), | |
| 146 | new CustomMedal('event', 'paragon', 'Tessellation Paragon'), | |
| 147 | new CustomMedal('event', 'didact_field', 'Didact Field Challenge', ['bronze', 'silver', 'gold']), | |
| 148 | new CustomMedal('event', 'umbra_deploy', 'Umbra Deploy Challenge', ['bronze', 'silver', 'gold']), | |
| 149 | new CustomMedal('event', 'avenir_shard_challenge', 'Avenir Shard Challenge'), | |
| 150 | new CustomMedal('event', 'aurora_glyph', 'Aurora Glyph Challenge', ['bronze', 'silver', 'gold']), | |
| 151 | new CustomMedal('event', 'myriad_hack', 'Myriad Hack Challenge', ['bronze', 'silver', 'gold']), | |
| 152 | new CustomMedal('event', 'dark_xm', 'Dark XM', ['bronze', 'silver', 'gold']), | |
| 153 | new CustomMedal('event', 'cassandra_neutralizer', 'Cassandra Neutralizer', ['bronze', 'silver', 'gold']), | |
| 154 | new CustomMedal('event', 'exo5', 'EXO5 Controller', ['100', '500', '2000']), | |
| 155 | new CustomMedal('event', 'magnus_builder', 'Magnus Builder', ['builder', 'architect']), | |
| 156 | new CustomMedal('event', 'lux_adventure', 'Via Lux Adventure', ['explorer', 'odyssey']), | |
| 157 | ]; | |
| 158 | } | |
| 159 | ||
| 160 | /** | |
| 161 | * The agent's medals, highest level first (as 4e-website's agent stats page); discontinued | |
| 162 | * medals only if earned. | |
| 163 | * | |
| 164 | * @return list<MedalLevel> | |
| 165 | */ | |
| 166 | public function medalLevels(AgentStat $stat): array | |
| 167 | { | |
| 168 | $stats = $stat->getStats(); | |
| 169 | $levels = []; | |
| 170 | foreach ($this->medals() as $medal) { | |
| 171 | $value = $stats[$medal->column] ?? null; | |
| 172 | $level = $medal->level($value); | |
| 173 | if (!$medal->discontinued || $level > 0) { | |
| 174 | $levels[] = new MedalLevel($medal, $value, $level); | |
| 175 | } | |
| 176 | } | |
| 177 | ||
| 178 | // usort is stable: the same levels keep 4e's order | |
| 179 | \usort($levels, static fn (MedalLevel $a, MedalLevel $b): int => $b->level <=> $a->level); | |
| 180 | ||
| 181 | return $levels; | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * The C.O.R.E badges for [$months] of subscription; none without a month. | |
| 186 | * | |
| 187 | * @return list<string> | |
| 188 | */ | |
| 189 | public function coreBadges(int $months): array | |
| 190 | { | |
| 191 | $badges = []; | |
| 192 | foreach (self::CORE_BADGES as $needed => $badge) { | |
| 193 | if ($months > 0 && $months >= $needed) { | |
| 194 | $badges[] = $badge; | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| 198 | return $badges; | |
| 199 | } | |
| 200 | ||
| 201 | /** | |
| 202 | * Stored custom medals (User::$customMedals) by group in 4e's order, the medals in its order. | |
| 203 | * Annual medals are stored as level 1 (bronze) to 5 (onyx). Unknown medals are left out. | |
| 204 | * | |
| 205 | * @param array<string, array<string, int|string>> $stored | |
| 206 | * | |
| 207 | * @return array<string, list<CustomMedalTier>> | |
| 208 | */ | |
| 209 | public function customMedalTiers(array $stored): array | |
| 210 | { | |
| 211 | $groups = []; | |
| 212 | foreach ($this->customMedals() as $medal) { | |
| 213 | $tier = $stored[$medal->group][$medal->key] ?? null; | |
| 214 | if (\is_int($tier)) { | |
| 215 | $tier = $medal->tiers[$tier - 1] ?? null; | |
| 216 | } | |
| 217 | ||
| 218 | if (null !== $tier) { | |
| 219 | $groups[$medal->group][] = new CustomMedalTier($medal, $tier); | |
| 220 | } | |
| 221 | } | |
| 222 | ||
| 223 | $sorted = []; | |
| 224 | foreach (self::CUSTOM_MEDAL_GROUPS as $group) { | |
| 225 | if (isset($groups[$group])) { | |
| 226 | $sorted[$group] = $groups[$group]; | |
| 227 | } | |
| 228 | } | |
| 229 | ||
| 230 | return $sorted; | |
| 231 | } | |
| 232 | } |