package com.evsuite.abrp; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Typeface; import android.os.SystemClock; import java.util.Locale; /** * The Empire Overview style, after the interface of a real-time strategy game like Age of * Empires: a wooden resource bar on top, an isometric world in the middle and a carved panel * at the bottom with the commands, the selected unit and a diamond-shaped minimap. * * The resource bar holds the battery, the range, the power and the usage over 5 minutes, the * age (by odometer, {@link EmpireAge}) and the clock. In the world the car is the selected unit * on a road, with the battery as its health bar; the ground moves past with the speed, which * stands large over the world. Below 20 % a warning blinks there. At the bottom: P R N D as * command buttons, the unit's portrait and stats (trip, usage since start, climate), and the * minimap with the odometer and the way to the next age. Unread values show "—" and fill * nothing; the world stands still when the speed is unread. * * The world animates (about 30 frames a second) only while the car moves and the view is on * screen. */ final class EmpireView extends ClusterCanvasView { private static final int WOOD = 0xFF3B2414, WOOD_LIGHT = 0xFF5A3A1E, WOOD_DARK = 0xFF24150B, TRIM = 0xFFC8A05A, PARCHMENT = 0xFFF2E2B8, TEAM = 0xFF3D6BFF, HEALTH = 0xFF3CC23C, RED = 0xFFE03A2A, GRASS = 0xFF4E7A2E, GRASS_ALT = 0xFF557F33, ROAD = 0xFF8A6A3C, REGEN = 0xFF6FE07A; /** Metres per tile, as the ground moves past. */ private static final float TILE_M = 6f; 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 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); /** Tiles travelled: the whole part picks the trees, the fraction moves the ground. */ private double travelled; private long lastFrame; EmpireView(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); boolean moving = v.speedKmh != null && v.speedKmh > 0.5f; advance(moving); float top = 46f * u, bottom = h - 186f * u; drawWorld(c, w, top, bottom, u); drawSpeed(c, w, top, u); drawResources(c, w, top, u); drawPanel(c, w, h, bottom, u); keepAnimating(moving, 33); } private void advance(boolean moving) { long now = SystemClock.uptimeMillis(); float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f); lastFrame = now; if (moving) travelled = (travelled + v.speedKmh / 3.6f * dt / TILE_M) % 1_000_000d; } // ---------- Shared ---------- private void say(Canvas c, String s, float x, float y, float size, int colour, Typeface face, Paint.Align a, float u) { text.setTypeface(face); text.setTextSize(size); text.setTextAlign(a); text.setColor(0xCC000000); c.drawText(s, x + Math.max(1f, size * 0.05f), y + Math.max(1f, size * 0.05f), text); text.setColor(colour); c.drawText(s, x, y, text); } /** Wood with planks and a gold trim. */ private void wood(Canvas c, float l, float t, float rr, float b, float u) { fill.setColor(WOOD); c.drawRect(l, t, rr, b, fill); line.setColor(WOOD_DARK); line.setStrokeWidth(2f * u); for (float y = t + 22f * u; y < b; y += 22f * u) c.drawLine(l, y, rr, y, line); line.setColor(WOOD_LIGHT); line.setStrokeWidth(1f * u); for (float y = t + 23.5f * u; y < b; y += 22f * u) c.drawLine(l, y, rr, y, line); } /** A recessed frame with a gold edge. */ private void inset(Canvas c, float l, float t, float rr, float b, int bg, float u) { r.set(l, t, rr, b); fill.setColor(bg); c.drawRect(r, fill); line.setColor(WOOD_DARK); line.setStrokeWidth(4f * u); c.drawRect(r, line); line.setColor(TRIM); line.setStrokeWidth(1.5f * u); c.drawRect(r, line); } private void bar(Canvas c, float l, float t, float rr, float b, float frac, int colour, float u) { fill.setColor(0xFF140C06); c.drawRect(l, t, rr, b, fill); if (frac > 0f) { fill.setColor(colour); c.drawRect(l, t, l + (rr - l) * Math.min(1f, frac), b, fill); } line.setColor(0xFF000000); line.setStrokeWidth(1.5f * u); c.drawRect(l, t, rr, b, line); } // ---------- World ---------- /** Isometric grass with a road through the middle; the car sits on it, the ground moves. */ private void drawWorld(Canvas c, float w, float top, float bottom, float u) { c.save(); c.clipRect(0, top, w, bottom); float tw = 112f * u, th = 56f * u; float ox = w * 0.36f, oy = top + (bottom - top) * 0.62f; long whole = (long) Math.floor(travelled); float phase = (float) (travelled - whole); // Moving forward along the road (up and to the right) moves the ground down and left. float sx = -phase * tw / 2f, sy = phase * th / 2f; int n = (int) Math.ceil((w / tw + (bottom - top) / th)) + 2; for (int i = -n; i <= n; i++) { for (int j = -n; j <= n; j++) { float cx = ox + (i + j) * tw / 2f + sx, cy = oy + (i - j) * th / 2f + sy; if (cx < -tw || cx > w + tw || cy < top - th || cy > bottom + th) continue; long cell = j + whole; boolean road = i == 0; int colour = road ? ROAD : (hash(i, cell) % 3 == 0 ? GRASS_ALT : GRASS); diamond(c, cx, cy, tw / 2f, th / 2f, colour); if (road && (cell & 1) == 0) { // Wheel ruts. line.setColor(0x40000000); line.setStrokeWidth(2f * u); c.drawLine(cx - tw * 0.18f, cy + th * 0.02f, cx + tw * 0.08f, cy - th * 0.11f, line); } } } // Trees, drawn back to front so the nearer ones overlap. for (int i = n; i >= -n; i--) { for (int j = -n; j <= n; j++) { if (Math.abs(i) < 2) continue; long cell = j + whole; if (hash(i, cell) % 7 != 0) continue; float cx = ox + (i + j) * tw / 2f + sx, cy = oy + (i - j) * th / 2f + sy; if (cx < -tw || cx > w + tw || cy < top - th || cy > bottom + 2 * th) continue; tree(c, cx, cy, u); } } drawUnit(c, ox, oy, u); c.restore(); } private static int hash(long i, long j) { long x = i * 73_856_093L ^ j * 19_349_663L; x ^= x >>> 13; x *= 0x5bd1e995L; x ^= x >>> 15; return (int) ((x & 0x7fffffffL) % 1_000_003L); } private void diamond(Canvas c, float cx, float cy, float hw, float hh, int colour) { path.reset(); path.moveTo(cx, cy - hh); path.lineTo(cx + hw, cy); path.lineTo(cx, cy + hh); path.lineTo(cx - hw, cy); path.close(); fill.setColor(colour); c.drawPath(path, fill); line.setColor(0x18000000); line.setStrokeWidth(1f); c.drawPath(path, line); } private void tree(Canvas c, float x, float y, float u) { fill.setColor(0x40000000); c.drawOval(x - 20f * u, y - 6f * u, x + 20f * u, y + 6f * u, fill); fill.setColor(0xFF5A3A1E); c.drawRect(x - 3f * u, y - 14f * u, x + 3f * u, y, fill); for (int k = 0; k < 3; k++) { float base = y - 10f * u - k * 16f * u, half = (22f - k * 5f) * u; path.reset(); path.moveTo(x - half, base); path.lineTo(x, base - 28f * u); path.lineTo(x + half, base); path.close(); fill.setColor(k == 1 ? 0xFF2F5A23 : 0xFF274D1D); c.drawPath(path, fill); } } /** The car as the selected unit: a selection ring, the car along the road, its health bar. */ private void drawUnit(Canvas c, float x, float y, float u) { line.setColor(0xFFFFFFFF); line.setStrokeWidth(2f * u); c.drawOval(x - 62f * u, y - 24f * u, x + 62f * u, y + 24f * u, line); c.save(); c.translate(x, y - 6f * u); c.rotate(-26.565f); carProfile(c, 0f, 0f, 54f * u, TEAM); c.restore(); float frac = v.socPercent == null ? 0f : v.socPercent / 100f; boolean low = v.socPercent != null && v.socPercent < 20f; bar(c, x - 40f * u, y - 92f * u, x + 40f * u, y - 84f * u, frac, low ? RED : HEALTH, u); } /** A car in profile, facing right, in the player's colour. */ private void carProfile(Canvas c, float cx, float cy, float s, int colour) { path.reset(); path.moveTo(cx - s, cy + s * 0.15f); path.lineTo(cx - s * 0.92f, cy - s * 0.15f); path.quadTo(cx - s * 0.55f, cy - s * 0.22f, cx - s * 0.32f, cy - s * 0.5f); path.lineTo(cx + s * 0.28f, cy - s * 0.52f); path.quadTo(cx + s * 0.6f, cy - s * 0.24f, cx + s * 0.95f, cy - s * 0.12f); path.lineTo(cx + s, cy + s * 0.15f); path.close(); fill.setColor(colour); c.drawPath(path, fill); line.setColor(0xFF0A1440); line.setStrokeWidth(s * 0.04f); c.drawPath(path, line); // Windows. path.reset(); path.moveTo(cx - s * 0.26f, cy - s * 0.42f); path.lineTo(cx + s * 0.24f, cy - s * 0.43f); path.lineTo(cx + s * 0.44f, cy - s * 0.2f); path.lineTo(cx - s * 0.45f, cy - s * 0.2f); path.close(); fill.setColor(0xFFBFD8FF); c.drawPath(path, fill); fill.setColor(0xFF15100A); c.drawCircle(cx - s * 0.55f, cy + s * 0.17f, s * 0.21f, fill); c.drawCircle(cx + s * 0.55f, cy + s * 0.17f, s * 0.21f, fill); fill.setColor(0xFFB0B0B0); c.drawCircle(cx - s * 0.55f, cy + s * 0.17f, s * 0.1f, fill); c.drawCircle(cx + s * 0.55f, cy + s * 0.17f, s * 0.1f, fill); } // ---------- Speed ---------- private void drawSpeed(Canvas c, float w, float top, float u) { float x = w * 0.66f; say(c, fmt(v.speedKmh, "%.0f", "—"), x, top + 150f * u, 130f * u, PARCHMENT, serif, Paint.Align.CENTER, u); say(c, "km/h", x, top + 184f * u, 26f * u, PARCHMENT, serif, Paint.Align.CENTER, u); boolean low = v.socPercent != null && v.socPercent < 20f; if (low && (SystemClock.uptimeMillis() / 600) % 2 == 0) { say(c, "Your battery is running low!", x, top + 236f * u, 28f * u, 0xFFFFD040, serif, Paint.Align.CENTER, u); } } // ---------- Resource bar ---------- private void drawResources(Canvas c, float w, float top, float u) { wood(c, 0, 0, w, top, u); line.setColor(TRIM); line.setStrokeWidth(2f * u); c.drawLine(0, top - 1f * u, w, top - 1f * u, line); float y = top / 2f, x = 22f * u, slot = 170f * u; boolean low = v.socPercent != null && v.socPercent < 20f; icon(c, 0, x, y, u); say(c, fmt(v.socPercent, "%.0f %%", "—"), x + 30f * u, y + 9f * u, 26f * u, low ? RED : PARCHMENT, sans, Paint.Align.LEFT, u); x += slot; icon(c, 1, x, y, u); say(c, fmt(v.rangeKm, "%.0f km", "—"), x + 30f * u, y + 9f * u, 26f * u, PARCHMENT, sans, Paint.Align.LEFT, u); x += slot; icon(c, 2, x, y, u); boolean regen = v.powerKw != null && v.powerKw < -0.05f; String power = v.powerKw == null ? "—" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW", Math.abs(v.powerKw)); say(c, power, x + 30f * u, y + 9f * u, 26f * u, regen ? REGEN : PARCHMENT, sans, Paint.Align.LEFT, u); x += slot; icon(c, 3, x, y, u); say(c, fmt(v.usage5Min, "%.1f kWh/100", "—"), x + 30f * u, y + 9f * u, 26f * u, PARCHMENT, sans, Paint.Align.LEFT, u); say(c, v.clock.isEmpty() ? "—" : v.clock, w - 22f * u, y + 9f * u, 26f * u, PARCHMENT, sans, Paint.Align.RIGHT, u); say(c, EmpireAge.name(v.odometerKm), w - 130f * u, y + 9f * u, 26f * u, 0xFFFFD873, serif, Paint.Align.RIGHT, u); } /** Small resource pictures: 0 battery, 1 flag, 2 bolt, 3 leaf. */ private void icon(Canvas c, int kind, float x, float y, float u) { float s = 11f * u; fill.setColor(0xFF000000); r.set(x - s - 2f * u, y - s - 2f * u, x + s + 2f * u, y + s + 2f * u); c.drawRect(r, fill); line.setColor(TRIM); line.setStrokeWidth(1.2f * u); c.drawRect(r, line); path.reset(); switch (kind) { case 0: fill.setColor(HEALTH); c.drawRect(x - s * 0.5f, y - s * 0.6f, x + s * 0.5f, y + s * 0.85f, fill); c.drawRect(x - s * 0.22f, y - s * 0.85f, x + s * 0.22f, y - s * 0.6f, fill); break; case 1: fill.setColor(TEAM); path.moveTo(x - s * 0.5f, y - s * 0.8f); path.lineTo(x + s * 0.7f, y - s * 0.45f); path.lineTo(x - s * 0.5f, y - s * 0.1f); path.close(); c.drawPath(path, fill); line.setColor(PARCHMENT); line.setStrokeWidth(1.5f * u); c.drawLine(x - s * 0.5f, y - s * 0.85f, x - s * 0.5f, y + s * 0.85f, line); break; case 2: fill.setColor(0xFFFFD040); path.moveTo(x + s * 0.2f, y - s * 0.9f); path.lineTo(x - s * 0.55f, y + s * 0.1f); path.lineTo(x, y + s * 0.1f); path.lineTo(x - s * 0.2f, y + s * 0.9f); path.lineTo(x + s * 0.55f, y - s * 0.15f); path.lineTo(x, y - s * 0.15f); path.close(); c.drawPath(path, fill); break; default: fill.setColor(REGEN); path.moveTo(x - s * 0.7f, y + s * 0.7f); path.quadTo(x - s * 0.9f, y - s * 0.9f, x + s * 0.7f, y - s * 0.7f); path.quadTo(x + s * 0.9f, y + s * 0.8f, x - s * 0.7f, y + s * 0.7f); c.drawPath(path, fill); } } // ---------- Bottom panel ---------- private void drawPanel(Canvas c, float w, float h, float bottom, float u) { wood(c, 0, bottom, w, h, u); line.setColor(TRIM); line.setStrokeWidth(3f * u); c.drawLine(0, bottom + 1.5f * u, w, bottom + 1.5f * u, line); drawCommands(c, bottom, u); float mapW = 250f * u; drawUnitInfo(c, 224f * u, bottom, w - mapW - 40f * u, h, u); drawMinimap(c, w - mapW - 20f * u, bottom, w - 20f * u, h, u); } /** P R N D as command buttons, two by two. */ private void drawCommands(Canvas c, float bottom, float u) { String[] gears = { "P", "R", "N", "D" }; String now = Gear.label(v.gear); float s = 74f * u, gap = 8f * u, l = 30f * u, t = bottom + 16f * u; for (int i = 0; i < gears.length; i++) { float x = l + (i % 2) * (s + gap), y = t + (i / 2) * (s + gap); boolean on = gears[i].equals(now); inset(c, x, y, x + s, y + s, on ? 0xFF22346E : 0xFF1A120A, u); if (on) { line.setColor(0xFFFFD873); line.setStrokeWidth(3f * u); c.drawRect(x + 3f * u, y + 3f * u, x + s - 3f * u, y + s - 3f * u, line); } say(c, gears[i], x + s / 2f, y + s * 0.68f, 40f * u, on ? 0xFFFFFFFF : 0xFF9A8A6A, serif, Paint.Align.CENTER, u); } } /** The selected unit: portrait, health, name and stats. */ private void drawUnitInfo(Canvas c, float l, float t, float rr, float b, float u) { inset(c, l, t + 16f * u, rr, b - 14f * u, 0xFF1A120A, u); float ps = 96f * u, px = l + 16f * u, py = t + 28f * u; inset(c, px, py, px + ps, py + ps, 0xFF2C3E1E, u); carProfile(c, px + ps / 2f, py + ps * 0.6f, ps * 0.38f, TEAM); float frac = v.socPercent == null ? 0f : v.socPercent / 100f; boolean low = v.socPercent != null && v.socPercent < 20f; bar(c, px, py + ps + 8f * u, px + ps, py + ps + 22f * u, frac, low ? RED : HEALTH, u); say(c, fmt(v.socPercent, "%.0f / 100", "—"), px + ps / 2f, py + ps + 40f * u, 16f * u, PARCHMENT, sans, Paint.Align.CENTER, u); float x = px + ps + 24f * u; say(c, "MG4", x, py + 26f * u, 28f * u, 0xFFFFD873, serif, Paint.Align.LEFT, u); say(c, "Electric Cart", x + 80f * u, py + 26f * u, 20f * u, PARCHMENT, serif, Paint.Align.LEFT, u); String[][] stats = { { "Range", fmt(v.rangeKm, "%.0f km", "—") }, { "Trip", fmt(v.tripKm, "%.1f km", "—") }, { "Since start", fmt(v.usageTrip, "%.1f kWh/100 km", "—") }, { "Climate", fmt(v.climateCelsius, "%.0f °C", "—") }, }; float colW = (rr - x - 16f * u) / 2f; for (int i = 0; i < stats.length; i++) { float sx = x + (i % 2) * colW, sy = py + (66f + (i / 2) * 50f) * u; fill.setColor(TRIM); path.reset(); path.moveTo(sx + 6f * u, sy - 20f * u); path.lineTo(sx + 12f * u, sy - 14f * u); path.lineTo(sx + 6f * u, sy - 8f * u); path.lineTo(sx, sy - 14f * u); path.close(); c.drawPath(path, fill); say(c, stats[i][0], sx + 20f * u, sy - 7f * u, 17f * u, 0xFFBFAE88, sans, Paint.Align.LEFT, u); say(c, stats[i][1], sx + 20f * u, sy + 18f * u, 23f * u, PARCHMENT, sans, Paint.Align.LEFT, u); } } /** The diamond minimap: the player's land round the car, the road, the odometer under it. */ private void drawMinimap(Canvas c, float l, float t, float rr, float b, float u) { float cx = (l + rr) / 2f, cy = t + 60f * u, hw = (rr - l) / 2f, hh = 44f * u; path.reset(); path.moveTo(cx, cy - hh); path.lineTo(cx + hw, cy); path.lineTo(cx, cy + hh); path.lineTo(cx - hw, cy); path.close(); fill.setColor(0xFF000000); c.drawPath(path, fill); c.save(); c.clipPath(path); fill.setColor(0xFF35561F); c.drawOval(cx - hw * 0.7f, cy - hh * 0.65f, cx + hw * 0.5f, cy + hh * 0.75f, fill); fill.setColor(AccentColor.withAlpha(TEAM, 0x70)); c.drawOval(cx - hw * 0.32f, cy - hh * 0.3f, cx + hw * 0.2f, cy + hh * 0.34f, fill); line.setColor(ROAD); line.setStrokeWidth(3f * u); c.drawLine(cx - hw, cy + hh, cx + hw, cy - hh, line); c.restore(); line.setColor(TRIM); line.setStrokeWidth(2f * u); c.drawPath(path, line); fill.setColor(0xFFFFFFFF); c.drawCircle(cx, cy, 5f * u, fill); fill.setColor(TEAM); c.drawCircle(cx, cy, 3.5f * u, fill); say(c, fmt(v.odometerKm, "%,.0f km", "—"), cx, cy + hh + 24f * u, 20f * u, PARCHMENT, sans, Paint.Align.CENTER, u); String next = EmpireAge.next(v.odometerKm); Float toNext = EmpireAge.toNext(v.odometerKm); String towards = next == null || toNext == null ? EmpireAge.name(v.odometerKm) : String.format(Locale.US, "%s in %,.0f km", next, toNext); bar(c, l + 12f * u, b - 28f * u, rr - 12f * u, b - 14f * u, EmpireAge.progress(v.odometerKm), 0xFFFFD873, u); say(c, towards, cx, b - 34f * u, 15f * u, 0xFFBFAE88, sans, Paint.Align.CENTER, u); } }