From 068e4c2aa53ac0edbbcb3d5f368ce569d749c96b Mon Sep 17 00:00:00 2001 From: jwinterm Date: Thu, 3 Sep 2026 14:17:54 -0400 Subject: [PATCH] fix(scene): money no longer vanishes mid-flight, and the quiet tabs have something to watch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Send's bills were being cut off halfway across. Money mode reused the run's lap machinery, so every 9-14 seconds the cycle restarted and reset every bill to the start of its flight — and a roll whose delay plus duration ran past the cycle simply disappeared wherever it had got to. Bills now keep their own clock: cross, re-roll, wait, cross again, with the re-roll deciding afresh whether that bill flies at all. Nothing resets them from outside, so nothing can cut them short, and the sky thins and fills instead of carrying identical traffic for ever. 26 seconds of frames now contain no mid-screen disappearance. Both quiet tabs get an ambient clock, because a form is a long time to look at a dog breathing: * Send: a gold coin arcs across on its own trajectory now and then, bills tumble in proportion to their speed, and at night the odd shooting star. * Receive: a coin drops out of the sky, bounces, and settles in the grass beside the doge with a small sparkle burst — a coin arriving is the entire point of that tab — and he perks up when it lands. Birds fly by in daylight. It lands to one side of him and clear of the sun, which is where the first version put it. Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF --- src/assets/qml/scene.qml | 169 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 159 insertions(+), 10 deletions(-) diff --git a/src/assets/qml/scene.qml b/src/assets/qml/scene.qml index 2d75a7b2..7aef7ae6 100644 --- a/src/assets/qml/scene.qml +++ b/src/assets/qml/scene.qml @@ -130,6 +130,16 @@ Rectangle { // One roll per lap: how many blow through, when, how fast, how high, how big. property var moneyRolls: [] + function rerollMoney(index) { + var rolls = scene.moneyRolls.slice(); + var fresh = scene.rollMoney(); + rolls[index] = fresh[Math.floor(Math.random() * fresh.length)]; + // Re-roll whether this bill flies at all, so the sky thins and fills + // instead of carrying the same traffic for ever. + rolls[index].active = Math.random() < 0.72; + scene.moneyRolls = rolls; + } + function rollMoney() { var n = [0, 1, 1, 2, 2, 3][Math.floor(Math.random() * 6)]; var out = []; @@ -141,7 +151,8 @@ Rectangle { "dur": 4200 + Math.floor(Math.random() * 3600), "band": Math.random(), "size": 0.72 + Math.random() * 0.55, - "bob": 6 + Math.random() * 12 + "bob": 6 + Math.random() * 12, + "tumble": Math.random() * 2.5 }); } return out; @@ -247,7 +258,7 @@ Rectangle { // Two birds, rolled per lap, daytime skies only. Repeater { - model: (scene.night || !scene.birdLap) ? 0 : 2 + model: (scene.night || !(scene.birdLap || scene.sitMode)) ? 0 : 2 Item { id: bird property real p: 0 @@ -267,7 +278,7 @@ Rectangle { rotation: 22 - Math.sin(bird.p * 46 + index) * 16 } NumberAnimation on p { - running: scene.live && scene.birdLap + running: scene.live && (scene.birdLap || scene.sitMode) loops: Animation.Infinite from: 0; to: 1 duration: 21000 + index * 7000 @@ -297,11 +308,25 @@ Rectangle { var span = Math.max(10, scene.height * 0.42 - height * 0.4); return top + band * span + (roll ? Math.sin(p * 7.5) * roll.bob : 0); } - rotation: roll ? Math.sin(p * 6 + index) * 7 : 0 + rotation: roll ? Math.sin(p * (5 + roll.tumble) + index) * (6 + roll.tumble * 4) : 0 opacity: 0.92 - NumberAnimation { id: flyAnim; target: bill; property: "p"; from: 0; to: 1; duration: bill.roll ? bill.roll.dur : 6000 } - Timer { id: flyTimer; repeat: false; interval: bill.roll ? bill.roll.delay : 0; onTriggered: if (bill.roll && bill.roll.active) flyAnim.restart() } + NumberAnimation { + id: flyAnim + target: bill; property: "p"; from: 0; to: 1 + duration: bill.roll ? bill.roll.dur : 6000 + onFinished: bill.rescheduleSelf() + } + Timer { + id: flyTimer; repeat: false + interval: bill.roll ? bill.roll.delay : 0 + onTriggered: { + if (bill.roll && bill.roll.active) + flyAnim.restart(); + else + bill.rescheduleSelf(); // dormant this pass; try again later + } + } function armFor(beat) { flyAnim.stop(); @@ -312,6 +337,20 @@ Rectangle { && bill.roll && bill.roll.active) flyTimer.restart(); } + + // Money mode has no lap to hide inside, so each bill runs its own + // clock: cross, re-roll, wait, cross again. A shared cycle was + // cutting bills off mid-screen whenever a roll's delay plus its + // duration ran past the cycle length, which looked exactly like + // money vanishing halfway across. + function rescheduleSelf() { + if (!scene.moneyMode || !scene.live) + return; + scene.rerollMoney(index); + bill.p = 0; + flyTimer.interval = 900 + Math.floor(Math.random() * 5200); + flyTimer.restart(); + } Connections { target: scene function onBeatChanged() { bill.armFor(scene.beat) } @@ -439,6 +478,115 @@ Rectangle { property real sparkT: 1.0 NumberAnimation { id: sparkAnim; target: scene; property: "sparkT"; from: 0; to: 1; duration: 520 } + // A coin sails past on the Send screen — money going somewhere. + AnimatedImage { + id: flyingCoin + property real t: 1.0 + property real band: 0.3 + source: "qrc:/scene/goldcoin.gif" + playing: scene.live && visible + visible: !scene.reducedMotion && scene.moneyMode && t < 1.0 + width: 26; height: 26 + x: -width + t * (scene.width + width * 2) + // A shallow arc rather than a flat line, so it reads as thrown. + y: scene.height * band - Math.sin(t * Math.PI) * scene.height * 0.16 + rotation: t * 540 + opacity: Math.min(1, Math.sin(t * Math.PI) * 3) + NumberAnimation { id: flyingCoinAnim; target: flyingCoin; property: "t" + from: 0; to: 1; duration: 2600 } + } + + // ...and one lands in the field on the Receive screen. A coin arriving is + // the whole point of that tab, so it is worth more than a doge panting. + Item { + id: giftCoin + property real t: 1.0 + property real spotX: 0.5 + readonly property real landAt: 0.55 // t at which it touches down + readonly property bool landed: t >= landAt && t < 1.0 + visible: !scene.reducedMotion && scene.sitMode && t < 1.0 + width: scene.coinSize; height: width + x: scene.width * spotX - width / 2 + y: { + if (t >= landAt) + return scene.coinRestY - Math.abs(Math.sin((t - landAt) * 9)) * 10 + * (1 - (t - landAt) / (1 - landAt)); // settle bounce + var fall = t / landAt; + return -height + fall * fall * (scene.coinRestY + height); + } + AnimatedImage { + anchors.fill: parent + source: "qrc:/scene/goldcoin.gif" + playing: scene.live && giftCoin.visible + opacity: giftCoin.t > 0.9 ? (1 - giftCoin.t) * 10 : 1 + } + Repeater { + model: 6 + Rectangle { + readonly property real ang: (index / 6) * Math.PI * 2 + readonly property real burst: Math.max(0, Math.min(1, + (giftCoin.t - giftCoin.landAt) * 4)) + width: 3; height: 3; radius: 1.5; color: "#ffe9a3" + visible: giftCoin.landed && burst < 1 + opacity: 1 - burst + x: giftCoin.width / 2 - 1.5 + Math.cos(ang) * burst * 22 + y: giftCoin.height / 2 - 1.5 + Math.sin(ang) * burst * 22 + } + } + NumberAnimation { id: giftCoinAnim; target: giftCoin; property: "t" + from: 0; to: 1; duration: 3400 } + } + + // The quiet screens get their own clock: every few seconds something small + // happens, so the tab is never just a dog breathing. + property real sitterHop: 0.0 + SequentialAnimation { + id: sitterHopAnim + NumberAnimation { target: scene; property: "sitterHop"; to: 9; duration: 180; easing.type: Easing.OutQuad } + NumberAnimation { target: scene; property: "sitterHop"; to: 0; duration: 220; easing.type: Easing.InQuad } + NumberAnimation { target: scene; property: "sitterHop"; to: 4; duration: 140; easing.type: Easing.OutQuad } + NumberAnimation { target: scene; property: "sitterHop"; to: 0; duration: 120; easing.type: Easing.InQuad } + } + + Timer { + id: ambientTimer + repeat: true + running: scene.live && !scene.runMode + interval: 5000 + onTriggered: { + scene.ambientEvent(); + interval = 4200 + Math.floor(Math.random() * 7000); + } + } + + function ambientEvent() { + var roll = Math.random(); + if (scene.moneyMode) { + if (roll < 0.62) { + flyingCoin.band = 0.16 + Math.random() * 0.30; + flyingCoinAnim.duration = 2100 + Math.floor(Math.random() * 2200); + flyingCoinAnim.restart(); + } else if (scene.night) { + shootingStar.p = 1.0; + starAnim.restart(); + } + return; + } + // sit mode + if (roll < 0.5) { + // Beside him, on whichever side, and clear of the sun at 0.74. + giftCoin.spotX = (Math.random() < 0.5) ? 0.30 + Math.random() * 0.12 + : 0.58 + Math.random() * 0.10; + giftCoinAnim.restart(); + sitterHopAnim.restart(); // he notices + } else if (roll < 0.72) { + sitterHopAnim.restart(); + } else if (scene.night) { + shootingStar.p = 1.0; + starAnim.restart(); + } + } + // The doge, sitting this one out. Beat 0 only, and only on some laps. AnimatedImage { id: sitter @@ -446,7 +594,7 @@ Rectangle { playing: scene.live && visible width: 84; height: 67 x: scene.width * (scene.sitMode ? 0.5 : 0.16) - (scene.sitMode ? width / 2 : 0) - y: scene.groundY - height + 14 + y: scene.groundY - height + 14 - scene.sitterHop visible: !scene.reducedMotion && (scene.sitMode || (scene.runMode && scene.idleLap && scene.beat === 0)) opacity: visible ? 1.0 : 0.0 @@ -728,6 +876,9 @@ Rectangle { scene.picked = 0; scene.hopY = 0.0; scene.sparkT = 1.0; + scene.sitterHop = 0.0; + flyingCoin.t = 1.0; + giftCoin.t = 1.0; if (!scene.live) return; @@ -739,9 +890,7 @@ Rectangle { if (scene.moneyMode) { scene.moneyRolls = scene.rollMoney(); scene.beat = 2; // arms the bills through armFor() - beatTimer.interval = 9000 + Math.floor(Math.random() * 5000); - beatTimer.start(); - return; + return; // each bill reschedules itself } scene.runBeat = 6800 + Math.floor(Math.random() * 3000);