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)} + + + + + + + {nearSprites.map(spriteShadow)} {shot && showShuttleTrail && land && ( <> @@ -388,6 +460,12 @@ export function Court3DView({ ))} + + + {/* Near-court mascots stand in front of the net, backs to the camera. */} + {nearSprites.map(spriteView)} + + {/* Shuttle chip */} Date: Sun, 5 Jul 2026 23:35:20 +0100 Subject: [PATCH 2/3] fix(court): near mascots occlude the shuttle at the hit The shuttle layer drew over everything, so it slid across a near player's body on approach. Near figures now sandwich the shuttle by court depth each frame: a shuttle between the player and the net is beyond him from the camera and his body occludes it; once it passes behind him it draws over him. Sprites also depth-sort within a side. --- components/Court3DView.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/components/Court3DView.tsx b/components/Court3DView.tsx index 0d909e6..5b2faf0 100644 --- a/components/Court3DView.tsx +++ b/components/Court3DView.tsx @@ -260,11 +260,19 @@ export function Court3DView({ h, fx: f[0], fy: f[1], + zc: z, near: facingAway, }; }); - const farSprites = sprites.filter((s) => !s.near); - const nearSprites = sprites.filter((s) => s.near); + const byDepth = (a: { zc: number }, c: { zc: number }) => a.zc - c.zc; + const farSprites = sprites.filter((s) => !s.near).sort(byDepth); + const nearSprites = sprites.filter((s) => s.near).sort(byDepth); + // Near figures sandwich the shuttle by depth: a shuttle between a player + // and the net sits beyond him from the camera, so his body occludes it at + // the hit; once it passes behind him it draws over him instead. + const shuttleZNow = shuttleCourt[1]; + const nearBehindShuttle = nearSprites.filter((s) => s.zc < shuttleZNow); + const nearOverShuttle = nearSprites.filter((s) => s.zc >= shuttleZNow); const spriteShadow = (s: (typeof sprites)[number]) => ( - {/* Near-court mascots stand in front of the net, backs to the camera. */} - {nearSprites.map(spriteView)} + {/* Near-court mascots stand in front of the net, backs to the camera; + those closer to the net than the shuttle render under it. */} + {nearBehindShuttle.map(spriteView)} {/* Shuttle chip */} @@ -482,6 +491,9 @@ export function Court3DView({ + {/* 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