Port SFC assets and effects to typed Jerboa

ober

a487d6f7f23a890b88bf6f28b988b5e018a50518

diff --git a/templates/original-tactics-parts/SfcAssets.ss b/templates/original-tactics-parts/SfcAssets.ss
index 7a76736..43218b0 100644
--- a/templates/original-tactics-parts/SfcAssets.ss
+++ b/templates/original-tactics-parts/SfcAssets.ss
@@ -38,9 +38,58 @@
           (CONFIRM)
           (CANCEL)
           (PHASER)
+          (PHASER_1)
+          (PHASER_2)
+          (PHASER_3)
+          (PHASER_4)
+          (PHASER_G)
+          (PHASER_G2)
+          (PHASER_X)
+          (PHASER_HEAVY)
           (PHOTON)
+          (PHOTON_SFC2)
+          (PHOTON_OVERLOAD)
+          (PHOTON_HEAVY)
           (PLASMA)
+          (PLASMA_SEEK)
+          (PLASMA_FAST)
+          (PLASMA_LARGE)
+          (PLASMA_ENVELOPING)
+          (PLASMA_SNARE)
           (DISRUPTOR)
+          (DISRUPTOR_HEAVY)
+          (DRONE)
+          (DRONE_FAST)
+          (HELLBORE)
+          (HELLBORE_HEAVY)
+          (HELLBORE_IMPACT)
+          (FUSION)
+          (FUSION_SHORT)
+          (ESG)
+          (ESG_LOOP)
+          (ESG_END)
+          (TR_BEAM)
+          (MAULER)
+          (MAULER_SHORT)
+          (PPD)
+          (PPD_IMPACT)
+          (PPD_SEND)
+          (PPD_SEEK)
+          (WEB)
+          (WEB_BREAK)
+          (TRACTOR)
+          (TRACTOR_FAIL)
+          (TRACTOR_RELEASE)
+          (TRANSPORTER)
+          (FIGHTER_DISCHARGE)
+          (MINE_DISCHARGE)
+          (BREAKDOWN)
+          (CLOAK)
+          (DECLOAK)
+          (EXPLOSION_HEAVY)
+          (MISS_EXPLOSION)
+          (SHIELD_HEAVY)
+          (HULL_HEAVY)
           (SHIELD_HIT)
           (HULL_HIT)
           (EXPLOSION)
@@ -54,12 +103,61 @@
           (kotlin-value SfcSound CANCEL))
         (extern (sfcPhaser) : SfcSound
           (kotlin-value SfcSound PHASER))
+        (extern (sfcPhaser1) : SfcSound (kotlin-value SfcSound PHASER_1))
+        (extern (sfcPhaser2) : SfcSound (kotlin-value SfcSound PHASER_2))
+        (extern (sfcPhaser3) : SfcSound (kotlin-value SfcSound PHASER_3))
+        (extern (sfcPhaser4) : SfcSound (kotlin-value SfcSound PHASER_4))
+        (extern (sfcPhaserG) : SfcSound (kotlin-value SfcSound PHASER_G))
+        (extern (sfcPhaserG2) : SfcSound (kotlin-value SfcSound PHASER_G2))
+        (extern (sfcPhaserX) : SfcSound (kotlin-value SfcSound PHASER_X))
+        (extern (sfcPhaserHeavy) : SfcSound (kotlin-value SfcSound PHASER_HEAVY))
         (extern (sfcPhoton) : SfcSound
           (kotlin-value SfcSound PHOTON))
+        (extern (sfcPhotonSfc2) : SfcSound (kotlin-value SfcSound PHOTON_SFC2))
+        (extern (sfcPhotonOverload) : SfcSound (kotlin-value SfcSound PHOTON_OVERLOAD))
+        (extern (sfcPhotonHeavy) : SfcSound (kotlin-value SfcSound PHOTON_HEAVY))
         (extern (sfcPlasma) : SfcSound
           (kotlin-value SfcSound PLASMA))
+        (extern (sfcPlasmaSeek) : SfcSound (kotlin-value SfcSound PLASMA_SEEK))
+        (extern (sfcPlasmaFast) : SfcSound (kotlin-value SfcSound PLASMA_FAST))
+        (extern (sfcPlasmaLarge) : SfcSound (kotlin-value SfcSound PLASMA_LARGE))
+        (extern (sfcPlasmaEnveloping) : SfcSound (kotlin-value SfcSound PLASMA_ENVELOPING))
+        (extern (sfcPlasmaSnare) : SfcSound (kotlin-value SfcSound PLASMA_SNARE))
         (extern (sfcDisruptor) : SfcSound
           (kotlin-value SfcSound DISRUPTOR))
+        (extern (sfcDisruptorHeavy) : SfcSound (kotlin-value SfcSound DISRUPTOR_HEAVY))
+        (extern (sfcDrone) : SfcSound (kotlin-value SfcSound DRONE))
+        (extern (sfcDroneFast) : SfcSound (kotlin-value SfcSound DRONE_FAST))
+        (extern (sfcHellbore) : SfcSound (kotlin-value SfcSound HELLBORE))
+        (extern (sfcHellboreHeavy) : SfcSound (kotlin-value SfcSound HELLBORE_HEAVY))
+        (extern (sfcHellboreImpact) : SfcSound (kotlin-value SfcSound HELLBORE_IMPACT))
+        (extern (sfcFusion) : SfcSound (kotlin-value SfcSound FUSION))
+        (extern (sfcFusionShort) : SfcSound (kotlin-value SfcSound FUSION_SHORT))
+        (extern (sfcEsg) : SfcSound (kotlin-value SfcSound ESG))
+        (extern (sfcEsgLoop) : SfcSound (kotlin-value SfcSound ESG_LOOP))
+        (extern (sfcEsgEnd) : SfcSound (kotlin-value SfcSound ESG_END))
+        (extern (sfcTrBeam) : SfcSound (kotlin-value SfcSound TR_BEAM))
+        (extern (sfcMauler) : SfcSound (kotlin-value SfcSound MAULER))
+        (extern (sfcMaulerShort) : SfcSound (kotlin-value SfcSound MAULER_SHORT))
+        (extern (sfcPpd) : SfcSound (kotlin-value SfcSound PPD))
+        (extern (sfcPpdImpact) : SfcSound (kotlin-value SfcSound PPD_IMPACT))
+        (extern (sfcPpdSend) : SfcSound (kotlin-value SfcSound PPD_SEND))
+        (extern (sfcPpdSeek) : SfcSound (kotlin-value SfcSound PPD_SEEK))
+        (extern (sfcWeb) : SfcSound (kotlin-value SfcSound WEB))
+        (extern (sfcWebBreak) : SfcSound (kotlin-value SfcSound WEB_BREAK))
+        (extern (sfcTractor) : SfcSound (kotlin-value SfcSound TRACTOR))
+        (extern (sfcTractorFail) : SfcSound (kotlin-value SfcSound TRACTOR_FAIL))
+        (extern (sfcTractorRelease) : SfcSound (kotlin-value SfcSound TRACTOR_RELEASE))
+        (extern (sfcTransporter) : SfcSound (kotlin-value SfcSound TRANSPORTER))
+        (extern (sfcFighterDischarge) : SfcSound (kotlin-value SfcSound FIGHTER_DISCHARGE))
+        (extern (sfcMineDischarge) : SfcSound (kotlin-value SfcSound MINE_DISCHARGE))
+        (extern (sfcBreakdown) : SfcSound (kotlin-value SfcSound BREAKDOWN))
+        (extern (sfcCloak) : SfcSound (kotlin-value SfcSound CLOAK))
+        (extern (sfcDecloak) : SfcSound (kotlin-value SfcSound DECLOAK))
+        (extern (sfcExplosionHeavy) : SfcSound (kotlin-value SfcSound EXPLOSION_HEAVY))
+        (extern (sfcMissExplosion) : SfcSound (kotlin-value SfcSound MISS_EXPLOSION))
+        (extern (sfcShieldHeavy) : SfcSound (kotlin-value SfcSound SHIELD_HEAVY))
+        (extern (sfcHullHeavy) : SfcSound (kotlin-value SfcSound HULL_HEAVY))
         (extern (sfcShieldHit) : SfcSound
           (kotlin-value SfcSound SHIELD_HIT))
         (extern (sfcHullHit) : SfcSound
@@ -189,6 +287,8 @@
                   (text : String)
                   (locale : Locale)) : String
           (kotlin-member-call lowercase))
