fix(scene): park hidden tabs, and put some weight behind the doge

The +1s raining out of an empty sky came from a scene running on a tab
that had never been shown. An unshown QQuickWidget is 0px wide, and at
0px every coin sits at x=0, so the doge "collected" the whole field in
one step; the pickup pops from that phantom lap were still mid-flight
when the tab was finally selected and the real width arrived. Their
`SequentialAnimation on opacity { running: <bound> }` was cut off by the
width change, which leaves the property wherever it stopped — so a +1
hung in the air for the rest of the lap, next to a coin nobody had
taken.

Both halves are fixed:

  * SceneWidget parks its scene on hide and starts a fresh lap on show,
    and the QML refuses to run at all until it has been laid out. A
    background tab now costs nothing, too.
  * Every one-shot flourish (+1, bark, dust, GAME OVER) is started and
    reset from an explicit handler, so an interrupted lap cannot strand
    one on screen.

The rest is the ambient scene earning its keep:

  * The face-plant has weight. He tips over his front paws — pivoting on
    the paw at the grass line, not the middle of the sprite, which used
    to swing his snout through the ground — squashes at the nose, bends
    through the body, throws dust off the impact, and lies there
    breathing. GAME OVER now lands a beat later so the crash reads on
    its own, and the lap ends shortly after instead of running out the
    clock on a wreck.
  * He has opinions: HRNMMROOF, BONK, SNOOT.EXE and friends on a
    wipeout; MUCH COIN, AMAZE, VERY WIN when he clears the field, with a
    victory hop to go with it.
  * More variety per lap: zero to three bills at their own speeds and
    heights, a 1-in-3 fat +5 coin with a halo, a doge sitting in the
    field during the empty beat, drifting clouds, a soft corona on the
    sun, blinking stars and the odd shooting star at night, birds by
    day, and a run speed that is no longer always eight seconds.

Verified by rendering scene.qml headless (qml6 + xvfb, frame grabs) at
the real widget sizes, and by driving two scenes through a QTabWidget to
watch the hidden-tab case before and after.

Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
This commit is contained in:
jwinterm
2026-09-02 22:34:23 -04:00
parent b200d997b0
commit e5b8ef6a00
3 changed files with 601 additions and 83 deletions
+23
View File
@@ -4,6 +4,7 @@
#include "SceneWidget.h"
#include <QQmlContext>
#include <QQuickItem>
#include "utils/config.h"
@@ -16,6 +17,28 @@ SceneWidget::SceneWidget(QWidget *parent, Variant variant)
this->rootContext()->setContextProperty("sceneCtx", m_backend);
this->setResizeMode(QQuickWidget::SizeRootObjectToView);
this->setSource(QUrl("qrc:/qml/scene.qml"));
this->callScene("suspend");
}
void SceneWidget::showEvent(QShowEvent *event) {
QQuickWidget::showEvent(event);
this->callScene("restart");
}
void SceneWidget::hideEvent(QHideEvent *event) {
this->callScene("suspend");
QQuickWidget::hideEvent(event);
}
void SceneWidget::resizeEvent(QResizeEvent *event) {
QQuickWidget::resizeEvent(event);
if (this->isVisible())
this->callScene("restart");
}
void SceneWidget::callScene(const char *method) {
if (auto *root = this->rootObject())
QMetaObject::invokeMethod(root, method);
}
void SceneWidget::refreshReducedMotion() {