From 76c6692013c6355260024e79dd3abadb9a3091b3 Mon Sep 17 00:00:00 2001 From: kojira Date: Thu, 20 Aug 2026 08:57:17 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E7=8A=B6=E6=85=8B=E3=81=AE=E5=95=8F?= =?UTF-8?q?=E3=81=84=E5=90=88=E3=82=8F=E3=81=9B=E3=81=8C=E7=B5=82=E3=82=8F?= =?UTF-8?q?=E3=82=8B=E3=81=BE=E3=81=A7=E3=81=8A=E3=81=BF=E3=81=8F=E3=81=98?= =?UTF-8?q?=E3=82=92=E5=BC=95=E3=81=91=E3=81=AA=E3=81=8F=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit おみくじを引くボタンを押して演出(matter.js)が始まった直後に、引く前の画面へ 戻される。リロードではなく Vue の状態が上書きされている。 保存済みの状態で通信を待たずに画面を出すようにした(#236)結果、peek の応答が 返る前からボタンが押せる。押すと state が animating になるが、その後に到着した peek の応答が applyResponse で state を available/cooldown に戻すため、演出が 畳まれて元の画面に戻っていた。 - 問い合わせが1度でも完了するまでボタンを disabled にする(statusChecked)。 確認中であることも表示する - 演出中に届いた peek の応答は state を触らない(抽選側が状態を持っている) - 演出中にクールダウンが明けたときのタイマーも state を触らない - startScene の入口でも確認前は止める Co-Authored-By: Claude Opus 5 --- docs/backend.md | 5 +++++ web/pages/omikuji.vue | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/backend.md b/docs/backend.md index 5c32de9..52fa271 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -464,6 +464,11 @@ Node版との差分(意図的な改善): - あくまで表示を先出しするだけで、**実際に引けるかは常にサーバーが決める** (端末の時計は書き換えられる)。残り時間は8時間(最長のクールダウン)で頭打ち。 別端末では保存が無いので、従来どおり peek の応答から復元される。 +- **問い合わせが終わるまで「引く」は押せない**(`statusChecked`)。保存済みの状態で + 先に画面を出すため、確認前でもボタンが見えている。ここで押せると、演出の最中に + peek の応答が返って `state` を上書きし、**演出が畳まれて引く前の画面に戻る** + (リロードされたように見える)。併せて、演出中に届いた peek の応答と、演出中に + クールダウンが明けたときのタイマーは、どちらも `state` を触らないようにしている。 ### フロント diff --git a/web/pages/omikuji.vue b/web/pages/omikuji.vue index 6c871f4..726c8c3 100644 --- a/web/pages/omikuji.vue +++ b/web/pages/omikuji.vue @@ -19,9 +19,19 @@
- +
+ お伺いを立てています… +
(前回の結果は下に表示されています)
@@ -108,6 +118,9 @@ export default { data() { return { state: "loading", // loading | available | animating | cooldown | empty(物理乱数枯渇) | error + // サーバーへの状態問い合わせが1度でも完了したか。保存済みの状態で先に + // 画面を出すので、これが false の間は「引く」を押せないようにする。 + statusChecked: false, result: null, pendingResult: null, // 演出中に保持(着地まで表示しない) remaining: 0, // 次に引けるまでの秒 @@ -165,14 +178,22 @@ export default { { github_id: this.user.github_id, peek: true }, { headers: { Authorization: `Bearer ${token}` } } ); + // 演出が始まっていたら、この応答はもう古い。抽選側が状態を持っている + // ので触らない(触ると演出が畳まれて引く前の画面に戻ってしまう)。 + if (this.state === "animating") return; this.applyResponse(res.data); } catch (e) { + if (this.state === "animating") return; this.state = "error"; + } finally { + this.statusChecked = true; } }, // 「引く」→ 演出(鈴の緒儀式)を開始。実際の抽選は鈴が鳴った時(onRang)。 startScene() { if (this.state === "animating") return; + // 確認前は引かせない(ボタンも disabled だが、念のため入口でも止める)。 + if (!this.statusChecked) return; this.pendingResult = null; this._pendingRemaining = 0; this.state = "animating"; @@ -272,6 +293,8 @@ export default { clearInterval(this.timerId); this.timerId = null; this.remaining = 0; + // 演出中にクールダウンが明けても状態を触らない(演出が消えてしまう)。 + if (this.state === "animating") return; // 引ける状態へ。前回結果は残しつつボタンを出す。 this.state = "available"; saveOmikujiState( From bde50ce45d3f1aef9f96d0bcecd051fe50b53233 Mon Sep 17 00:00:00 2001 From: kojira Date: Thu, 20 Aug 2026 08:59:06 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E5=8F=A4=E3=81=84=E5=BF=9C=E7=AD=94?= =?UTF-8?q?=E3=81=AE=E5=88=A4=E5=AE=9A=E3=82=92=E3=80=8C=E6=BC=94=E5=87=BA?= =?UTF-8?q?=E4=B8=AD=E3=81=8B=E3=80=8D=E3=81=8B=E3=82=89=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E3=81=AE=E4=B8=96=E4=BB=A3=E3=81=AB=E5=A4=89=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「今 animating か」で応答を捨てると、演出が結果より先に終わったときの取り直し (onLanded)を潰してしまう。あの経路はまだ animating のまま fetchStatus を呼ぶため、 応答を捨てると演出が閉じずに固まる(直そうとしたバグより悪い)。 sceneCount の世代を要求時に控え、応答時に変わっていたら捨てる。マウント時の 問い合わせの応答だけが捨てられ、onLanded からの取り直しは通る。 Co-Authored-By: Claude Opus 5 --- docs/backend.md | 7 +++++-- web/pages/omikuji.vue | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/backend.md b/docs/backend.md index 52fa271..305f2da 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -467,8 +467,11 @@ Node版との差分(意図的な改善): - **問い合わせが終わるまで「引く」は押せない**(`statusChecked`)。保存済みの状態で 先に画面を出すため、確認前でもボタンが見えている。ここで押せると、演出の最中に peek の応答が返って `state` を上書きし、**演出が畳まれて引く前の画面に戻る** - (リロードされたように見える)。併せて、演出中に届いた peek の応答と、演出中に - クールダウンが明けたときのタイマーは、どちらも `state` を触らないようにしている。 + (リロードされたように見える)。併せて、**その問い合わせより後に演出が始まっていたら + 応答を捨てる**(`sceneCount` の世代で判定)。「今 animating か」で判定してはいけない + — 演出が結果より先に終わったときの取り直し(`onLanded`)はまだ `animating` のまま + 呼ぶため、それを捨てると演出が閉じずに固まる。演出中にクールダウンが明けたときの + タイマーも `state` を触らない。 ### フロント diff --git a/web/pages/omikuji.vue b/web/pages/omikuji.vue index 726c8c3..2fa6a77 100644 --- a/web/pages/omikuji.vue +++ b/web/pages/omikuji.vue @@ -121,6 +121,8 @@ export default { // サーバーへの状態問い合わせが1度でも完了したか。保存済みの状態で先に // 画面を出すので、これが false の間は「引く」を押せないようにする。 statusChecked: false, + // 演出を始めた回数。飛行中の問い合わせの応答が古いかどうかの判定に使う。 + sceneCount: 0, result: null, pendingResult: null, // 演出中に保持(着地まで表示しない) remaining: 0, // 次に引けるまでの秒 @@ -170,6 +172,8 @@ export default { if (this.state !== "cooldown" && this.state !== "available") { this.state = "loading"; } + // 応答が返るまでに演出が始まったかを見分けるための世代。 + const sceneAtRequest = this.sceneCount; const token = await this.getToken(); if (!token) return; try { @@ -178,12 +182,17 @@ export default { { github_id: this.user.github_id, peek: true }, { headers: { Authorization: `Bearer ${token}` } } ); - // 演出が始まっていたら、この応答はもう古い。抽選側が状態を持っている - // ので触らない(触ると演出が畳まれて引く前の画面に戻ってしまう)。 - if (this.state === "animating") return; + // この問い合わせを投げた後に演出が始まっていたら、応答はもう古い。 + // 抽選側が状態を持っているので触らない(触ると演出が畳まれて引く前の + // 画面に戻ってしまう)。 + // + // 「今 animating か」で判定してはいけない。演出が結果より先に終わった + // ときの取り直し(onLanded)はまだ animating のまま呼ぶため、応答を + // 捨てると演出が閉じずに固まる。 + if (this.sceneCount !== sceneAtRequest) return; this.applyResponse(res.data); } catch (e) { - if (this.state === "animating") return; + if (this.sceneCount !== sceneAtRequest) return; this.state = "error"; } finally { this.statusChecked = true; @@ -194,6 +203,8 @@ export default { if (this.state === "animating") return; // 確認前は引かせない(ボタンも disabled だが、念のため入口でも止める)。 if (!this.statusChecked) return; + // 飛行中の問い合わせがあれば、その応答は古いものとして捨てさせる。 + this.sceneCount++; this.pendingResult = null; this._pendingRemaining = 0; this.state = "animating";