+        (extern (sfcSoundName (sound : SfcSound)) : String
+          (kotlin-member-get name))
         (extern (assetsEnsureSoundPool) : SoundPool
           (kotlin-call SfcAssets ensureSoundPool))
         (extern (assetsEnsureSoundLoaded
@@ -202,32 +302,16 @@
                   (sampleId : Int32)
                   (status : Int32)) : Unit
           (kotlin-call SfcAssets markLoaded))
+        (def (soundStem (sound : SfcSound)) : String
+          (if (equal? sound (sfcHellboreHeavy))
+            "hellbore_heavy_a"
+            (if (equal? sound (sfcMissExplosion))
+              "missile_explosion"
+              (lowercaseLocale (sfcSoundName sound) (localeUs)))))
         (def (soundPath (sound : SfcSound)) : (Nullable String)
-          (if (equal? sound (sfcButton))
-            (nullable-some "sfc/sound/button.wav")
-            (if (equal? sound (sfcConfirm))
-              (nullable-some "sfc/sound/confirm.wav")
-              (if (equal? sound (sfcCancel))
-                (nullable-some "sfc/sound/cancel.wav")
-                (if (equal? sound (sfcPhaser))
-                  (nullable-some "sfc/sound/phaser.wav")
-                  (if (equal? sound (sfcPhoton))
-                    (nullable-some "sfc/sound/photon.wav")
-                    (if (equal? sound (sfcPlasma))
-                      (nullable-some "sfc/sound/plasma.wav")
-                      (if (equal? sound (sfcDisruptor))
-                        (nullable-some "sfc/sound/disruptor.wav")
-                        (if (equal? sound (sfcShieldHit))
-                          (nullable-some "sfc/sound/shield_hit.wav")
-                          (if (equal? sound (sfcHullHit))
-                            (nullable-some "sfc/sound/hull_hit.wav")
-                            (if (equal? sound (sfcExplosion))
-                              (nullable-some "sfc/sound/explosion.wav")
-                              (if (equal? sound (sfcRedAlert))
-                                (nullable-some "sfc/sound/red_alert.wav")
-                                (if (equal? sound (sfcHet))
-                                  (nullable-some "sfc/sound/het.wav")
-                                  (nullable-none String))))))))))))))
+          (nullable-some
+            (string-append "sfc/sound/"
+              (string-append (soundStem sound) ".wav"))))
         (def (artPath (key : String)) : (Nullable String)
           (if (equal? key "federation")
             (nullable-some "sfc2/art/federation.png")
@@ -299,9 +383,58 @@
               (assetsEnsureSoundLoaded context (sfcConfirm))
               (assetsEnsureSoundLoaded context (sfcCancel))
               (assetsEnsureSoundLoaded context (sfcPhaser))
+              (assetsEnsureSoundLoaded context (sfcPhaser1))
+              (assetsEnsureSoundLoaded context (sfcPhaser2))
+              (assetsEnsureSoundLoaded context (sfcPhaser3))
+              (assetsEnsureSoundLoaded context (sfcPhaser4))
+              (assetsEnsureSoundLoaded context (sfcPhaserG))
+              (assetsEnsureSoundLoaded context (sfcPhaserG2))
+              (assetsEnsureSoundLoaded context (sfcPhaserX))
+              (assetsEnsureSoundLoaded context (sfcPhaserHeavy))
               (assetsEnsureSoundLoaded context (sfcPhoton))
+              (assetsEnsureSoundLoaded context (sfcPhotonSfc2))
+              (assetsEnsureSoundLoaded context (sfcPhotonOverload))
+              (assetsEnsureSoundLoaded context (sfcPhotonHeavy))
               (assetsEnsureSoundLoaded context (sfcPlasma))
+              (assetsEnsureSoundLoaded context (sfcPlasmaSeek))
+              (assetsEnsureSoundLoaded context (sfcPlasmaFast))
+              (assetsEnsureSoundLoaded context (sfcPlasmaLarge))
+              (assetsEnsureSoundLoaded context (sfcPlasmaEnveloping))
+              (assetsEnsureSoundLoaded context (sfcPlasmaSnare))
               (assetsEnsureSoundLoaded context (sfcDisruptor))
+              (assetsEnsureSoundLoaded context (sfcDisruptorHeavy))
+              (assetsEnsureSoundLoaded context (sfcDrone))
+              (assetsEnsureSoundLoaded context (sfcDroneFast))
+              (assetsEnsureSoundLoaded context (sfcHellbore))
+              (assetsEnsureSoundLoaded context (sfcHellboreHeavy))
+              (assetsEnsureSoundLoaded context (sfcHellboreImpact))
+              (assetsEnsureSoundLoaded context (sfcFusion))
+              (assetsEnsureSoundLoaded context (sfcFusionShort))
+              (assetsEnsureSoundLoaded context (sfcEsg))
+              (assetsEnsureSoundLoaded context (sfcEsgLoop))
+              (assetsEnsureSoundLoaded context (sfcEsgEnd))
+              (assetsEnsureSoundLoaded context (sfcTrBeam))
+              (assetsEnsureSoundLoaded context (sfcMauler))
+              (assetsEnsureSoundLoaded context (sfcMaulerShort))
+              (assetsEnsureSoundLoaded context (sfcPpd))
+              (assetsEnsureSoundLoaded context (sfcPpdImpact))
+              (assetsEnsureSoundLoaded context (sfcPpdSend))
+              (assetsEnsureSoundLoaded context (sfcPpdSeek))
+              (assetsEnsureSoundLoaded context (sfcWeb))
+              (assetsEnsureSoundLoaded context (sfcWebBreak))
+              (assetsEnsureSoundLoaded context (sfcTractor))
+              (assetsEnsureSoundLoaded context (sfcTractorFail))
+              (assetsEnsureSoundLoaded context (sfcTractorRelease))
+              (assetsEnsureSoundLoaded context (sfcTransporter))
+              (assetsEnsureSoundLoaded context (sfcFighterDischarge))
+              (assetsEnsureSoundLoaded context (sfcMineDischarge))
+              (assetsEnsureSoundLoaded context (sfcBreakdown))
+              (assetsEnsureSoundLoaded context (sfcCloak))
+              (assetsEnsureSoundLoaded context (sfcDecloak))
+              (assetsEnsureSoundLoaded context (sfcExplosionHeavy))
+              (assetsEnsureSoundLoaded context (sfcMissExplosion))
+              (assetsEnsureSoundLoaded context (sfcShieldHeavy))
+              (assetsEnsureSoundLoaded context (sfcHullHeavy))
               (assetsEnsureSoundLoaded context (sfcShieldHit))
               (assetsEnsureSoundLoaded context (sfcHullHit))
               (assetsEnsureSoundLoaded context (sfcExplosion))
diff --git a/templates/original-tactics-parts/SfcShipArtView.ss b/templates/original-tactics-parts/SfcShipArtView.ss
index 05da16c..b22754b 100644
--- a/templates/original-tactics-parts/SfcShipArtView.ss
+++ b/templates/original-tactics-parts/SfcShipArtView.ss
@@ -7,7 +7,10 @@
                       (android graphics Canvas)
                       (android graphics Color)
                       (android graphics Paint)
+                      (android graphics Paint Cap)
+                      (android graphics Paint Join)
                       (android graphics Paint Style)
+                      (android graphics Path)
                       (android graphics Rect)
                       (android graphics RectF)
                       (android view View)
@@ -22,17 +25,24 @@
         (type Int32)
         (type JSONObject)
         (type Paint)
+        (type Cap)
+        (type Join)
+        (type Path)
         (type Style)
         (type Rect)
         (type RectF)
         (type Resources)
         (type View)
+        (record CardLivery
+          ((hull : Int32) (trim : Int32) (accent : Int32)))
         (extern (paintAntiAliasFlag) : Int32
           (kotlin-value Paint ANTI_ALIAS_FLAG))
         (extern (paintStyleFill) : Style
           (kotlin-value Paint Style FILL))
         (extern (paintStyleStroke) : Style
           (kotlin-value Paint Style STROKE))
+        (extern (paintCapRound) : Cap (kotlin-value Paint Cap ROUND))
+        (extern (paintJoinRound) : Join (kotlin-value Paint Join ROUND))
         (extern (paintColorSet (paint : Paint) (color : Int32)) : Unit
           (kotlin-member-set color))
         (extern (paintStyleSet (paint : Paint) (style : Style)) : Unit
@@ -49,6 +59,10 @@
                   (paint : Paint)
                   (bold : Bool)) : Unit
           (kotlin-member-set isFakeBoldText))
+        (extern (paintStrokeCapSet (paint : Paint) (cap : Cap)) : Unit
+          (kotlin-member-set strokeCap))
+        (extern (paintStrokeJoinSet (paint : Paint) (join : Join)) : Unit
+          (kotlin-member-set strokeJoin))
         (extern (colorRgb
                   (red : Int32)
                   (green : Int32)
@@ -85,6 +99,10 @@
                   (x : Float32)
                   (y : Float32)) : Unit
           (kotlin-member-call scale))
+        (extern (canvasTranslate (canvas : Canvas) (x : Float32) (y : Float32)) : Unit
+          (kotlin-member-call translate))
+        (extern (canvasRotate (canvas : Canvas) (degrees : Float32)) : Unit
+          (kotlin-member-call rotate))
         (extern (canvasRestoreToCount
                   (canvas : Canvas)
                   (state : Int32)) : Unit
@@ -104,6 +122,20 @@
                   (ry : Float32)
                   (paint : Paint)) : Unit
           (kotlin-member-call drawRoundRect))
