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:
@@ -75,11 +75,11 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
|
|||||||
|
|
||||||
| Tab | Contents |
|
| Tab | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest; Retro, Terminal and Grid with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
||||||
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
||||||
| Live data | on/off, ABRP API key and user token, upload frequency |
|
| Live data | on/off, ABRP API key and user token, upload frequency |
|
||||||
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
||||||
| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal and Grid add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
|
| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal, Grid and Visor add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
|
||||||
| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log |
|
| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log |
|
||||||
|
|
||||||
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
|
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ APIs.
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
**Overview styles:** the Overview comes in fifteen looks, from an 80s fluorescent display to a
|
**Overview styles:** the Overview comes in seventeen looks, from an 80s fluorescent display to a
|
||||||
starship console, a watch face, a neon grid, a time machine and a fantasy game. [THEMES.md](THEMES.md) shows each one, with and without the
|
starship console, a watch face, a neon grid, a time machine, a fantasy game, a helmet display and a strategy game. [THEMES.md](THEMES.md) shows each one, with and without the
|
||||||
navigation, and what inspired it.
|
navigation, and what inspired it.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -88,7 +88,7 @@ with the server using a token of its own, obtained by pairing with a phone (belo
|
|||||||
|
|
||||||
| Tab | What it shows |
|
| Tab | What it shows |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, lease allowance, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, MG, Minimal, Terminal, Nixie tubes, Chronograph, Grid, Time circuits and Quest ([THEMES.md](THEMES.md) shows them all). A style with its own colours (Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, and Retro, Terminal and Grid with "Original") colours the whole app to match, e.g. turquoise for Retro. The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways |
|
| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, lease allowance, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, MG, Minimal, Terminal, Nixie tubes, Chronograph, Grid, Time circuits, Quest, Visor and Empire ([THEMES.md](THEMES.md) shows them all). A style with its own colours (Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, and Retro, Terminal, Grid and Visor with "Original") colours the whole app to match, e.g. turquoise for Retro. The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways |
|
||||||
| **Consumption** | Lifetime, last 7 and 30 days, Trip A and B (distance, energy, average consumption and speed, SOC change), and a daily log of the last four months |
|
| **Consumption** | Lifetime, last 7 and 30 days, Trip A and B (distance, energy, average consumption and speed, SOC change), and a daily log of the last four months |
|
||||||
| **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve |
|
| **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve |
|
||||||
| **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency |
|
| **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency |
|
||||||
|
|||||||
@@ -138,6 +138,22 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
|||||||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||||||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||||||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||||
|
- 2026-10-26 — App 2.38.0: Overview styles **Visor** (after Halo's helmet display, `VisorView`)
|
||||||
|
and **Empire** (after Age of Empires' interface, `EmpireView`). Visor: shield bar = battery (24
|
||||||
|
segments, red and blinking "BATTERY LOW" below 20 %), ammunition counter = range (number and
|
||||||
|
rounds, one per 10 km), P R N D as grenade slots, a reticle with the speed and power round it
|
||||||
|
(green and anticlockwise in regen), a motion tracker whose blips are the power trail (rim 60 kW,
|
||||||
|
40 kW regen) and whose sweep turns only while moving (~30 fps, on screen only), the clock and an
|
||||||
|
objective panel; original blue or the accent colour (`hasOriginal`). Empire: a wooden resource
|
||||||
|
bar (battery, range, power, 5-min usage, the age, the clock), an isometric world whose ground
|
||||||
|
and trees move with the speed (6 m tiles, ~30 fps only while moving), the car as the selected
|
||||||
|
unit with the battery as its health bar, the speed large over the world, a carved panel with
|
||||||
|
P R N D as commands, the unit's portrait and stats, and a diamond minimap with the odometer and
|
||||||
|
the way to the next age; ages by odometer in `EmpireAge` (pure, tested: Dark, Feudal from
|
||||||
|
10,000 km, Castle 30,000, Imperial 60,000); its own palette, the app in the player's blue.
|
||||||
|
`stylePanes` is now built from the canvas list. THEMES.md with two screenshots each. Emulator:
|
||||||
|
sample values, 15 % at a standstill, with and without the navigation, and no data (dashes); no
|
||||||
|
crash. Not tried on the car.
|
||||||
- 2026-10-26 — App 2.37.0: Overview styles **Time circuits** (after Back to the Future,
|
- 2026-10-26 — App 2.37.0: Overview styles **Time circuits** (after Back to the Future,
|
||||||
`TimeCircuitsView`) and **Quest** (after World of Warcraft's interface, `QuestView`); both
|
`TimeCircuitsView`) and **Quest** (after World of Warcraft's interface, `QuestView`); both
|
||||||
with their own palette, the app in red and gold with them. Time circuits: three LED rows in a
|
with their own palette, the app in red and gold with them. Time circuits: three LED rows in a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Overview styles
|
# Overview styles
|
||||||
|
|
||||||
The Overview can look fifteen ways: pick one under **Settings → Overview style**. Every style shows
|
The Overview can look seventeen ways: pick one under **Settings → Overview style**. Every style shows
|
||||||
the same readings (battery, range, power, speed, gear, usage, trip, climate, odometer, time) and
|
the same readings (battery, range, power, speed, gear, usage, trip, climate, odometer, time) and
|
||||||
shows "—" for anything the car did not answer, never a made-up zero.
|
shows "—" for anything the car did not answer, never a made-up zero.
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ emulator at the car's resolution with **sample values** (87 km/h in D, 76 %, 18.
|
|||||||
emulator has no car; on the car the values are live.
|
emulator has no car; on the car the values are live.
|
||||||
|
|
||||||
**Colours.** Most styles are drawn in the accent colour you pick from the MG4 paints (Settings →
|
**Colours.** Most styles are drawn in the accent colour you pick from the MG4 paints (Settings →
|
||||||
Accent colour; shown here in Dynamic Red). Retro, Terminal and Grid can also show their original
|
Accent colour; shown here in Dynamic Red). Retro, Terminal, Grid and Visor can also show their original
|
||||||
colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits and Quest always do; while one of those is shown, the whole app
|
colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest and Empire always do; while one of those is shown, the whole app
|
||||||
(navigation, buttons, switches) takes on its colours too.
|
(navigation, buttons, switches) takes on its colours too.
|
||||||
|
|
||||||
| Style | Look | Colours |
|
| Style | Look | Colours |
|
||||||
@@ -31,6 +31,8 @@ colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits and Quest always do; wh
|
|||||||
| [Grid](#grid) | neon lines on a glowing grid floor | original or accent |
|
| [Grid](#grid) | neon lines on a glowing grid floor | original or accent |
|
||||||
| [Time circuits](#time-circuits) | a time machine's LED displays | own |
|
| [Time circuits](#time-circuits) | a time machine's LED displays | own |
|
||||||
| [Quest](#quest) | a fantasy game's interface | own |
|
| [Quest](#quest) | a fantasy game's interface | own |
|
||||||
|
| [Visor](#visor) | a soldier's helmet display | original or accent |
|
||||||
|
| [Empire](#empire) | a strategy game's interface | own |
|
||||||
|
|
||||||
## Tiles
|
## Tiles
|
||||||
|
|
||||||
@@ -186,3 +188,31 @@ flashes "Low battery!". The app turns gold with it.
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
## Visor
|
||||||
|
|
||||||
|
After the helmet display of a science-fiction shooter like Halo: thin light-blue lines and
|
||||||
|
slanted frames on a dark visor. The shield bar on top is the battery, turning red and blinking
|
||||||
|
"BATTERY LOW" below 20 %; the ammunition counter is the range, as a number and as rounds, one per
|
||||||
|
10 km, with P R N D as the grenade slots under it; the reticle in the middle holds the speed and
|
||||||
|
fills with power round it, green and the other way while regenerating; the motion tracker shows
|
||||||
|
the last two minutes of power as blips, red while drawing and green while regenerating, and
|
||||||
|
sweeps while you drive. The objective lists trip, usage, climate and odometer. In its original
|
||||||
|
blue (shown, and the app turns blue with it) or in the accent colour.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Empire
|
||||||
|
|
||||||
|
After the interface of a real-time strategy game like Age of Empires: a wooden resource bar on
|
||||||
|
top with the battery, range, power and usage, the clock, and the age you have reached by
|
||||||
|
odometer — Dark Age, then Feudal from 10,000 km, Castle from 30,000 and Imperial from 60,000. In
|
||||||
|
the isometric world below, the car is the selected unit on a road, its health bar the battery,
|
||||||
|
while the ground and trees move past as fast as you drive and the speed stands large over the
|
||||||
|
land. The carved panel at the bottom has P R N D as command buttons, the unit's portrait and
|
||||||
|
stats, and a diamond minimap with the odometer and the way to the next age. Below 20 % it warns
|
||||||
|
"Your battery is running low!". The app turns the player's blue with it.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 140
|
versionCode 141
|
||||||
versionName "2.37.0"
|
versionName "2.38.0"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
||||||
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -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 final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>();
|
||||||
private TerminalView terminalView;
|
private TerminalView terminalView;
|
||||||
private GridView gridView;
|
private GridView gridView;
|
||||||
|
private VisorView visorView;
|
||||||
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
|
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
|
||||||
private View[] stylePanes;
|
private View[] stylePanes;
|
||||||
private View consumptionPane;
|
private View consumptionPane;
|
||||||
@@ -344,15 +345,18 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
mgPane = drawnPane(octagonView);
|
mgPane = drawnPane(octagonView);
|
||||||
terminalView = new TerminalView(this);
|
terminalView = new TerminalView(this);
|
||||||
gridView = new GridView(this);
|
gridView = new GridView(this);
|
||||||
|
visorView = new VisorView(this);
|
||||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||||
new NixieView(this), new ChronographView(this), gridView, new TimeCircuitsView(this),
|
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()];
|
View[] canvasPanes = new View[canvasStyles.size()];
|
||||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||||
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane,
|
View[] fixedPanes = { vehiclePane, gaugesPane, cockpitPane, retroPane,
|
||||||
futuristicPane, sciFiPane, cartoonPane, mgPane,
|
futuristicPane, sciFiPane, cartoonPane, mgPane };
|
||||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4],
|
// By OverviewStyle ordinal: the fixed styles, then the canvas styles in their order.
|
||||||
canvasPanes[5], canvasPanes[6] };
|
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.
|
// The Overview page holds every style; Settings → Overview style picks the one shown.
|
||||||
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
|
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
|
||||||
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
|
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
|
||||||
@@ -1721,6 +1725,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
retroView.setAccentPalette(!originalColours(OverviewStyle.RETRO));
|
retroView.setAccentPalette(!originalColours(OverviewStyle.RETRO));
|
||||||
terminalView.setAccentPalette(!originalColours(OverviewStyle.TERMINAL));
|
terminalView.setAccentPalette(!originalColours(OverviewStyle.TERMINAL));
|
||||||
gridView.setAccentPalette(!originalColours(OverviewStyle.GRID));
|
gridView.setAccentPalette(!originalColours(OverviewStyle.GRID));
|
||||||
|
visorView.setAccentPalette(!originalColours(OverviewStyle.VISOR));
|
||||||
// The accent choices depend on the style ("Original colours" for Retro and Terminal).
|
// The accent choices depend on the style ("Original colours" for Retro and Terminal).
|
||||||
if (settingsPane != null) bindAccentColor();
|
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_minimal), getString(R.string.overview_style_terminal),
|
||||||
getString(R.string.overview_style_nixie),
|
getString(R.string.overview_style_nixie),
|
||||||
getString(R.string.overview_style_chronograph), getString(R.string.overview_style_grid),
|
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. */
|
/** 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,
|
* 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.
|
* 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
|
* - {@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()}.
|
* accent colours, remembered per style under {@link #originalKey()}.
|
||||||
*
|
*
|
||||||
* Pure Java, unit-tested.
|
* Pure Java, unit-tested.
|
||||||
@@ -28,7 +28,9 @@ enum OverviewStyle {
|
|||||||
CHRONOGRAPH(true, false),
|
CHRONOGRAPH(true, false),
|
||||||
GRID(true, true),
|
GRID(true, true),
|
||||||
TIME_CIRCUITS(false, false),
|
TIME_CIRCUITS(false, false),
|
||||||
QUEST(false, false);
|
QUEST(false, false),
|
||||||
|
VISOR(true, true),
|
||||||
|
EMPIRE(false, false);
|
||||||
|
|
||||||
static final String PREF_KEY = "overview_style";
|
static final String PREF_KEY = "overview_style";
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ final class StylePalette {
|
|||||||
case GRID: return R.style.ThemeOverlay_EV_Style_Grid;
|
case GRID: return R.style.ThemeOverlay_EV_Style_Grid;
|
||||||
case TIME_CIRCUITS: return R.style.ThemeOverlay_EV_Style_TimeCircuits;
|
case TIME_CIRCUITS: return R.style.ThemeOverlay_EV_Style_TimeCircuits;
|
||||||
case QUEST: return R.style.ThemeOverlay_EV_Style_Quest;
|
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;
|
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_grid">Grid</string>
|
||||||
<string name="overview_style_time_circuits">Time circuits</string>
|
<string name="overview_style_time_circuits">Time circuits</string>
|
||||||
<string name="overview_style_quest">Quest</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_nixie">Nixie tubes</string>
|
||||||
<string name="overview_style_chronograph">Chronograph</string>
|
<string name="overview_style_chronograph">Chronograph</string>
|
||||||
<string name="cockpit_battery">BATTERY</string>
|
<string name="cockpit_battery">BATTERY</string>
|
||||||
|
|||||||
@@ -84,4 +84,24 @@
|
|||||||
<item name="evAccentStroke">#806E5628</item>
|
<item name="evAccentStroke">#806E5628</item>
|
||||||
<item name="evAccentTrack">#6A5426</item>
|
<item name="evAccentTrack">#6A5426</item>
|
||||||
</style>
|
</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>
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Reference in New Issue
Block a user