Complete typed tactics command and log views

ober

a753b398b1f2a6d8bec26910d91250ed241c99db

diff --git a/templates/original-tactics-parts/BattleCommandView.kt.ss b/templates/original-tactics-parts/BattleCommandView.kt.ss
index ad1b632..0606cf5 100644
--- a/templates/original-tactics-parts/BattleCommandView.kt.ss
+++ b/templates/original-tactics-parts/BattleCommandView.kt.ss
@@ -30,6 +30,8 @@
         (type JSONArray)
         (type JSONObject)
         (type Number)
+        (type List)
+        (type ProjectedRangePoint)
         (type Int32)
         (type Float32)
 
@@ -122,6 +124,22 @@
         (extern (bcvStringReplace (text : String) (old : String)
                                   (replacement : String)) : String
           (kotlin-member-call replace))
+        (extern (bcvStringCompare (text : String) (other : String)) : Int32
+          (kotlin-member-call compareTo))
+        (extern (bcvStringLowercase (text : String)) : String
+          (kotlin-member-call lowercase))
+        (extern (bcvStringStartsWith (text : String) (prefix : String)) : Bool
+          (kotlin-member-call startsWith))
+        (extern (bcvStringEndsWith (text : String) (suffix : String)) : Bool
+          (kotlin-member-call endsWith))
+        (extern (bcvStringTrim (text : String)) : String
+          (kotlin-member-call trim))
+        (extern (bcvStringSubstring (text : String) (start : Int32)) : String
+          (kotlin-member-call substring))
+        (extern (bcvObjectHas (object : JSONObject) (key : String)) : Bool
+          (kotlin-member-call has))
+        (extern (bcvObjectLength (object : JSONObject)) : Int32
+          (kotlin-member-call length))
         (extern (bcvJsonKeys (json : JSONObject)) : (Iterator String)
           (kotlin-member-call keys))
         (extern (bcvIteratorHasNext (items : (Iterator String))) : Bool
@@ -137,6 +155,27 @@
           (kotlin-call maneuverAbbreviation))
         (extern (bcvShipDisplayLabel (ship : (Nullable JSONObject))) : String
           (kotlin-call shipDisplayLabel))
+        (extern (bcvArrayPutString (array : JSONArray) (value : String)) : JSONArray
+          (kotlin-member-call put))
+        (extern (bcvProjectedRangeForecast
+                  (alpha : (Nullable JSONObject)) (beta : (Nullable JSONObject))
+                  (alphaManeuvers : (Nullable Any)) (betaManeuvers : (Nullable Any))
+                  (currentImpulse : Int32) (maxMoves : Int32)
+                  (labelLimit : Int32)) : (List ProjectedRangePoint)
+          (kotlin-call projectedRangeForecast))
+        (extern (bcvHexRangeFromPositions
+                  (alpha : (Nullable JSONObject))
+                  (beta : (Nullable JSONObject))) : (Nullable Int32)
+          (kotlin-call hexRangeFromPositions))
+        (extern (bcvListSize (items : (List ProjectedRangePoint))) : Int32
+          (kotlin-member-get size))
+        (extern (bcvListGet (items : (List ProjectedRangePoint))
+                            (index : Int32)) : ProjectedRangePoint
+          (kotlin-member-call get))
+        (extern (bcvPointImpulse (point : ProjectedRangePoint)) : Int32
+          (kotlin-member-get impulse))
+        (extern (bcvPointRange (point : ProjectedRangePoint)) : Int32
+          (kotlin-member-get range))
 
         (def (bcvConfigurePaint (paint : Paint) (color : Int32)
                                 (style : Style)
@@ -154,6 +193,9 @@
             (bcvPaintFakeBoldSet paint bold)
             paint))
         (def (bcvMaxInt (a : Int32) (b : Int32)) : Int32 (if (> a b) a b))
+        (def (bcvMinInt (a : Int32) (b : Int32)) : Int32 (if (< a b) a b))
+        (def (bcvClampInt (value : Int32) (low : Int32) (high : Int32)) : Int32
+          (if (< value low) low (if (> value high) high value)))
         (def (bcvMaxFloat (a : Float32) (b : Float32)) : Float32 (if (> a b) a b))
         (def (bcvMinFloat (a : Float32) (b : Float32)) : Float32 (if (< a b) a b))
         (def (bcvClampFloat (value : Float32) (low : Float32) (high : Float32)) : Float32
@@ -411,18 +453,35 @@
                                       "    A ")
                                     alpha)
                                   "    B ")
