Use typed tactical map view
ober
341d5059ed0546f2646f8ca2f3a9bedb7cb35e78
--- a/templates/original-tactics.ss +++ b/templates/original-tactics.ss @@ -3579,1859 +3579,7 @@ "original-tactics-parts/SsdDamageView.kt.ss") (include-fragment "original-tactics-parts/SystemsReadinessView.kt.ss") - (kotlin-file-lines - "com/jerboa/originaltactics/TacticalMapView.kt" - ("package com.jerboa.originaltactics" "" - "import android.animation.ValueAnimator" - "import android.content.Context" - "import android.graphics.Canvas" - "import android.graphics.Color" - "import android.graphics.DashPathEffect" - "import android.graphics.Paint" - "import android.graphics.Path" - "import android.graphics.RectF" - "import android.os.SystemClock" - "import android.view.MotionEvent" - "import android.view.ScaleGestureDetector" - "import android.view.View" - "import android.view.animation.DecelerateInterpolator" - "import org.json.JSONArray" "import org.json.JSONObject" - "import kotlin.math.PI" "import kotlin.math.cos" - "import kotlin.math.max" "import kotlin.math.min" - "import kotlin.math.sin" "import kotlin.math.sqrt" "" - "class TacticalMapView(context: Context) : View(context) {" - " private var game: JSONObject? = null" - " private var events: JSONArray = JSONArray()" - " private var tacticalStatus: JSONObject? = null" - " private var tacticalDashboard: JSONObject? = null" - " private var drawWidth = 0f" - " private var drawHeight = 0f" - " private var userScale = 1f" - " private var userPanX = 0f" - " private var userPanY = 0f" - " private var lastTouchX = 0f" - " private var lastTouchY = 0f" - " private var movementProgress = 1f" - " private var movementAnimator: ValueAnimator? = null" - " private var alphaMoveStart: ShipPose? = null" - " private var alphaMoveEnd: ShipPose? = null" - " private var betaMoveStart: ShipPose? = null" - " private var betaMoveEnd: ShipPose? = null" - " private var impactStartedAt = 0L" - " private var alphaImpactDamage = 0" - " private var betaImpactDamage = 0" - " private var alphaImpactShieldDamage = 0" - " private var betaImpactShieldDamage = 0" - " private var alphaImpactInternalDamage = 0" - " private var betaImpactInternalDamage = 0" - " private val scaleDetector = ScaleGestureDetector(" - " context," - " object : ScaleGestureDetector.SimpleOnScaleGestureListener() {" - " override fun onScale(detector: ScaleGestureDetector): Boolean {" - " userScale = (userScale * detector.scaleFactor).coerceIn(0.6f, 2.8f)" - " invalidate()" " return true" - " }" " }" " )" "" - " private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(247, 249, 252)" - " style = Paint.Style.FILL" " }" - " private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(201, 210, 222)" - " style = Paint.Style.STROKE" - " strokeWidth = 1.5f" " }" - " private val strongGridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(117, 131, 153)" - " style = Paint.Style.STROKE" - " strokeWidth = 2.5f" " }" - " private val rangePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(77, 93, 121)" - " style = Paint.Style.STROKE" - " strokeWidth = 4f" " }" - " private val rangeBandPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(80, 77, 93, 121)" - " style = Paint.Style.STROKE" - " strokeWidth = 2f" - " pathEffect = DashPathEffect(floatArrayOf(10f, 12f), 0f)" - " }" - " private val rangeBandTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(82, 94, 114)" - " textSize = 16f" " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" - " private val alphaFireLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(21, 96, 189)" - " style = Paint.Style.STROKE" - " strokeWidth = 5f" - " pathEffect = DashPathEffect(floatArrayOf(16f, 12f), 0f)" - " }" - " private val betaFireLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(184, 67, 54)" - " style = Paint.Style.STROKE" - " strokeWidth = 5f" - " pathEffect = DashPathEffect(floatArrayOf(16f, 12f), 0f)" - " }" - " private val fireSolutionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(70, 167, 106)" - " style = Paint.Style.STROKE" - " strokeWidth = 5f" - " strokeCap = Paint.Cap.ROUND" " }" - " private val fireSolutionFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(44, 70, 167, 106)" - " style = Paint.Style.FILL" " }" - " private val fireSolutionTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 18f" - " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" - " private val weaponArcPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(28, 70, 167, 106)" - " style = Paint.Style.FILL" " }" - " private val weaponArcStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(120, 70, 167, 106)" - " style = Paint.Style.STROKE" - " strokeWidth = 2f" " }" - " private val overlayPanelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(218, 14, 22, 34)" - " style = Paint.Style.FILL" " }" - " private val overlayBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(190, 92, 116, 150)" - " style = Paint.Style.STROKE" - " strokeWidth = 2f" " }" - " private val overlayChipPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(42, 57, 78)" - " style = Paint.Style.FILL" " }" - " private val overlayReadyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(65, 170, 104)" - " style = Paint.Style.FILL" " }" - " private val overlayWaitPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(215, 145, 52)" - " style = Paint.Style.FILL" " }" - " private val overlayTitlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 18f" - " isFakeBoldText = true" " }" - " private val overlayTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(210, 222, 239)" - " textSize = 15f" " isFakeBoldText = true" - " }" - " private val overlayDimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(157, 174, 198)" - " textSize = 14f" " }" - " private val alphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(21, 96, 189)" - " style = Paint.Style.FILL" " }" - " private val betaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(184, 67, 54)" - " style = Paint.Style.FILL" " }" - " private val seekerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(215, 131, 39)" - " style = Paint.Style.FILL" " }" - " private val seekerTargetPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(215, 131, 39)" - " style = Paint.Style.STROKE" - " strokeWidth = 4f" - " pathEffect = DashPathEffect(floatArrayOf(10f, 12f), 0f)" - " }" - " private val shuttlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(106, 83, 168)" - " style = Paint.Style.FILL" " }" - " private val webPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(63, 164, 210)" - " style = Paint.Style.STROKE" - " strokeWidth = 5f" " }" - " private val webFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(58, 63, 164, 210)" - " style = Paint.Style.FILL" " }" - " private val minePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(200, 57, 68)" - " style = Paint.Style.FILL" " }" - " private val mineStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" - " style = Paint.Style.STROKE" - " strokeWidth = 3f" " }" - " private val shipStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" - " style = Paint.Style.STROKE" - " strokeWidth = 4f" " }" - " private val shipShadowStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(150, 20, 26, 36)" - " style = Paint.Style.STROKE" - " strokeWidth = 7f" - " strokeCap = Paint.Cap.ROUND" - " strokeJoin = Paint.Join.ROUND" " }" - " private val shipDetailPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(185, 255, 255, 255)" - " style = Paint.Style.STROKE" - " strokeWidth = 2.5f" - " strokeCap = Paint.Cap.ROUND" - " strokeJoin = Paint.Join.ROUND" " }" - " private val shipBadgePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" - " style = Paint.Style.FILL" " textSize = 13f" - " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" - " private val shipBadgeBackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(190, 20, 26, 36)" - " style = Paint.Style.FILL" " }" - " private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(28, 35, 48)" - " textSize = 31f" " isFakeBoldText = true" - " }" - " private val smallTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(49, 58, 73)" - " textSize = 24f" " }" - " private val pillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(220, 255, 255, 255)" - " style = Paint.Style.FILL" " }" - " private val shieldBackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(151, 160, 174)" - " style = Paint.Style.STROKE" - " strokeWidth = 7f" - " strokeCap = Paint.Cap.ROUND" " }" - " private val shieldPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(45, 137, 86)" - " style = Paint.Style.STROKE" - " strokeWidth = 7f" - " strokeCap = Paint.Cap.ROUND" " }" - " private val hitShieldPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(245, 185, 60)" - " style = Paint.Style.STROKE" - " strokeWidth = 12f" - " strokeCap = Paint.Cap.ROUND" " }" - " private val shieldNumberPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(28, 35, 48)" - " textSize = 20f" " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" - " private val paPanelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(214, 146, 45)" - " style = Paint.Style.STROKE" - " strokeWidth = 8f" - " strokeCap = Paint.Cap.ROUND" " }" - " private val paPanelFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(34, 214, 146, 45)" - " style = Paint.Style.FILL" " }" - " private val movementTrailPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(145, 68, 82, 105)" - " style = Paint.Style.STROKE" - " strokeWidth = 5f" - " strokeCap = Paint.Cap.ROUND" - " pathEffect = DashPathEffect(floatArrayOf(12f, 10f), 0f)" - " }" - " private val movementWakePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(100, 68, 82, 105)" - " style = Paint.Style.STROKE" - " strokeWidth = 4f" - " strokeCap = Paint.Cap.ROUND" " }" - " private val projectedPathPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(150, 21, 96, 189)" - " style = Paint.Style.STROKE" - " strokeWidth = 4f" - " strokeCap = Paint.Cap.ROUND" - " strokeJoin = Paint.Join.ROUND" - " pathEffect = DashPathEffect(floatArrayOf(8f, 8f), 0f)" - " }" - " private val projectedPointPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(210, 21, 96, 189)" - " style = Paint.Style.FILL" " }" - " private val projectedTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(28, 35, 48)" - " textSize = 17f" " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" - " private val projectedRangePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(130, 68, 82, 105)" - " style = Paint.Style.STROKE" - " strokeWidth = 3f" - " strokeCap = Paint.Cap.ROUND" - " pathEffect = DashPathEffect(floatArrayOf(7f, 9f), 0f)" - " }" - " private val projectedDangerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(207, 71, 62)" - " style = Paint.Style.STROKE" - " strokeWidth = 5f" - " strokeCap = Paint.Cap.ROUND" " }" - " private val projectedRangeTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(28, 35, 48)" - " textSize = 16f" " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" - " private val impactRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(245, 185, 60)" - " style = Paint.Style.STROKE" - " strokeWidth = 8f" " }" - " private val impactFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(150, 245, 185, 60)" - " style = Paint.Style.FILL" " }" - " private val blockedBadgePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.argb(232, 43, 31, 24)" - " style = Paint.Style.FILL" " }" - " private val blockedBadgeBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.rgb(225, 171, 58)" - " style = Paint.Style.STROKE" - " strokeWidth = 2.5f" " }" - " private val blockedBadgeTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.WHITE" " textSize = 17f" - " isFakeBoldText = true" - " textAlign = Paint.Align.CENTER" " }" "" - " fun setGame(nextGame: JSONObject?, nextEvents: JSONArray = JSONArray()) {" - " val previousBattle = game?.optJSONObject(\"battle-state\")" - " val previousAlpha = previousBattle?.optJSONObject(\"alpha-position\")?.let { poseFrom(it) }" - " val previousBeta = previousBattle?.optJSONObject(\"beta-position\")?.let { poseFrom(it) }" - " val nextBattle = nextGame?.optJSONObject(\"battle-state\")" - " val nextAlpha = nextBattle?.optJSONObject(\"alpha-position\")?.let { poseFrom(it) }" - " val nextBeta = nextBattle?.optJSONObject(\"beta-position\")?.let { poseFrom(it) }" - "" " game = nextGame" " events = nextEvents" - " if (nextGame == null) {" - " events = JSONArray()" - " tacticalStatus = null" - " tacticalDashboard = null" - " movementAnimator?.cancel()" - " movementProgress = 1f" - " clearImpactPulse()" " resetView()" - " return" " }" "" - " if (previousAlpha != null && previousBeta != null && nextAlpha != null && nextBeta != null &&" - " (poseChanged(previousAlpha, nextAlpha) || poseChanged(previousBeta, nextBeta))" - " ) {" - " startMovementAnimation(previousAlpha, nextAlpha, previousBeta, nextBeta)" - " } else {" " finishMovementAnimation()" - " }" " updateImpactPulse(nextEvents)" - " invalidate()" " }" "" - " fun setTacticalStatus(nextStatus: JSONObject?) {" - " tacticalStatus = nextStatus" " invalidate()" - " }" "" - " fun setTacticalDashboard(nextDashboard: JSONObject?) {" - " tacticalDashboard = nextDashboard" - " nextDashboard?.optJSONObject(\"status\")?.let { tacticalStatus = it }" - " invalidate()" " }" "" " fun resetView() {" - " userScale = 1f" " userPanX = 0f" - " userPanY = 0f" " invalidate()" " }" "" - " override fun onTouchEvent(event: MotionEvent): Boolean {" - " parent?.requestDisallowInterceptTouchEvent(true)" - " scaleDetector.onTouchEvent(event)" - " when (event.actionMasked) {" - " MotionEvent.ACTION_DOWN -> {" - " lastTouchX = event.x" - " lastTouchY = event.y" - " return true" " }" - " MotionEvent.ACTION_MOVE -> {" - " if (!scaleDetector.isInProgress && event.pointerCount == 1) {" - " val scale = resources.displayMetrics.density.coerceAtLeast(1f)" - " userPanX += (event.x - lastTouchX) / scale" - " userPanY += (event.y - lastTouchY) / scale" - " lastTouchX = event.x" - " lastTouchY = event.y" - " invalidate()" " }" - " return true" " }" - " MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {" - " parent?.requestDisallowInterceptTouchEvent(false)" - " return true" " }" " }" - " return true" " }" "" - " override fun onDraw(canvas: Canvas) {" - " super.onDraw(canvas)" - " val canvasState = canvas.save()" - " val scale = resources.displayMetrics.density.coerceAtLeast(1f)" - " drawWidth = width.toFloat() / scale" - " drawHeight = height.toFloat() / scale" - " canvas.scale(scale, scale)" " try {" - " drawContent(canvas)" " } finally {" - " canvas.restoreToCount(canvasState)" " }" - " }" "" " private fun drawContent(canvas: Canvas) {" - " canvas.drawRect(0f, 0f, drawWidth, drawHeight, backgroundPaint)" - "" - " val battle = game?.optJSONObject(\"battle-state\")" - " val alphaPosition = battle?.optJSONObject(\"alpha-position\")" - " val betaPosition = battle?.optJSONObject(\"beta-position\")" - " if (battle == null || alphaPosition == null || betaPosition == null) {" - " drawEmptyState(canvas)" " return" - " }" "" - " val alphaPose = animatedPose(poseFrom(alphaPosition), alphaMoveStart, alphaMoveEnd)" - " val betaPose = animatedPose(poseFrom(betaPosition), betaMoveStart, betaMoveEnd)" - " val orders = game?.optJSONObject(\"orders\")" - " val currentImpulse = game?.optInt(\"impulse\", 0) ?: 0" - " val alphaProjected = projectedMoves(" - " alphaPosition," - " maneuverMap(orders?.opt(\"alpha-maneuvers\"))," - " currentImpulse," - " PROJECTED_MOVE_LIMIT" " )" - " val betaProjected = projectedMoves(" - " betaPosition," - " maneuverMap(orders?.opt(\"beta-maneuvers\"))," - " currentImpulse," - " PROJECTED_MOVE_LIMIT" " )" - " val transform = mapTransform(alphaPose, betaPose, alphaProjected, betaProjected)" - " drawGrid(canvas, transform, alphaPosition, betaPosition, alphaProjected, betaProjected)" - " drawTerrain(canvas, transform, battle)" "" - " val alphaPoint = transform.toPoint(alphaPose.q, alphaPose.r)" - " val betaPoint = transform.toPoint(betaPose.q, betaPose.r)" - " drawRangeBands(canvas, alphaPoint, transform, Color.rgb(21, 96, 189), -1)" - " drawRangeBands(canvas, betaPoint, transform, Color.rgb(184, 67, 54), 1)" - " drawRecentMovementWakes(canvas, transform)" - " canvas.drawLine(alphaPoint.x, alphaPoint.y, betaPoint.x, betaPoint.y, rangePaint)" - " drawWeaponArcFans(canvas, transform, alphaPose, tacticalStatus?.optJSONObject(\"alpha\"), Color.rgb(21, 96, 189))" - " drawWeaponArcFans(canvas, transform, betaPose, tacticalStatus?.optJSONObject(\"beta\"), Color.rgb(184, 67, 54))" - " drawFireSolutions(canvas, transform, alphaPoint, betaPoint)" - " drawMovementTrails(canvas, transform)" - " drawProjectedHelmPlots(canvas, transform, alphaPosition, betaPosition, alphaProjected, betaProjected)" - " drawProjectedRangeForecast(canvas, transform, alphaPosition, betaPosition, alphaProjected, betaProjected)" - " drawFireTargetLine(canvas, transform, alphaPosition, battle.optJSONObject(\"alpha-fire-target\"), alphaFireLinePaint)" - " drawFireTargetLine(canvas, transform, betaPosition, battle.optJSONObject(\"beta-fire-target\"), betaFireLinePaint)" - " drawSeekerTargetLines(canvas, transform, battle)" - "" - " drawUnitCollection(canvas, transform, battle.opt(\"seeking-weapons\"), seekerPaint, \"S\")" - " drawUnitCollection(canvas, transform, battle.opt(\"shuttles\"), shuttlePaint, \"U\")" - "" " drawShip(" " canvas," - " transform," " \"ALPHA\"," - " alphaPose," " alphaPosition," - " battle.optJSONObject(\"alpha\")," - " alphaPaint," " -1," - " shieldFacingIndex(battle.opt(\"alpha-shield-facing-hit\"))" - " )" " drawShip(" " canvas," - " transform," " \"BETA\"," - " betaPose," " betaPosition," - " battle.optJSONObject(\"beta\")," - " betaPaint," " 1," - " shieldFacingIndex(battle.opt(\"beta-shield-facing-hit\"))" - " )" - " drawImpactEffects(canvas, alphaPoint, betaPoint)" - " drawBlockedManeuverBadges(canvas, alphaPoint, betaPoint)" - " drawVolleyBadge(canvas, battle, betaPoint, \"alpha-volley\", \"A\", alphaPaint, -1)" - " drawVolleyBadge(canvas, battle, alphaPoint, \"beta-volley\", \"B\", betaPaint, 1)" - " drawBattleBadge(canvas, battle)" - " drawTerrainBadge(canvas, battle)" - " drawTacticalOverlay(canvas, battle)" " }" "" - " private fun drawTacticalOverlay(canvas: Canvas, battle: JSONObject) {" - " val dashboard = tacticalDashboard" - " val status = tacticalStatus ?: dashboard?.optJSONObject(\"status\")" - " val activity = dashboard?.optJSONObject(\"impulse-activity\")" - " val top = 8f" " val left = 8f" - " val width = (drawWidth * 0.58f).coerceIn(210f, drawWidth - 16f)" - " val height = 84f" - " val rect = RectF(left, top, left + width, top + height)" - " canvas.drawRoundRect(rect, 9f, 9f, overlayPanelPaint)" - " canvas.drawRoundRect(rect, 9f, 9f, overlayBorderPaint)" - "" " overlayTitlePaint.textAlign = Paint.Align.LEFT" - " val phaseImpulse = activity?.optInt(\"impulse\", status?.optInt(\"impulse\", 0) ?: 0) ?: (status?.optInt(\"impulse\", 0) ?: 0)" - " val phaseTitle = \"IMPULSE $phaseImpulse\"" - " canvas.drawText(phaseTitle, rect.left + 9f, rect.top + 22f, overlayTitlePaint)" - "" " overlayDimPaint.textAlign = Paint.Align.RIGHT" - " val counts = status?.optJSONObject(\"counts\")" - " val countText = \"S${counts?.optInt(\"active-seeking\", countUnits(battle.opt(\"seeking-weapons\"))) ?: countUnits(battle.opt(\"seeking-weapons\"))}\" +" - " \" U${counts?.optInt(\"active-small-units\", countUnits(battle.opt(\"shuttles\"))) ?: countUnits(battle.opt(\"shuttles\"))}\" +" - " \" W${counts?.optInt(\"active-webs\", 0) ?: 0}\" +" - " \" M${counts?.optInt(\"active-mines\", 0) ?: 0}\"" - " canvas.drawText(countText, rect.right - 9f, rect.top + 21f, overlayDimPaint)" - "" - " val phaseText = phaseSlotSummary(activity?.optJSONArray(\"stage-slots\"))" - " overlayDimPaint.textAlign = Paint.Align.LEFT" - " canvas.drawText(fitText(phaseText.ifBlank { \"No phase slot data yet\" }, overlayDimPaint, rect.width() - 18f), rect.left + 9f, rect.top + 43f, overlayDimPaint)" - "" - " val alphaReady = availableActionCount(dashboard, \"alpha\")" - " val alphaTotal = totalActionCount(dashboard, \"alpha\")" - " val betaReady = availableActionCount(dashboard, \"beta\")" - " val betaTotal = totalActionCount(dashboard, \"beta\")" - " val chipTop = rect.bottom - 28f" - " val chipMax = ((rect.width() - 30f) * 0.34f).coerceIn(82f, 150f)" - " val alphaWidth = drawOverlayChip(" - " canvas," " rect.left + 9f," - " chipTop," - " actionChipText(dashboard, \"alpha\", alphaReady, alphaTotal)," - " alphaReady > 0," " chipMax" - " )" - " val betaX = rect.left + 17f + alphaWidth" - " val betaWidth = drawOverlayChip(" - " canvas," " betaX," - " chipTop," - " actionChipText(dashboard, \"beta\", betaReady, betaTotal)," - " betaReady > 0," " chipMax" - " )" - " val nextEvent = latestCombatEventText()" - " overlayDimPaint.textAlign = Paint.Align.RIGHT" - " val eventLeft = betaX + betaWidth + 8f" - " canvas.drawText(fitText(nextEvent, overlayDimPaint, rect.right - eventLeft - 9f), rect.right - 9f, chipTop + 17f, overlayDimPaint)" - " }" "" - " private fun drawOverlayChip(canvas: Canvas, x: Float, y: Float, text: String, ready: Boolean, maxWidth: Float): Float {" - " val fitted = fitText(text, overlayTextPaint, maxWidth - 15f)" - " val width = (overlayTextPaint.measureText(fitted) + 15f).coerceAtMost(maxWidth)" - " val rect = RectF(x, y, x + width, y + 20f)" - " canvas.drawRoundRect(rect, 10f, 10f, if (ready) overlayReadyPaint else overlayWaitPaint)" - " overlayTextPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(fitted, rect.centerX(), rect.top + 14.5f, overlayTextPaint)" - " return width" " }" "" - " private fun phaseSlotSummary(slots: JSONArray?): String {" - " if (slots == null || slots.length() == 0) return \"\"" - " val names = mutableListOf<String>()" - " for (index in 0 until slots.length()) {" - " val slot = slots.optJSONObject(index) ?: continue" - " if (!slot.optBoolean(\"implemented?\", false)) continue" - " val action = slot.optString(\"action\", \"\").ifBlank { slot.optString(\"engine-hook\", \"\") }" - " if (action.isBlank()) continue" - " val compact = action" - " .replace(\"Launch \", \"Ln \")" - " .replace(\"Fire Weapons\", \"Fire\")" - " .replace(\"Announce \", \"\")" - " if (!names.contains(compact)) names.add(compact)" - " if (names.size >= 4) break" " }" - " return if (names.isEmpty()) \"Phase slots pending implementation\" else names.joinToString(\" | \")" - " }" "" - " private fun availableActionCount(dashboard: JSONObject?, actor: String): Int {" - " val actions = dashboard?.optJSONObject(\"actions\")?.optJSONArray(actor) ?: return 0" - " var count = 0" - " for (index in 0 until actions.length()) {" - " if (actions.optJSONObject(index)?.optBoolean(\"available?\", false) == true) count += 1" - " }" " return count" " }" "" - " private fun totalActionCount(dashboard: JSONObject?, actor: String): Int =" - " dashboard?.optJSONObject(\"actions\")?.optJSONArray(actor)?.length() ?: 0" - "" - " private fun actionChipText(dashboard: JSONObject?, actor: String, ready: Int, total: Int): String {" - " val prefix = if (actor == \"alpha\") \"A\" else \"B\"" - " val labels = readyActionLabels(dashboard, actor)" - " return if (labels.isEmpty()) \"$prefix CMD $ready/$total\" else \"$prefix ${labels.joinToString(\"/\")}\"" - " }" "" - " private fun readyActionLabels(dashboard: JSONObject?, actor: String): List<String> {" - " val actions = dashboard?.optJSONObject(\"actions\")?.optJSONArray(actor) ?: return emptyList()" - " val readyById = linkedMapOf<String, JSONObject>()" - " for (index in 0 until actions.length()) {" - " val action = actions.optJSONObject(index) ?: continue" - " val id = action.optString(\"id\", \"\")" - " if (id.isNotBlank() && action.optBoolean(\"available?\", false)) readyById[id] = action" - " }" " return OVERLAY_ACTION_PRIORITY" - " .filter { readyById.containsKey(it) }" - " .map { overlayActionLabel(it) }" - " .take(3)" " }" "" - " private fun overlayActionLabel(id: String): String =" - " when (id) {" - " \"direct-fire\" -> \"fire\"" - " \"seeking-launch\" -> \"seek\"" - " \"wild-weasel-preparation\" -> \"ww\"" - " \"suicide-shuttle\" -> \"suicide\"" - " \"scatter-pack\" -> \"scatter\"" - " \"shuttle-operations\" -> \"units\"" - " \"carrier-fighter-operations\" -> \"ftr\"" - " \"pf-operations\" -> \"pf\"" - " \"tractors\" -> \"tractor\"" - " \"transporters\" -> \"tran\"" - " \"electronic-warfare\" -> \"ew\"" - " \"shield-control\" -> \"shield\"" - " \"continuous-damage-repair\", \"emergency-damage-repair\", \"repair-system\" -> \"repair\"" - " \"web-actions\" -> \"web\"" - " \"mine-actions\" -> \"mine\"" - " else -> id.replace('-', ' ')" " }" "" - " private fun countUnits(raw: Any?): Int =" - " when (raw) {" - " is JSONArray -> raw.length()" - " is JSONObject -> {" - " if (raw.has(\"position\")) 1 else {" - " var total = 0" - " val keys = raw.keys()" - " while (keys.hasNext()) {" - " if (raw.opt(keys.next()) != null) total += 1" - " }" " total" - " }" " }" " else -> 0" - " }" "" - " private fun shieldFacingIndex(value: Any?): Int =" - " when (value) {" - " is Number -> value.toInt()" - " is String -> value.removePrefix(\"shield-\").toIntOrNull() ?: -1" - " else -> -1" - " }.takeIf { it in 1..6 } ?: -1" "" - " private fun latestCombatEventText(): String {" - " for (index in events.length() - 1 downTo 0) {" - " val event = events.optJSONObject(index) ?: continue" - " val type = event.optString(\"type\", \"\")" - " if (type.isBlank() || type == \"impulse-activity\") continue" - " return type.replace('-', ' ').uppercase()" - " }" " return \"NO COMBAT EVENT\"" " }" "" - " private fun fitText(text: String, paint: Paint, maxWidth: Float): String {" - " if (maxWidth <= 8f) return \"\"" - " if (paint.measureText(text) <= maxWidth) return text" - " var candidate = text" - " while (candidate.length > 4 && paint.measureText(\"$candidate...\") > maxWidth) {" - " candidate = candidate.dropLast(1)" " }" - " return if (candidate.length <= 4) candidate else \"$candidate...\"" - " }" "" - " private fun drawRangeBands(canvas: Canvas, center: Point, transform: MapTransform, color: Int, labelSide: Int) {" - " rangeBandPaint.color = Color.argb(76, Color.red(color), Color.green(color), Color.blue(color))" - " rangeBandTextPaint.color = Color.argb(190, Color.red(color), Color.green(color), Color.blue(color))" - " RANGE_BANDS.forEach { band ->" - " val radius = hexCenterDistance(transform, band)" - " if (radius < 18f) return@forEach" - " canvas.drawCircle(center.x, center.y, radius, rangeBandPaint)" - " val labelX = (center.x + labelSide * radius * 0.70f).coerceIn(18f, drawWidth - 18f)" - " val labelY = (center.y - radius * 0.70f).coerceIn(20f, drawHeight - 8f)" - " canvas.drawText(\"R$band\", labelX, labelY, rangeBandTextPaint)" - " }" - " rangeBandTextPaint.color = Color.rgb(82, 94, 114)" - " }" "" - " private fun hexCenterDistance(transform: MapTransform, range: Int): Float =" - " (sqrt(3.0) * range.toDouble()).toFloat() * transform.scale" - "" - " private fun drawFireSolutions(canvas: Canvas, transform: MapTransform, alphaPoint: Point, betaPoint: Point) {" - " val status = tacticalStatus ?: return" - " drawFireSolution(canvas, alphaPoint, betaPoint, actorFireSolution(status.optJSONObject(\"alpha\")), Color.rgb(21, 96, 189), -1)" - " drawFireSolution(canvas, betaPoint, alphaPoint, actorFireSolution(status.optJSONObject(\"beta\")), Color.rgb(184, 67, 54), 1)" - " }" "" " private fun drawFireSolution(" - " canvas: Canvas," " source: Point," - " target: Point," " solution: FireSolution," - " sideColor: Int," " labelSide: Int" " ) {" - " if (solution.installed <= 0) return" - " val color = when {" - " solution.inArc > 0 -> Color.rgb(70, 167, 106)" - " solution.ready > 0 || solution.armed > 0 -> Color.rgb(230, 155, 63)" - " else -> Color.rgb(126, 137, 155)" " }" - " fireSolutionPaint.color = Color.argb(160, Color.red(color), Color.green(color), Color.blue(color))" - " fireSolutionPaint.pathEffect = when {" - " solution.inArc > 0 -> null" - " solution.ready > 0 || solution.armed > 0 -> DashPathEffect(floatArrayOf(18f, 10f), 0f)" - " else -> DashPathEffect(floatArrayOf(6f, 12f), 0f)" - " }" - " fireSolutionFillPaint.color = Color.argb(34, Color.red(sideColor), Color.green(sideColor), Color.blue(sideColor))" - "" - " canvas.drawLine(source.x, source.y, target.x, target.y, fireSolutionPaint)" - " canvas.drawCircle(target.x, target.y, 18f, fireSolutionPaint)" - " canvas.drawCircle(source.x, source.y, 24f, fireSolutionFillPaint)" - "" " val midpointX = (source.x + target.x) / 2f" - " val midpointY = (source.y + target.y) / 2f" - " val text = when {" - " solution.inArc > 0 -> \"LOCK ${solution.inArc}/${solution.ready}\"" - " solution.ready > 0 -> \"OUT ARC ${solution.ready}\"" - " solution.armed > 0 -> \"ARMED ${solution.armed}\"" - " else -> \"COLD\"" " }" - " val width = fireSolutionTextPaint.measureText(text) + 18f" - " val x = (midpointX + labelSide * 34f).coerceIn(width / 2f + 6f, drawWidth - width / 2f - 6f)" - " val y = (midpointY - 18f).coerceIn(24f, drawHeight - 8f)" - " canvas.drawRoundRect(RectF(x - width / 2f, y - 20f, x + width / 2f, y + 5f), 9f, 9f, pillPaint)" - " fireSolutionTextPaint.color = color" - " canvas.drawText(text, x, y - 3f, fireSolutionTextPaint)" - " fireSolutionTextPaint.color = Color.WHITE" - " fireSolutionPaint.pathEffect = null" " }" "" - " private fun actorFireSolution(actor: JSONObject?): FireSolution {" - " val weapons = actor?.optJSONArray(\"weapon-status\") ?: return FireSolution()" - " var installed = 0" " var armed = 0" - " var ready = 0" " var inArc = 0" - " for (index in 0 until weapons.length()) {" - " val weapon = weapons.optJSONObject(index) ?: continue" - " installed += weapon.optInt(\"installed\", 0).coerceAtLeast(0)" - " armed += weapon.optInt(\"armed\", 0).coerceAtLeast(0)" - " ready += weapon.optInt(\"fireable\", 0).coerceAtLeast(0)" - " inArc += weapon.optInt(\"fireable-in-arc\", 0).coerceAtLeast(0)" - " }" - " return FireSolution(installed, armed, ready, inArc)" - " }" "" " private fun drawWeaponArcFans(" - " canvas: Canvas," " transform: MapTransform," - " pose: ShipPose," " actor: JSONObject?," - " sideColor: Int" " ) {" - " val fans = actorWeaponArcFans(actor)" - " if (fans.isEmpty()) return" - " val center = transform.toPoint(pose.q, pose.r)" - " val radius = hexCenterDistance(transform, 5).coerceAtLeast(transform.scale * 2.4f)" - " val facingDegrees = (facingAngleRadians(pose.facing) * 180.0 / PI).toFloat()" - " fans.forEach { fan ->" - " val alpha = when (fan.strength) {" - " 3 -> 42" " 2 -> 28" - " else -> 18" " }" - " weaponArcPaint.color = Color.argb(alpha, Color.red(sideColor), Color.green(sideColor), Color.blue(sideColor))" - " weaponArcStrokePaint.color = Color.argb(alpha * 3, Color.red(sideColor), Color.green(sideColor), Color.blue(sideColor))" - " drawWeaponArcFan(canvas, center, radius, facingDegrees, fan.code)" - " }" " }" "" - " private fun drawWeaponArcFan(canvas: Canvas, center: Point, radius: Float, facingDegrees: Float, code: String) {" - " val sectors = firingArcSectors(code)" - " if (sectors.isEmpty()) return" - " val bounds = RectF(center.x - radius, center.y - radius, center.x + radius, center.y + radius)" - " sectors.forEach { sector ->" - " if (sector.sweep >= 359f) {" - " canvas.drawCircle(center.x, center.y, radius, weaponArcPaint)" - " canvas.drawCircle(center.x, center.y, radius, weaponArcStrokePaint)" - " } else {" - " val start = facingDegrees + sector.centerOffset - sector.sweep / 2f" - " canvas.drawArc(bounds, start, sector.sweep, true, weaponArcPaint)" - " canvas.drawArc(bounds, start, sector.sweep, true, weaponArcStrokePaint)" - " }" " }" " }" "" - " private fun actorWeaponArcFans(actor: JSONObject?): List<ArcFan> {" - " val weapons = actor?.optJSONArray(\"weapon-status\") ?: return emptyList()" - " val strengths = linkedMapOf<String, Int>()" - " for (index in 0 until weapons.length()) {" - " val weapon = weapons.optJSONObject(index) ?: continue" - " if (weapon.optInt(\"installed\", 0) <= 0) continue" - " val strength = when {" - " weapon.optInt(\"fireable-in-arc\", 0) > 0 -> 3" - " weapon.optInt(\"fireable\", 0) > 0 -> 2" - " weapon.optInt(\"armed\", 0) > 0 -> 1" - " else -> 0" " }" - " if (strength <= 0) continue" - " val arcs = weapon.optJSONArray(\"arc-codes\") ?: JSONArray().put(weapon.optString(\"arc\", \"\"))" - " for (arcIndex in 0 until arcs.length()) {" - " val code = arcs.optString(arcIndex, \"\").trim().uppercase()" - " if (code.isBlank()) continue" - " strengths[code] = maxOf(strengths[code] ?: 0, strength)" - " }" " }" - " return strengths.entries" - " .sortedWith(compareByDescending<Map.Entry<String, Int>> { it.value }.thenBy { arcSortBucket(it.key) }.thenBy { it.key })" - " .take(4)" - " .map { ArcFan(it.key, it.value) }" " }" "" - " private fun firingArcSectors(code: String): List<ArcSector> {" - " val normalized = code.uppercase().replace(\" \", \"\")" - " return when {" - " normalized == \"360\" || normalized == \"ALL\" -> listOf(ArcSector(0f, 360f))" - " normalized == \"FA\" || normalized == \"F\" -> listOf(ArcSector(0f, 120f))" - " normalized == \"FH\" -> listOf(ArcSector(0f, 180f))" - " normalized == \"RA\" || normalized == \"A\" -> listOf(ArcSector(180f, 120f))" - " normalized == \"RH\" -> listOf(ArcSector(180f, 180f))" - " normalized == \"LF\" -> listOf(ArcSector(-60f, 120f))" - " normalized == \"RF\" -> listOf(ArcSector(60f, 120f))" - " normalized == \"L\" || normalized == \"LS\" -> listOf(ArcSector(-90f, 180f))" - " normalized == \"R\" || normalized == \"RS\" -> listOf(ArcSector(90f, 180f))" - " normalized == \"LP\" || normalized == \"PORT\" -> listOf(ArcSector(-90f, 120f))" - " normalized == \"RP\" || normalized == \"STARBOARD\" -> listOf(ArcSector(90f, 120f))" - " \"+\" in normalized -> normalized.split(\"+\").flatMap { firingArcSectors(it) }" - " \"/\" in normalized -> normalized.split(\"/\").flatMap { firingArcSectors(it) }" - " else -> emptyList()" " }" " }" "" - " private fun arcSortBucket(code: String): Int =" - " when (code.uppercase()) {" - " \"FA\", \"F\", \"FH\" -> 0" - " \"LF\", \"RF\", \"L\", \"R\", \"LS\", \"RS\" -> 1" - " \"RA\", \"RH\", \"A\" -> 2" - " \"360\", \"ALL\" -> 3" " else -> 4" - " }" "" - " private fun drawEmptyState(canvas: Canvas) {" - " labelPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"No active combat\", drawWidth / 2f, drawHeight / 2f - 12f, labelPaint)" - " smallTextPaint.textAlign = Paint.Align.CENTER" - " canvas.drawText(\"Create or restore a game\", drawWidth / 2f, drawHeight / 2f + 24f, smallTextPaint)" - " }" "" " private fun mapTransform(" - " alpha: ShipPose," " beta: ShipPose," - " alphaProjected: List<ProjectedMove> = emptyList()," - " betaProjected: List<ProjectedMove> = emptyList()" - " ): MapTransform {" - " val points = mutableListOf(rawHexPoint(alpha.q, alpha.r), rawHexPoint(beta.q, beta.r))" - " alphaProjected.forEach { points.add(rawHexPoint(it.pose.q, it.pose.r)) }" - " betaProjected.forEach { points.add(rawHexPoint(it.pose.q, it.pose.r)) }" - " val minX = points.minOf { it.x }" - " val maxX = points.maxOf { it.x }" - " val minY = points.minOf { it.y }" - " val maxY = points.maxOf { it.y }" - " val spanX = max(4f, maxX - minX + 6f)" - " val spanY = max(4f, maxY - minY + 5f)" - " val scale = min(drawWidth / spanX, drawHeight / spanY).coerceIn(16f, 64f) * userScale" - " val centerRaw = Point((minX + maxX) / 2f, (minY + maxY) / 2f)" - " val origin = Point(" - " drawWidth / 2f - centerRaw.x * scale + userPanX," - " drawHeight / 2f - centerRaw.y * scale + userPanY" - " )" " return MapTransform(scale, origin)" - " }" "" " private fun drawGrid(" - " canvas: Canvas," " transform: MapTransform," - " alpha: JSONObject," " beta: JSONObject," - " alphaProjected: List<ProjectedMove> = emptyList()," - " betaProjected: List<ProjectedMove> = emptyList()" - " ) {" - " var minQ = min(alpha.optInt(\"q\"), beta.optInt(\"q\"))" - " var maxQ = max(alpha.optInt(\"q\"), beta.optInt(\"q\"))" - " var minR = min(alpha.optInt(\"r\"), beta.optInt(\"r\"))" - " var maxR = max(alpha.optInt(\"r\"), beta.optInt(\"r\"))" - " (alphaProjected + betaProjected).forEach { step ->" - " minQ = min(minQ, step.pose.q)" - " maxQ = max(maxQ, step.pose.q)" - " minR = min(minR, step.pose.r)" - " maxR = max(maxR, step.pose.r)" " }" - " minQ -= 7" " maxQ += 7" " minR -= 7" - " maxR += 7" " for (q in minQ..maxQ) {" - " for (r in minR..maxR) {" - " val center = transform.toPoint(q, r)" - " if (center.x < -transform.scale || center.x > drawWidth + transform.scale) continue" - " if (center.y < -transform.scale || center.y > drawHeight + transform.scale) continue" - " drawHex(canvas, center.x, center.y, transform.scale * 0.94f, gridPaint)" - " if (q == 0 && r == 0) {" - " drawHex(canvas, center.x, center.y, transform.scale * 0.94f, strongGridPaint)" - " }" " }" " }" " }" "" - " private fun drawHex(canvas: Canvas, cx: Float, cy: Float, radius: Float, paint: Paint) {" - " val path = Path()" " for (i in 0 until 6) {" - " val angle = (-90.0 + i * 60.0) * PI / 180.0" - " val x = cx + (cos(angle) * radius).toFloat()" - " val y = cy + (sin(angle) * radius).toFloat()" - " if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)" - " }" " path.close()" - " canvas.drawPath(path, paint)" " }" "" - " private fun drawShip(" " canvas: Canvas," - " transform: MapTransform," " label: String," - " pose: ShipPose," " position: JSONObject," - " ship: JSONObject?," " fillPaint: Paint," - " labelSide: Int," " hitShield: Int" " ) {" - " val center = transform.toPoint(pose.q, pose.r)" - " val radius = transform.scale * 0.54f" - " val angle = facingAngleRadians(pose.facing)" - " val dx = cos(angle).toFloat()" - " val dy = sin(angle).toFloat()" - " val px = -dy" " val py = dx" "" - " drawShieldRing(canvas, center, radius * 1.35f, ship, hitShield)" - " drawFacingGuide(canvas, center, radius * 1.9f, angle, fillPaint)" - "" - " drawRaceShipSilhouette(canvas, center, radius, angle, ship, fillPaint)" - " drawRaceBadge(canvas, center, radius, ship)" "" - " val labelX = (center.x + labelSide * transform.scale * 1.25f).coerceIn(8f, drawWidth - 8f)" - " val labelY = (center.y - transform.scale * 1.15f).coerceIn(34f, drawHeight - 12f)" - " drawShipLabel(canvas, label, labelX, labelY, ship, position, labelSide)" - " }" "" " private fun drawRaceShipSilhouette(" - " canvas: Canvas," " center: Point," - " radius: Float," " angle: Double," - " ship: JSONObject?," " fillPaint: Paint" - " ) {" " val state = canvas.save()" - " canvas.translate(center.x, center.y)" - " canvas.rotate((angle * 180.0 / PI).toFloat())" - " val iconRadius = radius * 1.24f" - " when (shipRaceKey(ship)) {" - " \"federation\" -> drawFederationSilhouette(canvas, iconRadius, fillPaint)" - " \"klingon\" -> drawKlingonSilhouette(canvas, iconRadius, fillPaint)" - " \"romulan\" -> drawRomulanSilhouette(canvas, iconRadius, fillPaint)" - " \"kzinti\" -> drawKzintiSilhouette(canvas, iconRadius, fillPaint)" - " \"andromedan\" -> drawAndromedanSilhouette(canvas, iconRadius, fillPaint)" - " \"tholian\" -> drawTholianSilhouette(canvas, iconRadius, fillPaint)" - " \"gorn\" -> drawGornSilhouette(canvas, iconRadius, fillPaint)" - " \"hydran\" -> drawHydranSilhouette(canvas, iconRadius, fillPaint)" - " \"lyran\" -> drawLyranSilhouette(canvas, iconRadius, fillPaint)" - " \"isc\" -> drawIscSilhouette(canvas, iconRadius, fillPaint)" - " \"orion\" -> drawOrionSilhouette(canvas, iconRadius, fillPaint)" - " else -> drawGenericSilhouette(canvas, iconRadius, fillPaint)" - " }" " canvas.restoreToCount(state)" " }" - "" - " private fun drawFederationSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.14f, r * 0.10f, r * 0.14f), r * 0.10f, r * 0.10f, fillPaint)" - " canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.70f, r * 0.18f, -r * 0.48f), r * 0.12f, r * 0.12f, fillPaint)" - " canvas.drawRoundRect(RectF(-r * 0.92f, r * 0.48f, r * 0.18f, r * 0.70f), r * 0.12f, r * 0.12f, fillPaint)" - " canvas.drawOval(RectF(-r * 0.08f, -r * 0.58f, r * 1.10f, r * 0.58f), fillPaint)" - " canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.14f, r * 0.10f, r * 0.14f), r * 0.10f, r * 0.10f, shipStrokePaint)" - " canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.70f, r * 0.18f, -r * 0.48f), r * 0.12f, r * 0.12f, shipStrokePaint)" - " canvas.drawRoundRect(RectF(-r * 0.92f, r * 0.48f, r * 0.18f, r * 0.70f), r * 0.12f, r * 0.12f, shipStrokePaint)" - " canvas.drawOval(RectF(-r * 0.08f, -r * 0.58f, r * 1.10f, r * 0.58f), shipStrokePaint)" - " canvas.drawLine(r * 0.16f, 0f, r * 0.84f, 0f, shipDetailPaint)" - " }" "" - " private fun drawKlingonSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " val wings = Path().apply {" - " moveTo(r * 0.10f, -r * 0.14f)" - " lineTo(-r * 1.06f, -r * 0.78f)" - " lineTo(-r * 0.70f, -r * 0.18f)" - " lineTo(-r * 0.20f, 0f)" - " lineTo(-r * 0.70f, r * 0.18f)" - " lineTo(-r * 1.06f, r * 0.78f)" - " lineTo(r * 0.10f, r * 0.14f)" - " close()" " }" - " canvas.drawPath(wings, fillPaint)" - " canvas.drawPath(wings, shipStrokePaint)" - " canvas.drawRoundRect(RectF(-r * 0.18f, -r * 0.10f, r * 0.76f, r * 0.10f), r * 0.08f, r * 0.08f, fillPaint)" - " canvas.drawRoundRect(RectF(r * 0.58f, -r * 0.28f, r * 1.10f, r * 0.28f), r * 0.14f, r * 0.14f, fillPaint)" - " canvas.drawRoundRect(RectF(-r * 0.18f, -r * 0.10f, r * 0.76f, r * 0.10f), r * 0.08f, r * 0.08f, shipStrokePaint)" - " canvas.drawRoundRect(RectF(r * 0.58f, -r * 0.28f, r * 1.10f, r * 0.28f), r * 0.14f, r * 0.14f, shipStrokePaint)" - " }" "" - " private fun drawRomulanSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " val path = Path().apply {" - " moveTo(r * 1.18f, 0f)" - " lineTo(r * 0.18f, -r * 0.30f)" - " lineTo(-r * 1.12f, -r * 0.88f)" - " lineTo(-r * 0.64f, 0f)" - " lineTo(-r * 1.12f, r * 0.88f)" - " lineTo(r * 0.18f, r * 0.30f)" - " close()" " }" - " canvas.drawPath(path, fillPaint)" - " canvas.drawPath(path, shipStrokePaint)" - " canvas.drawLine(-r * 0.64f, 0f, r * 0.72f, 0f, shipDetailPaint)" - " }" "" - " private fun drawKzintiSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " val port = Path().apply {" - " moveTo(r * 0.95f, -r * 0.45f)" - " lineTo(-r * 0.96f, -r * 0.70f)" - " lineTo(-r * 0.60f, -r * 0.20f)" - " lineTo(r * 0.76f, -r * 0.12f)" - " close()" " }" - " val starboard = Path().apply {" - " moveTo(r * 0.95f, r * 0.45f)" - " lineTo(-r * 0.96f, r * 0.70f)" - " lineTo(-r * 0.60f, r * 0.20f)" - " lineTo(r * 0.76f, r * 0.12f)" - " close()" " }" - " val center = Path().apply {" - " moveTo(r * 1.05f, 0f)" - " lineTo(-r * 0.38f, -r * 0.22f)" - " lineTo(-r * 0.70f, 0f)" - " lineTo(-r * 0.38f, r * 0.22f)" - " close()" " }" - " drawFilledPath(canvas, port, fillPaint)" - " drawFilledPath(canvas, starboard, fillPaint)" - " drawFilledPath(canvas, center, fillPaint)" " }" - "" - " private fun drawAndromedanSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " canvas.drawOval(RectF(-r * 1.08f, -r * 0.50f, r * 1.08f, r * 0.50f), fillPaint)" - " canvas.drawOval(RectF(-r * 1.08f, -r * 0.50f, r * 1.08f, r * 0.50f), shipStrokePaint)" - " canvas.drawOval(RectF(-r * 0.34f, -r * 0.30f, r * 0.34f, r * 0.30f), shipDetailPaint)" - " canvas.drawCircle(-r * 0.70f, 0f, r * 0.18f, shipDetailPaint)" - " canvas.drawCircle(r * 0.70f, 0f, r * 0.18f, shipDetailPaint)" - " }" "" - " private fun drawTholianSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " val path = Path().apply {" - " moveTo(r * 1.10f, 0f)" - " lineTo(0f, -r * 0.92f)" - " lineTo(-r * 1.02f, 0f)" - " lineTo(0f, r * 0.92f)" " close()" - " }" - " drawFilledPath(canvas, path, fillPaint)" - " canvas.drawLine(-r * 0.62f, 0f, r * 0.62f, 0f, shipDetailPaint)" - " canvas.drawLine(0f, -r * 0.56f, 0f, r * 0.56f, shipDetailPaint)" - " }" "" - " private fun drawGornSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.46f, r * 0.28f, r * 0.46f), r * 0.13f, r * 0.13f, fillPaint)" - " canvas.drawRoundRect(RectF(-r * 0.92f, -r * 0.46f, r * 0.28f, r * 0.46f), r * 0.13f, r * 0.13f, shipStrokePaint)" - " val head = Path().apply {" - " moveTo(r * 1.08f, 0f)" - " lineTo(r * 0.20f, -r * 0.42f)" - " lineTo(r * 0.20f, r * 0.42f)" - " close()" " }" - " drawFilledPath(canvas, head, fillPaint)" - " canvas.drawLine(-r * 0.72f, 0f, r * 0.18f, 0f, shipDetailPaint)" - " }" "" - " private fun drawHydranSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.18f, r * 1.00f, r * 0.18f), r * 0.12f, r * 0.12f, fillPaint)" - " canvas.drawRoundRect(RectF(-r * 0.88f, -r * 0.18f, r * 1.00f, r * 0.18f), r * 0.12f, r * 0.12f, shipStrokePaint)" - " canvas.drawOval(RectF(-r * 0.75f, -r * 0.76f, r * 0.48f, -r * 0.34f), fillPaint)" - " canvas.drawOval(RectF(-r * 0.75f, r * 0.34f, r * 0.48f, r * 0.76f), fillPaint)" - " canvas.drawOval(RectF(-r * 0.75f, -r * 0.76f, r * 0.48f, -r * 0.34f), shipStrokePaint)" - " canvas.drawOval(RectF(-r * 0.75f, r * 0.34f, r * 0.48f, r * 0.76f), shipStrokePaint)" - " }" "" - " private fun drawLyranSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " val port = Path().apply {" - " moveTo(r * 0.95f, -r * 0.28f)" - " lineTo(-r * 0.98f, -r * 0.68f)" - " lineTo(-r * 0.74f, -r * 0.18f)" - " lineTo(r * 0.82f, -r * 0.08f)" - " close()" " }" - " val starboard = Path().apply {" - " moveTo(r * 0.95f, r * 0.28f)" - " lineTo(-r * 0.98f, r * 0.68f)" - " lineTo(-r * 0.74f, r * 0.18f)" - " lineTo(r * 0.82f, r * 0.08f)" - " close()" " }" - " drawFilledPath(canvas, port, fillPaint)" - " drawFilledPath(canvas, starboard, fillPaint)" - " canvas.drawLine(-r * 0.52f, 0f, r * 0.78f, 0f, shipDetailPaint)" - " }" "" - " private fun drawIscSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " canvas.drawOval(RectF(-r * 0.82f, -r * 0.20f, r * 1.00f, r * 0.20f), fillPaint)" - " canvas.drawOval(RectF(-r * 0.82f, -r * 0.20f, r * 1.00f, r * 0.20f), shipStrokePaint)" - " canvas.drawOval(RectF(-r * 0.74f, -r * 0.76f, r * 0.38f, -r * 0.38f), fillPaint)" - " canvas.drawOval(RectF(-r * 0.74f, r * 0.38f, r * 0.38f, r * 0.76f), fillPaint)" - " canvas.drawOval(RectF(-r * 0.74f, -r * 0.76f, r * 0.38f, -r * 0.38f), shipStrokePaint)" - " canvas.drawOval(RectF(-r * 0.74f, r * 0.38f, r * 0.38f, r * 0.76f), shipStrokePaint)" - " canvas.drawCircle(r * 0.78f, 0f, r * 0.20f, shipDetailPaint)" - " }" "" - " private fun drawOrionSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " val path = Path().apply {" - " moveTo(r * 1.12f, 0f)" - " lineTo(-r * 0.95f, -r * 0.56f)" - " lineTo(-r * 0.52f, 0f)" - " lineTo(-r * 0.95f, r * 0.56f)" - " close()" " }" - " drawFilledPath(canvas, path, fillPaint)" - " canvas.drawLine(-r * 0.45f, 0f, r * 0.76f, 0f, shipDetailPaint)" - " }" "" - " private fun drawGenericSilhouette(canvas: Canvas, r: Float, fillPaint: Paint) {" - " val path = Path().apply {" - " moveTo(r * 1.35f, 0f)" - " lineTo(-r * 0.85f, r * 0.90f)" - " lineTo(-r * 0.45f, 0f)" - " lineTo(-r * 0.85f, -r * 0.90f)" - " close()" " }" - " drawFilledPath(canvas, path, fillPaint)" " }" "" - " private fun drawFilledPath(canvas: Canvas, path: Path, fillPaint: Paint) {" - " canvas.drawPath(path, fillPaint)" - " canvas.drawPath(path, shipShadowStrokePaint)" - " canvas.drawPath(path, shipStrokePaint)" " }" "" - " private fun drawRaceBadge(canvas: Canvas, center: Point, radius: Float, ship: JSONObject?) {" - " val badge = shipRaceBadge(ship)" - " if (badge.isBlank()) return" - " val width = shipBadgePaint.measureText(badge) + 12f" - " val height = 17f" - " val y = center.y + radius * 0.95f" - " val rect = RectF(center.x - width / 2f, y - height / 2f, center.x + width / 2f, y + height / 2f)" - " canvas.drawRoundRect(rect, 6f, 6f, shipBadgeBackPaint)" - " canvas.drawText(badge, center.x, y + 4.5f, shipBadgePaint)" - " }" "" - " private fun shipRaceBadge(ship: JSONObject?): String =" - " when (shipRaceKey(ship)) {" - " \"andromedan\" -> \"AND\"" - " \"federation\" -> \"FED\"" - " \"klingon\" -> \"KLI\"" - " \"romulan\" -> \"ROM\"" - " \"kzinti\" -> \"KZN\"" - " \"tholian\" -> \"THO\"" - " \"gorn\" -> \"GOR\"" - " \"hydran\" -> \"HYD\"" - " \"lyran\" -> \"LYR\"" - " \"isc\" -> \"ISC\"" - " \"orion\" -> \"ORI\"" - " else -> \"\"" " }" "" - " private fun shipRaceKey(ship: JSONObject?): String {" - " val properties = ship?.optJSONObject(\"properties\")" - " val text = listOf(" - " properties?.optString(\"race\", \"\")," - " properties?.optString(\"empire\", \"\")," - " properties?.optString(\"ship-group\", \"\")," - " ship?.optString(\"template-id\", \"\")," - " ship?.optString(\"id\", \"\")," - " ship?.optString(\"name\", \"\")" - " ).joinToString(\" \").lowercase()" - " return when {" - " \"andromedan\" in text || \"and-\" in text || \"andro\" in text -> \"andromedan\"" - " \"federation\" in text || \"fed-\" in text || \" starfleet \" in \" $text \" -> \"federation\"" - " \"klingon\" in text || \"klingon-\" in text || \"kli-\" in text -> \"klingon\""