+        (extern (canvasDrawOval (canvas : Canvas) (rect : RectF) (paint : Paint)) : Unit
+          (kotlin-member-call drawOval))
+        (extern (canvasDrawCircle (canvas : Canvas) (x : Float32) (y : Float32)
+                                  (radius : Float32) (paint : Paint)) : Unit
+          (kotlin-member-call drawCircle))
+        (extern (canvasDrawLine (canvas : Canvas) (x1 : Float32) (y1 : Float32)
+                                (x2 : Float32) (y2 : Float32) (paint : Paint)) : Unit
+          (kotlin-member-call drawLine))
+        (extern (canvasDrawPath (canvas : Canvas) (path : Path) (paint : Paint)) : Unit
+          (kotlin-member-call drawPath))
+        (extern (canvasDrawArc (canvas : Canvas) (rect : RectF)
+                               (start : Float32) (sweep : Float32)
+                               (useCenter : Bool) (paint : Paint)) : Unit
+          (kotlin-member-call drawArc))
         (extern (canvasDrawText
                   (canvas : Canvas)
                   (text : String)
@@ -126,6 +158,8 @@
           (kotlin-member-get right))
         (extern (rectFBottom (rect : RectF)) : Float32
           (kotlin-member-get bottom))
+        (extern (rectFCenterX (rect : RectF)) : Float32 (kotlin-member-call centerX))
+        (extern (rectFCenterY (rect : RectF)) : Float32 (kotlin-member-call centerY))
         (extern (rectFWidth (rect : RectF)) : Float32
           (kotlin-member-call width))
         (extern (rectFHeight (rect : RectF)) : Float32
@@ -136,6 +170,17 @@
           (kotlin-member-get height))
         (extern (float32ToInt (value : Float32)) : Int32
           (kotlin-member-call toInt))
+        (extern (minimumFloat (a : Float32) (b : Float32)) : Float32
+          (kotlin-call Math min))
+        (extern (pathMoveTo (path : Path) (x : Float32) (y : Float32)) : Unit
+          (kotlin-member-call moveTo))
+        (extern (pathLineTo (path : Path) (x : Float32) (y : Float32)) : Unit
+          (kotlin-member-call lineTo))
+        (extern (pathCubicTo (path : Path) (x1 : Float32) (y1 : Float32)
+                             (x2 : Float32) (y2 : Float32)
+                             (x3 : Float32) (y3 : Float32)) : Unit
+          (kotlin-member-call cubicTo))
+        (extern (pathClose (path : Path)) : Unit (kotlin-member-call close))
         (extern (jsonObjectChild
                   (json : JSONObject)
                   (key : String)) : (Nullable JSONObject)
@@ -151,6 +196,13 @@
                   (ship : (Nullable JSONObject))
                   (fallback : String)) : String
           (kotlin-call shipDisplayLabel))
+        (extern (shipTechnical
+                  (ship : (Nullable JSONObject))
+                  (fallback : String)) : String
+          (kotlin-call shipTechnicalLabel))
+        (extern (jsonString (json : JSONObject) (key : String) (fallback : String)) : String
+          (kotlin-member-call optString))
+        (extern (lowercaseText (text : String)) : String (kotlin-member-call lowercase))
         (def (configureShipArtPaint
                (paint : Paint)
                (color : Int32)
@@ -171,6 +223,15 @@
             (paintTextSizeSet paint size)
             (paintFakeBoldSet paint bold)
             paint))
+        (def (configureShipStrokePaint
+               (paint : Paint) (color : Int32) (width : Float32)) : Paint
+          (begin
+            (paintColorSet paint color)
+            (paintStyleSet paint (paintStyleStroke))
+            (paintStrokeWidthSet paint width)
+            (paintStrokeCapSet paint (paintCapRound))
+            (paintStrokeJoinSet paint (paintJoinRound))
+            paint))
         (def (nullableUppercaseOr
                (text : (Nullable String))
                (fallback : String)) : String
@@ -237,6 +298,26 @@
               (float32 15.0)
               #f)
             (modifiers private))
