From f5d653e84afa697f27183f75da53282657b628bf Mon Sep 17 00:00:00 2001 From: Andy Vandenberghe Date: Thu, 24 Sep 2026 12:48:24 +0200 Subject: [PATCH] Add the authored lotro_unknown.png placeholder (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit class_id 0 — the class a character gets when a sync cannot map it — has always seeded this imagename with no file behind it, so it rendered as a broken image until avathar/bbguild#389 added a fallback chain. That chain ends at _unknown.png, so without this file its last step could never fire. Authored rather than sourced: no wiki or API has a generic "unknown class" icon, which is why avathar/bbguild#391 calls these out as the one part of the icon backlog that placeholders are the right answer for. A '?' glyph matches what the family already does here (bbguildwow's wow_unknown is a red '?' on a plate, bbguildlotro's race unknown a grey '?' on gold). Warm gold with a dark outline, so it stays legible on both light prosilver and dark styles; drawn at 8x and downsampled; 48x48 to match the set. lotro_icon_coverage_test parses the installer's seeded imagename values and asserts both icon directories cover them, with the remaining gaps listed explicitly — so a new gap fails CI instead of going unnoticed. Refs #3 Co-Authored-By: Claude Opus 5 (1M context) --- images/class_images/lotro_unknown.png | Bin 0 -> 2123 bytes tests/game/lotro_icon_coverage_test.php | 96 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 images/class_images/lotro_unknown.png create mode 100644 tests/game/lotro_icon_coverage_test.php diff --git a/images/class_images/lotro_unknown.png b/images/class_images/lotro_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..8bd3c89faa81e63efa2a9394c006738ae066d9c3 GIT binary patch literal 2123 zcmV-R2()V`<#2uea+0B&ZM0O1Jjnr^dY7cr4Va1KtzE`QwT<_30RFb5euTV8UisO zfutyjk3|27f(?-HQcEpO8wxhW(ucP6HB-uP2ikeudED1IXYalIaqlz@O|&y}TgId- zIX~{rzI%V)S>N7w?Qa1W;zC@A|LdT0Jn!lAbM=|NFJmd70LUqhIqw-D00Mv+|Dvl4 zuFcPA3=NXZAOTZ~MJjiozd!$x-w^=&aS{;Y0Oq!3S^<0+0yhYv`3y7>C|+5E%M1)N zqZ{Bi*x-eu*`b})Ippc&&nE#1z|6^H(&S@3N&2Rgq9F`5%7KQ~77PX$AOeICh`9=0 zOaTD2;Vo?K1KoYOH>W~^rUHI$OWl<$#M7>$t|$~kl!HjmY_6kAJDaGzrJm|iK1dLX z#SkC%kD~kdfPMG9A&#_>b!nfN*&1Vh(w!ZCWU?e^va;!%*?5&yyje=okQ*sQb32>V z*H$d1>#puZ^Nb`MM}j~V0Wn~V#aOqzoqjtE1&p}Q})b|oO6 zogfLC1bB3GG&MU$?^05>+$K_idStjw5d$$`$ojZd;@tSo;MOiUFd? z07Oudkch`1h``JsL5L!QLNPqGFB%&X*!lLmShM~)jv`~FB%_U3aIA0O$V5&jIZ^Pl zFPgDP3HrrYJ}`?GwyVc}dMiq$2-Y$Hg6}!d+FJv2eEg5mZTtD+@66k z3?T?(`2d&CX+~Yjqs`k7*|_gG7ABb+Ex%aJG}kl(O98bV&~sWFpHzz0`h(2_nEUvyuX#Y=Wq6*#U2C7= zJ^SB>@4LB>^z7ruM~17|_^dR2HnF{;OCo{Kl*$nr+rhG@`fAh6O>AN^xHqaftNS`8>wUr##k^jJlDapC37hZ4NE~8*U-X= zU=tJAD|~rNDkB87mqU$tm(8MC&1s~P9_rE_+FKg1<@H0@vp;K_8v+FhjOiGk#X;P!rj~LCjYw(!TFG=IwpG zSo7oOL?Z4Yo$@fNIZdx@+0VY`aHMrX*zoSdALa)Dpkr2JMyZM%n9o#&QUXK-07S$S z!gRKS8Gtx;?BvPWZJ8~8OnoVPVg%jC2P!2dGr(ZqcXYyc9j)z-qaRej&u*!|MM(Y= z5hY9Ih?iY=DG5QRh)7B3J248a&G>#fQI#y$JOcpW*ytOfQIEPxE^*Vc9*h(b#fc!8 z;b>zI3)uTB;2Z8$N<7X028IfJ*PWlH4GxdZX{~?EanyZ5sBJljaNoB- zCsu!T2@1uqqJ3@9*pR@cZSP>mTiqs|^prOC?}y(X{9qz~W|Ju;&M2Qh8l5c-564`! zu2>Eu$C2v6dvBtfub+q9NC`p^w6>^AdpO+FkJUf;Gmnmyjq6IK8H;;*2md({0%8&b zxH>tV_J}UP1weCJGb>z2t}B(pDB-*6R}ZhGo3EdT!QtX5@bzgAM~?Ml&H86CG+fla z=O|f3Iboom>gmg^0wA8Pfamj0pm}^zA-<@!;l}y|-+JSG0m z6tI?&O2)A9rQNvqp%>U%=7jI6Akt49?aO`rwAzO!Q`=8D#%p*%bVWJTbV)~}nAwy@ z7-=vAao@$ReZBbE6R&c@ci}ifM3GtB{eJHIRT)p(u@JQ~20A)AJOVWuW6_jJKuQ6v zEwna}lCb6VLkI%Rjw7wsyy|FnXhYSZu$Y3~qH48?y?ox`1RQG_@pz0f^>Iig00z=2 z5BWmCLXa@Ve%O{Bdb%5s)dF!!UQx{mH2A(ALP4a0RL zm>K;|O&%&13r!bahyjS%tquQDQg#)JfgKz!3Q;N0V40Ch#88*^Ex<5_Zs_S7d~K?h zN=-A+*47r2R7w}iA+DU?iaVEIh4Bo;T!mx3gZTY3J8jHWimjaw;I*XyZJ*-0H6vh* zAvh$W$f9Lt9qzd0lNimH0RYK(3~%jy55If*ALA*jnMYbP0;lvuKol9|Mv55A2LLLc z^M&A?7_tKADbAHGt!nJ4Gc({i3Yq#il#*2mxkx8t)0=?ny4IRN#9V2QzT3}_|7I%( zVda2v9f=Q4j*<|>%xtG)gF^tYb6PW-lp}5|l_Og$MdPO^m;ogPl8IQwPBDB<)t#O` z$0z`Tkl!`hJmos-Dlg`o-UDDTTgyDGEkD|m&F%bOm!glO#dOMuntM@mr_jbZb3iyy zy^_*&Z8wSYd5JogO+N8oPSa`K=(`XX;zFF~_zz@#wpVy;v*-W-002ovPDHLkV1mhN B?tK6N literal 0 HcmV?d00001 diff --git a/tests/game/lotro_icon_coverage_test.php b/tests/game/lotro_icon_coverage_test.php new file mode 100644 index 0000000..4afab07 --- /dev/null +++ b/tests/game/lotro_icon_coverage_test.php @@ -0,0 +1,96 @@ +.png for character rows and + * roster_classes/.png for the grid, so a seeded class with no + * file behind it has no icon. avathar/bbguild#389 makes that degrade + * instead of rendering broken, but the asset is still missing. + */ +class lotro_icon_coverage_test extends TestCase +{ + /** Gaps that are known and tracked; see #3 */ + private const KNOWN_GAPS = array( + 'lotro_brawler', + 'lotro_mariner', + ); + + /** Same, for the grid's artwork directory */ + private const KNOWN_ROSTER_GAPS = array( + 'lotro_beorning', + 'lotro_blackarrow', + 'lotro_brawler', + 'lotro_defiler', + 'lotro_mariner', + 'lotro_reaver', + 'lotro_stalker', + 'lotro_unknown', + 'lotro_warleader', + 'lotro_weaver', + ); + + /** + * @return list Every imagename the installer seeds + */ + private function seeded_imagenames(): array + { + $files = glob(dirname(__DIR__, 2) . '/game/*_installer.php'); + $src = file_get_contents($files[0]); + + preg_match_all("/'imagename'\s*=>\s*'([^']+)'/", $src, $m); + + $names = array_values(array_unique(array_filter(array_map('trim', $m[1])))); + sort($names); + + return $names; + } + + private function assertNoUnexpectedGaps(string $dir, array $known): void + { + $base = dirname(__DIR__, 2) . '/images/' . $dir . '/'; + $missing = array(); + + foreach ($this->seeded_imagenames() as $name) + { + if (!file_exists($base . $name . '.png')) + { + $missing[] = $name; + } + } + + $unexpected = array_values(array_diff($missing, $known)); + + $this->assertSame( + array(), + $unexpected, + $dir . ' is missing icons that are not tracked as known gaps: ' . implode(', ', $unexpected) + ); + } + + public function test_class_images_cover_every_seeded_class(): void + { + $this->assertNoUnexpectedGaps('class_images', self::KNOWN_GAPS); + } + + public function test_roster_classes_cover_every_seeded_class(): void + { + $this->assertNoUnexpectedGaps('roster_classes', self::KNOWN_ROSTER_GAPS); + } + + public function test_the_installer_seeds_the_expected_class_count(): void + { + // Guards the parser: if the installer's array syntax changes, the + // coverage assertions must not silently pass on an empty list. + $this->assertCount(19, $this->seeded_imagenames()); + } +}