-                                beta)))
+                                beta))
+                            (forecastText
+                              (rangeForecastLine battle orderJson currentImpulse)))
                         (begin
                           (bcvCanvasDrawRoundRect canvas rect (float32 8.0)
                             (float32 8.0) panelPaint)
                           (bcvCanvasDrawRoundRect canvas rect (float32 8.0)
                             (float32 8.0) borderPaint)
-                          (bcvPaintTextAlignSet valuePaint (bcvPaintAlignCenter))
-                          (bcvCanvasDrawText canvas
-                            (fitText ordersText valuePaint
-                              (- (bcvRectWidth rect) (float32 18.0)))
-                            (bcvRectCenterX rect)
-                            (+ (bcvRectTop rect) (float32 24.0)) valuePaint))))))))
+                          (if (string-blank? forecastText)
+                              (begin
+                                (bcvPaintTextAlignSet valuePaint (bcvPaintAlignCenter))
+                                (bcvCanvasDrawText canvas
+                                  (fitText ordersText valuePaint
+                                    (- (bcvRectWidth rect) (float32 18.0)))
+                                  (bcvRectCenterX rect)
+                                  (+ (bcvRectTop rect) (float32 24.0)) valuePaint))
+                              (begin
+                                (bcvPaintTextAlignSet valuePaint (bcvPaintAlignLeft))
+                                (bcvCanvasDrawText canvas
+                                  (fitText ordersText valuePaint
+                                    (- (bcvRectWidth rect) (float32 18.0)))
+                                  (+ (bcvRectLeft rect) (float32 9.0))
+                                  (+ (bcvRectTop rect) (float32 16.0)) valuePaint)
+                                (bcvPaintTextAlignSet smallPaint (bcvPaintAlignLeft))
+                                (bcvCanvasDrawText canvas
+                                  (fitText forecastText smallPaint
+                                    (- (bcvRectWidth rect) (float32 18.0)))
+                                  (+ (bcvRectLeft rect) (float32 9.0))
+                                  (+ (bcvRectTop rect) (float32 32.0)) smallPaint))))))))))
 
           (def (drawShipCard (canvas : Canvas) (rect : RectF) (side : String)
                              (ship : (Nullable JSONObject))
@@ -487,15 +546,16 @@
                   (- (bcvRectWidth rect) (float32 18.0)) "HULL"
                   internalNow (maxInternals ship internalNow)))
               (let ((shieldNow (shieldTotal ship #f))
-                    (shieldMax (shieldTotal ship #t))
-                    (paPanels (counterValue (bcvNullableObjectAny ship "boxes") "52"))
+                    (shieldMax (bcvMaxInt (shieldTotal ship #t) (shieldTotal ship #f)))
+                    (paPanels (paPanelCount ship))
+                    (paCharge (paPanelCharge ship))
                     (paMax (paPanelMax ship)))
                 (if (and (<= shieldMax (int32 0))
                          (or (> paPanels (int32 0)) (> paMax (int32 1))))
                     (drawMetricBar canvas (+ (bcvRectLeft rect) (float32 9.0))
                       (+ (bcvRectTop rect) (float32 116.0))
-                      (- (bcvRectWidth rect) (float32 18.0)) "PA PANELS"
-                      paPanels paMax)
+                      (- (bcvRectWidth rect) (float32 18.0)) "PA CHG"
+                      paCharge paMax)
                     (drawMetricBar canvas (+ (bcvRectLeft rect) (float32 9.0))
                       (+ (bcvRectTop rect) (float32 116.0))
                       (- (bcvRectWidth rect) (float32 18.0)) "SHIELDS"
@@ -534,12 +594,20 @@
                         (bcvNullableObjectAny ship "direct-fire-cooldown")))
                     (launch
                       (counterTotal
-                        (bcvNullableObjectAny ship "seeking-launch-limits"))))
+                        (bcvNullableObjectAny ship "seeking-launch-limits")))
+                    (families (weaponFamilyLine statusActor))
+                    (primary (primaryWeaponLine statusActor)))
                 (bcvCanvasDrawText canvas
                   (fitText
-                    (string-append
-                      (string-append "COOL " (bcvIntText cool))
-                      (string-append "   LAUNCH " (bcvIntText launch)))
+                    (if (not (string-blank? families))
+                        (string-append families
+                          (string-append "   COOL " (bcvIntText cool)))
+                        (if (not (string-blank? primary))
+                            (string-append primary
+                              (string-append "   COOL " (bcvIntText cool)))
+                            (string-append
+                              (string-append "COOL " (bcvIntText cool))
+                              (string-append "   LAUNCH " (bcvIntText launch)))))
                     smallPaint (- (bcvRectWidth rect) (float32 18.0)))
                   (+ (bcvRectLeft rect) (float32 9.0))
                   (bcvMinFloat (- (bcvRectBottom rect) (float32 10.0))
@@ -588,10 +656,8 @@
               (bcvCanvasDrawText canvas "TACTICAL LOG"
                 (+ (bcvRectLeft rect) (float32 9.0))
                 (+ (bcvRectTop rect) (float32 18.0)) smallPaint)
-              (let ((start
-                      (bcvMaxInt (int32 0)
-                        (- (json-array-length events) (int32 3)))))
-                (if (= start (json-array-length events))
+              (let ((lines (recentEventLines (int32 3))))
+                (if (= (json-array-length lines) (int32 0))
                     (begin
                       (bcvPaintTextAlignSet labelPaint (bcvPaintAlignLeft))
                       (bcvCanvasDrawText canvas "No combat events yet"
@@ -599,26 +665,61 @@
                         (+ (bcvRectTop rect) (float32 42.0)) labelPaint))
                     (begin
                       (for/fold ((y (+ (bcvRectTop rect) (float32 40.0))))
-                                ((index (in-range start (json-array-length events))))
-                        (let ((event (json-array-opt-json-object events index)))
-                          (if (or (nullable-null? event)
-                                  (> y (- (bcvRectBottom rect) (float32 6.0))))
-                              y
+                                ((index (in-range (int32 0) (json-array-length lines))))
+                        (if (> y (- (bcvRectBottom rect) (float32 6.0))) y
+                            (begin
+                              (if (= index (- (json-array-length lines) (int32 1)))
+                                  (bcvCanvasDrawCircle canvas
+                                    (+ (bcvRectLeft rect) (float32 10.0))
+                                    (- y (float32 5.0)) (float32 4.0) accentPaint)
+                                  (begin))
+                              (bcvPaintTextAlignSet labelPaint (bcvPaintAlignLeft))
+                              (bcvCanvasDrawText canvas
+                                (fitText (json-array-opt-string lines index)
+                                  labelPaint (- (bcvRectWidth rect) (float32 28.0)))
+                                (+ (bcvRectLeft rect) (float32 20.0)) y labelPaint)
+                              (+ y (float32 19.0)))))
+                      (begin))))
+              (bcvPaintColorSet smallPaint
+                (bcvColorRgb (int32 165) (int32 177) (int32 195)))))
+          (def (recentEventLines (limit : Int32)) : JSONArray
+            (modifiers private)
+            (let ((backward (new JSONArray)) (out (new JSONArray)))
+              (begin
+                (recentEventLinesAt backward (- (json-array-length events) (int32 1)) limit)
+                (reverseEventLines backward out (- (json-array-length backward) (int32 1)))
+                out)))
+          (def (recentEventLinesAt (out : JSONArray) (index : Int32)
+                                   (limit : Int32)) : Unit
+            (modifiers private)
+            (if (or (< index (int32 0)) (>= (json-array-length out) limit)) (begin)
+                (let ((event (json-array-opt-json-object events index)))
+                  (if (nullable-null? event)
+                      (recentEventLinesAt out (- index (int32 1)) limit)
+                      (let ((kind (json-object-opt-string-default
+                                    (nullable-get event) "type" "")))
+                        (begin
+                          (if (or (string-blank? kind) (equal? kind "impulse-activity"))
+                              (begin)
                               (begin
-                                (bcvPaintTextAlignSet labelPaint (bcvPaintAlignLeft))
-                                (bcvCanvasDrawText canvas
-                                  (fitText (summarizeEvent (nullable-get event))
-                                    labelPaint (- (bcvRectWidth rect) (float32 28.0)))
-                                  (+ (bcvRectLeft rect) (float32 20.0)) y labelPaint)
-                                (+ y (float32 19.0))))))
-                      (begin))))))
+                                (bcvArrayPutString out (summarizeEvent (nullable-get event)))
+                                (begin)))
+                          (recentEventLinesAt out (- index (int32 1)) limit)))))))
+          (def (reverseEventLines (source : JSONArray) (out : JSONArray)
+                                  (index : Int32)) : Unit
+            (modifiers private)
+            (if (< index (int32 0)) (begin)
+                (begin
+                  (bcvArrayPutString out (json-array-opt-string source index))
+                  (reverseEventLines source out (- index (int32 1))))))
 
           (def (summarizeEvent (event : JSONObject)) : String
             (modifiers private)
             (let ((turn (json-object-opt-int32-default event "turn" (int32 -1)))
                   (impulse (json-object-opt-int32-default event "impulse" (int32 -1)))
                   (actor (json-object-opt-string-default event "actor" ""))
-                  (kind (json-object-opt-string-default event "type" "event")))
+                  (kind (json-object-opt-string-default event "type" "event"))
+                  (payload (json-object-opt-json-object event "payload")))
               (let ((prefix
                       (if (and (>= turn (int32 0)) (>= impulse (int32 0)))
                           (string-append
@@ -631,9 +732,266 @@
                       (if (string-blank? actor)
                           ""
                           (string-append " " (string-uppercase actor)))))
-                (string-append
-                  (string-append (string-append prefix actorText) " ")
-                  (bcvStringReplace kind "-" " ")))))
+                (let ((detail (eventDetail kind payload)))
+                  (string-append (string-append (string-append prefix actorText) " ") detail)))))
+
+          (def (eventDetail (kind : String)
+                            (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (if (equal? kind "fire-weapons") (fireSummary payload)
+                (if (equal? kind "seeking-launch") (launchSummary payload)
+                    (if (equal? kind "seeking-impact") (impactSummary payload)
+                        (if (or (equal? kind "movement") (equal? kind "turn")
+                                (equal? kind "sideslip") (equal? kind "high-energy-turn"))
+                            (movementSummary payload)
+                            (if (or (equal? kind "turn-blocked")
+                                    (equal? kind "maneuver-blocked")
+                                    (equal? kind "fire-control-blocked"))
+                                (blockedSummary payload)
+                                (if (or (equal? kind "ew-control") (equal? kind "ew-support")
+                                        (equal? kind "ew-support-rejected")
+                                        (equal? kind "ew-support-lost")
+                                        (equal? kind "lock-on-control")
+                                        (equal? kind "shield-control")
+                                        (equal? kind "energy-allocation")
+                                        (equal? kind "wild-swac"))
+                                    (controlSummary payload)
+                                    (if (or (equal? kind "web-created")
+                                            (equal? kind "web-reduced")
+                                            (equal? kind "web-caster")
+                                            (equal? kind "web-snare")
+                                            (equal? kind "web-breaker"))
+                                        (webSummary payload)
+                                        (if (equal? kind "turn-completed") "turn complete"
+                                            (if (equal? kind "orders-submitted") "orders committed"
+                                                (if (nullable-null? payload) kind
+                                                    (let ((reason
+                                                            (json-object-opt-string-default
+                                                              (nullable-get payload) "reason" kind)))
+                                                      (if (string-blank? reason) kind reason))))))))))))
+
+          )
+          (def (fireSummary (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (if (nullable-null? payload) "weapons fire"
+                (let ((summaries (json-object-opt-json-array
+                                   (nullable-get payload) "fire-target-summaries")))
+                  (let ((details (fireSummaryAt summaries (int32 0) "" (int32 0))))
+                    (if (not (string-blank? (BattlePlotText-text details)))
+                        (BattlePlotText-text details)
+                        (let ((alpha (bcvNullableObjectInt
+                                       (json-object-opt-json-object
+                                         (nullable-get payload) "alpha-volley")
+                                       "damage" (int32 0)))
+                              (beta (bcvNullableObjectInt
+                                      (json-object-opt-json-object
+                                        (nullable-get payload) "beta-volley")
+                                      "damage" (int32 0)))
+                              (range (let ((before (json-object-opt-any
+                                                    (nullable-get payload) "range-before")))
+                                       (if (nullable-null? before)
+                                           (let ((current (json-object-opt-any
+                                                            (nullable-get payload) "range")))
+                                             (if (nullable-null? current) "-"
+                                                 (bcvAnyToString (nullable-get current))))
+                                           (bcvAnyToString (nullable-get before))))))
+                          (string-append "weapons fire R"
+                            (string-append range
+                              (string-append " A:" (string-append (bcvIntText alpha)
+                                (string-append " B:" (bcvIntText beta))))))))))))
+          (def (fireSummaryAt (summaries : (Nullable JSONArray)) (index : Int32)
+                              (out : String) (count : Int32)) : BattlePlotText
+            (modifiers private)
+            (if (or (nullable-null? summaries)
+                    (>= index (json-array-length (nullable-get summaries)))
+                    (>= count (int32 2))) (make-BattlePlotText out count)
+                (let ((summary (json-array-opt-json-object (nullable-get summaries) index)))
+                  (if (nullable-null? summary)
+                      (fireSummaryAt summaries (+ index (int32 1)) out count)
+                      (let ((damage (bcvNumberValue (json-object-opt-any
+                                      (nullable-get summary) "damage")))
+                            (internals (bcvNumberValue (json-object-opt-any
+                                         (nullable-get summary) "internals"))))
+                        (if (and (<= damage (int32 0)) (<= internals (int32 0)))
+                            (fireSummaryAt summaries (+ index (int32 1)) out count)
+                            (let ((face (shieldFaceLabel
+                                          (json-object-opt-any (nullable-get summary)
+                                            "shield-facing"))))
+                              (let ((faceText (if (string-blank? face) ""
+                                                (string-append " "
+                                                  (string-append face
+                                                    (string-append " "
+                                                      (string-append (bcvIntText
+                                                        (bcvNumberValue (json-object-opt-any
+                                                          (nullable-get summary) "shield-before")))
+                                                        (string-append ">" (bcvIntText
+                                                          (bcvNumberValue (json-object-opt-any
+                                                            (nullable-get summary) "shield-after"))))))))))
+                                    (internalText (if (> internals (int32 0))
+                                                    (string-append " int " (bcvIntText internals)) "")))
+                                (let ((part
+                                        (string-append
+                                          (sideLabel (json-object-opt-string-default
+                                            (nullable-get summary) "attacker" ""))
+                                          (string-append ">"
+                                            (string-append (sideLabel
+                                              (json-object-opt-string-default
+                                                (nullable-get summary) "target" ""))
+                                              (string-append " dmg "
+                                                (string-append (bcvIntText damage)
+                                                  (string-append faceText internalText))))))))
+                                (fireSummaryAt summaries (+ index (int32 1))
+                                  (if (= count (int32 0)) part
+                                      (string-append out (string-append "  " part)))
+                                  (+ count (int32 1))))))))))))
+          (def (launchSummary (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (if (nullable-null? payload) "seeker launch"
+                (let ((seeker (json-object-opt-json-object
+                                (nullable-get payload) "seeker")))
+                  (if (nullable-null? seeker) "seeker launch"
+                      (let ((idValue (let ((id (json-object-opt-any
+                                                (nullable-get seeker) "id")))
+                                       (if (nullable-null? id)
+                                           (json-object-opt-any
+                                             (nullable-get seeker) "seeking-id") id))))
+                        (let ((idText (if (nullable-null? idValue) "?"
+                                          (bcvAnyToString (nullable-get idValue)))))
+                          (string-append "launch "
+                            (string-append (json-object-opt-string-default
+                              (nullable-get seeker) "kind" "seeker")
+                              (string-append " #"
+                                (string-append idText
+                                  (string-append " to "
+                                    (json-object-opt-string-default
+                                      (nullable-get seeker) "target" "?"))))))))))))
+          (def (impactSummary (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (if (nullable-null? payload) "seeker impact"
+                (let ((kind (json-object-opt-string-default (nullable-get payload) "kind"
+                              (json-object-opt-string-default
+                                (nullable-get payload) "seeker-kind" "seeker")))
+                      (target (json-object-opt-string-default
+                                (nullable-get payload) "target" "?"))
+                      (damage (json-object-opt-any (nullable-get payload) "damage"))
+                      (face (shieldFaceLabel (json-object-opt-any
+                              (nullable-get payload) "shield-facing"))))
+                  (let ((damageText (if (nullable-null? damage) "-"
+                                      (bcvAnyToString (nullable-get damage))))
+                        (faceText (if (string-blank? face) ""
+                                    (string-append " "
+                                      (string-append face
+                                        (string-append " "
+                                          (string-append (bcvIntText (bcvNumberValue
+                                            (json-object-opt-any (nullable-get payload) "shield-before")))
+                                            (string-append ">" (bcvIntText (bcvNumberValue
+                                              (json-object-opt-any (nullable-get payload) "shield-after")))))))))))
+                    (string-append kind
+                      (string-append " impact "
+                        (string-append damageText
+                          (string-append " on "
+                            (string-append target faceText))))))))
+          )
+          (def (movementSummary (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (if (nullable-null? payload) "maneuver"
+                (let ((after (json-object-opt-json-object
+                               (nullable-get payload) "position-after")))
+                  (if (nullable-null? after) "maneuver"
+                      (string-append "move q"
+                        (string-append (bcvIntText (json-object-opt-int32-default
+                          (nullable-get after) "q" (int32 0)))
+                          (string-append " r" (string-append
+                            (bcvIntText (json-object-opt-int32-default
+                              (nullable-get after) "r" (int32 0)))
+                            (string-append " f" (bcvIntText
+                              (json-object-opt-int32-default
+                                (nullable-get after) "facing" (int32 0)))))))))))
+          )
+          (def (blockedSummary (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (if (nullable-null? payload) "blocked"
+                (bcvStringTrim
+                  (string-append
+                    (string-uppercase (json-object-opt-string-default
+                      (nullable-get payload) "maneuver" ""))
+                    (string-append " " (bcvStringReplace
+                      (json-object-opt-string-default
+                        (nullable-get payload) "reason" "blocked") "-" " "))))))
+          (def (controlSummary (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (let ((text (payloadFieldSummary payload #t)))
+              (if (string-blank? text) "control" text)))
+          (def (webSummary (payload : (Nullable JSONObject))) : String
+            (modifiers private)
+            (let ((text (payloadFieldSummary payload #f)))
+              (if (string-blank? text) "web action" text)))
+          (def (payloadFieldSummary (payload : (Nullable JSONObject))
+                                    (control : Bool)) : String
+            (modifiers private)
+            (payloadFieldSummaryAt payload control (int32 0) "" (int32 0)))
+          (def (payloadFieldSummaryAt (payload : (Nullable JSONObject)) (control : Bool)
+                                      (index : Int32) (out : String)
+                                      (count : Int32)) : String
+            (modifiers private)
+            (if (or (nullable-null? payload) (>= count (int32 3))
+                    (>= index (if control (int32 10) (int32 7)))) out
+                (let ((key (payloadKeyAt control index)))
+                  (let ((value (json-object-opt-any (nullable-get payload) key)))
+                    (if (or (not (bcvObjectHas (nullable-get payload) key))
+                            (nullable-null? value))
+                        (payloadFieldSummaryAt payload control (+ index (int32 1)) out count)
+                        (payloadFieldSummaryAt payload control (+ index (int32 1))
+                          (let ((part (string-append (fieldLabel key)
+                                        (string-append " " (compactPayloadValue value)))))
+                            (if (= count (int32 0)) part
+                                (string-append out (string-append " " part))))
+                          (+ count (int32 1)))))))
+          )
+          (def (payloadKeyAt (control : Bool) (index : Int32)) : String
+            (modifiers private)
+            (if control
+                (if (= index (int32 0)) "mode"
+                    (if (= index (int32 1)) "reason"
+                        (if (= index (int32 2)) "ecm"
+                            (if (= index (int32 3)) "eccm"
+                                (if (= index (int32 4)) "ew-shift"
+                                    (if (= index (int32 5)) "power"
+                                        (if (= index (int32 6)) "reserve-power"
+                                            (if (= index (int32 7)) "fire-control-active?"
+                                                (if (= index (int32 8)) "shield" "target")))))))))
+                (if (= index (int32 0)) "strength"
+                    (if (= index (int32 1)) "web-strength"
+                        (if (= index (int32 2)) "web-hexes"
+                            (if (= index (int32 3)) "hexes"
+                                (if (= index (int32 4)) "damage"
+                                    (if (= index (int32 5)) "range" "reason"))))))))
+          (def (fieldLabel (key : String)) : String
+            (modifiers private)
+            (string-uppercase
+              (bcvStringReplace (bcvStringReplace
+                (bcvStringReplace (if (bcvStringEndsWith key "?")
+                                    (string-take key (- (string-length-int32 key) (int32 1))) key)
+                  "-active" "") "shield-facing" "shield") "-" " ")))
+          (def (compactPayloadValue (value : (Nullable Any))) : String
+            (modifiers private)
+            (if (nullable-null? value) ""
+                (let ((object (bcvAsJsonObject (nullable-get value))))
+                  (if (not (nullable-null? object))
+                      (let ((q (json-object-opt-any (nullable-get object) "q"))
+                            (r (json-object-opt-any (nullable-get object) "r"))
+                            (facing (json-object-opt-any (nullable-get object) "facing")))
+                        (if (and (not (nullable-null? q)) (not (nullable-null? r)))
+                            (string-append "q" (string-append (bcvAnyToString (nullable-get q))
+                              (string-append "/r" (string-append (bcvAnyToString (nullable-get r))
+                                (string-append "/f" (if (nullable-null? facing) "-"
+                                  (bcvAnyToString (nullable-get facing))))))))
+                            (string-append "{" (string-append
+                              (bcvIntText (bcvObjectLength (nullable-get object))) "}"))))
+                      (let ((array (bcvAsJsonArray (nullable-get value))))
+                        (if (nullable-null? array)
+                            (bcvStringReplace (bcvAnyToString (nullable-get value)) "-" " ")
+                            (bcvIntText (json-array-length (nullable-get array)))))))))
 
           (def (maneuverPlotText (raw : (Nullable Any))
                                  (currentImpulse : Int32)
@@ -652,14 +1010,13 @@
                                 (if (string-blank? maneuver)
                                     out
                                     (make-BattlePlotText
-                                      (string-append
-                                        (BattlePlotText-text out)
-                                        (string-append
-                                          (if (= (BattlePlotText-count out) (int32 0)) "" "/")
-                                          (string-append
-                                            (string-append "I" (bcvIntText impulse))
-                                            (string-append " "
-                                              (bcvManeuverAbbreviation maneuver)))))
+                                      (let ((part (string-append
+                                                    (string-append "I" (bcvIntText impulse))
+                                                    (string-append " "
+                                                      (bcvManeuverAbbreviation maneuver)))))
+                                        (if (= (BattlePlotText-count out) (int32 0)) part
+                                            (string-append (BattlePlotText-text out)
+                                              (string-append "/" part))))
                                       (+ (BattlePlotText-count out) (int32 1)))))))))
                   (if (= (BattlePlotText-count state) (int32 0))
                       "CLEAR"
@@ -703,6 +1060,58 @@
                             (json-array-opt-string (nullable-get pair)
                               (int32 1))))))))
 
+          (def (rangeForecastLine (battle : JSONObject) (orders : JSONObject)
+                                  (currentImpulse : Int32)) : String
+            (modifiers private)
+            (let ((forecast (bcvProjectedRangeForecast
+                              (json-object-opt-json-object battle "alpha-position")
+                              (json-object-opt-json-object battle "beta-position")
+                              (json-object-opt-any orders "alpha-maneuvers")
+                              (json-object-opt-any orders "beta-maneuvers")
+                              currentImpulse (int32 8) (int32 5))))
+              (if (= (bcvListSize forecast) (int32 0)) ""
+                  (let ((currentRange (bcvHexRangeFromPositions
+                                        (json-object-opt-json-object battle "alpha-position")
+                                        (json-object-opt-json-object battle "beta-position")))
+                        (finalRange (bcvPointRange
+                                     (bcvListGet forecast
+                                       (- (bcvListSize forecast) (int32 1))))))
+                    (let ((trend (if (nullable-null? currentRange) "FORECAST"
+                                     (if (< finalRange (nullable-get currentRange)) "CLOSING"
+                                         (if (> finalRange (nullable-get currentRange)) "OPENING"
+                                             "RANGE")))))
+                      (string-append trend
+                        (string-append " " (forecastPoints forecast (int32 0) ""))))))))
+          (def (forecastPoints (forecast : (List ProjectedRangePoint))
+                               (index : Int32) (out : String)) : String
+            (modifiers private)
+            (if (or (>= index (bcvListSize forecast)) (>= index (int32 4))) out
+                (let ((point (bcvListGet forecast index)))
+                  (forecastPoints forecast (+ index (int32 1))
+                    (let ((part (string-append "I" (string-append
+                                  (bcvIntText (bcvPointImpulse point))
+                                  (string-append " R" (bcvIntText (bcvPointRange point)))))))
+                      (if (= index (int32 0)) part
+                          (string-append out (string-append "  " part))))))))
+          (def (maneuverEntries (raw : (Nullable Any))) : JSONArray
+            (modifiers private)
+            (let ((out (new JSONArray)))
+              (begin
+                (for/fold ((ignored (int32 0))) ((impulse (in-range (int32 1) (int32 33))))
+                  (if (nullable-null? raw) ignored
+                      (let ((maneuver (maneuverAt (nullable-get raw) impulse)))
+                        (begin
+                          (if (string-blank? maneuver) (begin)
+                              (begin
+                                (bcvArrayPutString out
+                                  (string-append (bcvIntText impulse)
+                                    (string-append ":" maneuver)))
+                                (begin)))
+                          ignored))))
+                out)))
+          (def (maneuverAbbrev (maneuver : String)) : String
+            (modifiers private) (bcvManeuverAbbreviation maneuver))
+
           (def (maxInternals (ship : (Nullable JSONObject))
                              (current : Int32)) : Int32
             (modifiers private)
@@ -725,8 +1134,30 @@
           (def (paPanelMax (ship : (Nullable JSONObject))) : Int32
             (modifiers private)
             (bcvMaxInt (int32 1)
-              (+ (counterValue (bcvNullableObjectAny ship "boxes") "52")
+              (+ (paPanelBoxCount ship)
                  (counterValue (bcvNullableObjectAny ship "destroyed-boxes") "52"))))
+          (def (paPanelCount (ship : (Nullable JSONObject))) : Int32
+            (modifiers private)
+            (if (nullable-null? ship) (int32 0)
+                (if (bcvObjectHas (nullable-get ship) "pa-panels")
+                    (bcvMaxInt (int32 0)
+                      (bcvNumberValue (json-object-opt-any (nullable-get ship) "pa-panels")))
+                    (bcvMaxInt (int32 0) (- (paPanelBoxCount ship) (paPanelCharge ship))))))
+          (def (paPanelBoxCount (ship : (Nullable JSONObject))) : Int32
+            (modifiers private)
+            (let ((boxes (bcvNumberValue (bcvNullableObjectAny ship "pa-panel-boxes")))
+                  (maximum (bcvNumberValue (bcvNullableObjectAny ship "pa-panel-max"))))
+              (if (> boxes (int32 0)) boxes
+                  (if (> maximum (int32 0)) maximum
+                      (counterValue (bcvNullableObjectAny ship "boxes") "52")))))
+          (def (paPanelCharge (ship : (Nullable JSONObject))) : Int32
+            (modifiers private)
+            (bcvClampInt
+              (bcvNumberValue (bcvNullableObjectAny ship "pa-panel-charge"))
+              (int32 0)
+              (bcvMaxInt (int32 1)
+                (bcvMaxInt (paPanelBoxCount ship)
+                  (bcvNumberValue (bcvNullableObjectAny ship "pa-panel-max"))))))
           (def (counterTotal (counter : (Nullable Any))) : Int32
             (modifiers private)
             (if (nullable-null? counter)
@@ -735,7 +1166,7 @@
                   (if (nullable-null? obj)
                       (let ((array (bcvAsJsonArray (nullable-get counter))))
                         (if (nullable-null? array)
-                            (bcvNumberValue counter)
+                            (int32 0)
                             (counterArrayTotal (nullable-get array) (int32 0))))
                       (counterObjectTotal (nullable-get obj))))))
           (def (counterObjectTotal (obj : JSONObject)) : Int32
@@ -827,6 +1258,187 @@
                             (bcvMaxInt (int32 0)
                               (json-object-opt-int32-default (nullable-get weapon)
                                 "fireable-in-arc" (int32 0))))))))))
+          (def (primaryWeaponLine (actor : (Nullable JSONObject))) : String
+            (modifiers private)
+            (let ((weapons (if (nullable-null? actor) (nullable-none JSONArray)
+                               (json-object-opt-json-array
+                                 (nullable-get actor) "weapon-status"))))
+              (if (nullable-null? weapons) ""
+                  (let ((index (bestPrimaryWeaponIndex (nullable-get weapons)
+                                 (int32 0) (int32 -1))))
+                    (if (< index (int32 0)) ""
+                        (let ((weapon (json-array-opt-json-object
+                                       (nullable-get weapons) index)))
+                          (if (nullable-null? weapon) ""
+                              (let ((arc (compactArray
+                                          (json-object-opt-json-array
+                                            (nullable-get weapon) "arc-codes") (int32 2))))
+                                (string-append
+                                  (shortWeapon (json-object-opt-string-default
+                                    (nullable-get weapon) "kind" "-"))
+                                  (string-append " "
+                                    (string-append (bcvIntText
+                                      (json-object-opt-int32-default
+                                        (nullable-get weapon) "fireable-in-arc" (int32 0)))
+                                      (string-append "/"
+                                        (string-append (bcvIntText
+                                          (json-object-opt-int32-default
+                                            (nullable-get weapon) "armed" (int32 0)))
+                                          (if (string-blank? arc) ""
+                                              (string-append " " arc)))))))))))))))
+          (def (weaponFamilyLine (actor : (Nullable JSONObject))) : String
+            (modifiers private)
+            (let ((weapons (if (nullable-null? actor) (nullable-none JSONArray)
+                               (json-object-opt-json-array
+                                 (nullable-get actor) "weapon-status"))))
+              (if (nullable-null? weapons) ""
+                  (weaponFamilyLineAt (nullable-get weapons) (int32 0)
+                    (int32 -1) (int32 -1) (int32 -1) ""))))
+          (def (weaponFamilyLineAt (weapons : JSONArray) (slot : Int32)
+                                   (first : Int32) (second : Int32) (third : Int32)
+                                   (out : String)) : String
+            (modifiers private)
+            (if (>= slot (int32 3)) out
+                (let ((best (bestWeaponIndex weapons (int32 0) (int32 -1)
+                              first second third)))
+                  (if (< best (int32 0)) out
+                      (let ((weapon (json-array-opt-json-object weapons best)))
+                        (if (nullable-null? weapon) out
+                            (let ((cooldown (bcvMaxInt (int32 0)
+                                              (json-object-opt-int32-default
+                                                (nullable-get weapon) "cooldown" (int32 0))))
+                                  (fireable (bcvMaxInt (int32 0)
+                                              (json-object-opt-int32-default
+                                                (nullable-get weapon) "fireable-in-arc"
+                                                (json-object-opt-int32-default
+                                                  (nullable-get weapon) "fireable" (int32 0)))))
+                                  (armed (bcvMaxInt (int32 0)
+                                           (json-object-opt-int32-default
+                                             (nullable-get weapon) "armed" (int32 0))))
+                                  (installed (bcvMaxInt (int32 0)
+                                               (json-object-opt-int32-default
+                                                 (nullable-get weapon) "installed" (int32 0)))))
+                              (let ((line (string-append
+                                            (shortWeapon (json-object-opt-string-default
+                                              (nullable-get weapon) "kind" "-"))
+                                            (string-append " "
+                                              (string-append (bcvIntText fireable)
+                                                (string-append "/"
+                                                  (string-append (bcvIntText armed)
+                                                    (string-append "/"
+                                                      (string-append (bcvIntText installed)
+                                                        (if (> cooldown (int32 0))
+                                                            (string-append " cd" (bcvIntText cooldown)) ""))))))))))
+                                (weaponFamilyLineAt weapons (+ slot (int32 1))
+                                  (if (= slot (int32 0)) best first)
+                                  (if (= slot (int32 1)) best second)
+                                  (if (= slot (int32 2)) best third)
+                                  (if (= slot (int32 0)) line
+                                      (string-append out (string-append "  " line)))))))))))
+          )
+          (def (bestPrimaryWeaponIndex (weapons : JSONArray) (index : Int32)
+                                       (best : Int32)) : Int32
+            (modifiers private)
+            (if (>= index (json-array-length weapons)) best
+                (let ((weapon (json-array-opt-json-object weapons index)))
+                  (if (nullable-null? weapon)
+                      (bestPrimaryWeaponIndex weapons (+ index (int32 1)) best)
+                      (if (< best (int32 0))
+                          (bestPrimaryWeaponIndex weapons (+ index (int32 1)) index)
+                          (let ((bestWeapon (json-array-opt-json-object weapons best)))
+                            (if (or (nullable-null? bestWeapon)
+                                    (> (weaponScore (nullable-get weapon))
+                                       (weaponScore (nullable-get bestWeapon))))
+                                (bestPrimaryWeaponIndex weapons (+ index (int32 1)) index)
+                                (bestPrimaryWeaponIndex weapons (+ index (int32 1)) best))))))))
+          (def (bestWeaponIndex (weapons : JSONArray) (index : Int32)
+                                (best : Int32) (skipA : Int32)
+                                (skipB : Int32) (skipC : Int32)) : Int32
+            (modifiers private)
+            (if (>= index (json-array-length weapons)) best
+                (let ((weapon (json-array-opt-json-object weapons index)))
+                  (if (or (= index skipA) (= index skipB) (= index skipC))
+                      (bestWeaponIndex weapons (+ index (int32 1)) best skipA skipB skipC)
+                      (if (nullable-null? weapon)
+                          (bestWeaponIndex weapons (+ index (int32 1)) best skipA skipB skipC)
+                          (if (<= (+ (json-object-opt-int32-default (nullable-get weapon) "installed" (int32 0))
+                                     (json-object-opt-int32-default (nullable-get weapon) "armed" (int32 0))
+                                     (json-object-opt-int32-default (nullable-get weapon) "fireable" (int32 0))
+                                     (json-object-opt-int32-default (nullable-get weapon) "cooldown" (int32 0)))
+                                  (int32 0))
+                              (bestWeaponIndex weapons (+ index (int32 1)) best skipA skipB skipC)
+                              (if (< best (int32 0))
+                                  (bestWeaponIndex weapons (+ index (int32 1)) index skipA skipB skipC)
+                                  (let ((bestWeapon (json-array-opt-json-object weapons best)))
+                                    (if (or (nullable-null? bestWeapon)
+                                            (> (weaponScore (nullable-get weapon))
+                                               (weaponScore (nullable-get bestWeapon)))
+                                            (and (= (weaponScore (nullable-get weapon))
+                                                    (weaponScore (nullable-get bestWeapon)))
+                                                 (< (bcvStringCompare
+                                                      (json-object-opt-string-default
+                                                        (nullable-get weapon) "kind" "")
+                                                      (json-object-opt-string-default
+                                                        (nullable-get bestWeapon) "kind" "")) (int32 0))))
+                                        (bestWeaponIndex weapons (+ index (int32 1)) index skipA skipB skipC)
+                                        (bestWeaponIndex weapons (+ index (int32 1)) best skipA skipB skipC)))))))))
+          )
+          (def (weaponScore (weapon : JSONObject)) : Int32
+            (modifiers private)
+            (+ (* (json-object-opt-int32-default weapon "fireable-in-arc" (int32 0)) (int32 1000))
+               (* (json-object-opt-int32-default weapon "fireable" (int32 0)) (int32 100))
+               (* (json-object-opt-int32-default weapon "armed" (int32 0)) (int32 10))
+               (json-object-opt-int32-default weapon "installed" (int32 0))))
+          (def (shortWeapon (kind : String)) : String
+            (modifiers private)
+            (string-uppercase
+              (bcvStringReplace
+                (bcvStringReplace
+                  (bcvStringReplace
+                    (bcvStringReplace
+                      (bcvStringReplace kind "phaser-" "PH-") "photon" "PHOT")
+                    "disruptor" "DISR") "plasma-" "PL-") "drone-" "DR-")))
+          (def (compactArray (array : (Nullable JSONArray)) (limit : Int32)) : String
+            (modifiers private)
+            (if (or (nullable-null? array)
+                    (= (json-array-length (nullable-get array)) (int32 0))) ""
+                (compactArrayAt (nullable-get array) limit (int32 0) "")))
+          (def (compactArrayAt (array : JSONArray) (limit : Int32)
+                               (index : Int32) (out : String)) : String
+            (modifiers private)
+            (if (or (>= index (json-array-length array)) (>= index limit))
+                (if (> (json-array-length array) limit)
+                    (string-append out (string-append "/+"
+                      (bcvIntText (- (json-array-length array) limit)))) out)
+                (let ((value (json-array-opt-any array index)))
+                  (compactArrayAt array limit (+ index (int32 1))
+                    (let ((part (if (nullable-null? value) "null"
+                                    (bcvAnyToString (nullable-get value)))))
+                      (if (= index (int32 0)) part
+                          (string-append out (string-append "/" part))))))))
+          (def (numberValue (value : (Nullable Any))) : Int32
+            (modifiers private) (bcvNumberValue value))
+          (def (sideLabel (value : String)) : String
+            (modifiers private)
+            (let ((lower (bcvStringLowercase value)))
+              (if (equal? lower "alpha") "A"
+                  (if (equal? lower "beta") "B"
+                      (string-take (string-uppercase value)
+                        (bcvMinInt (int32 6) (string-length-int32 value)))))))
+          (def (shieldFaceLabel (value : (Nullable Any))) : String
+            (modifiers private)
+            (if (nullable-null? value) ""
+                (let ((text (bcvAnyToString (nullable-get value))))
+                  (if (or (equal? text "#f") (equal? text "false")) ""
+                      (if (equal? text "all") "ALL"
+                          (let ((plain (if (bcvStringStartsWith text "shield-")
+                                           (bcvStringSubstring text (int32 7)) text)))
+                            (let ((number (string->int32-or-null plain)))
+                              (if (nullable-null? number) (string-uppercase text)
+                                  (string-append "SH" (bcvIntText (nullable-get number))))))))))
+          )
+          (def (forEachCounterPair (counter : (Nullable Any))) : Unit
+            (modifiers private) (begin))
           (def (barColor (ratio : Float32)) : Int32
             (modifiers private)
             (if (> ratio (float32 0.66))
@@ -845,8 +1457,8 @@
           (def (fitTextEnd (text : String) (paint : Paint)
                            (maxWidth : Float32) (end : Int32)) : String
             (modifiers private)
-            (if (<= end (int32 1))
-                (string-append (string-take text (int32 1)) "...")
+            (if (<= end (int32 4))
+                (string-take text end)
                 (let ((candidate
                         (string-append (string-take text end) "...")))
                   (if (<= (bcvPaintMeasureText paint candidate) maxWidth)
diff --git a/templates/original-tactics-parts/CombatLogView.kt.ss b/templates/original-tactics-parts/CombatLogView.kt.ss
index f020e00..c09917d 100644
--- a/templates/original-tactics-parts/CombatLogView.kt.ss
+++ b/templates/original-tactics-parts/CombatLogView.kt.ss
@@ -27,6 +27,7 @@
         (type JSONArray)
         (type JSONObject)
         (type Any)
+        (type Number)
         (type Int32)
         (type Float32)
         (extern (clvPaintAntiAliasFlag) : Int32
@@ -39,6 +40,8 @@
           (kotlin-value Paint Align LEFT))
         (extern (clvPaintAlignCenter) : Align
           (kotlin-value Paint Align CENTER))
+        (extern (clvPaintAlignRight) : Align
+          (kotlin-value Paint Align RIGHT))
         (extern (clvColorWhite) : Int32
           (kotlin-value Color WHITE))
         (extern (clvColorRgb (red : Int32) (green : Int32) (blue : Int32)) : Int32
@@ -55,6 +58,8 @@
           (kotlin-member-set isFakeBoldText))
         (extern (clvPaintTextAlignSet (paint : Paint) (value : Align)) : Unit
           (kotlin-member-set textAlign))
+        (extern (clvPaintMeasureText (paint : Paint) (text : String)) : Float32
+          (kotlin-member-call measureText))
         (extern (clvViewInvalidate (view : CombatLogView)) : Unit
           (kotlin-member-call invalidate))
         (extern (clvSuperOnDraw (canvas : Canvas)) : Unit
@@ -87,12 +92,57 @@
                                   (y : Float32)
                                   (paint : Paint)) : Unit
           (kotlin-member-call drawText))
+        (extern (clvRectLeft (rect : RectF)) : Float32 (kotlin-member-get left))
+        (extern (clvRectTop (rect : RectF)) : Float32 (kotlin-member-get top))
+        (extern (clvRectRight (rect : RectF)) : Float32 (kotlin-member-get right))
+        (extern (clvRectBottom (rect : RectF)) : Float32 (kotlin-member-get bottom))
+        (extern (clvRectWidth (rect : RectF)) : Float32 (kotlin-member-call width))
         (extern (clvJsonOptInt (object : JSONObject)
                                (key : String)
                                (fallback : Int32)) : Int32
           (kotlin-member-call optInt))
         (extern (clvJsonArrayLength (array : JSONArray)) : Int32
           (kotlin-member-call length))
+        (extern (clvArrayPutObject (array : JSONArray) (value : JSONObject)) : JSONArray
+          (kotlin-member-call put))
+        (extern (clvAsObject (value : Any)) : (Nullable JSONObject)
+          (kotlin-safe-cast JSONObject))
+        (extern (clvAsArray (value : Any)) : (Nullable JSONArray)
+          (kotlin-safe-cast JSONArray))
+        (extern (clvAsNumber (value : Any)) : (Nullable Number)
+          (kotlin-safe-cast Number))
+        (extern (clvAsString (value : Any)) : (Nullable String)
+          (kotlin-safe-cast String))
+        (extern (clvNumberInt (value : Number)) : Int32 (kotlin-member-call toInt))
+        (extern (clvAnyString (value : Any)) : String (kotlin-member-call toString))
+        (extern (clvStringContains (text : String) (part : String)) : Bool
+          (kotlin-member-call contains))
+        (extern (clvStringLowercase (text : String)) : String
+          (kotlin-member-call lowercase))
+        (extern (clvStringEndsWith (text : String) (suffix : String)) : Bool
+          (kotlin-member-call endsWith))
+        (extern (clvStringStartsWith (text : String) (prefix : String)) : Bool
+          (kotlin-member-call startsWith))
+        (extern (clvStringSubstring (text : String) (start : Int32)) : String
+          (kotlin-member-call substring))
+        (extern (clvStringTrim (text : String)) : String
+          (kotlin-member-call trim))
+        (extern (clvStringReplace (text : String) (old : String)
+                                  (replacement : String)) : String
+          (kotlin-member-call replace))
+        (extern (clvObjectHas (object : JSONObject) (key : String)) : Bool
+          (kotlin-member-call has))
+        (extern (clvObjectLength (object : JSONObject)) : Int32
+          (kotlin-member-call length))
+        (extern (clvJsonKeys (object : JSONObject)) : (Iterator String)
+          (kotlin-member-call keys))
+        (extern (clvIteratorHasNext (items : (Iterator String))) : Bool
+          (kotlin-member-call hasNext))
+        (extern (clvIteratorNext (items : (Iterator String))) : String
+          (kotlin-member-call next))
+        (extern (clvShortShipLabel (ship : (Nullable JSONObject))
+                                   (limit : Int32) (fallback : String)) : String
+          (kotlin-call shortShipDisplayLabel))
         (def (clvConfigurePaint (paint : Paint)
                                 (color : Int32)
                                 (style : Style)) : Paint
@@ -114,6 +164,14 @@
                   (clvDisplayMetricsDensity
                     (clvResourcesDisplayMetrics (clvViewResources view)))))
             (if (< density (float32 1.0)) (float32 1.0) density)))
+        (def (clvMaxInt (a : Int32) (b : Int32)) : Int32 (if (> a b) a b))
+        (def (clvClampInt (value : Int32) (low : Int32) (high : Int32)) : Int32
+          (if (< value low) low (if (> value high) high value)))
+        (def (clvNullableInt (object : (Nullable JSONObject))
+                             (key : String) (fallback : Int32)) : Int32
+          (if (nullable-null? object) fallback
+              (json-object-opt-int32-default (nullable-get object) key fallback)))
+        (record LogStringState ((text : String) (count : Int32)))
         (class CombatLogView ((context : Context))
           (extends View context)
           (var game : (Nullable JSONObject) (nullable-none JSONObject)
@@ -208,54 +266,628 @@
           (def (drawContent (canvas : Canvas)) : Unit
             (modifiers private)
             (begin
-              (clvCanvasDrawRoundRect
-                canvas
-                (new RectF
-                  (float32 0.0)
-                  (float32 0.0)
-                  drawWidth
-                  drawHeight)
-                (float32 10.0)
-                (float32 10.0)
-                backgroundPaint)
+              (clvCanvasDrawRoundRect canvas
+                (new RectF (float32 0.0) (float32 0.0) drawWidth drawHeight)
+                (float32 10.0) (float32 10.0) backgroundPaint)
               (if (nullable-null? game)
-                (begin
-                  (clvPaintTextAlignSet titlePaint (clvPaintAlignCenter))
-                  (clvCanvasDrawText
-                    canvas
-                    "COMBAT LOG"
-                    (/ drawWidth (float32 2.0))
-                    (- (/ drawHeight (float32 2.0)) (float32 4.0))
-                    titlePaint)
-                  (clvPaintTextAlignSet dimPaint (clvPaintAlignCenter))
-                  (clvCanvasDrawText
-                    canvas
-                    "No active engagement"
-                    (/ drawWidth (float32 2.0))
-                    (+ (/ drawHeight (float32 2.0)) (float32 22.0))
-                    dimPaint))
-                (let ((current (nullable-get game)))
+                  (begin
+                    (clvPaintTextAlignSet titlePaint (clvPaintAlignCenter))
+                    (clvCanvasDrawText canvas "COMBAT LOG" (/ drawWidth (float32 2.0))
+                      (- (/ drawHeight (float32 2.0)) (float32 4.0)) titlePaint)
+                    (clvPaintTextAlignSet dimPaint (clvPaintAlignCenter))
+                    (clvCanvasDrawText canvas "No active engagement"
+                      (/ drawWidth (float32 2.0))
+                      (+ (/ drawHeight (float32 2.0)) (float32 22.0)) dimPaint))
+                  (let ((current (nullable-get game)))
+                    (let ((rows (recentEvents (int32 12))))
+                      (begin
+                        (drawHeader canvas current)
+                        (drawShipStrip canvas
+                          (json-object-opt-json-object current "battle-state"))
+                        (if (= (json-array-length rows) (int32 0))
+                            (begin
+                              (clvPaintTextAlignSet dimPaint (clvPaintAlignCenter))
+                              (clvCanvasDrawText canvas "No combat events yet"
+                                (/ drawWidth (float32 2.0)) (float32 112.0) dimPaint))
+                            (drawEventRows canvas rows (int32 0) (float32 84.0)))))))))
+          (def (drawHeader (canvas : Canvas) (current : JSONObject)) : Unit
+            (modifiers private)
+            (let ((status (if (nullable-null? dashboard) (nullable-none JSONObject)
+                              (json-object-opt-json-object (nullable-get dashboard) "status")))
+                  (battle (json-object-opt-json-object current "battle-state"))
+                  (activity (if (nullable-null? dashboard) (nullable-none JSONObject)