From 90287cf1d276cbb25db1388e4b7caa87625100a4 Mon Sep 17 00:00:00 2001
From: Haritabh Gupta <20576107+haritabh17@users.noreply.github.com>
Date: Sun, 5 Jul 2026 23:04:02 +0100
Subject: [PATCH 1/3] feat(court): render mascots as full figures in the 3D
world view
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The 3D view drew placeholder pins for every player; mascot looks now
render as the same 2D figure art, billboarded into the projection:
- far-court figures face the camera, near-court figures show their
backs with handedness mirroring — the flat court's orientation rules,
which cover every angle the +/-18-degree yaw camera can reach
- perspective-scaled with a ground contact shadow; feet plant on the
shadow at full tilt and the figure recenters at b=0 so the 2D<->3D
morph stays seamless
- scene split into layers around the net so far figures stand behind
it and near figures in front; each mascot keeps its own Svg root
(per-mascot gradient ids collide inside a shared root)
- non-mascot looks keep the existing pins; shuttle untouched
- CUF 06 now dresses both singles players in mascots first, with a
slow yaw sweep on the standing figures for the PR video
---
.maestro/06-tilt-3d-drill.yaml | 23 ++++++-
components/BadmintonCourt.tsx | 4 +-
components/Court3DView.tsx | 116 +++++++++++++++++++++++++++------
3 files changed, 121 insertions(+), 22 deletions(-)
diff --git a/.maestro/06-tilt-3d-drill.yaml b/.maestro/06-tilt-3d-drill.yaml
index f63939b..8446eff 100644
--- a/.maestro/06-tilt-3d-drill.yaml
+++ b/.maestro/06-tilt-3d-drill.yaml
@@ -1,5 +1,6 @@
-# CUF: load a vault drill, tilt into the 3D world view, start playback and
-# pan-rotate the camera while the rally is flying.
+# CUF: dress both players in mascots, load a vault drill, tilt into the 3D
+# world view — mascots render as full figures (far side faces the camera,
+# near side shows its back) — then start playback and pan-rotate mid-rally.
appId: com.haritabhgupta.badmintoncourtsimulator
---
- launchApp:
@@ -7,6 +8,15 @@ appId: com.haritabhgupta.badmintoncourtsimulator
# fresh installs open on the first-run tour
- assertVisible: "Skip tour"
- tapOn: "Skip tour"
+# mascots for both singles players: Fox far court (P1), Koala near court (P3)
+- tapOn: "Customize"
+- assertVisible: "INCLUDED"
+- tapOn: "Fox"
+- assertVisible: "Fox · Player 1"
+- tapOn: "P3"
+- tapOn: "Koala"
+- assertVisible: "Koala · Player 3"
+- tapOn: "Close settings"
- tapOn: "Drills"
- tapOn: "Drill Vault"
- assertVisible: "Four-Corner Shadow"
@@ -16,6 +26,15 @@ appId: com.haritabhgupta.badmintoncourtsimulator
- tapOn: "3D"
# the hint chip only renders once the court has tilted in
- assertVisible: "drag to rotate and tilt"
+# slow yaw sweep on the standing figures: Koala's back near, Fox facing far
+- swipe:
+ start: 30%, 50%
+ end: 70%, 50%
+ duration: 1200
+- swipe:
+ start: 70%, 50%
+ end: 30%, 50%
+ duration: 1200
- tapOn: "Play"
# 3D playback advances through the steps on its own
- extendedWaitUntil:
diff --git a/components/BadmintonCourt.tsx b/components/BadmintonCourt.tsx
index 5763227..db62b3a 100644
--- a/components/BadmintonCourt.tsx
+++ b/components/BadmintonCourt.tsx
@@ -331,6 +331,8 @@ export default function BadmintonCourt() {
...courtPointFromScreen(pos.x + c.size / 2, pos.y + c.size / 2, linesRect),
color: c.color,
size: c.size,
+ look: effectiveLooks[id],
+ leftHanded: c.isLeftHanded,
};
};
const shuttleHalf = customizations.Shuttle.size / 2;
@@ -345,7 +347,7 @@ export default function BadmintonCourt() {
linesRect
),
}));
- }, [customizations, getStepsSnapshot, isDoubles, linesRect]);
+ }, [customizations, effectiveLooks, getStepsSnapshot, isDoubles, linesRect]);
const advance3D = useCallback(() => {
if (canRedo) redo();
diff --git a/components/Court3DView.tsx b/components/Court3DView.tsx
index 394dc7b..0d909e6 100644
--- a/components/Court3DView.tsx
+++ b/components/Court3DView.tsx
@@ -14,7 +14,8 @@ import Svg, {
Stop,
} from 'react-native-svg';
import { LINE_UNITS, LinesRect } from './CourtSvg';
-import { CourtTheme, courtThemeById } from '../constants/customization';
+import { MascotView, MASCOT_ASPECT } from './mascots';
+import { CourtTheme, courtThemeById, isMascot, LookId, MascotId } from '../constants/customization';
import { palette, radii, sora } from '../constants/theme';
import {
arcPath,
@@ -41,6 +42,9 @@ import {
export interface Pin3D extends CourtPoint {
color: string;
size: number;
+ /** Player look; mascot looks render as full figures instead of pins. */
+ look?: LookId;
+ leftHanded?: boolean;
}
export interface Step3D {
@@ -211,21 +215,79 @@ export function Court3DView({
const sw = shuttleSize * clamp(sp3[2], 0.8, 1.3);
const squash = 1 - 0.58 * b; // ground ellipses foreshorten with the tilt
- // Player pins glide between the previous and current step with the flight
- const pins = cur.players.map((p, i) => {
+ // Players glide between the previous and current step with the flight.
+ // Mascot looks become full figures (billboarded art); other looks keep pins.
+ const players = cur.players.map((p, i) => {
const from = prev?.players[i] ?? p;
const t = prev ? tc : 1;
const x = from.x + (p.x - from.x) * t;
const z = from.z + (p.z - from.z) * t;
const f = project(x, z, 0);
- const w = p.size * clamp(f[2], 0.8, 1.3);
- const stemH = Math.max(0, 13 * b * f[2]);
- // Flat: disc centered on its court spot; tilted: disc stands on a stem.
- const discCy = f[1] - stemH - w / 2 + (w / 2 + 3) * (1 - b);
- const glyph = w * 0.44;
- return { key: i, color: p.color, fx: f[0], fy: f[1], w, stemH, discCy, glyph };
+ return { p, i, z, f };
});
+ const pins = players
+ .filter(({ p }) => !(p.look && isMascot(p.look)))
+ .map(({ p, i, f }) => {
+ const w = p.size * clamp(f[2], 0.8, 1.3);
+ const stemH = Math.max(0, 13 * b * f[2]);
+ // Flat: disc centered on its court spot; tilted: disc stands on a stem.
+ const discCy = f[1] - stemH - w / 2 + (w / 2 + 3) * (1 - b);
+ const glyph = w * 0.44;
+ return { key: i, color: p.color, fx: f[0], fy: f[1], w, stemH, discCy, glyph };
+ });
+
+ // The camera looks from the high-z end, so near-court figures (z past the
+ // net) show their backs — same orientation rule the flat court uses.
+ const sprites = players
+ .filter(({ p }) => p.look && isMascot(p.look))
+ .map(({ p, i, z, f }) => {
+ const facingAway = z > NET_Y;
+ const w = p.size * clamp(f[2], 0.8, 1.3) * (1 + 0.45 * b);
+ const h = w * MASCOT_ASPECT;
+ // b=0: centered on its court spot (matches the flat court exactly);
+ // b=1: feet planted on the contact shadow.
+ const top = f[1] - h / 2 - (h / 2 - w * 0.08) * b;
+ return {
+ key: i,
+ mascot: p.look as MascotId,
+ band: p.color,
+ facingAway,
+ flipped: facingAway ? !!p.leftHanded : !p.leftHanded,
+ left: f[0] - w / 2,
+ top,
+ w,
+ h,
+ fx: f[0],
+ fy: f[1],
+ near: facingAway,
+ };
+ });
+ const farSprites = sprites.filter((s) => !s.near);
+ const nearSprites = sprites.filter((s) => s.near);
+
+ const spriteShadow = (s: (typeof sprites)[number]) => (
+
+ );
+
+ const spriteView = (s: (typeof sprites)[number]) => (
+
+
+
+ );
+
const playerTrails =
showPlayerTrails && prev
? cur.players
@@ -278,16 +340,6 @@ export function Court3DView({
opacity={0.85 * b}
/>
)}
-
-
-
{playerTrails.map((t) => (
))}
+ {farSprites.map(spriteShadow)}
+
+
+ {/* Far-court mascots stand behind the net (each carries its own Svg
+ root so per-mascot gradient ids can never collide). */}
+ {farSprites.map(spriteView)}
+
+
+
+ {/* Near-court mascots stand in front of the net, backs to the camera. */}
+ {nearSprites.map(spriteView)}
+
+
+ {/* Near figures the shuttle has passed: they now occlude it. */}
+ {nearOverShuttle.map(spriteView)}
+
{/* Orbit / pinch surface (dock and header sit above and win touches) */}
Date: Mon, 6 Jul 2026 00:03:15 +0100
Subject: [PATCH 3/3] ci(cuf): retry a failed flow once before failing the job
maestro inputText over gRPC intermittently hits DEADLINE_EXCEEDED on CI
emulators (flow 03 flaked with a green local run and a green previous
CI run). A real regression still fails twice.
---
scripts/record-cufs.sh | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/scripts/record-cufs.sh b/scripts/record-cufs.sh
index 1712dac..a4ef181 100755
--- a/scripts/record-cufs.sh
+++ b/scripts/record-cufs.sh
@@ -11,7 +11,15 @@ rc=0
for flow in .maestro/*.yaml; do
name=$(basename "$flow" .yaml)
adb shell "screenrecord --bit-rate 4000000 --time-limit 170 /sdcard/$name.mp4" &
- maestro test "$flow" || rc=1
+ if ! maestro test "$flow"; then
+ # One retry per flow: maestro's inputText over gRPC flakes on CI
+ # emulators (DEADLINE_EXCEEDED); a genuine regression fails twice.
+ adb shell pkill -INT screenrecord || true
+ sleep 3
+ echo "::warning::flow $name failed once, retrying"
+ adb shell "screenrecord --bit-rate 4000000 --time-limit 170 /sdcard/$name.mp4" &
+ maestro test "$flow" || rc=1
+ fi
adb shell pkill -INT screenrecord || true
sleep 3
adb pull "/sdcard/$name.mp4" "cuf-videos/$name.mp4" || true