+          (val proceduralHullPaint : Paint
+            (configureShipArtPaint (new Paint (paintAntiAliasFlag))
+              (colorRgb (int32 160) (int32 170) (int32 184))
+              (paintStyleFill) (float32 0.0))
+            (modifiers private))
+          (val proceduralTrimPaint : Paint
+            (configureShipArtPaint (new Paint (paintAntiAliasFlag))
+              (colorRgb (int32 90) (int32 138) (int32 195))
+              (paintStyleFill) (float32 0.0))
+            (modifiers private))
+          (val proceduralAccentPaint : Paint
+            (configureShipArtPaint (new Paint (paintAntiAliasFlag))
+              (colorRgb (int32 210) (int32 90) (int32 70))
+              (paintStyleFill) (float32 0.0))
+            (modifiers private))
+          (val proceduralStrokePaint : Paint
+            (configureShipStrokePaint (new Paint (paintAntiAliasFlag))
+              (colorArgb (int32 235) (int32 244) (int32 248) (int32 255))
+              (float32 3.0))
+            (modifiers private))
           (def (setGame (nextGame : (Nullable JSONObject))) : Unit
             (begin
               (set! game nextGame)
@@ -326,7 +407,7 @@
                 (float32 10.0) (float32 10.0) cardPaint)
               (let ((image (shipArtBitmap context ship)))
                 (if (nullable-null? image)
-                  (begin)
+                  (drawProceduralMiniature canvas rect ship)
                   (begin
                     (drawBitmapCover canvas (nullable-get image) rect)
                     (canvasDrawRoundRect
@@ -354,6 +435,318 @@
                 (+ (rectFLeft rect) (float32 12.0))
                 (- (rectFBottom rect) (float32 12.0))
                 labelPaint)))
+          (def (drawProceduralMiniature
+                 (canvas : Canvas) (rect : RectF)
+                 (ship : (Nullable JSONObject))) : Unit
+            (modifiers private)
+            (let ((race (cardRaceKey ship)))
+              (let ((livery (cardLivery race))
+                    (state (canvasSave canvas))
+                    (r (* (minimumFloat (rectFWidth rect) (rectFHeight rect))
+                          (float32 0.27))))
+                (begin
+                  (paintColorSet proceduralHullPaint (CardLivery-hull livery))
+                  (paintColorSet proceduralTrimPaint (CardLivery-trim livery))
+                  (paintColorSet proceduralAccentPaint (CardLivery-accent livery))
+                  (canvasTranslate canvas (rectFCenterX rect)
+                    (- (rectFCenterY rect) (float32 4.0)))
+                  (canvasRotate canvas (float32 -10.0))
+                  (if (or (equal? race "federation") (equal? race "aurora"))
+                    (drawFederationMiniature canvas r)
+                    (if (or (equal? race "klingon") (equal? race "romulan")
+                            (equal? race "kzinti") (equal? race "lyran")
+                            (equal? race "orion"))
+                      (drawWedgeMiniature canvas r)
+                      (if (equal? race "andromedan")
+                        (drawAndromedanMiniature canvas r livery)
+                        (if (or (equal? race "tholian") (equal? race "seltorian"))
+                          (drawDiamondMiniature canvas r)
+                          (drawGenericMiniature canvas r)))))
+                  (canvasDrawLine canvas (* r (float32 -0.66)) (float32 0.0)
+                    (* r (float32 0.74)) (float32 0.0) proceduralStrokePaint)
+                  (canvasRestoreToCount canvas state)))))
+          (def (drawFederationMiniature (canvas : Canvas) (r : Float32)) : Unit
+            (modifiers private)
+            (begin
+              (canvasDrawRoundRect canvas
+                (new RectF (* r (float32 -1.05)) (* r (float32 -0.12))
+                  (* r (float32 0.15)) (* r (float32 0.12)))
+                (* r (float32 0.08)) (* r (float32 0.08)) proceduralHullPaint)
+              (canvasDrawRoundRect canvas
+                (new RectF (* r (float32 -1.08)) (* r (float32 -0.76))
+                  (* r (float32 0.10)) (* r (float32 -0.54)))
+                (* r (float32 0.10)) (* r (float32 0.10)) proceduralTrimPaint)
+              (canvasDrawRoundRect canvas
+                (new RectF (* r (float32 -1.08)) (* r (float32 0.54))
+                  (* r (float32 0.10)) (* r (float32 0.76)))
+                (* r (float32 0.10)) (* r (float32 0.10)) proceduralTrimPaint)
+              (canvasDrawOval canvas
+                (new RectF (* r (float32 -0.05)) (* r (float32 -0.64))
+                  (* r (float32 1.18)) (* r (float32 0.64))) proceduralHullPaint)
+              (canvasDrawCircle canvas (* r (float32 0.54)) (float32 0.0)
+                (* r (float32 0.10)) proceduralAccentPaint)))
+          (def (drawWedgeMiniature (canvas : Canvas) (r : Float32)) : Unit
+            (modifiers private)
+            (let ((path (new Path)))
+              (begin
+                (pathMoveTo path (* r (float32 1.18)) (float32 0.0))
+                (pathLineTo path (* r (float32 -1.05)) (* r (float32 -0.72)))
+                (pathLineTo path (* r (float32 -0.62)) (float32 0.0))
+                (pathLineTo path (* r (float32 -1.05)) (* r (float32 0.72)))
+                (pathClose path)
+                (canvasDrawPath canvas path proceduralHullPaint)
+                (canvasDrawCircle canvas (* r (float32 0.52)) (float32 0.0)
+                  (* r (float32 0.12)) proceduralAccentPaint))))
+          (def (drawDiamondMiniature (canvas : Canvas) (r : Float32)) : Unit
+            (modifiers private)
+            (let ((path (new Path)))
+              (begin
+                (pathMoveTo path (* r (float32 1.12)) (float32 0.0))
+                (pathLineTo path (float32 0.0) (* r (float32 -0.96)))
+                (pathLineTo path (* r (float32 -1.05)) (float32 0.0))
+                (pathLineTo path (float32 0.0) (* r (float32 0.96)))
+                (pathClose path)
+                (canvasDrawPath canvas path proceduralHullPaint)
+                (canvasDrawCircle canvas (float32 0.0) (float32 0.0)
+                  (* r (float32 0.13)) proceduralAccentPaint))))
+          (def (drawGenericMiniature (canvas : Canvas) (r : Float32)) : Unit
+            (modifiers private)
+            (let ((path (new Path)))
+              (begin
+                (pathMoveTo path (* r (float32 1.18)) (float32 0.0))
+                (pathLineTo path (* r (float32 0.16)) (* r (float32 -0.34)))
+                (pathLineTo path (* r (float32 -0.92)) (* r (float32 -0.46)))
+                (pathLineTo path (* r (float32 -0.56)) (float32 0.0))
+                (pathLineTo path (* r (float32 -0.92)) (* r (float32 0.46)))
+                (pathLineTo path (* r (float32 0.16)) (* r (float32 0.34)))
+                (pathClose path)
+                (canvasDrawPath canvas path proceduralHullPaint)
+                (canvasDrawLine canvas (* r (float32 -0.54)) (float32 0.0)
+                  (* r (float32 0.72)) (float32 0.0) proceduralStrokePaint)
+                (canvasDrawCircle canvas (* r (float32 0.44)) (float32 0.0)
+                  (* r (float32 0.11)) proceduralAccentPaint))))
+          (def (drawAndromedanMiniature
+                 (canvas : Canvas) (r : Float32) (livery : CardLivery)) : Unit
+            (modifiers private)
+            (let ((redPanelPaint
+                    (configureShipArtPaint (new Paint (paintAntiAliasFlag))
+                      (colorRgb (int32 160) (int32 45) (int32 50))
+                      (paintStyleFill) (float32 0.0)))
+                  (redGridPaint
+                    (configureShipStrokePaint (new Paint (paintAntiAliasFlag))
+                      (colorRgb (int32 238) (int32 92) (int32 88)) (float32 1.6)))
+                  (yellowTrimPaint
+                    (configureShipStrokePaint (new Paint (paintAntiAliasFlag))
+                      (CardLivery-accent livery) (float32 3.0)))
+                  (silverBandPaint
+                    (configureShipStrokePaint (new Paint (paintAntiAliasFlag))
+                      (CardLivery-trim livery) (float32 5.0)))
+                  (outer (new Path)))
+              (begin
+                (pathMoveTo outer (* r (float32 1.05)) (float32 0.0))
+                (pathCubicTo outer (* r (float32 0.90)) (* r (float32 -0.62))
+                  (* r (float32 0.34)) (* r (float32 -0.96))
+                  (* r (float32 -0.36)) (* r (float32 -0.86)))
+                (pathCubicTo outer (* r (float32 -1.02)) (* r (float32 -0.74))
+                  (* r (float32 -1.10)) (* r (float32 -0.30))
+                  (* r (float32 -0.94)) (float32 0.0))
+                (pathCubicTo outer (* r (float32 -1.10)) (* r (float32 0.30))
+                  (* r (float32 -1.02)) (* r (float32 0.74))
+                  (* r (float32 -0.36)) (* r (float32 0.86)))
+                (pathCubicTo outer (* r (float32 0.34)) (* r (float32 0.96))
+                  (* r (float32 0.90)) (* r (float32 0.62))
+                  (* r (float32 1.05)) (float32 0.0))
+                (pathClose outer)
+                (canvasDrawPath canvas outer proceduralHullPaint)
+                (let ((top (new RectF (* r (float32 -0.78)) (* r (float32 -0.84))
+                              (* r (float32 -0.24)) (* r (float32 -0.52))))
+                      (bottom (new RectF (* r (float32 -0.78)) (* r (float32 0.52))
+                                 (* r (float32 -0.24)) (* r (float32 0.84)))))
+                  (begin
+                    (canvasDrawRoundRect canvas top (* r (float32 0.05))
+                      (* r (float32 0.05)) redPanelPaint)
+                    (canvasDrawRoundRect canvas bottom (* r (float32 0.05))
+                      (* r (float32 0.05)) redPanelPaint)
+                    (for/fold ((ignored (int32 0)))
+                              ((i (in-range (int32 1) (int32 5))))
+                      (let ((gx (+ (rectFLeft top)
+                                   (/ (* (rectFWidth top) (float32 i)) (float32 5.0)))))
+                        (begin
+                          (canvasDrawLine canvas gx (rectFTop top) gx (rectFBottom top) redGridPaint)
+                          (canvasDrawLine canvas gx (rectFTop bottom) gx (rectFBottom bottom) redGridPaint)
+                          ignored))))
+                (canvasDrawArc canvas
+                  (new RectF (* r (float32 0.24)) (* r (float32 -0.72))
+                    (* r (float32 1.02)) (* r (float32 -0.10)))
+                  (float32 205.0) (float32 120.0) #f silverBandPaint)
+                (canvasDrawArc canvas
+                  (new RectF (* r (float32 0.24)) (* r (float32 0.10))
+                    (* r (float32 1.02)) (* r (float32 0.72)))
+                  (float32 35.0) (float32 120.0) #f silverBandPaint)
+                (canvasDrawArc canvas
+                  (new RectF (* r (float32 -0.58)) (* r (float32 -0.58))
+                    (* r (float32 0.58)) (* r (float32 0.58)))
+                  (float32 -38.0) (float32 76.0) #f yellowTrimPaint)
+                (canvasDrawOval canvas
+                  (new RectF (* r (float32 -0.64)) (* r (float32 -0.56))
+                    (* r (float32 0.64)) (* r (float32 0.56))) proceduralHullPaint)
+                (canvasDrawCircle canvas (float32 0.0) (float32 0.0)
+                  (* r (float32 0.12)) proceduralAccentPaint)))))
+
+          (def (cardField (json : (Nullable JSONObject)) (key : String)) : String
+            (modifiers private)
+            (if (nullable-null? json) ""
+              (jsonString (nullable-get json) key "")))
+          (def (cardRaceText (ship : (Nullable JSONObject))) : String
+            (modifiers private)
+            (if (nullable-null? ship) ""
+              (let ((json (nullable-get ship)))
+                (let ((props (jsonObjectChild json "properties")))
+                  (lowercaseText
+                    (string-append (cardField props "race")
+                      (string-append " " (string-append (cardField props "empire")
+                        (string-append " " (string-append (cardField props "ship-group")
+                          (string-append " " (string-append (cardField props "display-name")
+                            (string-append " " (string-append (cardField props "title-candidate")
+                              (string-append " " (string-append (cardField props "ship-type")
+                                (string-append " " (string-append (cardField props "source")
+                                  (string-append " " (string-append (cardField props "source-module")
+                                    (string-append " " (string-append (jsonString json "template-id" "")
+                                      (string-append " " (string-append (jsonString json "display-name" "")
+                                        (string-append " " (string-append (jsonString json "ship-key" "")
+                                          (string-append " " (string-append (jsonString json "def-path" "")
+                                            (string-append " " (string-append (jsonString json "id" "")
+                                              (string-append " " (string-append (jsonString json "name" "")
+                                                (string-append " " (string-append (shipLabel ship "")
+                                                  (string-append " " (shipTechnical ship ""))))))))))))))))))))))))))))))))))))
+          (def (paddedCardText (text : String)) : String
+            (modifiers private)
+            (string-append " " (string-append text " ")))
+          (def (cardRaceKey (ship : (Nullable JSONObject))) : String
+            (modifiers private)
+            (let ((text (cardRaceText ship)))
+              (let ((padded (paddedCardText text)))
+                (if (or (string-contains? text "andromedan") (string-contains? text "andro")) "andromedan"
+                  (if (or (string-contains? text "archeo-tholian") (string-contains? text "archeo tholian")) "archeo-tholian"
+                    (if (or (string-contains? text "oriontigerheart") (string-contains? text "tigerheart")) "orion"
+                      (if (string-contains? text "peladine") "peladine"
+                        (if (string-contains? text "juggernaut") "juggernaut"
+                          (if (string-contains? text "monster") "monster"
+                            (if (string-contains? text "frax") "frax"
+                              (if (string-contains? text "jindarian") "jindarian"
+                                (if (string-contains? text "vudar") "vudar"
+                                  (if (string-contains? text "dragon") "dragon"
+                                    (if (string-contains? text "flivver") "flivver"
+                                      (if (or (string-contains? padded " ssd truth ") (string-contains? text "ssd-truth")) "ssd-truth"
+                                        (if (string-contains? text "maesron") "maesron"
+                                          (if (string-contains? text "koligahr") "koligahr"
+                                            (if (string-contains? text "trobrin") "trobrin"
+                                              (if (or (string-contains? text "vari combine") (string-contains? padded " vari ")) "vari"
+                                                (if (string-contains? text "probr") "probr"
+                                                  (if (string-contains? text "drex") "drex"
+                                                    (if (string-contains? text "alunda") "alunda"
+                                                      (if (string-contains? text "loriyill") "loriyill"
+                                                        (if (string-contains? text "iridani") "iridani"
+                                                          (if (string-contains? text "ymatrian") "ymatrian"
+                                                            (if (string-contains? text "worb") "worb"
+                                                              (if (or (string-contains? text "aurora") (string-contains? text "federal republic")) "aurora"
+                                                                (if (string-contains? text "singer") "singer"
+                                                                  (if (string-contains? text "ryn") "ryn"
+                                                                    (if (string-contains? text "qixa") "qixa"
+                                                                      (if (string-contains? text "branthodon") "branthodon"
+                                                                        (if (or (string-contains? text "federation") (string-contains? text "fed-")
+                                                                                (string-contains? padded " fed ") (string-contains? padded " starfleet ")) "federation"
+                                                                          (if (or (string-contains? text "klingon") (string-contains? text "kli-")
+                                                                                  (string-contains? padded " kli ")) "klingon"
+                                                                            (if (or (string-contains? text "romulan") (string-contains? text "rom-")
+                                                                                    (string-contains? padded " rom ")) "romulan"
+                                                                              (if (or (string-contains? text "kzinti") (string-contains? text "kzinti-")
+                                                                                      (string-contains? text "kzin-") (string-contains? padded " kzn ")
+                                                                                      (string-contains? text "mirak")) "kzinti"
+                                                                                (if (string-contains? text "tholian") "tholian"
+                                                                                  (if (string-contains? text "seltorian") "seltorian"
+                                                                                    (if (string-contains? text "gorn") "gorn"
+                                                                                      (if (string-contains? text "hydran") "hydran"
+                                                                                        (if (or (string-contains? padded " ldr ") (string-contains? text "ldr-")) "ldr"
+                                                                                          (if (or (string-contains? text "lyran") (string-contains? text "lyran-")
+                                                                                                  (string-contains? text "lyr-")) "lyran"
+                                                                                            (if (or (string-contains? text "isc")
+                                                                                                    (string-contains? text "inter-stellar concordium")) "isc"
+                                                                                              (if (string-contains? text "orion") "orion"
+                                                                                                (if (or (string-contains? padded " wyn ")
+                                                                                                        (string-contains? text "wyn-")) "wyn" "generic"))))))))))))))))))))))))))))))))))))))))))))
+
+          (def (cardLivery (race : String)) : CardLivery
+            (modifiers private)
+            (if (or (equal? race "federation") (equal? race "aurora"))
+              (make-CardLivery (colorRgb (int32 238) (int32 240) (int32 236))
+                (colorRgb (int32 56) (int32 114) (int32 177)) (colorRgb (int32 190) (int32 54) (int32 48)))
+              (if (equal? race "klingon")
+                (make-CardLivery (colorRgb (int32 82) (int32 96) (int32 91))
+                  (colorRgb (int32 68) (int32 132) (int32 84)) (colorRgb (int32 190) (int32 58) (int32 46)))
+                (if (equal? race "romulan")
+                  (make-CardLivery (colorRgb (int32 186) (int32 192) (int32 184))
+                    (colorRgb (int32 42) (int32 48) (int32 47)) (colorRgb (int32 173) (int32 42) (int32 42)))
+                  (if (equal? race "kzinti")
+                    (make-CardLivery (colorRgb (int32 204) (int32 127) (int32 45))
+                      (colorRgb (int32 105) (int32 62) (int32 37)) (colorRgb (int32 239) (int32 196) (int32 78)))
+                    (if (equal? race "gorn")
+                      (make-CardLivery (colorRgb (int32 98) (int32 139) (int32 91))
+                        (colorRgb (int32 168) (int32 182) (int32 148)) (colorRgb (int32 55) (int32 91) (int32 82)))
+                      (if (equal? race "andromedan")
+                        (make-CardLivery (colorRgb (int32 64) (int32 126) (int32 76))
+                          (colorRgb (int32 176) (int32 184) (int32 188)) (colorRgb (int32 236) (int32 198) (int32 62)))
+                        (if (or (equal? race "tholian") (equal? race "archeo-tholian"))
+                          (make-CardLivery (colorRgb (int32 182) (int32 105) (int32 50))
+                            (colorRgb (int32 236) (int32 184) (int32 72)) (colorRgb (int32 168) (int32 57) (int32 43)))
+                          (if (equal? race "seltorian")
+                            (make-CardLivery (colorRgb (int32 68) (int32 133) (int32 74))
+                              (colorRgb (int32 62) (int32 112) (int32 186)) (colorRgb (int32 216) (int32 81) (int32 47)))
+                            (if (or (equal? race "hydran") (equal? race "vari") (equal? race "qixa"))
+                              (make-CardLivery (colorRgb (int32 112) (int32 78) (int32 151))
+                                (colorRgb (int32 230) (int32 188) (int32 70)) (colorRgb (int32 67) (int32 145) (int32 197)))
+                              (if (or (equal? race "lyran") (equal? race "koligahr") (equal? race "branthodon"))
+                                (make-CardLivery (colorRgb (int32 170) (int32 133) (int32 74))
+                                  (colorRgb (int32 239) (int32 194) (int32 91)) (colorRgb (int32 84) (int32 61) (int32 41)))
+                                (if (or (equal? race "ldr") (equal? race "wyn"))
+                                  (make-CardLivery (colorRgb (int32 238) (int32 238) (int32 230))
+                                    (colorRgb (int32 45) (int32 77) (int32 150)) (colorRgb (int32 196) (int32 52) (int32 46)))
+                                  (if (or (equal? race "isc") (equal? race "maesron")
+                                          (equal? race "loriyill") (equal? race "worb"))
+                                    (make-CardLivery (colorRgb (int32 57) (int32 107) (int32 170))
+                                      (colorRgb (int32 32) (int32 57) (int32 111)) (colorRgb (int32 210) (int32 78) (int32 48)))
+                                    (if (or (equal? race "orion") (equal? race "alunda")
+                                            (equal? race "singer") (equal? race "ryn"))
+                                      (make-CardLivery (colorRgb (int32 38) (int32 49) (int32 45))
+                                        (colorRgb (int32 181) (int32 49) (int32 44)) (colorRgb (int32 64) (int32 128) (int32 205)))
+                                      (if (equal? race "peladine")
+                                        (make-CardLivery (colorRgb (int32 104) (int32 146) (int32 190))
+                                          (colorRgb (int32 210) (int32 217) (int32 218)) (colorRgb (int32 229) (int32 180) (int32 68)))
+                                        (if (or (equal? race "juggernaut") (equal? race "monster") (equal? race "dragon"))
+                                          (make-CardLivery (colorRgb (int32 151) (int32 37) (int32 34))
+                                            (colorRgb (int32 42) (int32 45) (int32 48)) (colorRgb (int32 228) (int32 126) (int32 44)))
+                                          (if (equal? race "frax")
+                                            (make-CardLivery (colorRgb (int32 121) (int32 138) (int32 156))
+                                              (colorRgb (int32 42) (int32 85) (int32 132)) (colorRgb (int32 204) (int32 65) (int32 54)))
+                                            (if (equal? race "jindarian")
+                                              (make-CardLivery (colorRgb (int32 126) (int32 111) (int32 91))
+                                                (colorRgb (int32 83) (int32 72) (int32 61)) (colorRgb (int32 214) (int32 168) (int32 82)))
+                                              (if (equal? race "vudar")
+                                                (make-CardLivery (colorRgb (int32 76) (int32 86) (int32 108))
+                                                  (colorRgb (int32 205) (int32 174) (int32 67)) (colorRgb (int32 121) (int32 78) (int32 150)))
+                                                (if (equal? race "flivver")
+                                                  (make-CardLivery (colorRgb (int32 95) (int32 122) (int32 134))
+                                                    (colorRgb (int32 204) (int32 188) (int32 93)) (colorRgb (int32 177) (int32 73) (int32 49)))
+                                                  (if (or (equal? race "trobrin") (equal? race "probr")
+                                                          (equal? race "drex") (equal? race "ymatrian"))
+                                                    (make-CardLivery (colorRgb (int32 133) (int32 70) (int32 60))
+                                                      (colorRgb (int32 218) (int32 122) (int32 66)) (colorRgb (int32 88) (int32 159) (int32 190)))
+                                                    (if (or (equal? race "ssd-truth") (equal? race "generic"))
+                                                      (make-CardLivery (colorRgb (int32 154) (int32 165) (int32 178))
+                                                        (colorRgb (int32 83) (int32 102) (int32 128)) (colorRgb (int32 210) (int32 143) (int32 56)))
+                                                      (make-CardLivery (colorRgb (int32 120) (int32 132) (int32 146))
+                                                        (colorRgb (int32 80) (int32 96) (int32 118))
+                                                        (colorRgb (int32 208) (int32 145) (int32 58)))))))))))))))))))))))))
           (def (drawBitmapCover
                  (canvas : Canvas)
                  (bitmap : Bitmap)
diff --git a/templates/original-tactics-parts/SfcWeaponEffects.ss b/templates/original-tactics-parts/SfcWeaponEffects.ss
new file mode 100644
index 0000000..a3bfba2
--- /dev/null
+++ b/templates/original-tactics-parts/SfcWeaponEffects.ss
@@ -0,0 +1,753 @@
+(import (jerboa prelude))
+
+(def fragment
+  '((typed-kotlin-file "com/jerboa/originaltactics/SfcWeaponEffects.kt"
+      (kotlin-imports
+        (java util Locale)
+        (org json JSONArray)
+        (org json JSONObject))
+      (typed-library (com jerboa originaltactics)
+        (export SfcWeaponEffectKind SfcWeaponEffects)
+        (type Any)
+        (type Int32)
+        (type JSONArray)
+        (type JSONObject)
+        (type Locale)
+        (type Number)
+        (type SfcSound)
+
+        (enum SfcWeaponEffectKind ()
+          (PHASER) (PHASER_1) (PHASER_2) (PHASER_3) (PHASER_4)
+          (PHASER_G) (PHASER_X) (PHOTON) (PHOTON_OVERLOAD)
+          (PLASMA) (PLASMA_BOLT) (DISRUPTOR) (DISRUPTOR_OVERLOAD)
+          (HELLBORE) (FUSION) (ESG) (TR_BEAM) (MAULER) (PPD) (DRONE)
+          (WEB) (TRACTOR) (TRANSPORTER) (STASIS) (DISPLACEMENT)
+          (PA_PANEL) (DISENGAGEMENT) (MINE) (BREAKDOWN) (CLOAK) (GENERIC))
+        (record SfcWeaponEffectCue
+          ((actor : String)
+           (target : String)
+           (kind : SfcWeaponEffectKind)
+           (damage : Int32)
+           (hits : Int32)
+           (label : String)
+           (enveloping : Bool)))
+
+        ;; Kotlin/Android boundary declarations.  All classification and cue
+        ;; construction below remains ordinary typed Jerboa.
+        (extern (fxLocaleUs) : Locale (kotlin-value Locale US))
+        (extern (fxLowerLocale (value : String) (locale : Locale)) : String
+          (kotlin-member-call lowercase))
+        (extern (fxReplace (value : String) (old : String) (next : String)) : String
+          (kotlin-member-call replace))
+        (extern (fxStartsWith (value : String) (prefix : String)) : Bool
+          (kotlin-member-call startsWith))
+        (extern (fxEndsWith (value : String) (suffix : String)) : Bool
+          (kotlin-member-call endsWith))
+        (extern (fxStringCompare (value : String) (other : String)) : Int32
+          (kotlin-member-call compareTo))
+        (extern (fxAnyText (value : Any)) : String (kotlin-member-call toString))
+        (extern (fxAsNumber (value : Any)) : (Nullable Number) (kotlin-safe-cast Number))
+        (extern (fxAsString (value : Any)) : (Nullable String) (kotlin-safe-cast String))
+        (extern (fxNumberInt (value : Number)) : Int32 (kotlin-member-call toInt))
+        (extern (fxStringInt (value : String)) : (Nullable Int32) (kotlin-member-call toIntOrNull))
+        (extern (fxKindName (value : SfcWeaponEffectKind)) : String (kotlin-member-get name))
+        (extern (fxJsonLength (value : JSONArray)) : Int32 (kotlin-member-call length))
+        (extern (fxJsonObjectAt (value : JSONArray) (index : Int32)) : (Nullable JSONObject)
+          (kotlin-member-call optJSONObject))
+        (extern (fxJsonObject (value : JSONObject) (key : String)) : (Nullable JSONObject)
+          (kotlin-member-call optJSONObject))
+        (extern (fxJsonArray (value : JSONObject) (key : String)) : (Nullable JSONArray)
+          (kotlin-member-call optJSONArray))
+        (extern (fxJsonString (value : JSONObject) (key : String) (fallback : String)) : String
+          (kotlin-member-call optString))
+        (extern (fxJsonBool (value : JSONObject) (key : String) (fallback : Bool)) : Bool
+          (kotlin-member-call optBoolean))
+        (extern (fxJsonAny (value : JSONObject) (key : String)) : (Nullable Any)
+          (kotlin-member-call opt))
+        (extern (fxJsonText (value : JSONObject)) : String (kotlin-member-call toString))
+        (extern (fxArrayPutObject (value : JSONArray) (item : JSONObject)) : JSONArray
+          (kotlin-member-call put))
+        (extern (fxObjectPutString (value : JSONObject) (key : String) (item : String)) : JSONObject
+          (kotlin-member-call put))
+        (extern (fxObjectPutObject (value : JSONObject) (key : String) (item : JSONObject)) : JSONObject
+          (kotlin-member-call put))
+        (extern (fxAsJsonObject (value : Any)) : (Nullable JSONObject)
+          (kotlin-safe-cast JSONObject))
+        (extern (fxListTakeCue (value : (List SfcWeaponEffectCue)) (count : Int32))
+          : (List SfcWeaponEffectCue) (kotlin-member-call take))
+        (extern (fxListTakeSound (value : (List SfcSound)) (count : Int32))
+          : (List SfcSound) (kotlin-member-call take))
+        (extern (fxSoundContains (value : (MutableList SfcSound)) (sound : SfcSound)) : Bool
+          (kotlin-member-call contains))
+        (extern (fxCueAddAt (value : (MutableList SfcWeaponEffectCue)) (index : Int32)
+                            (cue : SfcWeaponEffectCue)) : Unit
+          (kotlin-member-call add))
+
+        (extern (fxPhaser) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHASER))
+        (extern (fxPhaser1) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHASER_1))
+        (extern (fxPhaser2) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHASER_2))
+        (extern (fxPhaser3) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHASER_3))
+        (extern (fxPhaser4) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHASER_4))
+        (extern (fxPhaserG) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHASER_G))
+        (extern (fxPhaserX) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHASER_X))
+        (extern (fxPhoton) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHOTON))
+        (extern (fxPhotonOverload) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PHOTON_OVERLOAD))
+        (extern (fxPlasma) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PLASMA))
+        (extern (fxPlasmaBolt) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PLASMA_BOLT))
+        (extern (fxDisruptor) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind DISRUPTOR))
+        (extern (fxDisruptorOverload) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind DISRUPTOR_OVERLOAD))
+        (extern (fxHellbore) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind HELLBORE))
+        (extern (fxFusion) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind FUSION))
+        (extern (fxEsg) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind ESG))
+        (extern (fxTrBeam) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind TR_BEAM))
+        (extern (fxMauler) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind MAULER))
+        (extern (fxPpd) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PPD))
+        (extern (fxDrone) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind DRONE))
+        (extern (fxWeb) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind WEB))
+        (extern (fxTractor) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind TRACTOR))
+        (extern (fxTransporter) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind TRANSPORTER))
+        (extern (fxStasis) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind STASIS))
+        (extern (fxDisplacement) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind DISPLACEMENT))
+        (extern (fxPaPanel) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind PA_PANEL))
+        (extern (fxDisengagement) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind DISENGAGEMENT))
+        (extern (fxMine) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind MINE))
+        (extern (fxBreakdown) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind BREAKDOWN))
+        (extern (fxCloak) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind CLOAK))
+        (extern (fxGeneric) : SfcWeaponEffectKind (kotlin-value SfcWeaponEffectKind GENERIC))
+
+        (extern (sndPhaser) : SfcSound (kotlin-value SfcSound PHASER))
+        (extern (sndPhaser1) : SfcSound (kotlin-value SfcSound PHASER_1))
+        (extern (sndPhaser2) : SfcSound (kotlin-value SfcSound PHASER_2))
+        (extern (sndPhaser3) : SfcSound (kotlin-value SfcSound PHASER_3))
+        (extern (sndPhaser4) : SfcSound (kotlin-value SfcSound PHASER_4))
+        (extern (sndPhaserG) : SfcSound (kotlin-value SfcSound PHASER_G))
+        (extern (sndPhaserG2) : SfcSound (kotlin-value SfcSound PHASER_G2))
+        (extern (sndPhaserX) : SfcSound (kotlin-value SfcSound PHASER_X))
+        (extern (sndPhaserHeavy) : SfcSound (kotlin-value SfcSound PHASER_HEAVY))
+        (extern (sndPhotonSfc2) : SfcSound (kotlin-value SfcSound PHOTON_SFC2))
+        (extern (sndPhotonHeavy) : SfcSound (kotlin-value SfcSound PHOTON_HEAVY))
+        (extern (sndPlasmaSeek) : SfcSound (kotlin-value SfcSound PLASMA_SEEK))
+        (extern (sndPlasmaFast) : SfcSound (kotlin-value SfcSound PLASMA_FAST))
+        (extern (sndPlasmaLarge) : SfcSound (kotlin-value SfcSound PLASMA_LARGE))
+        (extern (sndPlasmaEnveloping) : SfcSound (kotlin-value SfcSound PLASMA_ENVELOPING))
+        (extern (sndPlasmaSnare) : SfcSound (kotlin-value SfcSound PLASMA_SNARE))
+        (extern (sndDisruptor) : SfcSound (kotlin-value SfcSound DISRUPTOR))
+        (extern (sndDisruptorHeavy) : SfcSound (kotlin-value SfcSound DISRUPTOR_HEAVY))
+        (extern (sndDrone) : SfcSound (kotlin-value SfcSound DRONE))
+        (extern (sndDroneFast) : SfcSound (kotlin-value SfcSound DRONE_FAST))
+        (extern (sndHellbore) : SfcSound (kotlin-value SfcSound HELLBORE))
+        (extern (sndHellboreHeavy) : SfcSound (kotlin-value SfcSound HELLBORE_HEAVY))
+        (extern (sndHellboreImpact) : SfcSound (kotlin-value SfcSound HELLBORE_IMPACT))
+        (extern (sndFusion) : SfcSound (kotlin-value SfcSound FUSION))
+        (extern (sndFusionShort) : SfcSound (kotlin-value SfcSound FUSION_SHORT))
+        (extern (sndEsg) : SfcSound (kotlin-value SfcSound ESG))
+        (extern (sndEsgLoop) : SfcSound (kotlin-value SfcSound ESG_LOOP))
+        (extern (sndEsgEnd) : SfcSound (kotlin-value SfcSound ESG_END))
+        (extern (sndTrBeam) : SfcSound (kotlin-value SfcSound TR_BEAM))
+        (extern (sndMauler) : SfcSound (kotlin-value SfcSound MAULER))
+        (extern (sndPpdImpact) : SfcSound (kotlin-value SfcSound PPD_IMPACT))
+        (extern (sndPpdSend) : SfcSound (kotlin-value SfcSound PPD_SEND))
+        (extern (sndPpdSeek) : SfcSound (kotlin-value SfcSound PPD_SEEK))
+        (extern (sndWeb) : SfcSound (kotlin-value SfcSound WEB))
+        (extern (sndWebBreak) : SfcSound (kotlin-value SfcSound WEB_BREAK))
+        (extern (sndTractor) : SfcSound (kotlin-value SfcSound TRACTOR))
+        (extern (sndTransporter) : SfcSound (kotlin-value SfcSound TRANSPORTER))
+        (extern (sndMineDischarge) : SfcSound (kotlin-value SfcSound MINE_DISCHARGE))
+        (extern (sndMissExplosion) : SfcSound (kotlin-value SfcSound MISS_EXPLOSION))
+        (extern (sndBreakdown) : SfcSound (kotlin-value SfcSound BREAKDOWN))
+        (extern (sndCloak) : SfcSound (kotlin-value SfcSound CLOAK))
+        (extern (sndDecloak) : SfcSound (kotlin-value SfcSound DECLOAK))
+        (extern (sndShieldHeavy) : SfcSound (kotlin-value SfcSound SHIELD_HEAVY))
+        (extern (sndConfirm) : SfcSound (kotlin-value SfcSound CONFIRM))
+        (extern (sndCancel) : SfcSound (kotlin-value SfcSound CANCEL))
+
+        (def (fxTextHas (text : String) (needle : String)) : Bool
+          (string-contains? text needle))
+        (def (fxLower (value : String)) : String
+          (fxLowerLocale value (fxLocaleUs)))
+        (def (fxAnyTextOrEmpty (value : (Nullable Any))) : String
+          (if (nullable-null? value) "" (fxAnyText (nullable-get value))))
+        (def (fxIntValue (value : (Nullable Any))) : Int32
+          (if (nullable-null? value) (int32 0)
+            (let ((item (nullable-get value)))
+              (let ((number (fxAsNumber item)))
+                (if (nullable-null? number)
+                  (let ((text (fxAsString item)))
+                    (if (nullable-null? text) (int32 0)
+                      (let ((parsed (fxStringInt (nullable-get text))))
+                        (if (nullable-null? parsed) (int32 0) (nullable-get parsed)))))
+                  (fxNumberInt (nullable-get number)))))))
+        (def (fxOpponent (actor : String)) : String
+          (if (equal? actor "alpha") "beta" "alpha"))
+        (def (fxSide (value : String) (fallback : String)) : String
+          (if (or (equal? value "alpha") (equal? value "beta")) value fallback))
+
+        (object SfcWeaponEffects
+          (def (recent (events : JSONArray)
+                       (max : Int32 (default (int32 8)))) : (List SfcWeaponEffectCue)
+            (let ((out (mutable-list-empty SfcWeaponEffectCue)))
+              (begin
+                (recentAt events (- (fxJsonLength events) (int32 1)) out max #t)
+                (fxListTakeCue out max))))
+          (def (recentAcross (events : JSONArray)
+                             (max : Int32 (default (int32 12)))) : (List SfcWeaponEffectCue)
+            (let ((out (mutable-list-empty SfcWeaponEffectCue)))
+              (begin
+                (recentAt events (- (fxJsonLength events) (int32 1)) out max #f)
+                (fxListTakeCue out max))))
+          (def (recentAt (events : JSONArray) (index : Int32)
+                         (out : (MutableList SfcWeaponEffectCue))
+                         (max : Int32) (oneEvent : Bool)) : Unit
+            (modifiers private)
+            (if (or (< index (int32 0)) (>= (list-size out) max))
+              (begin)
+              (let ((event (fxJsonObjectAt events index)))
+                (if (nullable-null? event)
+                  (recentAt events (- index (int32 1)) out max oneEvent)
+                  (begin
+                    (appendEventCues out (nullable-get event) max)
+                    (if (and oneEvent (> (list-size out) (int32 0)))
+                      (begin)
+                      (recentAt events (- index (int32 1)) out max oneEvent)))))))
+          (def (signature (cues : (List SfcWeaponEffectCue))) : String
+            (signatureAt cues (int32 0) ""))
+          (def (signatureAt (cues : (List SfcWeaponEffectCue)) (index : Int32)
+                            (out : String)) : String
+            (modifiers private)
+            (if (>= index (list-size cues)) out
+              (let ((cue (list-ref cues index)))
+                (let ((separator (if (> index (int32 0)) "|" "")))
+                  (signatureAt cues (+ index (int32 1))
+                    (string-append out
+                      (string-append separator (cueSignature cue))))))))
+          (def (cueSignature (cue : SfcWeaponEffectCue)) : String
+            (modifiers private)
+            (string-append (SfcWeaponEffectCue-actor cue)
+              (string-append ":" (string-append (SfcWeaponEffectCue-target cue)
+                (string-append ":" (string-append (fxKindName (SfcWeaponEffectCue-kind cue))
+                  (string-append ":" (string-append
+                    (int32->string (SfcWeaponEffectCue-damage cue))
+                    (string-append ":" (string-append
+                      (int32->string (SfcWeaponEffectCue-hits cue))
+                      (string-append ":"
+                        (if (SfcWeaponEffectCue-enveloping cue) "true" "false"))))))))))))
+
+          (def (soundsForEvents (events : JSONArray)
+                                (max : Int32 (default (int32 4)))) : (List SfcSound)
+            (soundsForCues (recentAcross events (int32 24)) max))
+          (def (soundForWeaponPayload (payload : (Nullable JSONObject))) : SfcSound
+            (if (nullable-null? payload) (soundForText "")
+              (let ((wrapper (new JSONObject)) (events (new JSONArray)))
+                (begin
+                  (fxObjectPutString wrapper "type" "fire-weapons")
+                  (fxObjectPutObject wrapper "payload" (nullable-get payload))
+                  (fxArrayPutObject events wrapper)
+                  (let ((sounds (soundsForEvents events (int32 1))))
+                    (if (> (list-size sounds) (int32 0))
+                      (list-ref sounds (int32 0))
+                      (soundForText (fxJsonText (nullable-get payload)))))))))
+          (def (soundForSeekingPayload (payload : (Nullable JSONObject))) : SfcSound
+            (let ((text (if (nullable-null? payload) ""
+                          (fxLower (fxJsonText (nullable-get payload))))))
+              (if (and (fxTextHas text "ppd") (fxTextHas text "impact")) (sndPpdImpact)
+                (if (and (fxTextHas text "ppd")
+                         (or (fxTextHas text "seek") (fxTextHas text "pulse"))) (sndPpdSeek)
+                  (if (fxTextHas text "ppd") (sndPpdSend)
+                    (if (fxTextHas text "envelop") (sndPlasmaEnveloping)
+                      (if (or (fxTextHas text "plasma-r") (fxTextHas text "plasma-s")
+                              (fxTextHas text "plasma-g")) (sndPlasmaLarge)
+                        (if (or (fxTextHas text "plasma-f") (fxTextHas text "plasma-d")
+                                (fxTextHas text "plasma-fast")) (sndPlasmaFast)
+                          (if (fxTextHas text "plasma-snare") (sndPlasmaSnare)
+                            (if (fxTextHas text "plasma") (sndPlasmaSeek)
+                              (if (or (fxTextHas text "x-drone") (fxTextHas text "speed-32")
+                                      (fxTextHas text "fast")) (sndDroneFast)
+                                (if (or (fxTextHas text "drone") (fxTextHas text "missile"))
+                                  (sndDrone) (sndPlasmaSeek)))))))))))))
+
+          (def (soundsForCues (cues : (List SfcWeaponEffectCue)) (max : Int32)) : (List SfcSound)
+            (modifiers private)
+            (let ((ranked (mutable-list-empty SfcWeaponEffectCue))
+                  (distinct (mutable-list-empty SfcSound)))
+              (begin
+                (sortCuesAt cues (int32 0) ranked)