package com.evsuite.abrp; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Typeface; import android.os.SystemClock; import java.util.Locale; /** * The Quest Overview style, after the interface of a fantasy online game like World of Warcraft: * gold-trimmed frames over a dusk landscape, the car as the player character. * * Top left, the player frame: a round portrait, the gear in the level badge, the battery as the * health bar and power as the energy bar. Top right, the minimap with the clock. In the middle * the speed in large gold numbers, and under it the cast bar — "Accelerating" while drawing * power, "Regenerating" while recovering it, "Coasting" in between and "Resting" in P. Right, * the quest tracker: a road trip whose objectives are the trip, the usage and the climate. * Bottom left, a chat window with system lines; bottom, P R N D as action buttons and the * odometer as the experience bar (a level per 1000 km). Below 20 % a raid warning flashes. * Unread values show "—" and fill nothing. Drawn per reading; only the raid warning blinks, by * the readings that redraw it. */ final class QuestView extends ClusterCanvasView { private static final int GOLD = 0xFFC9A24A, GOLD_DARK = 0xFF7A5C24, YELLOW = 0xFFFFD100, PANEL = 0xE01A130D, HEALTH = 0xFF26B33A, ENERGY = 0xFFF2D21B, REGEN = 0xFF3FBF6F, XP = 0xFF8E44C9, RED = 0xFFFF3B30, SYSTEM = 0xFFFFE96A; private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint skyPaint = new Paint(); private final Path path = new Path(); private final RectF r = new RectF(); private final Typeface serif = Typeface.create(Typeface.SERIF, Typeface.BOLD); private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD); private int skyFor = -1; QuestView(Context context) { super(context); line.setStyle(Paint.Style.STROKE); } @Override protected void onDraw(Canvas c) { super.onDraw(c); float w = getWidth(), h = getHeight(); if (w <= 0 || h <= 0) return; float u = Math.min(h / 640f, w / 1100f); drawLandscape(c, w, h); drawPlayer(c, u); drawMinimap(c, w, u); drawSpeed(c, w, u); drawCastBar(c, w, u); drawQuests(c, w, u); drawChat(c, u); drawActionBar(c, w, h, u); drawXp(c, w, h, u); } // ---------- World ---------- private void drawLandscape(Canvas c, float w, float h) { if (skyFor != (int) h) { skyPaint.setShader(new LinearGradient(0, 0, 0, h, new int[] { 0xFF0B1424, 0xFF1C2A3A, 0xFF2A2418 }, new float[] { 0f, 0.55f, 1f }, Shader.TileMode.CLAMP)); skyFor = (int) h; } c.drawRect(0, 0, w, h, skyPaint); // Two ranges of hills, the near one darker. hills(c, w, h, h * 0.62f, 0xFF16202A, 0.9f); hills(c, w, h, h * 0.72f, 0xFF10171D, 1.6f); } private void hills(Canvas c, float w, float h, float base, int colour, float wave) { path.reset(); path.moveTo(0, h); for (int i = 0; i <= 24; i++) { float x = w * i / 24f; float y = base - (float) (Math.sin(i * wave) * 0.5 + Math.sin(i * wave * 0.37 + 1) * 0.5) * h * 0.06f; path.lineTo(x, y); } path.lineTo(w, h); path.close(); fill.setColor(colour); c.drawPath(path, fill); } // ---------- Frames ---------- /** A dark panel with a gold rim. */ private void panel(Canvas c, float l, float t, float rr, float b, float u) { r.set(l, t, rr, b); fill.setColor(PANEL); c.drawRoundRect(r, 6f * u, 6f * u, fill); line.setColor(GOLD_DARK); line.setStrokeWidth(4f * u); c.drawRoundRect(r, 6f * u, 6f * u, line); line.setColor(GOLD); line.setStrokeWidth(1.5f * u); c.drawRoundRect(r, 6f * u, 6f * u, line); } /** Text with a dark outline, as game interfaces write over the world. */ private void outlined(Canvas c, String s, float x, float y, float size, int colour, Typeface face, Paint.Align align, float u) { text.setTypeface(face); text.setTextSize(size); text.setTextAlign(align); text.setStyle(Paint.Style.STROKE); text.setStrokeWidth(Math.max(2f * u, size * 0.08f)); text.setStrokeJoin(Paint.Join.ROUND); text.setColor(0xFF000000); c.drawText(s, x, y, text); text.setStyle(Paint.Style.FILL); text.setColor(colour); c.drawText(s, x, y, text); } /** A bar with a dark track, a fill of [frac] and centred text. */ private void bar(Canvas c, float l, float t, float rr, float b, float frac, int colour, String label, float u) { r.set(l, t, rr, b); fill.setColor(0xFF0A0A0A); c.drawRect(r, fill); if (frac > 0f) { r.set(l, t, l + (rr - l) * Math.min(1f, frac), b); fill.setColor(colour); c.drawRect(r, fill); fill.setColor(0x33FFFFFF); c.drawRect(r.left, t, r.right, t + (b - t) * 0.35f, fill); } line.setColor(GOLD_DARK); line.setStrokeWidth(2f * u); c.drawRect(l, t, rr, b, line); outlined(c, label, (l + rr) / 2f, b - (b - t) * 0.22f, (b - t) * 0.72f, 0xFFFFFFFF, sans, Paint.Align.CENTER, u); } // ---------- Player ---------- private void drawPlayer(Canvas c, float u) { float px = 86f * u, py = 86f * u, pr = 60f * u; // Name plate and bars, behind the portrait. float l = px + 20f * u, rr = px + 380f * u; panel(c, l, py - 56f * u, rr, py + 46f * u, u); outlined(c, "MG4", l + 60f * u, py - 28f * u, 24f * u, YELLOW, serif, Paint.Align.LEFT, u); float soc = v.socPercent == null ? 0f : v.socPercent / 100f; boolean low = v.socPercent != null && v.socPercent < 20f; bar(c, l + 56f * u, py - 18f * u, rr - 12f * u, py + 10f * u, soc, low ? RED : HEALTH, fmt(v.socPercent, "%.0f %%", "—") + " · " + fmt(v.rangeKm, "%.0f km", "—"), u); boolean regen = v.powerKw != null && v.powerKw < -0.5f; float energy = v.powerKw == null ? 0f : Math.abs(v.powerKw) / 150f; bar(c, l + 56f * u, py + 14f * u, rr - 12f * u, py + 36f * u, energy, regen ? REGEN : ENERGY, v.powerKw == null ? "—" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW", Math.abs(v.powerKw)), u); // The portrait: a gold ring round the car's name. fill.setColor(0xFF140F0A); c.drawCircle(px, py, pr, fill); line.setColor(GOLD_DARK); line.setStrokeWidth(10f * u); c.drawCircle(px, py, pr, line); line.setColor(GOLD); line.setStrokeWidth(3f * u); c.drawCircle(px, py, pr + 3f * u, line); drawCarGlyph(c, px, py + 6f * u, pr * 0.62f, u); // The level badge: the gear. float bx = px - pr * 0.72f, by = py + pr * 0.72f, br = 20f * u; fill.setColor(0xFF140F0A); c.drawCircle(bx, by, br, fill); line.setColor(GOLD); line.setStrokeWidth(3f * u); c.drawCircle(bx, by, br, line); String gear = Gear.label(v.gear); outlined(c, gear.isEmpty() ? "?" : gear, bx, by + 8f * u, 22f * u, YELLOW, serif, Paint.Align.CENTER, u); } /** A small car in profile for the portrait. */ private void drawCarGlyph(Canvas c, float cx, float cy, float s, float u) { path.reset(); path.moveTo(cx - s, cy + s * 0.15f); path.lineTo(cx - s * 0.92f, cy - s * 0.12f); path.quadTo(cx - s * 0.55f, cy - s * 0.2f, cx - s * 0.35f, cy - s * 0.48f); path.lineTo(cx + s * 0.3f, cy - s * 0.5f); path.quadTo(cx + s * 0.62f, cy - s * 0.22f, cx + s * 0.95f, cy - s * 0.1f); path.lineTo(cx + s, cy + s * 0.15f); path.close(); fill.setColor(GOLD); c.drawPath(path, fill); fill.setColor(0xFF140F0A); c.drawCircle(cx - s * 0.55f, cy + s * 0.18f, s * 0.2f, fill); c.drawCircle(cx + s * 0.55f, cy + s * 0.18f, s * 0.2f, fill); fill.setColor(GOLD); c.drawCircle(cx - s * 0.55f, cy + s * 0.18f, s * 0.11f, fill); c.drawCircle(cx + s * 0.55f, cy + s * 0.18f, s * 0.11f, fill); } // ---------- Minimap ---------- private void drawMinimap(Canvas c, float w, float u) { float cx = w - 120f * u, cy = 128f * u, mr = 84f * u; outlined(c, "The Open Road", cx, 30f * u, 20f * u, YELLOW, serif, Paint.Align.CENTER, u); fill.setColor(0xFF1E3524); c.drawCircle(cx, cy, mr, fill); // Roads and a river, clipped to the map. c.save(); path.reset(); path.addCircle(cx, cy, mr, Path.Direction.CW); c.clipPath(path); line.setColor(0xFF8A7650); line.setStrokeWidth(5f * u); c.drawLine(cx, cy + mr, cx, cy - mr, line); c.drawLine(cx - mr, cy - mr * 0.35f, cx + mr, cy + mr * 0.1f, line); line.setColor(0xFF2E5C8A); line.setStrokeWidth(7f * u); path.reset(); path.moveTo(cx - mr, cy + mr * 0.5f); path.quadTo(cx - mr * 0.2f, cy + mr * 0.2f, cx + mr, cy + mr * 0.7f); c.drawPath(path, line); c.restore(); // The player arrow. path.reset(); path.moveTo(cx, cy - 13f * u); path.lineTo(cx + 9f * u, cy + 10f * u); path.lineTo(cx, cy + 5f * u); path.lineTo(cx - 9f * u, cy + 10f * u); path.close(); fill.setColor(YELLOW); c.drawPath(path, fill); line.setColor(0xFF000000); line.setStrokeWidth(1.5f * u); c.drawPath(path, line); // The stone rim. line.setColor(0xFF4A4036); line.setStrokeWidth(12f * u); c.drawCircle(cx, cy, mr + 5f * u, line); line.setColor(GOLD); line.setStrokeWidth(2f * u); c.drawCircle(cx, cy, mr + 11f * u, line); c.drawCircle(cx, cy, mr - 1f * u, line); // The clock under it. panel(c, cx - 50f * u, cy + mr + 16f * u, cx + 50f * u, cy + mr + 48f * u, u); outlined(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + mr + 40f * u, 22f * u, 0xFFFFFFFF, sans, Paint.Align.CENTER, u); } // ---------- Speed and cast bar ---------- private void drawSpeed(Canvas c, float w, float u) { boolean low = v.socPercent != null && v.socPercent < 20f; if (low && (SystemClock.uptimeMillis() / 600) % 2 == 0) { outlined(c, "Low battery!", w / 2f, 64f * u, 34f * u, RED, serif, Paint.Align.CENTER, u); } outlined(c, fmt(v.speedKmh, "%.0f", "—"), w / 2f, 236f * u, 150f * u, YELLOW, serif, Paint.Align.CENTER, u); outlined(c, "km/h", w / 2f, 270f * u, 24f * u, GOLD, serif, Paint.Align.CENTER, u); } private void drawCastBar(Canvas c, float w, float u) { boolean parked = v.gear != null && v.gear == 1; Float p = v.powerKw; String spell; int colour; float frac; if (parked) { spell = "Resting"; colour = 0xFF5B7BD9; frac = 1f; } else if (p == null) { spell = "—"; colour = ENERGY; frac = 0f; } else if (p > 0.5f) { spell = "Accelerating"; colour = 0xFFFFB300; frac = p / 150f; } else if (p < -0.5f) { spell = "Regenerating"; colour = REGEN; frac = -p / 50f; } else { spell = "Coasting"; colour = 0xFF8F8F8F; frac = 0.02f; } float bw = Math.min(520f * u, w * 0.36f), l = (w - bw) / 2f + 18f * u, rr = l + bw, t = 296f * u, b = t + 30f * u; // The spell icon. float il = l - 40f * u; r.set(il, t - 3f * u, il + 36f * u, b + 3f * u); fill.setColor(0xFF140F0A); c.drawRect(r, fill); line.setColor(GOLD); line.setStrokeWidth(2f * u); c.drawRect(r, line); drawSpellIcon(c, r.centerX(), r.centerY(), 13f * u, spell, colour, u); r.set(l, t, rr, b); fill.setColor(0xFF0A0A0A); c.drawRect(r, fill); r.set(l, t, l + bw * Math.max(0f, Math.min(1f, frac)), b); fill.setColor(colour); c.drawRect(r, fill); fill.setColor(0x33FFFFFF); c.drawRect(r.left, t, r.right, t + 10f * u, fill); line.setColor(GOLD); line.setStrokeWidth(2.5f * u); c.drawRect(l, t, rr, b, line); outlined(c, spell, l + 10f * u, b - 8f * u, 20f * u, 0xFFFFFFFF, sans, Paint.Align.LEFT, u); outlined(c, p == null ? "—" : String.format(Locale.US, "%.1f kW", Math.abs(p)), rr - 10f * u, b - 8f * u, 20f * u, 0xFFFFFFFF, sans, Paint.Align.RIGHT, u); } private void drawSpellIcon(Canvas c, float cx, float cy, float s, String spell, int colour, float u) { fill.setColor(colour); path.reset(); switch (spell) { case "Accelerating": // a lightning bolt path.moveTo(cx + s * 0.2f, cy - s); path.lineTo(cx - s * 0.6f, cy + s * 0.1f); path.lineTo(cx, cy + s * 0.1f); path.lineTo(cx - s * 0.2f, cy + s); path.lineTo(cx + s * 0.6f, cy - s * 0.15f); path.lineTo(cx, cy - s * 0.15f); path.close(); c.drawPath(path, fill); break; case "Regenerating": // a leaf path.moveTo(cx - s * 0.8f, cy + s * 0.8f); path.quadTo(cx - s, cy - s, cx + s * 0.8f, cy - s * 0.8f); path.quadTo(cx + s, cy + s * 0.9f, cx - s * 0.8f, cy + s * 0.8f); c.drawPath(path, fill); break; case "Resting": outlined(c, "Zz", cx, cy + s * 0.5f, s * 1.5f, colour, serif, Paint.Align.CENTER, u); break; default: c.drawCircle(cx, cy, s * 0.5f, fill); } } // ---------- Quest tracker and chat ---------- private void drawQuests(Canvas c, float w, float u) { float x = w - 400f * u, y = 310f * u; fill.setColor(YELLOW); c.drawCircle(x - 18f * u, y - 9f * u, 11f * u, fill); outlined(c, "!", x - 18f * u, y - 1f * u, 18f * u, 0xFF000000, serif, Paint.Align.CENTER, u); outlined(c, "Road trip", x, y, 26f * u, YELLOW, serif, Paint.Align.LEFT, u); String[] objectives = { "Distance: " + fmt(v.tripKm, "%.1f km", "—"), "Usage, 5 min: " + fmt(v.usage5Min, "%.1f kWh/100 km", "—"), "Since start: " + fmt(v.usageTrip, "%.1f kWh/100 km", "—"), "Climate: " + fmt(v.climateCelsius, "%.0f °C", "—"), }; for (int i = 0; i < objectives.length; i++) { outlined(c, "– " + objectives[i], x + 8f * u, y + (34f + i * 30f) * u, 21f * u, 0xFFE8E8E8, sans, Paint.Align.LEFT, u); } } private void drawChat(Canvas c, float u) { float l = 22f * u, t = 368f * u, rr = l + 470f * u, b = 520f * u; r.set(l, t, rr, b); fill.setColor(0x66000000); c.drawRoundRect(r, 6f * u, 6f * u, fill); String state = v.gear == null ? "The car is not answering." : v.gear == 1 ? "You feel rested." : v.gear == 2 ? "You are moving backwards." : "You are on the road."; String[] lines = { "Range remaining: " + fmt(v.rangeKm, "%.0f km", "—") + ".", "Odometer: " + fmt(v.odometerKm, "%,.0f km", "—") + ".", state, }; for (int i = 0; i < lines.length; i++) { outlined(c, lines[i], l + 14f * u, t + (38f + i * 40f) * u, 21f * u, SYSTEM, sans, Paint.Align.LEFT, u); } } // ---------- Action bar and experience ---------- private void drawActionBar(Canvas c, float w, float h, float u) { String[] gears = { "P", "R", "N", "D" }; String now = Gear.label(v.gear); float s = 64f * u, gap = 8f * u, total = gears.length * s + (gears.length - 1) * gap; float x = (w - total) / 2f, top = h - 104f * u; panel(c, x - 12f * u, top - 10f * u, x + total + 12f * u, top + s + 10f * u, u); for (int i = 0; i < gears.length; i++) { boolean on = gears[i].equals(now); r.set(x, top, x + s, top + s); fill.setColor(on ? 0xFF3A2A12 : 0xFF141414); c.drawRect(r, fill); line.setColor(on ? YELLOW : GOLD_DARK); line.setStrokeWidth((on ? 4f : 2f) * u); c.drawRect(r, line); outlined(c, gears[i], x + s / 2f, top + s * 0.72f, 36f * u, on ? YELLOW : 0xFF9A9A9A, serif, Paint.Align.CENTER, u); outlined(c, String.valueOf(i + 1), x + s - 6f * u, top + 16f * u, 13f * u, 0xFFD0D0D0, sans, Paint.Align.RIGHT, u); x += s + gap; } } /** The odometer as experience: a level per 1000 km, the bar the way through this one. */ private void drawXp(Canvas c, float w, float h, float u) { float l = 22f * u, rr = w - 22f * u, t = h - 26f * u, b = h - 10f * u; r.set(l, t, rr, b); fill.setColor(0xFF0A0A0A); c.drawRect(r, fill); String label = "—"; if (v.odometerKm != null) { float into = v.odometerKm % 1000f; r.set(l, t, l + (rr - l) * into / 1000f, b); fill.setColor(XP); c.drawRect(r, fill); label = String.format(Locale.US, "Level %d · %.0f / 1000 km", (int) (v.odometerKm / 1000f) + 1, into); } line.setColor(GOLD_DARK); line.setStrokeWidth(1.5f * u); for (int i = 1; i < 20; i++) { float x = l + (rr - l) * i / 20f; c.drawLine(x, t, x, b, line); } c.drawRect(l, t, rr, b, line); outlined(c, label, w / 2f, b - 3f * u, 13f * u, 0xFFFFFFFF, sans, Paint.Align.CENTER, u); } }