App 2.38.0: Overview styles Visor and Empire
Visor, after Halo's helmet display: the battery as the shield bar, the range as the ammunition counter, P R N D as grenade slots, the speed in the reticle with power round it, and the power trail as blips on a motion tracker that sweeps while moving. Original blue or the accent. Empire, after Age of Empires: a wooden resource bar, an isometric world that moves with the speed, the car as the selected unit, P R N D as commands, a diamond minimap, and ages by odometer (EmpireAge, tested). The app takes the player's blue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* The Empire style's ages, by odometer: the Dark Age below 10,000 km, the Feudal Age below
|
||||
* 30,000, the Castle Age below 60,000 and the Imperial Age from there. Pure Java, unit-tested.
|
||||
*/
|
||||
final class EmpireAge {
|
||||
|
||||
private static final String[] NAMES = { "Dark Age", "Feudal Age", "Castle Age", "Imperial Age" };
|
||||
/** Where each age after the first begins, in km. */
|
||||
private static final float[] STARTS = { 10_000f, 30_000f, 60_000f };
|
||||
|
||||
private EmpireAge() {}
|
||||
|
||||
/** 0 to 3, or -1 when the odometer is unread. */
|
||||
static int index(Float km) {
|
||||
if (km == null || Float.isNaN(km)) return -1;
|
||||
int age = 0;
|
||||
while (age < STARTS.length && km >= STARTS[age]) age++;
|
||||
return age;
|
||||
}
|
||||
|
||||
/** The age's name, "—" when unread. */
|
||||
static String name(Float km) {
|
||||
int age = index(km);
|
||||
return age < 0 ? "—" : NAMES[age];
|
||||
}
|
||||
|
||||
/** The next age's name, or null in the last age and when unread. */
|
||||
static String next(Float km) {
|
||||
int age = index(km);
|
||||
return age < 0 || age >= STARTS.length ? null : NAMES[age + 1];
|
||||
}
|
||||
|
||||
/** Kilometres left to the next age, or null in the last age and when unread. */
|
||||
static Float toNext(Float km) {
|
||||
int age = index(km);
|
||||
return age < 0 || age >= STARTS.length ? null : STARTS[age] - km;
|
||||
}
|
||||
|
||||
/** How far through the current age, 0..1; 1 in the last age, 0 when unread. */
|
||||
static float progress(Float km) {
|
||||
int age = index(km);
|
||||
if (age < 0) return 0f;
|
||||
if (age >= STARTS.length) return 1f;
|
||||
float from = age == 0 ? 0f : STARTS[age - 1];
|
||||
return Math.max(0f, Math.min(1f, (km - from) / (STARTS[age] - from)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>();
|
||||
private TerminalView terminalView;
|
||||
private GridView gridView;
|
||||
private VisorView visorView;
|
||||
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
|
||||
private View[] stylePanes;
|
||||
private View consumptionPane;
|
||||
@@ -344,15 +345,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
mgPane = drawnPane(octagonView);
|
||||
terminalView = new TerminalView(this);
|
||||
gridView = new GridView(this);
|
||||
visorView = new VisorView(this);
|
||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||
new NixieView(this), new ChronographView(this), gridView, new TimeCircuitsView(this),
|
||||
new QuestView(this));
|
||||
new QuestView(this), visorView, new EmpireView(this));
|
||||
View[] canvasPanes = new View[canvasStyles.size()];
|
||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane,
|
||||
futuristicPane, sciFiPane, cartoonPane, mgPane,
|
||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4],
|
||||
canvasPanes[5], canvasPanes[6] };
|
||||
View[] fixedPanes = { vehiclePane, gaugesPane, cockpitPane, retroPane,
|
||||
futuristicPane, sciFiPane, cartoonPane, mgPane };
|
||||
// By OverviewStyle ordinal: the fixed styles, then the canvas styles in their order.
|
||||
stylePanes = new View[fixedPanes.length + canvasPanes.length];
|
||||
System.arraycopy(fixedPanes, 0, stylePanes, 0, fixedPanes.length);
|
||||
System.arraycopy(canvasPanes, 0, stylePanes, fixedPanes.length, canvasPanes.length);
|
||||
// The Overview page holds every style; Settings → Overview style picks the one shown.
|
||||
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
|
||||
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
|
||||
@@ -1721,6 +1725,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
retroView.setAccentPalette(!originalColours(OverviewStyle.RETRO));
|
||||
terminalView.setAccentPalette(!originalColours(OverviewStyle.TERMINAL));
|
||||
gridView.setAccentPalette(!originalColours(OverviewStyle.GRID));
|
||||
visorView.setAccentPalette(!originalColours(OverviewStyle.VISOR));
|
||||
// The accent choices depend on the style ("Original colours" for Retro and Terminal).
|
||||
if (settingsPane != null) bindAccentColor();
|
||||
}
|
||||
@@ -1752,7 +1757,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal),
|
||||
getString(R.string.overview_style_nixie),
|
||||
getString(R.string.overview_style_chronograph), getString(R.string.overview_style_grid),
|
||||
getString(R.string.overview_style_time_circuits), getString(R.string.overview_style_quest) };
|
||||
getString(R.string.overview_style_time_circuits), getString(R.string.overview_style_quest),
|
||||
getString(R.string.overview_style_visor), getString(R.string.overview_style_empire) };
|
||||
}
|
||||
|
||||
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
||||
|
||||
@@ -8,7 +8,7 @@ package com.evsuite.abrp;
|
||||
* colour for it. Styles with only their own fixed palette (the starship console, the cartoon,
|
||||
* the flight display, the nixie tubes) ignore it and Settings greys it out.
|
||||
* - {@link #hasOriginal}: has an original palette of its own as well (Retro's turquoise
|
||||
* display, the terminal's green phosphor, the Grid's neon cyan); Settings then offers "Original colours" above the
|
||||
* display, the terminal's green phosphor, the Grid's neon cyan, the Visor's blue); Settings then offers "Original colours" above the
|
||||
* accent colours, remembered per style under {@link #originalKey()}.
|
||||
*
|
||||
* Pure Java, unit-tested.
|
||||
@@ -28,7 +28,9 @@ enum OverviewStyle {
|
||||
CHRONOGRAPH(true, false),
|
||||
GRID(true, true),
|
||||
TIME_CIRCUITS(false, false),
|
||||
QUEST(false, false);
|
||||
QUEST(false, false),
|
||||
VISOR(true, true),
|
||||
EMPIRE(false, false);
|
||||
|
||||
static final String PREF_KEY = "overview_style";
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ final class StylePalette {
|
||||
case GRID: return R.style.ThemeOverlay_EV_Style_Grid;
|
||||
case TIME_CIRCUITS: return R.style.ThemeOverlay_EV_Style_TimeCircuits;
|
||||
case QUEST: return R.style.ThemeOverlay_EV_Style_Quest;
|
||||
case VISOR: return R.style.ThemeOverlay_EV_Style_Visor;
|
||||
case EMPIRE: return R.style.ThemeOverlay_EV_Style_Empire;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The Visor Overview style, after the helmet display of a science-fiction shooter like Halo:
|
||||
* thin light-blue lines on a dark visor, slanted panels and a reticle in the middle.
|
||||
*
|
||||
* Top centre, the shield bar: the battery, segmented, red and blinking below 20 % with
|
||||
* "BATTERY LOW". Top right, the ammunition counter: the range as a number and as rounds, one
|
||||
* per 10 km. Under it, four grenade slots for P R N D, the current one lit. Middle, the
|
||||
* reticle with the speed inside it and power round it — clockwise while drawing, green and
|
||||
* anticlockwise while regenerating. Bottom left, the motion tracker: its sweep turns while the
|
||||
* car moves, and its blips are the last two minutes of power (red drawing, green regenerating), further out the more
|
||||
* power (the rim is 60 kW, 40 kW regenerating).
|
||||
* Top left the clock, bottom right the objective with trip, usage, climate and odometer.
|
||||
* Unread values show "—" and light nothing.
|
||||
*
|
||||
* Two palettes: the original visor blue, or the chosen accent colour ({@link #setAccentPalette}).
|
||||
* The tracker animates (about 30 frames a second) only while the car moves and the view is on
|
||||
* screen; the low-battery blink rides on the readings that redraw it.
|
||||
*/
|
||||
final class VisorView extends ClusterCanvasView {
|
||||
|
||||
/** Shield segments, rounds in the counter (10 km each). */
|
||||
private static final int SHIELD = 24, ROUNDS = 50;
|
||||
private static final int RED = 0xFFFF3B30, REGEN = 0xFF4CFF7A, ENEMY = 0xFFFF4A3D;
|
||||
|
||||
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 vignette = new Paint();
|
||||
private final Path path = new Path();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
private final Typeface thin = Typeface.create("sans-serif-condensed", Typeface.NORMAL);
|
||||
private final int accent, accentLight;
|
||||
|
||||
private int main, light, dim;
|
||||
private float sweep;
|
||||
private long lastFrame;
|
||||
private int vignetteFor = -1;
|
||||
|
||||
VisorView(Context context) {
|
||||
super(context);
|
||||
accent = AccentColor.resolve(context, R.attr.evAccent);
|
||||
accentLight = AccentColor.resolve(context, R.attr.evAccentLight);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeJoin(Paint.Join.MITER);
|
||||
setAccentPalette(false);
|
||||
}
|
||||
|
||||
/** The chosen accent colour instead of the original visor blue. */
|
||||
void setAccentPalette(boolean useAccent) {
|
||||
main = useAccent ? accent : 0xFF5CC8FF;
|
||||
light = useAccent ? accentLight : 0xFFD2F0FF;
|
||||
dim = AccentColor.withAlpha(main, 0x40);
|
||||
vignetteFor = -1;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@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);
|
||||
drawVisor(c, w, h, u);
|
||||
drawClock(c, u);
|
||||
drawShield(c, w, u);
|
||||
drawAmmo(c, w, u);
|
||||
drawReticle(c, w, h, u);
|
||||
drawTracker(c, h, u);
|
||||
drawObjective(c, w, h, 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) sweep = (sweep + dt * 180f) % 360f;
|
||||
}
|
||||
|
||||
// ---------- Visor ----------
|
||||
|
||||
/** Dark glass, the helmet's rim darkening the corners, and its edge lines at the top. */
|
||||
private void drawVisor(Canvas c, float w, float h, float u) {
|
||||
c.drawColor(0xFF03080D);
|
||||
if (vignetteFor != (int) (w + h)) {
|
||||
vignette.setShader(new RadialGradient(w / 2f, h * 0.48f, Math.max(w, h) * 0.62f,
|
||||
new int[] { AccentColor.withAlpha(main, 0x16), 0x00000000, 0xE0000000 },
|
||||
new float[] { 0f, 0.62f, 1f }, Shader.TileMode.CLAMP));
|
||||
vignetteFor = (int) (w + h);
|
||||
}
|
||||
c.drawRect(0, 0, w, h, vignette);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
line.setColor(AccentColor.withAlpha(main, 0x50));
|
||||
path.reset();
|
||||
path.moveTo(0, 150f * u);
|
||||
path.quadTo(w * 0.08f, 16f * u, w * 0.3f, 8f * u);
|
||||
path.moveTo(w, 150f * u);
|
||||
path.quadTo(w * 0.92f, 16f * u, w * 0.7f, 8f * u);
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
|
||||
/** A slanted panel, as the display draws its frames. */
|
||||
private void slant(Canvas c, float l, float t, float rr, float b, float lean, int stroke, int bg, float u) {
|
||||
path.reset();
|
||||
path.moveTo(l + lean, t);
|
||||
path.lineTo(rr, t);
|
||||
path.lineTo(rr - lean, b);
|
||||
path.lineTo(l, b);
|
||||
path.close();
|
||||
fill.setColor(bg);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(stroke);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
|
||||
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
||||
text.setTypeface(tf);
|
||||
text.setTextSize(size);
|
||||
text.setTextAlign(a);
|
||||
text.setColor(colour);
|
||||
c.drawText(s, x, y, text);
|
||||
}
|
||||
|
||||
// ---------- Clock ----------
|
||||
|
||||
private void drawClock(Canvas c, float u) {
|
||||
float l = 28f * u, t = 26f * u;
|
||||
slant(c, l, t, l + 150f * u, t + 44f * u, 12f * u, main, AccentColor.withAlpha(main, 0x18), u);
|
||||
label(c, v.clock.isEmpty() ? "—" : v.clock, l + 75f * u, t + 33f * u, 30f * u, light, face, Paint.Align.CENTER);
|
||||
label(c, "LOCAL TIME", l + 12f * u, t + 64f * u, 14f * u, main, face, Paint.Align.LEFT);
|
||||
}
|
||||
|
||||
// ---------- Shield ----------
|
||||
|
||||
private void drawShield(Canvas c, float w, float u) {
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0;
|
||||
int colour = low ? RED : main;
|
||||
float bw = Math.min(520f * u, w * 0.36f), l = (w - bw) / 2f, t = 28f * u, b = t + 30f * u;
|
||||
slant(c, l - 14f * u, t - 8f * u, l + bw + 14f * u, b + 8f * u, 14f * u, AccentColor.withAlpha(colour, 0x90),
|
||||
0x66000000, u);
|
||||
int lit = v.socPercent == null ? 0 : Math.round(Math.max(0f, Math.min(100f, v.socPercent)) / 100f * SHIELD);
|
||||
float gap = 3f * u, seg = (bw - (SHIELD - 1) * gap) / SHIELD;
|
||||
for (int i = 0; i < SHIELD; i++) {
|
||||
float x = l + i * (seg + gap);
|
||||
path.reset();
|
||||
path.moveTo(x + 6f * u, t);
|
||||
path.lineTo(x + seg + 6f * u, t);
|
||||
path.lineTo(x + seg, b);
|
||||
path.lineTo(x, b);
|
||||
path.close();
|
||||
boolean on = i < lit && (!low || blinkOn);
|
||||
fill.setColor(on ? colour : AccentColor.withAlpha(colour, 0x22));
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
label(c, fmt(v.socPercent, "%.0f %%", "—"), w / 2f, b + 36f * u, 26f * u, low ? RED : light, face,
|
||||
Paint.Align.CENTER);
|
||||
if (low && blinkOn) {
|
||||
label(c, "BATTERY LOW", w / 2f, b + 66f * u, 22f * u, RED, face, Paint.Align.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Ammunition and grenades ----------
|
||||
|
||||
private void drawAmmo(Canvas c, float w, float u) {
|
||||
float rr = w - 30f * u, t = 26f * u;
|
||||
// The count: the range.
|
||||
label(c, fmt(v.rangeKm, "%.0f", "—"), rr, t + 52f * u, 60f * u, light, face, Paint.Align.RIGHT);
|
||||
label(c, "KM RANGE", rr, t + 74f * u, 15f * u, main, face, Paint.Align.RIGHT);
|
||||
// The rounds: two rows of 25, one per 10 km.
|
||||
int lit = v.rangeKm == null ? 0 : Math.round(Math.max(0f, v.rangeKm) / 10f);
|
||||
float rw = 5f * u, rh = 20f * u, gap = 3f * u, perRow = ROUNDS / 2f;
|
||||
float rowW = perRow * rw + (perRow - 1) * gap, l = rr - 150f * u - rowW;
|
||||
for (int i = 0; i < ROUNDS; i++) {
|
||||
int row = i / (ROUNDS / 2), col = i % (ROUNDS / 2);
|
||||
float x = l + col * (rw + gap), y = t + 8f * u + row * (rh + 8f * u);
|
||||
r.set(x, y, x + rw, y + rh);
|
||||
fill.setColor(i < lit ? light : AccentColor.withAlpha(main, 0x26));
|
||||
c.drawRect(r, fill);
|
||||
}
|
||||
// The grenade slots: P R N D.
|
||||
String[] gears = { "P", "R", "N", "D" };
|
||||
String now = Gear.label(v.gear);
|
||||
float gs = 34f * u, gx = l, gy = t + 66f * u;
|
||||
for (int i = 0; i < gears.length; i++) {
|
||||
boolean on = gears[i].equals(now);
|
||||
float x = gx + i * (gs + 6f * u);
|
||||
path.reset();
|
||||
path.moveTo(x + gs / 2f, gy);
|
||||
path.lineTo(x + gs, gy + gs * 0.5f);
|
||||
path.lineTo(x + gs / 2f, gy + gs);
|
||||
path.lineTo(x, gy + gs * 0.5f);
|
||||
path.close();
|
||||
fill.setColor(on ? main : 0x55000000);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(on ? light : dim);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawPath(path, line);
|
||||
label(c, gears[i], x + gs / 2f, gy + gs * 0.5f + 7f * u, 20f * u, on ? 0xFF03080D : main, face,
|
||||
Paint.Align.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Reticle ----------
|
||||
|
||||
private void drawReticle(Canvas c, float w, float h, float u) {
|
||||
float cx = w / 2f, cy = h * 0.53f, rad = 122f * u;
|
||||
line.setStrokeWidth(2f * u);
|
||||
line.setColor(AccentColor.withAlpha(main, 0x70));
|
||||
c.drawCircle(cx, cy, rad, line);
|
||||
// Four ticks.
|
||||
line.setColor(main);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawLine(cx, cy - rad - 16f * u, cx, cy - rad + 10f * u, line);
|
||||
c.drawLine(cx, cy + rad - 10f * u, cx, cy + rad + 16f * u, line);
|
||||
c.drawLine(cx - rad - 16f * u, cy, cx - rad + 10f * u, cy, line);
|
||||
c.drawLine(cx + rad - 10f * u, cy, cx + rad + 16f * u, cy, line);
|
||||
// Power round it: 150 kW is a full turn, regeneration 50 kW the other way.
|
||||
r.set(cx - rad - 10f * u, cy - rad - 10f * u, cx + rad + 10f * u, cy + rad + 10f * u);
|
||||
line.setStrokeWidth(8f * u);
|
||||
line.setStrokeCap(Paint.Cap.BUTT);
|
||||
line.setColor(AccentColor.withAlpha(main, 0x1C));
|
||||
c.drawArc(r, 0, 360, false, line);
|
||||
if (v.powerKw != null && Math.abs(v.powerKw) > 0.1f) {
|
||||
boolean regen = v.powerKw < 0;
|
||||
float sweepDeg = regen ? Math.max(-360f, v.powerKw / 50f * 360f) : Math.min(360f, v.powerKw / 150f * 360f);
|
||||
line.setColor(regen ? REGEN : light);
|
||||
c.drawArc(r, -90f, sweepDeg, false, line);
|
||||
}
|
||||
label(c, fmt(v.speedKmh, "%.0f", "—"), cx, cy + 34f * u, 104f * u, light, face, Paint.Align.CENTER);
|
||||
label(c, "KM/H", cx, cy + 66f * u, 18f * u, main, face, Paint.Align.CENTER);
|
||||
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, v.powerKw < -0.05f ? "+%.1f kW" : "%.1f kW",
|
||||
Math.abs(v.powerKw));
|
||||
label(c, power, cx, cy + rad + 48f * u, 24f * u,
|
||||
v.powerKw != null && v.powerKw < -0.05f ? REGEN : light, face, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
// ---------- Motion tracker ----------
|
||||
|
||||
/** The tracker: rings, the sweep while moving, and the power trail as blips round the car. */
|
||||
private void drawTracker(Canvas c, float h, float u) {
|
||||
float rad = 112f * u, cx = 40f * u + rad, cy = h - 34f * u - rad;
|
||||
fill.setColor(AccentColor.withAlpha(main, 0x14));
|
||||
c.drawCircle(cx, cy, rad, fill);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
line.setColor(AccentColor.withAlpha(main, 0x90));
|
||||
c.drawCircle(cx, cy, rad, line);
|
||||
line.setColor(AccentColor.withAlpha(main, 0x3A));
|
||||
c.drawCircle(cx, cy, rad * 0.66f, line);
|
||||
c.drawCircle(cx, cy, rad * 0.33f, line);
|
||||
c.drawLine(cx - rad, cy, cx + rad, cy, line);
|
||||
c.drawLine(cx, cy - rad, cx, cy + rad, line);
|
||||
if (v.speedKmh != null && v.speedKmh > 0.5f) {
|
||||
float a = (float) Math.toRadians(sweep - 90f);
|
||||
line.setColor(AccentColor.withAlpha(light, 0x90));
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawLine(cx, cy, cx + (float) Math.cos(a) * rad, cy + (float) Math.sin(a) * rad, line);
|
||||
}
|
||||
// Blips: the last two minutes, the newest nearest the top, each at the distance its power gives.
|
||||
float[] trail = v.powerTrail;
|
||||
if (trail != null && trail.length > 0) {
|
||||
int step = Math.max(1, trail.length / 24);
|
||||
for (int i = trail.length - 1, k = 0; i >= 0; i -= step, k++) {
|
||||
float p = trail[i];
|
||||
if (Float.isNaN(p) || Math.abs(p) < 1f) continue;
|
||||
float dist = (0.18f + 0.78f * Math.min(1f, Math.abs(p) / (p < 0 ? 40f : 60f))) * rad;
|
||||
float ang = (float) Math.toRadians(-90f + k * 15f);
|
||||
float bx = cx + (float) Math.cos(ang) * dist, by = cy + (float) Math.sin(ang) * dist;
|
||||
int colour = p < 0 ? REGEN : ENEMY;
|
||||
fill.setColor(AccentColor.withAlpha(colour, Math.max(0x40, 0xFF - k * 8)));
|
||||
c.drawCircle(bx, by, 6f * u, fill);
|
||||
}
|
||||
}
|
||||
// The car in the middle.
|
||||
path.reset();
|
||||
path.moveTo(cx, cy - 10f * u);
|
||||
path.lineTo(cx + 7f * u, cy + 8f * u);
|
||||
path.lineTo(cx - 7f * u, cy + 8f * u);
|
||||
path.close();
|
||||
fill.setColor(light);
|
||||
c.drawPath(path, fill);
|
||||
label(c, "60 kW", cx, cy - rad - 8f * u, 13f * u, main, thin, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
// ---------- Objective ----------
|
||||
|
||||
private void drawObjective(Canvas c, float w, float h, float u) {
|
||||
float rr = w - 30f * u, l = rr - 330f * u, t = h - 214f * u, b = h - 34f * u;
|
||||
slant(c, l, t, rr, b, 18f * u, AccentColor.withAlpha(main, 0x80), 0x80000000, u);
|
||||
label(c, "OBJECTIVE", l + 30f * u, t + 30f * u, 18f * u, main, face, Paint.Align.LEFT);
|
||||
String[][] rows = {
|
||||
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
||||
{ "5 MIN", fmt(v.usage5Min, "%.1f kWh/100", "—") },
|
||||
{ "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") },
|
||||
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
||||
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") },
|
||||
};
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
float y = t + (62f + i * 27f) * u, x = l + 26f * u - i * 3f * u;
|
||||
label(c, rows[i][0], x, y, 17f * u, main, thin, Paint.Align.LEFT);
|
||||
label(c, rows[i][1], rr - 30f * u - i * 3f * u, y, 19f * u, light, face, Paint.Align.RIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,8 @@
|
||||
<string name="overview_style_grid">Grid</string>
|
||||
<string name="overview_style_time_circuits">Time circuits</string>
|
||||
<string name="overview_style_quest">Quest</string>
|
||||
<string name="overview_style_visor">Visor</string>
|
||||
<string name="overview_style_empire">Empire</string>
|
||||
<string name="overview_style_nixie">Nixie tubes</string>
|
||||
<string name="overview_style_chronograph">Chronograph</string>
|
||||
<string name="cockpit_battery">BATTERY</string>
|
||||
|
||||
@@ -84,4 +84,24 @@
|
||||
<item name="evAccentStroke">#806E5628</item>
|
||||
<item name="evAccentTrack">#6A5426</item>
|
||||
</style>
|
||||
<!-- Visor: the blue of the helmet display. -->
|
||||
<style name="ThemeOverlay.EV.Style.Visor" parent="">
|
||||
<item name="evAccent">#5CC8FF</item>
|
||||
<item name="evOnAccent">#03121C</item>
|
||||
<item name="evAccentLight">#D2F0FF</item>
|
||||
<item name="evAccentDeep">#0C3048</item>
|
||||
<item name="evAccentSurface">#0A1A26</item>
|
||||
<item name="evAccentStroke">#80306A8A</item>
|
||||
<item name="evAccentTrack">#22587A</item>
|
||||
</style>
|
||||
<!-- Empire: the player's blue. -->
|
||||
<style name="ThemeOverlay.EV.Style.Empire" parent="">
|
||||
<item name="evAccent">#3D6BFF</item>
|
||||
<item name="evOnAccent">#FFFFFF</item>
|
||||
<item name="evAccentLight">#A8BEFF</item>
|
||||
<item name="evAccentDeep">#101E4A</item>
|
||||
<item name="evAccentSurface">#121A33</item>
|
||||
<item name="evAccentStroke">#80344A99</item>
|
||||
<item name="evAccentTrack">#2A3F82</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/** The Empire style's ages by odometer — pure JVM. */
|
||||
public class EmpireAgeTest {
|
||||
|
||||
@Test
|
||||
public void agesByOdometer() {
|
||||
assertEquals("Dark Age", EmpireAge.name(0f));
|
||||
assertEquals("Dark Age", EmpireAge.name(9_999f));
|
||||
assertEquals("Feudal Age", EmpireAge.name(10_000f));
|
||||
assertEquals("Castle Age", EmpireAge.name(45_000f));
|
||||
assertEquals("Imperial Age", EmpireAge.name(60_000f));
|
||||
assertEquals("Imperial Age", EmpireAge.name(250_000f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theWayToTheNextAge() {
|
||||
assertEquals("Castle Age", EmpireAge.next(12_000f));
|
||||
assertEquals(18_000f, EmpireAge.toNext(12_000f), 0.01f);
|
||||
assertEquals(0.1f, EmpireAge.progress(12_000f), 0.001f);
|
||||
assertEquals(0.5f, EmpireAge.progress(5_000f), 0.001f);
|
||||
assertNull(EmpireAge.next(70_000f));
|
||||
assertNull(EmpireAge.toNext(70_000f));
|
||||
assertEquals(1f, EmpireAge.progress(70_000f), 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadIsNoAge() {
|
||||
assertEquals("—", EmpireAge.name(null));
|
||||
assertEquals(-1, EmpireAge.index(Float.NaN));
|
||||
assertNull(EmpireAge.next(null));
|
||||
assertNull(EmpireAge.toNext(null));
|
||||
assertEquals(0f, EmpireAge.progress(null), 0f);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user