diff --git a/AGENTS.md b/AGENTS.md index 53ae597..04d2cb4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), | 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`), Blueprint (`BlueprintView`, top-view drawing with tyre pressures), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Aviation, Nixie tubes, Chronograph, Mascot (`MascotView`, an anime co-pilot whose face follows `MascotMood`; classic or `ChibiFigure`, `MascotCharacter`); the drawn styles share `ClusterValues`. A button in the top-right corner hides the app's navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`) | +| 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`), Blueprint (`BlueprintView`, top-view drawing with tyre pressures), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Aviation, Nixie tubes, Chronograph, Mascot (`MascotView`, an anime co-pilot whose face follows `MascotMood`; classic, `ChibiFigure` or `FoxFigure` on `ChibiBase`, chosen by `MascotCharacter`); the drawn styles share `ClusterValues`. A button in the top-right corner hides the app's navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`) | | Consumption / Charging | period totals, daily log, charging sessions and curve | | 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 | diff --git a/README.md b/README.md index 343c13b..99da8e1 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ with the server using a token of its own, obtained by pairing with a phone (belo | 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, 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, Blueprint, MG, Minimal, Terminal, Aviation, Nixie tubes, Chronograph and Mascot (an anime co-pilot who reacts to the drive, classic or chibi). The small button in the top-right corner hides the app's navigation for a full-width Overview; tap it again to bring it back | +| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, 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, Blueprint, MG, Minimal, Terminal, Aviation, Nixie tubes, Chronograph and Mascot (an anime co-pilot who reacts to the drive, classic, chibi or a chibi fox). The small button in the top-right corner hides the app's navigation for a full-width Overview; tap it again to bring it back | | **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 | | **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency | diff --git a/SPECS.md b/SPECS.md index 045fa8a..ee48494 100644 --- a/SPECS.md +++ b/SPECS.md @@ -131,6 +131,16 @@ 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 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). +- 2026-10-24 — App 2.30.0 (branch `styles-retro-future`): a third Mascot, the **chibi fox** + (`FoxFigure`): a sitting fox with a big head, pointed ears, cream cheeks, chest and tail tip, + dark paws and a scarf in the accent colour. Ears and tail act the mood with the face: the + tail wags while content and fast while cheering, stands stiff when determined and hangs low + when worried or asleep; ears droop when worried or asleep and lie back when fast, when the + scarf also flies. Its own lines (`fox_*`, "Kon!"). The chibi's shared parts (frame, eyes, + mouths, limbs by mood, effects) moved into `ChibiBase`, which both figures extend. Settings → + Mascot: Classic / Chibi / Fox. Emulator: all moods checked with sample values in a throwaway + build; the tail's swing is kept to about 18° so it stays clear of the values. Not tried on the + car. - 2026-10-24 — App 2.29.0 (branch `styles-retro-future`): a second Mascot, the **chibi** (`ChibiFigure`): small and full-figure, a big round head with a coral bob and a star clip, a hoodie in the accent colour, dark outlines like a sticker; an adult character in chibi diff --git a/app/build.gradle b/app/build.gradle index e057c14..63cd76c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 126 - versionName "2.29.0" + versionCode 127 + versionName "2.30.0" // 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. diff --git a/app/src/main/java/com/evsuite/abrp/ChibiBase.java b/app/src/main/java/com/evsuite/abrp/ChibiBase.java new file mode 100644 index 0000000..730decd --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/ChibiBase.java @@ -0,0 +1,316 @@ +package com.evsuite.abrp; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; + +/** + * What the chibi Mascot figures ({@link ChibiFigure}, {@link FoxFigure}) share: the frame + * (scaled down and moved left so the big head stays clear of the speed, a shadow, a hop while + * cheering, a lean), their big eyes and mouths per {@link MascotMood}, limbs placed by the mood, + * and the effects around them (Zs, sparkles, wind, "?", drops). Sticker style: dark outlines. + * + * Draws on {@link MascotView}'s 100 × 100 grid; holds no state. + */ +abstract class ChibiBase { + + protected final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); + protected final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG); + protected final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG); + protected final Path p = new Path(); + protected final Path q = new Path(); + protected final RectF r = new RectF(); + protected final int accent, accentLight, regen; + protected final int ink = 0xFF3A2A33, blush = 0x66FF7A8A, mouth = 0xFF8E3A48, tongue = 0xFFE88A95; + /** The eyes' colours, and what a narrowed lid is filled with (skin or fur). */ + private final int iris, irisLight, lid; + /** How far left the figure moves, in grid units. */ + private final float shiftX; + + ChibiBase(int accent, int accentLight, int regen, int iris, int irisLight, int lid, float shiftX) { + this.accent = accent; + this.accentLight = accentLight; + this.regen = regen; + this.iris = iris; + this.irisLight = irisLight; + this.lid = lid; + this.shiftX = shiftX; + line.setStyle(Paint.Style.STROKE); + line.setStrokeCap(Paint.Cap.ROUND); + line.setStrokeJoin(Paint.Join.ROUND); + line.setColor(ink); + text.setTextAlign(Paint.Align.CENTER); + text.setFakeBoldText(true); + } + + /** Where the speech bubble's tail points, on the grid. */ + abstract float tipX(); + + abstract float tipY(); + + /** The figure itself, standing or sitting on y = 96, centred on x = 50. */ + protected abstract void drawFigure(Canvas c, MascotMood mood, long t, boolean blink); + + final void draw(Canvas c, MascotMood mood, long t, boolean blink) { + c.save(); + c.translate(-shiftX, 0f); + c.scale(0.92f, 0.92f, 50f, 100f); + // A hop while cheering. + float hop = mood == MascotMood.CHEERING ? -3.5f * (float) Math.abs(Math.sin(t / 180.0)) : 0f; + fill.setColor(0x40000000); + float shadow = 18f + hop * 0.8f; + c.drawOval(oval(50f - shadow, 96.5f, 50f + shadow, 100f), fill); + + c.save(); + c.translate(0f, hop); + float lean = mood == MascotMood.FAST ? 5f : mood == MascotMood.SLEEPING ? -6f : 0f; + c.rotate(lean, 50f, 96f); + drawFigure(c, mood, t, blink); + c.restore(); + + switch (mood) { + case SLEEPING: zs(c, t); break; + case CHEERING: sparkles(c, t); break; + case FAST: wind(c, t); break; + case WAITING: question(c, t); break; + default: break; + } + c.restore(); + } + + /** Head tilt per mood, degrees. */ + static float tilt(MascotMood mood) { + return mood == MascotMood.REVERSE ? -10f : mood == MascotMood.SLEEPING ? 10f + : mood == MascotMood.WAITING ? 6f : 0f; + } + + /** Hands on the cheeks and a fist at the chest are drawn in front of the head. */ + static boolean handsInFront(MascotMood mood) { + return mood == MascotMood.WORRIED || mood == MascotMood.DETERMINED; + } + + /** Left and right hand positions for the mood: {lx, ly, rx, ry}. */ + static float[] hands(MascotMood mood, long t) { + float wave = (float) Math.sin(t / 220.0) * 3f; + switch (mood) { + case CONTENT: return new float[] { 33f, 82f, 82f + wave * 0.5f, 64f + wave }; + case CHEERING: return new float[] { 16f, 56f, 84f, 56f }; + case DETERMINED: return new float[] { 33f, 82f, 57f, 72f }; + case WORRIED: return new float[] { 34f, 55f, 66f, 55f }; + // Pointing back. + case REVERSE: return new float[] { 20f, 66f, 67f, 82f }; + case FAST: return new float[] { 30f, 76f, 70f, 76f }; + default: return new float[] { 33f, 82f, 67f, 82f }; + } + } + + /** A limb from the shoulder to a round hand or paw, outlined. */ + protected void limb(Canvas c, float sx, float sy, float hx, float hy, int sleeve, int hand) { + line.setStrokeWidth(7.4f); + c.drawLine(sx, sy, hx, hy, line); + line.setColor(sleeve); + line.setStrokeWidth(5f); + c.drawLine(sx, sy, hx, hy, line); + line.setColor(ink); + fill.setColor(hand); + line.setStrokeWidth(1.1f); + c.drawCircle(hx, hy, 3.2f, fill); + c.drawCircle(hx, hy, 3.2f, line); + } + + /** Cheeks, eyes at y 44, brows, mouth around y 55 and the worried drops. */ + protected void face(Canvas c, MascotMood mood, long t, boolean blink) { + if (mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.CONTENT + || mood == MascotMood.SLEEPING || mood == MascotMood.WORRIED) { + fill.setColor(blush); + c.drawOval(oval(29.5f, 50f, 37f, 54f), fill); + c.drawOval(oval(63f, 50f, 70.5f, 54f), fill); + } + eye(c, 39f, 44f, mood, blink); + eye(c, 61f, 44f, mood, blink); + brows(c, mood); + mouth(c, mood, t); + if (mood == MascotMood.WORRIED) { + fill.setColor(0xFF8FD3FF); + drop(c, 72f, 30f, 3f); + drop(c, 34.5f, 52f, 1.8f); + } + } + + private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink) { + line.setColor(ink); + line.setStrokeWidth(1.5f); + if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING && mood != MascotMood.FAST)) { + c.drawArc(oval(cx - 4.5f, cy - 3f, cx + 4.5f, cy + 2f), 20f, 140f, false, line); + return; + } + if (mood == MascotMood.FAST) { + // ^ ^ + c.drawArc(oval(cx - 4.5f, cy - 2f, cx + 4.5f, cy + 5f), 200f, 140f, false, line); + return; + } + float look = mood == MascotMood.REVERSE ? -2f : 0f; + oval(cx - 4.6f, cy - 5.6f, cx + 4.6f, cy + 5.6f); + if (mood == MascotMood.REVERSE) { + fill.setColor(Color.WHITE); + c.drawOval(r, fill); + fill.setColor(iris); + c.drawOval(oval(cx - 3f + look, cy - 4f, cx + 3f + look, cy + 4.5f), fill); + } else { + fill.setColor(iris); + c.drawOval(r, fill); + fill.setColor(irisLight); + c.drawOval(oval(cx - 3.6f, cy + 0.5f, cx + 3.6f, cy + 5f), fill); + } + fill.setColor(Color.WHITE); + if (mood == MascotMood.CHEERING) { + star(c, cx, cy - 0.5f, 3.6f); + } else { + c.drawCircle(cx - 1.6f + look, cy - 2.4f, 2f, fill); + c.drawCircle(cx + 1.8f + look, cy + 2.4f, 0.9f, fill); + } + if (mood == MascotMood.DETERMINED) { + // A flat lid over the top: focused. + fill.setColor(lid); + c.drawRect(cx - 5.2f, cy - 6.4f, cx + 5.2f, cy - 2f, fill); + line.setStrokeWidth(1.6f); + c.drawLine(cx - 5f, cy - 2f, cx + 5f, cy - 2f, line); + return; + } + line.setStrokeWidth(1.6f); + c.drawArc(oval(cx - 5f, cy - 5.9f, cx + 5f, cy + 5.6f), 200f, 140f, false, line); + } + + private void brows(Canvas c, MascotMood mood) { + line.setColor(ink); + line.setStrokeWidth(1.4f); + if (mood == MascotMood.DETERMINED) { + c.drawLine(34f, 33.5f, 43.5f, 36.5f, line); + c.drawLine(56.5f, 36.5f, 66f, 33.5f, line); + } else if (mood == MascotMood.WORRIED) { + c.drawLine(34f, 36f, 43f, 33f, line); + c.drawLine(57f, 33f, 66f, 36f, line); + } + } + + private void mouth(Canvas c, MascotMood mood, long t) { + line.setColor(ink); + line.setStrokeWidth(1.2f); + switch (mood) { + case CHEERING: + case FAST: + p.reset(); + p.moveTo(45f, 53.5f); + p.quadTo(50f, 61.5f, 55f, 53.5f); + p.close(); + fill.setColor(mouth); + c.drawPath(p, fill); + fill.setColor(tongue); + c.drawOval(oval(47.5f, 56.4f, 52.5f, 58.4f), fill); + c.drawPath(p, line); + break; + case DETERMINED: + fill.setColor(Color.WHITE); + oval(44.5f, 53f, 55.5f, 57f); + c.drawRoundRect(r, 1.5f, 1.5f, fill); + c.drawRoundRect(r, 1.5f, 1.5f, line); + c.drawLine(44.8f, 55f, 55.2f, 55f, line); + break; + case WORRIED: + p.reset(); + p.moveTo(45.5f, 56f); + p.quadTo(47f, 54.3f, 48.5f, 56f); + p.quadTo(50f, 57.7f, 51.5f, 56f); + p.quadTo(53f, 54.3f, 54.5f, 56f); + c.drawPath(p, line); + break; + case SLEEPING: { + fill.setColor(mouth); + c.drawOval(oval(48.6f, 54.5f, 51.4f, 57f), fill); + // A nose bubble that swells and pops. + float grow = (t % 2400) / 2400f; + fill.setColor(0x7090D8FF); + c.drawCircle(53.5f, 51f, 1f + grow * 4f, fill); + break; + } + case REVERSE: + fill.setColor(mouth); + c.drawOval(oval(48.6f, 54f, 51.4f, 57f), fill); + break; + case WAITING: + c.drawLine(47.5f, 55.5f, 52.5f, 55.5f, line); + break; + default: + // A cat smile: w. + p.reset(); + p.moveTo(45.5f, 54f); + p.quadTo(47.75f, 57.5f, 50f, 54.5f); + p.quadTo(52.25f, 57.5f, 54.5f, 54f); + c.drawPath(p, line); + } + } + + private void zs(Canvas c, long t) { + float phase = (t % 3000) / 3000f; + float[][] zs = { { 20f, 24f, 6f }, { 13f, 14f, 7.5f }, { 5f, 4f, 9f } }; + for (float[] z : zs) { + text.setTextSize(z[2]); + text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f)))); + c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text); + } + } + + private void sparkles(Canvas c, long t) { + float[][] at = { { 14f, 30f }, { 86f, 26f }, { 12f, 62f }, { 88f, 60f }, { 50f, 1.5f } }; + for (int i = 0; i < at.length; i++) { + float s = 2.4f + 1.8f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3)); + fill.setColor(i % 2 == 0 ? regen : Color.WHITE); + star(c, at[i][0], at[i][1], s); + } + } + + private void wind(Canvas c, long t) { + line.setStrokeWidth(1f); + line.setColor(0x88FFFFFF); + for (int i = 0; i < 5; i++) { + float x = 18f - ((t / 6f + i * 37f) % 20f); + float y = 20f + i * 14f; + c.drawLine(x - 10f, y, x, y, line); + } + line.setColor(ink); + } + + private void question(Canvas c, long t) { + text.setTextSize(14f); + text.setColor(accentLight); + float bob = (float) Math.sin(t / 400.0) * 1.2f; + c.drawText("?", 16f, 18f + bob, text); + } + + protected void drop(Canvas c, float x, float y, float s) { + p.reset(); + p.moveTo(x, y - s * 1.6f); + p.quadTo(x + s * 1.2f, y + s * 0.2f, x, y + s); + p.quadTo(x - s * 1.2f, y + s * 0.2f, x, y - s * 1.6f); + c.drawPath(p, fill); + } + + /** A four-pointed sparkle; leaves its outline in {@link #p}. */ + protected void star(Canvas c, float x, float y, float s) { + p.reset(); + p.moveTo(x, y - s); + p.quadTo(x, y, x + s, y); + p.quadTo(x, y, x, y + s); + p.quadTo(x, y, x - s, y); + p.quadTo(x, y, x, y - s); + p.close(); + c.drawPath(p, fill); + } + + protected RectF oval(float left, float top, float right, float bottom) { + r.set(left, top, right, bottom); + return r; + } +} diff --git a/app/src/main/java/com/evsuite/abrp/ChibiFigure.java b/app/src/main/java/com/evsuite/abrp/ChibiFigure.java index a6224d7..d9a4c12 100644 --- a/app/src/main/java/com/evsuite/abrp/ChibiFigure.java +++ b/app/src/main/java/com/evsuite/abrp/ChibiFigure.java @@ -2,9 +2,7 @@ package com.evsuite.abrp; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Path; -import android.graphics.RectF; /** * The chibi co-pilot for the Mascot Overview style ({@link MascotCharacter#CHIBI}): the small, @@ -14,71 +12,35 @@ import android.graphics.RectF; * Her whole body acts the mood: waves while content, throws both arms up and hops while * cheering (star eyes), pumps a fist when determined, holds her cheeks with a tear when worried, * sleeps with a nose bubble in P, glances and points back in R, leans into the wind when - * fast, and wonders "?" while waiting. - * - * Draws on the same 100 × 100 grid as {@link MascotView}'s classic figure; it holds no state. + * fast, and wonders "?" while waiting. The shared parts are in {@link ChibiBase}. */ -final class ChibiFigure { +final class ChibiFigure extends ChibiBase { - private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Path p = new Path(); - private final Path q = new Path(); - private final RectF r = new RectF(); - private final int accent, accentLight, regen; - private final int ink = 0xFF3A2A33, hair = 0xFFF0855C, hairLight = 0xFFFFB08A, skin = 0xFFFFE6D6, - iris = 0xFF6E45B8, irisLight = 0xFFA27BE6, blush = 0x66FF7A8A, trousers = 0xFF2D3142, - shoe = 0xFFF6F6F8, mouth = 0xFF8E3A48, tongue = 0xFFE88A95; + private final int hair = 0xFFF0855C, hairLight = 0xFFFFB08A, skin = 0xFFFFE6D6, trousers = 0xFF2D3142, + shoe = 0xFFF6F6F8; ChibiFigure(int accent, int accentLight, int regen) { - this.accent = accent; - this.accentLight = accentLight; - this.regen = regen; - line.setStyle(Paint.Style.STROKE); - line.setStrokeCap(Paint.Cap.ROUND); - line.setStrokeJoin(Paint.Join.ROUND); - line.setColor(ink); - text.setTextAlign(Paint.Align.CENTER); - text.setFakeBoldText(true); + super(accent, accentLight, regen, 0xFF6E45B8, 0xFFA27BE6, 0xFFFFE6D6, 6f); } - /** Where the speech bubble's tail points, on the grid. */ - static final float TIP_X = 70f, TIP_Y = 34f; + @Override + float tipX() { + return 70f; + } - void draw(Canvas c, MascotMood mood, long t, boolean blink) { - // Smaller and further left than the grid, so her big head stays clear of the speed. - c.save(); - c.translate(-6f, 0f); - c.scale(0.92f, 0.92f, 50f, 100f); - // A hop while cheering. - float hop = mood == MascotMood.CHEERING ? -3.5f * (float) Math.abs(Math.sin(t / 180.0)) : 0f; - fill.setColor(0x40000000); - float shadow = 18f + hop * 0.8f; - c.drawOval(oval(50f - shadow, 96.5f, 50f + shadow, 100f), fill); - - c.save(); - c.translate(0f, hop); - float lean = mood == MascotMood.FAST ? 5f : mood == MascotMood.SLEEPING ? -6f : 0f; - c.rotate(lean, 50f, 96f); + @Override + float tipY() { + return 34f; + } + @Override + protected void drawFigure(Canvas c, MascotMood mood, long t, boolean blink) { drawLegs(c); drawBody(c); - // Hands on her cheeks and the fist at her chest are in front of the head. - boolean inFront = mood == MascotMood.WORRIED || mood == MascotMood.DETERMINED; + boolean inFront = handsInFront(mood); if (!inFront) drawArms(c, mood, t); drawHead(c, mood, t, blink); if (inFront) drawArms(c, mood, t); - c.restore(); - - switch (mood) { - case SLEEPING: zs(c, t); break; - case CHEERING: sparkles(c, t); break; - case FAST: wind(c, t); break; - case WAITING: question(c, t); break; - default: break; - } - c.restore(); } private void drawLegs(Canvas c) { @@ -124,42 +86,15 @@ final class ChibiFigure { line.setColor(ink); } - /** Two arms from the shoulders to hands placed by the mood. */ private void drawArms(Canvas c, MascotMood mood, long t) { - float wave = (float) Math.sin(t / 220.0) * 3f; - float lx, ly, rx, ry; - switch (mood) { - case CONTENT: lx = 33f; ly = 82f; rx = 82f + wave * 0.5f; ry = 64f + wave; break; - case CHEERING: lx = 16f; ly = 56f; rx = 84f; ry = 56f; break; - case DETERMINED: lx = 33f; ly = 82f; rx = 57f; ry = 72f; break; - case WORRIED: lx = 34f; ly = 55f; rx = 66f; ry = 55f; break; - // Pointing back. - case REVERSE: lx = 20f; ly = 66f; rx = 67f; ry = 82f; break; - case FAST: lx = 30f; ly = 76f; rx = 70f; ry = 76f; break; - default: lx = 33f; ly = 82f; rx = 67f; ry = 82f; - } - arm(c, 39.5f, 69f, lx, ly); - arm(c, 60.5f, 69f, rx, ry); - } - - private void arm(Canvas c, float sx, float sy, float hx, float hy) { - line.setStrokeWidth(7.4f); - c.drawLine(sx, sy, hx, hy, line); - line.setColor(accent); - line.setStrokeWidth(5f); - c.drawLine(sx, sy, hx, hy, line); - line.setColor(ink); - fill.setColor(skin); - line.setStrokeWidth(1.1f); - c.drawCircle(hx, hy, 3.2f, fill); - c.drawCircle(hx, hy, 3.2f, line); + float[] h = hands(mood, t); + limb(c, 39.5f, 69f, h[0], h[1], accent, skin); + limb(c, 60.5f, 69f, h[2], h[3], accent, skin); } private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) { - float tilt = mood == MascotMood.REVERSE ? -10f : mood == MascotMood.SLEEPING ? 10f - : mood == MascotMood.WAITING ? 6f : 0f; c.save(); - c.rotate(tilt, 50f, 62f); + c.rotate(tilt(mood), 50f, 62f); // The bob behind the face. fill.setColor(hair); line.setStrokeWidth(1.3f); @@ -201,197 +136,7 @@ final class ChibiFigure { line.setStrokeWidth(0.8f); c.drawPath(p, line); - if (mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.CONTENT - || mood == MascotMood.SLEEPING || mood == MascotMood.WORRIED) { - fill.setColor(blush); - c.drawOval(oval(29.5f, 50f, 37f, 54f), fill); - c.drawOval(oval(63f, 50f, 70.5f, 54f), fill); - } - eye(c, 39f, 44f, mood, blink, t); - eye(c, 61f, 44f, mood, blink, t); - brows(c, mood); - mouth(c, mood, t); - if (mood == MascotMood.WORRIED) { - fill.setColor(0xFF8FD3FF); - drop(c, 72f, 30f, 3f); - drop(c, 34.5f, 52f, 1.8f); - } + face(c, mood, t, blink); c.restore(); } - - private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink, long t) { - line.setColor(ink); - line.setStrokeWidth(1.5f); - if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING && mood != MascotMood.FAST)) { - c.drawArc(oval(cx - 4.5f, cy - 3f, cx + 4.5f, cy + 2f), 20f, 140f, false, line); - return; - } - if (mood == MascotMood.FAST) { - // ^ ^ - c.drawArc(oval(cx - 4.5f, cy - 2f, cx + 4.5f, cy + 5f), 200f, 140f, false, line); - return; - } - float look = mood == MascotMood.REVERSE ? -2f : 0f; - oval(cx - 4.6f, cy - 5.6f, cx + 4.6f, cy + 5.6f); - if (mood == MascotMood.REVERSE) { - fill.setColor(Color.WHITE); - c.drawOval(r, fill); - fill.setColor(iris); - c.drawOval(oval(cx - 3f + look, cy - 4f, cx + 3f + look, cy + 4.5f), fill); - } else { - fill.setColor(iris); - c.drawOval(r, fill); - fill.setColor(irisLight); - c.drawOval(oval(cx - 3.6f, cy + 0.5f, cx + 3.6f, cy + 5f), fill); - } - if (mood == MascotMood.CHEERING) { - fill.setColor(Color.WHITE); - star(c, cx, cy - 0.5f, 3.6f); - } else { - fill.setColor(Color.WHITE); - c.drawCircle(cx - 1.6f + look, cy - 2.4f, 2f, fill); - c.drawCircle(cx + 1.8f + look, cy + 2.4f, 0.9f, fill); - } - if (mood == MascotMood.DETERMINED) { - // A flat lid over the top: focused. - fill.setColor(skin); - c.drawRect(cx - 5.2f, cy - 6.4f, cx + 5.2f, cy - 2f, fill); - line.setStrokeWidth(1.6f); - c.drawLine(cx - 5f, cy - 2f, cx + 5f, cy - 2f, line); - return; - } - line.setStrokeWidth(1.6f); - c.drawArc(oval(cx - 5f, cy - 5.9f, cx + 5f, cy + 5.6f), 200f, 140f, false, line); - } - - private void brows(Canvas c, MascotMood mood) { - line.setColor(ink); - line.setStrokeWidth(1.4f); - if (mood == MascotMood.DETERMINED) { - c.drawLine(34f, 33.5f, 43.5f, 36.5f, line); - c.drawLine(56.5f, 36.5f, 66f, 33.5f, line); - } else if (mood == MascotMood.WORRIED) { - c.drawLine(34f, 36f, 43f, 33f, line); - c.drawLine(57f, 33f, 66f, 36f, line); - } - } - - private void mouth(Canvas c, MascotMood mood, long t) { - line.setColor(ink); - line.setStrokeWidth(1.2f); - switch (mood) { - case CHEERING: - case FAST: - p.reset(); - p.moveTo(45f, 53.5f); - p.quadTo(50f, 61.5f, 55f, 53.5f); - p.close(); - fill.setColor(mouth); - c.drawPath(p, fill); - fill.setColor(tongue); - c.drawOval(oval(47.5f, 56.4f, 52.5f, 58.4f), fill); - c.drawPath(p, line); - break; - case DETERMINED: - fill.setColor(Color.WHITE); - oval(44.5f, 53f, 55.5f, 57f); - c.drawRoundRect(r, 1.5f, 1.5f, fill); - c.drawRoundRect(r, 1.5f, 1.5f, line); - c.drawLine(44.8f, 55f, 55.2f, 55f, line); - break; - case WORRIED: - p.reset(); - p.moveTo(45.5f, 56f); - p.quadTo(47f, 54.3f, 48.5f, 56f); - p.quadTo(50f, 57.7f, 51.5f, 56f); - p.quadTo(53f, 54.3f, 54.5f, 56f); - c.drawPath(p, line); - break; - case SLEEPING: { - fill.setColor(mouth); - c.drawOval(oval(48.6f, 54.5f, 51.4f, 57f), fill); - // A nose bubble that swells and pops. - float grow = (t % 2400) / 2400f; - fill.setColor(0x7090D8FF); - c.drawCircle(53.5f, 51f, 1f + grow * 4f, fill); - break; - } - case REVERSE: - fill.setColor(mouth); - c.drawOval(oval(48.6f, 54f, 51.4f, 57f), fill); - break; - case WAITING: - c.drawLine(47.5f, 55.5f, 52.5f, 55.5f, line); - break; - default: - // A cat smile: w. - p.reset(); - p.moveTo(45.5f, 54f); - p.quadTo(47.75f, 57.5f, 50f, 54.5f); - p.quadTo(52.25f, 57.5f, 54.5f, 54f); - c.drawPath(p, line); - } - } - - private void zs(Canvas c, long t) { - float phase = (t % 3000) / 3000f; - float[][] zs = { { 20f, 24f, 6f }, { 13f, 14f, 7.5f }, { 5f, 4f, 9f } }; - for (float[] z : zs) { - text.setTextSize(z[2]); - text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f)))); - c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text); - } - } - - private void sparkles(Canvas c, long t) { - float[][] at = { { 14f, 30f }, { 86f, 26f }, { 12f, 62f }, { 88f, 60f }, { 50f, 1.5f } }; - for (int i = 0; i < at.length; i++) { - float s = 2.4f + 1.8f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3)); - fill.setColor(i % 2 == 0 ? regen : Color.WHITE); - star(c, at[i][0], at[i][1], s); - } - } - - private void wind(Canvas c, long t) { - line.setStrokeWidth(1f); - line.setColor(0x88FFFFFF); - for (int i = 0; i < 5; i++) { - float x = 18f - ((t / 6f + i * 37f) % 20f); - float y = 20f + i * 14f; - c.drawLine(x - 10f, y, x, y, line); - } - line.setColor(ink); - } - - private void question(Canvas c, long t) { - text.setTextSize(14f); - text.setColor(accentLight); - float bob = (float) Math.sin(t / 400.0) * 1.2f; - c.drawText("?", 16f, 18f + bob, text); - } - - private void drop(Canvas c, float x, float y, float s) { - p.reset(); - p.moveTo(x, y - s * 1.6f); - p.quadTo(x + s * 1.2f, y + s * 0.2f, x, y + s); - p.quadTo(x - s * 1.2f, y + s * 0.2f, x, y - s * 1.6f); - c.drawPath(p, fill); - } - - /** A four-pointed sparkle; leaves its outline in {@link #p}. */ - private void star(Canvas c, float x, float y, float s) { - p.reset(); - p.moveTo(x, y - s); - p.quadTo(x, y, x + s, y); - p.quadTo(x, y, x, y + s); - p.quadTo(x, y, x - s, y); - p.quadTo(x, y, x, y - s); - p.close(); - c.drawPath(p, fill); - } - - private RectF oval(float left, float top, float right, float bottom) { - r.set(left, top, right, bottom); - return r; - } } diff --git a/app/src/main/java/com/evsuite/abrp/FoxFigure.java b/app/src/main/java/com/evsuite/abrp/FoxFigure.java new file mode 100644 index 0000000..f43af4a --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/FoxFigure.java @@ -0,0 +1,206 @@ +package com.evsuite.abrp; + +import android.graphics.Canvas; +import android.graphics.Path; + +/** + * The chibi fox for the Mascot Overview style ({@link MascotCharacter#FOX}): a sitting fox with + * a big head, pointed ears, cream cheeks and chest, dark paws, a scarf in the accent colour and + * a large brush tail with a cream tip. + * + * Ears and tail act the mood with the face ({@link ChibiBase}): the tail wags while content and + * fast while cheering (paws up, star eyes), stands stiff when determined (paw at the chest), + * hangs low with drooping ears when worried (paws on the cheeks), lies flat in P (ears down, + * nose bubble); ears go back and the scarf flies when fast; a paw points back in R. + */ +final class FoxFigure extends ChibiBase { + + private final int fur = 0xFFF08A3C, furLight = 0xFFFFB46E, cream = 0xFFFFF4E6, paw = 0xFF4A3028, + earInside = 0xFF5A3A30; + + FoxFigure(int accent, int accentLight, int regen) { + super(accent, accentLight, regen, 0xFF9A5A1A, 0xFFE0A040, 0xFFF08A3C, 10f); + } + + @Override + float tipX() { + return 66f; + } + + @Override + float tipY() { + return 38f; + } + + @Override + protected void drawFigure(Canvas c, MascotMood mood, long t, boolean blink) { + drawTail(c, mood, t); + drawBody(c); + drawScarf(c, mood, t); + boolean inFront = handsInFront(mood); + if (!inFront) drawPaws(c, mood, t); + drawHead(c, mood, t, blink); + if (inFront) drawPaws(c, mood, t); + } + + /** The brush, rotated about its root by the mood; wagging is a swing over time. */ + private void drawTail(Canvas c, MascotMood mood, long t) { + // Kept within about 18° to the right, so the tail stays clear of the values. + float angle; + switch (mood) { + case CONTENT: angle = (float) Math.sin(t / 250.0) * 8f; break; + case CHEERING: angle = (float) Math.sin(t / 110.0) * 14f; break; + case DETERMINED: angle = -8f; break; + case WORRIED: angle = 15f; break; + case SLEEPING: angle = 18f; break; + case FAST: angle = 16f; break; + case REVERSE: angle = (float) Math.sin(t / 400.0) * 5f; break; + default: angle = (float) Math.sin(t / 600.0) * 4f; + } + c.save(); + c.rotate(angle, 60f, 90f); + p.reset(); + p.moveTo(57f, 92f); + p.cubicTo(80f, 96f, 94f, 78f, 88f, 56f); + p.cubicTo(86f, 46f, 78f, 40f, 73f, 44f); + p.cubicTo(78f, 56f, 76f, 74f, 57f, 86f); + p.close(); + fill.setColor(fur); + c.drawPath(p, fill); + c.save(); + c.clipPath(p); + fill.setColor(cream); + c.drawCircle(83f, 46f, 10f, fill); + c.restore(); + line.setStrokeWidth(1.3f); + c.drawPath(p, line); + c.restore(); + } + + private void drawBody(Canvas c) { + p.reset(); + p.moveTo(37f, 93f); + p.cubicTo(33f, 80f, 40f, 66f, 50f, 66f); + p.cubicTo(60f, 66f, 67f, 80f, 63f, 93f); + p.quadTo(50f, 96f, 37f, 93f); + p.close(); + fill.setColor(fur); + c.drawPath(p, fill); + line.setStrokeWidth(1.2f); + c.drawPath(p, line); + fill.setColor(cream); + c.drawOval(oval(43f, 71f, 57f, 91f), fill); + // Hind paws. + fill.setColor(paw); + for (float x : new float[] { 37.5f, 52.5f }) { + oval(x, 90f, x + 10f, 97.5f); + c.drawOval(r, fill); + c.drawOval(r, line); + } + } + + /** The scarf round the neck; its end hangs, or flies back when fast. */ + private void drawScarf(Canvas c, MascotMood mood, long t) { + fill.setColor(accent); + line.setStrokeWidth(1.1f); + if (mood == MascotMood.FAST) { + float flap = (float) Math.sin(t / 90.0) * 2f; + p.reset(); + p.moveTo(40f, 64f); + p.quadTo(30f, 60f + flap, 20f, 63f - flap); + p.lineTo(21f, 69f - flap); + p.quadTo(30f, 67f + flap, 40f, 70f); + p.close(); + } else { + p.reset(); + p.addRoundRect(oval(54f, 65f, 61f, 80f), 2f, 2f, Path.Direction.CW); + } + c.drawPath(p, fill); + c.drawPath(p, line); + oval(36f, 62f, 64f, 70f); + c.drawRoundRect(r, 4f, 4f, fill); + c.drawRoundRect(r, 4f, 4f, line); + } + + private void drawPaws(Canvas c, MascotMood mood, long t) { + float[] h = hands(mood, t); + limb(c, 41f, 72f, h[0], h[1], fur, paw); + limb(c, 59f, 72f, h[2], h[3], fur, paw); + } + + private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) { + c.save(); + c.rotate(tilt(mood), 50f, 62f); + // Ears behind the head, drooping or laid back by the mood; the right one mirrored. + float droop = mood == MascotMood.WORRIED ? 38f : mood == MascotMood.SLEEPING ? 26f + : mood == MascotMood.FAST ? 22f : mood == MascotMood.CONTENT ? (float) Math.sin(t / 900.0) * 3f : 0f; + ear(c, droop); + c.save(); + c.scale(-1f, 1f, 50f, 0f); + ear(c, droop); + c.restore(); + + // The head: round on top, a tuft at each cheek. + p.reset(); + p.moveTo(50f, 16f); + p.cubicTo(66f, 16f, 78f, 26f, 79f, 40f); + p.lineTo(86f, 52f); + p.lineTo(77f, 53f); + p.cubicTo(70f, 63f, 60f, 66f, 50f, 66f); + p.cubicTo(40f, 66f, 30f, 63f, 23f, 53f); + p.lineTo(14f, 52f); + p.lineTo(21f, 40f); + p.cubicTo(22f, 26f, 34f, 16f, 50f, 16f); + p.close(); + fill.setColor(fur); + c.drawPath(p, fill); + // Cream cheeks and muzzle, inside the head. + c.save(); + c.clipPath(p); + fill.setColor(cream); + c.drawOval(oval(22f, 47f, 78f, 84f), fill); + c.drawOval(oval(12f, 45f, 34f, 60f), fill); + c.drawOval(oval(66f, 45f, 88f, 60f), fill); + c.restore(); + line.setStrokeWidth(1.3f); + c.drawPath(p, line); + line.setColor(furLight); + line.setStrokeWidth(1.6f); + c.drawArc(oval(30f, 19f, 56f, 36f), 200f, 60f, false, line); + line.setColor(ink); + + // Whiskers, then the nose and the face. + line.setStrokeWidth(0.6f); + c.drawLine(28f, 53f, 19f, 51f, line); + c.drawLine(28f, 55.5f, 19f, 56.5f, line); + c.drawLine(72f, 53f, 81f, 51f, line); + c.drawLine(72f, 55.5f, 81f, 56.5f, line); + face(c, mood, t, blink); + fill.setColor(ink); + c.drawOval(oval(47.6f, 49f, 52.4f, 52f), fill); + c.restore(); + } + + /** The left ear, rotated outwards about its root by {@code droop} degrees. */ + private void ear(Canvas c, float droop) { + c.save(); + c.rotate(-droop, 35f, 23f); + p.reset(); + p.moveTo(23f, 34f); + p.lineTo(26f, 3f); + p.lineTo(47f, 20f); + p.close(); + fill.setColor(fur); + c.drawPath(p, fill); + line.setStrokeWidth(1.3f); + c.drawPath(p, line); + p.reset(); + p.moveTo(27.5f, 27f); + p.lineTo(29f, 10f); + p.lineTo(40f, 20f); + p.close(); + fill.setColor(earInside); + c.drawPath(p, fill); + c.restore(); + } +} diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index e266ae0..7a2023b 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -1267,7 +1267,8 @@ public class MainActivity extends AppCompatActivity { /** Who sits in the Mascot style; the drop-down shows only while that style is chosen. */ private void bindMascotCharacter() { MascotCharacter[] characters = MascotCharacter.values(); - String[] labels = { getString(R.string.mascot_character_classic), getString(R.string.mascot_character_chibi) }; + String[] labels = { getString(R.string.mascot_character_classic), getString(R.string.mascot_character_chibi), + getString(R.string.mascot_character_fox) }; int current = MascotCharacter.fromPref(prefs.getString(MascotCharacter.PREF_KEY, null)).ordinal(); bindChoices(settingsPane.findViewById(R.id.mascot_choices), labels, current, position -> { prefs.edit().putString(MascotCharacter.PREF_KEY, characters[position].name()).apply(); diff --git a/app/src/main/java/com/evsuite/abrp/MascotCharacter.java b/app/src/main/java/com/evsuite/abrp/MascotCharacter.java index d69bafc..da06685 100644 --- a/app/src/main/java/com/evsuite/abrp/MascotCharacter.java +++ b/app/src/main/java/com/evsuite/abrp/MascotCharacter.java @@ -2,13 +2,15 @@ package com.evsuite.abrp; /** * Who sits in the Mascot Overview style, in the order of the Settings drop-down: the classic - * co-pilot (head and shoulders, headset, racing jacket) or the chibi (small, full figure, big - * head, hoodie, bubbly). Each has her own drawing and lines; both follow {@link MascotMood}. + * co-pilot (head and shoulders, headset, racing jacket), the chibi (small, full figure, big + * head, hoodie, bubbly) or the chibi fox (sitting, scarf, a tail that wags). Each has their + * own drawing and lines; all follow {@link MascotMood}. * Pure Java, unit-tested. */ enum MascotCharacter { CLASSIC, - CHIBI; + CHIBI, + FOX; static final String PREF_KEY = "mascot_character"; diff --git a/app/src/main/java/com/evsuite/abrp/MascotView.java b/app/src/main/java/com/evsuite/abrp/MascotView.java index 289d46e..0914144 100644 --- a/app/src/main/java/com/evsuite/abrp/MascotView.java +++ b/app/src/main/java/com/evsuite/abrp/MascotView.java @@ -24,8 +24,8 @@ import android.os.SystemClock; * power, 5-minute usage and the trip; the clock top right, left of the full-screen button. * Unread values show "—". * - * Two characters ({@link MascotCharacter}): this classic co-pilot, or the chibi drawn by - * {@link ChibiFigure}, each with her own lines. + * Three characters ({@link MascotCharacter}): this classic co-pilot, the chibi + * ({@link ChibiFigure}) or the chibi fox ({@link FoxFigure}), each with their own lines. * * Drawn on a 100 × 100 grid scaled to the pane. Nothing animates on its own: blinking, the Zs, * sparkles and wind move because readings redraw the view every 0.2 s. @@ -37,7 +37,9 @@ final class MascotView extends ClusterCanvasView { { R.array.mascot_waiting, R.array.mascot_reverse, R.array.mascot_worried, R.array.mascot_sleeping, R.array.mascot_cheering, R.array.mascot_determined, R.array.mascot_fast, R.array.mascot_content }, { R.array.chibi_waiting, R.array.chibi_reverse, R.array.chibi_worried, R.array.chibi_sleeping, - R.array.chibi_cheering, R.array.chibi_determined, R.array.chibi_fast, R.array.chibi_content } }; + R.array.chibi_cheering, R.array.chibi_determined, R.array.chibi_fast, R.array.chibi_content }, + { R.array.fox_waiting, R.array.fox_reverse, R.array.fox_worried, R.array.fox_sleeping, + R.array.fox_cheering, R.array.fox_determined, R.array.fox_fast, R.array.fox_content } }; private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint stroke = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -46,7 +48,8 @@ final class MascotView extends ClusterCanvasView { private final RectF r = new RectF(); private final MascotMood.Holder holder = new MascotMood.Holder(); private final String[][][] lines = new String[LINES.length][][]; - private final ChibiFigure chibi; + /** The drawn figures by {@link MascotCharacter} ordinal; null for the classic one, drawn here. */ + private final ChibiBase[] figures; private MascotCharacter character = MascotCharacter.CLASSIC; private final int accent, accentDark, accentLight, regen, warn, white, grey, track; private final int hair = 0xFF2E2440, hairLight = 0xFF5A4880, skin = 0xFFFBE2D0, skinShade = 0xFFE9BFA9, @@ -68,7 +71,8 @@ final class MascotView extends ClusterCanvasView { lines[i] = new String[LINES[i].length][]; for (int j = 0; j < LINES[i].length; j++) lines[i][j] = context.getResources().getStringArray(LINES[i][j]); } - chibi = new ChibiFigure(accent, accentLight, regen); + figures = new ChibiBase[] { null, new ChibiFigure(accent, accentLight, regen), + new FoxFigure(accent, accentLight, regen) }; stroke.setStyle(Paint.Style.STROKE); stroke.setStrokeCap(Paint.Cap.ROUND); stroke.setStrokeJoin(Paint.Join.ROUND); @@ -98,13 +102,13 @@ final class MascotView extends ClusterCanvasView { c.save(); c.translate(ox, oy); c.scale(k, k); - boolean chibiShown = character == MascotCharacter.CHIBI; - if (chibiShown) chibi.draw(c, mood, t, t % 4200 < 260); + ChibiBase figure = figures[character.ordinal()]; + if (figure != null) figure.draw(c, mood, t, t % 4200 < 260); else drawCharacter(c, mood, t); c.restore(); float x0 = ox + 78f * k; - float tipX = chibiShown ? ChibiFigure.TIP_X : 70f, tipY = chibiShown ? ChibiFigure.TIP_Y : 46f; + float tipX = figure != null ? figure.tipX() : 70f, tipY = figure != null ? figure.tipY() : 46f; drawBubble(c, mood, t, x0, h, ox + tipX * k, oy + tipY * k, w); drawValues(c, x0, ox + 93f * k, w, h); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c511c81..8f217a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -143,6 +143,41 @@ Mascot Classic Chibi + Fox + + + Kon? Anyone there? + Sniff sniff… no car yet. + + + Backing up, ears up! + Mind my tail! + + + Battery\'s low, kon… + Find a charger, please! + + + Curled up in P… zzz + Fox nap time. + + + Kon kon! Free energy! + Tail-wagging regen! + + + Pounce! + Full speed, kon! + + + Ears back, here we go! + Zoomies! + + + Kon! Nice drive. + Happy tail, happy fox. + Sniffing the road ahead. + Hellooo? Car? diff --git a/app/src/test/java/com/evsuite/abrp/MascotMoodTest.java b/app/src/test/java/com/evsuite/abrp/MascotMoodTest.java index a9b2787..38623d5 100644 --- a/app/src/test/java/com/evsuite/abrp/MascotMoodTest.java +++ b/app/src/test/java/com/evsuite/abrp/MascotMoodTest.java @@ -64,6 +64,7 @@ public class MascotMoodTest { @Test public void characterFromPref() { assertEquals(MascotCharacter.CHIBI, MascotCharacter.fromPref("CHIBI")); + assertEquals(MascotCharacter.FOX, MascotCharacter.fromPref("FOX")); assertEquals(MascotCharacter.CLASSIC, MascotCharacter.fromPref("CLASSIC")); assertEquals(MascotCharacter.CLASSIC, MascotCharacter.fromPref(null)); assertEquals(MascotCharacter.CLASSIC, MascotCharacter.fromPref("NOPE"));