Move SSD weapon box type matching to typed Kotlin

ober

da004d7b1dd540055d2e74316974efb7d5cc805d

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 137c49b..5e56fdc 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -15,13 +15,28 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/BoxTypeResolve.kt"
       (typed-library (com sfb ssdreview)
-        (export resolveBoxType)
+        (export resolveBoxType boxTypeIsWeapon)
         (type Int32)
         (record BoxTypeMatch
           ((count : Int32)
            (found : (Nullable BoxType))))
         (def (boxTypeCompact (value : String)) : String
           (string-filter-letter-or-digit (string-lowercase value)))
+        (def (boxTypeNonWeaponTerm? (normalized : String)) : Bool
+          (string-matches-regex?
+            normalized
+            ".*(charge|degradation|fighterbox|hangar|hit|internalweaponbay|link|mechlink|rail|round|stabilizer|targetaccentuator|targetacquisitiongear|targetacquisitionguide).*"))
+        (def (boxTypeWeaponTerm? (normalized : String)) : Bool
+          (string-matches-regex?
+            normalized
+            ".*(add|antiproton|atomicmissile|axiontorpedo|bioelectricbolt|bombthrower|bosondrill|cannon|clusterbomb|deathboltrack|disruptor|drone|energyhowitzer|esg|fireball|fusion|gausscannon|hellbore|hellgun|hypercannon|hyperdrone|implosionbolt|implosiontorpedo|ioncannon|ionpulsecannon|kineticcannon|kineticwave|laser|massdriver|megaphaser|missile|mine|morter|mortar|neutronbeam|neutrongun|novacannon|optionmount|particlebeam|particlecannon|phaser|photon|plasma|positronlancet|ppd|prospectingcannon|proton|pulsecannon|pulseemitter|quantumcannon|railgun|rocket|sfg|shortrangecannon|sonicpulser|spaceauger|stingtorpedo|subspacerocket|tachyonbeam|tachyongun|tachyonmissile|torp|torpedo|trh|trl|webbreaker|webcaster|websnare).*"))
+        (def (boxTypeIsWeapon (raw : String)) : Bool
+          (let ((normalized (boxTypeCompact raw)))
+            (if (string-blank? normalized)
+                #f
+                (if (boxTypeNonWeaponTerm? normalized)
+                    #f
+                    (boxTypeWeaponTerm? normalized)))))
         (def (boxTypeExactMatch (box : BoxType)
                                 (text : String)
                                 (lowered : String)
@@ -2587,92 +2602,6 @@
        ""
        "object BoxTypes {"
        "    private var cache: List<BoxType>? = null"
-       "    private val weaponTerms = listOf("
-       "        \"add\","
-       "        \"antiproton\","
-       "        \"atomicmissile\","
-       "        \"axiontorpedo\","
-       "        \"bioelectricbolt\","
-       "        \"bombthrower\","
-       "        \"bosondrill\","
-       "        \"cannon\","
-       "        \"clusterbomb\","
-       "        \"deathboltrack\","
-       "        \"disruptor\","
-       "        \"drone\","
-       "        \"energyhowitzer\","
-       "        \"esg\","
-       "        \"fireball\","
-       "        \"fusion\","
-       "        \"gausscannon\","
-       "        \"hellbore\","
-       "        \"hellgun\","
-       "        \"hypercannon\","
-       "        \"hyperdrone\","
-       "        \"implosionbolt\","
-       "        \"implosiontorpedo\","
-       "        \"ioncannon\","
-       "        \"ionpulsecannon\","
-       "        \"kineticcannon\","
-       "        \"kineticwave\","
-       "        \"laser\","
-       "        \"massdriver\","
-       "        \"megaphaser\","
-       "        \"missile\","
-       "        \"mine\","
-       "        \"morter\","
-       "        \"mortar\","
-       "        \"neutronbeam\","
-       "        \"neutrongun\","
-       "        \"novacannon\","
-       "        \"optionmount\","
-       "        \"particlebeam\","
-       "        \"particlecannon\","
-       "        \"phaser\","
-       "        \"photon\","
-       "        \"plasma\","
-       "        \"positronlancet\","
-       "        \"ppd\","
-       "        \"prospectingcannon\","
-       "        \"proton\","
-       "        \"pulsecannon\","
-       "        \"pulseemitter\","
-       "        \"quantumcannon\","
-       "        \"railgun\","
-       "        \"rocket\","
-       "        \"sfg\","
-       "        \"shortrangecannon\","
-       "        \"sonicpulser\","
-       "        \"spaceauger\","
-       "        \"stingtorpedo\","
-       "        \"subspacerocket\","
-       "        \"tachyonbeam\","
-       "        \"tachyongun\","
-       "        \"tachyonmissile\","
-       "        \"torp\","
-       "        \"torpedo\","
-       "        \"trh\","
-       "        \"trl\","
-       "        \"webbreaker\","
-       "        \"webcaster\","
-       "        \"websnare\""
-       "    )"
-       "    private val nonWeaponTerms = listOf("
-       "        \"charge\","
-       "        \"degradation\","
-       "        \"fighterbox\","
-       "        \"hangar\","
-       "        \"hit\","
-       "        \"internalweaponbay\","
-       "        \"link\","
-       "        \"mechlink\","
-       "        \"rail\","
-       "        \"round\","
-       "        \"stabilizer\","
-       "        \"targetaccentuator\","
-       "        \"targetacquisitiongear\","
-       "        \"targetacquisitionguide\""
-       "    )"
        ""
        "    fun load(context: Context): List<BoxType> {"
        "        cache?.let { return it }"
@@ -2701,9 +2630,7 @@
        "        val text = raw.trim()"
        "        if (text.isBlank()) return false"
        "        val resolved = resolve(context, text)"
-       "        val normalized = compact(resolved?.name ?: text)"
-       "        if (nonWeaponTerms.any { normalized.contains(it) }) return false"
-       "        return weaponTerms.any { normalized.contains(it) }"
+       "        return boxTypeIsWeapon(resolved?.name ?: text)"
        "    }"
        ""
        "}"