From 9fbd260b2d8a28b200250850196d5d9453f8f8d7 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:36:15 +0000 Subject: [PATCH 01/51] fix(pwa): lock the viewport scale on touch devices Pinch and double-tap zoom broke the fixed layout on phones, so the viewport meta now pins the scale. iOS ignores user-scalable=no, so body also opts into touch-action: manipulation. bump version to 0.2.4 Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- src/client/index.css | 2 ++ src/routes/__root.tsx | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8e2ce0e..3563b17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alibai", - "version": "0.2.3", + "version": "0.2.4", "private": true, "type": "module", "scripts": { diff --git a/src/client/index.css b/src/client/index.css index 60a445d..dc7de1e 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -66,6 +66,8 @@ body { body { font-family: var(--font-gothic); + /* iOS は user-scalable=no を無視するので、ダブルタップの拡大はここで止める。 */ + touch-action: manipulation; } /* diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 90d0963..e1d32c3 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -15,7 +15,11 @@ export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: 'UTF-8' }, - { name: 'viewport', content: 'width=device-width, initial-scale=1.0, viewport-fit=cover' }, + { + name: 'viewport', + content: + 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover', + }, { title: 'AlibAI' }, // ホーム画面から開いたときブラウザの殻を外す。display: standalone を読まない // 古い iOS はこの2枚だけが頼りで、black-translucent は From 8725cf9c568153eab2d64eaeb2d01ab18604c18b Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:37:05 +0000 Subject: [PATCH 02/51] feat(client): reveal chat log one sentence at a time and raise turn limits - add usePacedReveal hook and paragraphs helper to split assistant replies by sentence, holding back the trailing unfinished one while streaming - raise question limits for 3-5 person cases (30 -> 45) with a new product cap - sort characters deterministically for public introductions - fetch and cache sound effects during deployment - add direction mockups for desktop review Co-Authored-By: Claude --- .github/workflows/deployment.yaml | 12 + __tests__/client/chat-log.test.ts | 65 +- .../scenario-public-introduction.test.ts | 15 + __tests__/shared/turns.test.ts | 10 +- mocks/direction-desktop.html | 1108 ++++++++++++++++ mocks/direction.html | 1123 +++++++++++++++++ src/client/components/AlibiChart.tsx | 83 +- src/client/components/ChatLog.stories.tsx | 119 ++ src/client/components/ChatLog.tsx | 70 +- src/client/components/Motions.stories.tsx | 347 +++++ src/client/hooks/usePacedReveal.ts | 62 + src/client/index.css | 213 +++- src/client/lib/paragraphs.ts | 25 + src/client/lib/settings-store.ts | 9 +- .../screens/InterrogationScreen.stories.tsx | 54 + src/client/screens/InterrogationScreen.tsx | 74 +- src/server/env.ts | 4 +- src/server/read/scenarios.ts | 36 +- src/shared/turns.ts | 14 +- wrangler.jsonc | 4 +- 20 files changed, 3348 insertions(+), 99 deletions(-) create mode 100644 mocks/direction-desktop.html create mode 100644 mocks/direction.html create mode 100644 src/client/components/ChatLog.stories.tsx create mode 100644 src/client/components/Motions.stories.tsx create mode 100644 src/client/hooks/usePacedReveal.ts diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index 2c64fd5..4dd7fbc 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -43,6 +43,18 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # public/se/ は規約上リポジトリに置けないので、checkout 直後は空。 + # ここで取らないと音源の無いアセットがそのまま公開される。 + # キャッシュが当たれば fetch-se は既存ファイルを skip し、相手のサイトは叩かない。 + - name: Cache sound effects + uses: actions/cache@v4 + with: + path: public/se + key: se-${{ hashFiles('scripts/fetch-se.ts') }} + + - name: Fetch sound effects + run: bun run se:fetch + - name: Build run: bun run build diff --git a/__tests__/client/chat-log.test.ts b/__tests__/client/chat-log.test.ts index dcfb470..ad5e009 100644 --- a/__tests__/client/chat-log.test.ts +++ b/__tests__/client/chat-log.test.ts @@ -15,53 +15,82 @@ const textsOf = (item: ReturnType[number]): string[] => item.kind === 'topic' ? [item.text] : item.lines.map((line) => line.text) describe('groupTurns', () => { - test('空行で区切られた返答を行に割る', () => { - const items = groupTurns([ - turn('a1', 'assistant', 'えっ、Yさんですか?\n\nそうですね。いい人でしたよ。'), - ]) + test('返答を一文ずつに割る', () => { + const items = groupTurns( + [turn('a1', 'assistant', 'えっ、Yさんですか?\n\nそうですね。いい人でしたよ。')], + false, + ) expect(items).toHaveLength(1) - expect(textsOf(items[0])).toEqual(['えっ、Yさんですか?', 'そうですね。いい人でしたよ。']) + expect(textsOf(items[0])).toEqual(['えっ、Yさんですか?', 'そうですね。', 'いい人でしたよ。']) }) test('改行が続いても区切りは1つと数える', () => { - const items = groupTurns([turn('a1', 'assistant', '前半\n\n\n\n後半')]) + const items = groupTurns([turn('a1', 'assistant', '前半\n\n\n\n後半')], false) expect(textsOf(items[0])).toEqual(['前半', '後半']) }) test('割った行にはそれぞれ別の鍵が付く', () => { - const items = groupTurns([turn('a1', 'assistant', '一つ目\n\n二つ目')]) + const items = groupTurns([turn('a1', 'assistant', '一つ目\n\n二つ目')], false) const item = items[0] expect(item.kind === 'block' ? item.lines.map((line) => line.id) : []).toEqual(['a1:0', 'a1:1']) }) test('続けて喋った分は塊にまとまり、行が並ぶ', () => { - const items = groupTurns([ - turn('a1', 'assistant', '一つ目\n\n二つ目'), - turn('a2', 'assistant', '三つ目'), - ]) + const items = groupTurns( + [turn('a1', 'assistant', '一つ目\n\n二つ目'), turn('a2', 'assistant', '三つ目')], + false, + ) expect(items).toHaveLength(1) expect(textsOf(items[0])).toEqual(['一つ目', '二つ目', '三つ目']) }) test('話題を挟むと塊が分かれる', () => { - const items = groupTurns([ - turn('a1', 'assistant', '前の返答'), - turn('t1', 'topic', '次の話題'), - turn('u1', 'user', '次の質問'), - ]) + const items = groupTurns( + [ + turn('a1', 'assistant', '前の返答'), + turn('t1', 'topic', '次の話題'), + turn('u1', 'user', '次の質問'), + ], + false, + ) expect(items.map((item) => item.kind)).toEqual(['block', 'topic', 'block']) }) + test('流れている最中は、書きかけの一文を出さない', () => { + const items = groupTurns([turn('a1', 'assistant', 'そうですね。いい人でし')], true) + + expect(textsOf(items[0])).toEqual(['そうですね。']) + }) + + test('流れ終われば、句点で終わらない一文も出す', () => { + const items = groupTurns([turn('a1', 'assistant', 'そうですね。……雨でしたから')], false) + + expect(textsOf(items[0])).toEqual(['そうですね。', '……雨でしたから']) + }) + + test('末尾より前の返答は、流れている最中でも全部出す', () => { + const items = groupTurns( + [turn('a1', 'assistant', '前の返答は書き終わっている'), turn('a2', 'assistant', '書きかけ')], + true, + ) + + expect(textsOf(items[0])).toEqual(['前の返答は書き終わっている']) + }) + test('返答待ちの空のターンは置かない', () => { - expect(groupTurns([turn('a1', 'assistant', '')])).toEqual([]) + expect(groupTurns([turn('a1', 'assistant', '')], true)).toEqual([]) }) test('最初に届いたのが改行だけでも、まだ置かない', () => { - expect(groupTurns([turn('a1', 'assistant', '\n\n')])).toEqual([]) + expect(groupTurns([turn('a1', 'assistant', '\n\n')], true)).toEqual([]) + }) + + test('一文目が出来上がるまでは、名前も出さない', () => { + expect(groupTurns([turn('a1', 'assistant', 'そうですね')], true)).toEqual([]) }) }) diff --git a/__tests__/server/scenario-public-introduction.test.ts b/__tests__/server/scenario-public-introduction.test.ts index 5d29a8b..75d7c80 100644 --- a/__tests__/server/scenario-public-introduction.test.ts +++ b/__tests__/server/scenario-public-introduction.test.ts @@ -13,3 +13,18 @@ describe('normalizePublicIntroduction', () => { expect(normalizePublicIntroduction(' ')).toBe('この事件の関係者。') }) }) + +import { sortCharactersById } from '@/server/read/scenarios' + +test('sortCharactersById > UUID順で決定的に並べ、元配列は変更しない', () => { + const input = [ + { id: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', name: 'C', publicIntroduction: 'C' }, + { id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', name: 'A', publicIntroduction: 'A' }, + { id: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', name: 'B', publicIntroduction: 'B' }, + ] + + const sorted = sortCharactersById(input) + + expect(sorted.map((character) => character.name)).toEqual(['A', 'B', 'C']) + expect(input.map((character) => character.name)).toEqual(['C', 'A', 'B']) +}) diff --git a/__tests__/shared/turns.test.ts b/__tests__/shared/turns.test.ts index aa12bad..f429ec9 100644 --- a/__tests__/shared/turns.test.ts +++ b/__tests__/shared/turns.test.ts @@ -135,11 +135,11 @@ describe('clampLimits', () => { }) /* - ターン数と1ターンの質問数を両方上限まで上げられると 30 問になる。 - 10分で遊ぶゲームの形が変わるので、積にも天井を置く。 + ターン数と1ターンの質問数を両方上限まで上げられると 45 問になる。 + 3〜5人の事件で全員に片端から聞けてしまうので、積にも天井を置く。 */ test('質問の総数が上限を超えない', () => { - const limits = clampLimits({ maxTurns: 10, questionsPerTurn: 3 }, fallback) + const limits = clampLimits({ maxTurns: 15, questionsPerTurn: 3 }, fallback) expect(limits.maxTurns * limits.questionsPerTurn).toBeLessThanOrEqual( LIMIT_CEILINGS.totalQuestions, @@ -147,9 +147,9 @@ describe('clampLimits', () => { }) test('積の天井に当たったらターン数ではなく1ターンの質問数を削る', () => { - const limits = clampLimits({ maxTurns: 10, questionsPerTurn: 3 }, fallback) + const limits = clampLimits({ maxTurns: 15, questionsPerTurn: 3 }, fallback) - expect(limits.maxTurns).toBe(10) + expect(limits.maxTurns).toBe(15) expect(limits.questionsPerTurn).toBe(2) }) diff --git a/mocks/direction-desktop.html b/mocks/direction-desktop.html new file mode 100644 index 0000000..237455e --- /dev/null +++ b/mocks/direction-desktop.html @@ -0,0 +1,1108 @@ + + +AlibAI デスクトップ + + + +
+ +
+
Desktop · 1280×780
+

机の上に、表と
聞き込みを同時に置く

+

+ デスクトップで増えるのは面積ではなく、同時に見えることです。 + スマホ版は一度にひとつしか持てないので、相手を替えるたび概要へ戻り、時刻軸は細い帯でした。 + 画面が広がったぶんを余白と装飾に配るのではなく、時刻軸を実寸のアリバイ表に開いて左半分に据え置き、 + 右半分だけを仕事に応じて差し替えます。表は聞くほど埋まり、告発のときには拡大する必要がありません—— + 最初から実寸で出ているので。 +

+
+ +
+
骨格
+

左は据え置き、右だけが替わる

+

+ 概要・聞き込み・告発・結果は同じ一枚の机です。画面が変わるのではなく、机の右半分に載るものが変わります。 + 場所が動かないので、聞き込みの途中で表を見ても、告発に進んでも、目は同じ場所に戻れます。 + 設定だけはこの机に載りません(ターン数は回数であって時刻ではないため)。 +

+
+
+
+
+
+
+
+
+
+
概要空の表と、誰から聞くか
+
+
+
+
+
+
+ + + +
+
+
+
+
聞き込み埋まる表と、ひとりとの会話
+
+
+
+
+
+
+ + + +
+
+
+
+
告発埋まりきった表と、指し示す欄
+
+
+
+
+
+
+ + + + + + +
+
+
+
+
結果申告と実際を並べた表と、判定
+
+
+
+ +
+
画面
+

七つの画面

+

+ 色・書体・「箱を作らず罫線で分ける」は既存のまま。ここで決めるのは広い画面での置き方だけです。 +

+ + +
+
+ 一 事件を選ぶ + 縦積みをやめ、種別を左の余白に垂らして題字の列を一本に通します。同じ種別が続くあいだは繰り返さないので、 + 余白のラベルがそのまま群の見出しになります。時間帯・人数・難度・所要は右で桁を揃え、縦に読み比べられるようにしました—— + これは行の中に畳んでいたスマホ版ではできなかったことです。 +
+
+
+
+
+

AlibAI

+
聞き込みで、犯人を指し示す
+
+ 設定 +
+
+ 種別事件時間帯 + 人数難度所要 + + 殺人 + 雨の古書店、十九時八分のレシート + 18:20–19:203人★★約10分 + + + 崖崩れの時計博物館、十一分の狂い + 20:40–21:304人★★★約15分 + + + 暴風の水族館、午前零時の給餌灯 + 23:30–00:204人★★★約15分 + + + 山荘の停電、三十七分の空白 + 21:05–22:105人★★★★約20分 + + 盗難 + 最終電車と遅延証明書 + 23:10–00:053人★★約10分 + + + 貸金庫、閉館後の二十分 + 17:40–18:304人★★★約15分 + + 失踪 + 霧の連絡船、乗客名簿の一人 + 05:40–07:104人★★★約15分 + + + 冬の温室、鍵のかかった扉の内側 + 14:20–15:403人★★約10分 + + 放火 + 乾いた倉庫、消えた見張り番 + 02:10–03:254人★★★★約20分 +
+
+
+
+ + +
+
+ 二 事件の記録 + せり上がる演出はそのまま。広い画面でも本文は640pxで止めます——行が長くなるほど読み返しづらく、 + ここは唯一の読み物なので。左右の余白は埋めません。暗いまま置いておくのが、この画面の仕事です。 +
+
+
+ 記録 0001 +
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。

+

事件に関わるのは三人です。店員の牧野千尋、常連の収集家・黒田征司、向かいの喫茶店主・瀬名真琴。

+
+
+
+
+ +
+
+
+ + +
+
+ 三 概要 + 机がここで組み上がります。左はまだ一本も引かれていないアリバイ表——これから埋める空欄を先に見せておくと、 + 供述から時刻が立つ意味が最初の一問から分かります。右は配役と手がかりの見え方、そして最初の相手。 + 被害者にも列を与えます。刻限(死亡推定)だけは最初から引かれていて、埋めるべき範囲を示します。 +
+
+
+
+ ← 事件を選ぶ +
全12ターン約10分
+
+
+
+
+ アリバイ表 + 18:20 – 19:20 +
+
+ + 牧野千尋店員 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + + + 死亡推定 18:50 +
+
+ 実線 裏付けあり + 破線 本人の申告のみ +
+
+ +
+

雨の古書店、十九時八分のレシート

+

閉店後の一時間。この六十分を、誰かひとりだけが説明しきれずにいます。

+ +
+ まず誰から話を聞くか +
+
+ + 牧野千尋店員。生真面目で、話しはじめると止まらない +
+
+ + 黒田征司常連の収集家。初版本の商談に来ていた +
+
+ + 瀬名真琴向かいの喫茶店主。雨脚を見ていた +
+
+ + 水野英治青雨堂店主 + 被害者 +
+
+
+ +
+ 手がかりの見え方 +
+ EasyNormalHardNo Hope +
+

場所と人物、それぞれの合計だけが見えます。

+
+ +
牧野千尋に聞き込みをする
+
事件の記録をもう一度読む
+
+
+
+
+
+ + +
+
+ 四 聞き込み + この案の主題。供述で確定した時刻と、表に立つ目盛りは同じひとつのもので、片方に触れるともう片方が起きます + (図では 午後七時八分 と郵便窓口の目盛りが対になっています)。 + スマホでは同時に見えないので作れなかった対応づけです。相手を替えるのに概要へ戻る必要もありません—— + 表の列見出しがそのまま切り替えで、いま聞いている人の列だけが起きています。 + ひとりが続けて喋るあいだ名前は一度きり、左の縦罫だけが伸びるのは既存のまま。 +
+
+
+
+ 雨の古書店、十九時八分のレシート +
4 / 12 ターン08:41告発する
+
+
+
+
+ アリバイ表 + 18:20 – 19:20 +
+
+ + 牧野千尋聞き込み中 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + 店内 + 郵便局へ、雨のなかを + 郵便窓口19:08 受付 + + + + 店内18:23 来店 + 帰宅したと申告 + + + + 向かいの喫茶店 + 青雨堂19:12 通報 + + + + 店の奥 + + + 死亡推定 18:50 + + + 食い違い + +
+
+ 実線 裏付けあり + 破線 本人の申告のみ +
+
+ +
+
+
牧野千尋
+
店員。生真面目で、話しはじめると止まらない
+
+ +
+
+ 探偵 +

閉店したあと、店に残っていたのは誰ですか。

+
+
+ 牧野千尋 +

わたしと、店長と、黒田さんです。黒田さんは初版本の話で六時二十三分ごろに見えました。

+

わたしは奥の帳場にいましたから、そのあたりはよく覚えています。

+
+
+ 探偵 +

店を出たあと、まっすぐ郵便局へ向かったんですね。

+
+
+ 牧野千尋 +

はい。発送があったので、午後六時三十六分には店を出ています。

+

窓口の受付は午後七時八分でした。レシートも残っています。

+

……三十分以上かかる道のりでしたけど。雨でしたから。

+
+
+ +
+
訊けそうなこと
+ レシートを見せてもらえますか + 瀬名さんは、あなたが出ていくのを見ていないそうです +
+ +
+ 何について訊く? + 訊く +
+
+
+
+
+
+ + +
+
+ 五 告発 + 机はそのまま、右半分だけが告発の欄に替わります。表を拡大しません——スマホ版が拡大していたのは + 帯だったからで、こちらは最初から実寸です。表は答え欄ではなく考えるための場所なので、名指しするのは + 誰が・どうやって・なぜの三つだけ。朱が出るのはこの画面だけです。 +
+
+
+
+ ← 聞き込みに戻る +
11 / 12 ターン21:04
+
+
+
+
+ アリバイ表 + 18:20 – 19:20 +
+
+ + 牧野千尋店員 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + 店内 + 郵便局へ + 郵便窓口19:08 受付 + + + + 店内 + 裏の路地18:41 忘れ傘 + 帰宅したと申告 + + + + 向かいの喫茶店 + 青雨堂の軒先18:39 雨宿り + 喫茶店に戻る + 青雨堂19:12 通報 + + + + 店の奥 + + + 死亡推定 18:50 +
+
+ 実線 裏付けあり + 破線 本人の申告のみ +
+
+ +
+

犯人を指し示す

+

誰が、どうやって、なぜ。提出すると取り消せません。

+ + 犯人 +
+
牧野千尋
+
黒田征司
+
瀬名真琴
+
+ +
+ 殺害方法 +
どうやって殺したのか
+
+ +
+ 動機 +
なぜ殺したのか
+
+ +
この推理を提出する
+
+
+
+
+
+ + +
+
+ 六 結果 + 表の中身をここで一度だけ入れ替え、「青雨堂にいた時間」だけを、申告(破線)と実際(実線)の二本で並べます。 + 瀬名と黒田はほぼ重なり、牧野の実線だけが申告より下へ伸びて、死亡推定の線に届いています—— + 彼女が出たと言った 18:36 のあと、まだ店にいた。読まなくても分かるのがこの一枚の仕事です。 + 真相の文は表に載せません。列は 108px しかなく、一文を入れれば隣へはみ出してどちらの列の話かが消えるので、 + 言葉は右の一覧に置きます。外れに赤は使いません(責められている画面になるので)。白緑は当たったところにだけ。 +
+
+
+
+ 雨の古書店、十九時八分のレシート +
解決08:41
+
+
+
+
+ 青雨堂にいた時間 + 18:20 – 19:20 +
+
+ + 牧野千尋犯人 + 黒田征司収集家 + 瀬名真琴喫茶店主 + 水野英治被害者 +
+
+ + + + + + + + + 18:20 + 18:30 + 18:40 + 18:50 + 19:00 + 19:10 + 19:20 + + + + + + 申告より
14分ぶん長い
+
+ + + + + + + + + + + + + + + + + + 死亡 18:50 +
+
+ 破線 申告 + 実線 実際 +
+
+ +
+

事件解決

+

牧野千尋を送検しました。

+ +
+ 判定 +
+
犯人牧野千尋 正解
+
殺害方法正解
+
動機惜しい
+
+
+ +
+ 記録 +
+
解決タイム08:41
+
質問回数9回
+
発見した証拠6個
+
+
+ +
+ 真相 +
+
18:28黒田が水野に、記録に残さない現金取引を持ちかける
+
18:37水野がすり替えに気づき、牧野を問い詰める
+
18:50牧野が店の奥で水野を襲う
+
19:08牧野が郵便窓口で小包を発送する
+
+
+ +
+ この事件をもう一度 + 次の事件へ +
+
+
+
+
+
+ + +
+
+ 七 設定 + 物語の外にある唯一の画面。机には載せず、表も引きません——ターン数や往復は回数であって盤面の時刻ではなく、 + 同じ表で描けば軸は「なんとなく量を表す装置」に落ちます。数字を等幅にしないのも同じ理由。 + 広い画面でも一列に保ち、ラベルと操作を左右で組みます。提供元を選ぶまでモデルは触れません。 +
+
+
+
+ ← 事件を選ぶ +

設定

+

この端末にだけ保存されます。サーバには残らず、あなたのプレイにだけ効きます。

+ +
+ 使うモデル +
+
+
会話
+
NPCの受け答えと、探偵が組み立てる質問
+
+
+
提供元Anthropic
+
モデルClaude Sonnet 5
+
+
+
+
+
判定
+
証拠の開示と、推理の採点
+
+
+
提供元既定のまま
+
モデル既定のまま
+
+
+

Google は APIキーが未設定のため選べません。

+
+ +
+ 進行 +

新しく始める事件から効きます。進行中のものは変わりません。

+
+
ターン数5
+
1ターンの質問1
+
1話題の往復3
+
+

+ 5ターン × 1問 = 全部で5問(上限20問)。
+ 1話題ごとに、モデルを7回呼びます。 +

+
+
+
+
+
+ +
+ +
+
決めごと
+

この案が守る五つ

+
+
+ 一 表は据え置く + 概要から結果まで、アリバイ表は同じ位置・同じ縮尺にいます。拡大も移動もしません。 + 目が戻る場所が動かないことが、広い画面でいちばん効きます。 +
+
+ 二 供述と目盛りは同じもの + 確定した時刻は、会話のなかの語と表の目盛りがひとつの対象です。片方に触れればもう片方が起きます。 + 別々に描くと、表は会話の要約になってしまい、確かめる道具でなくなります。 +
+
+ 三 等幅は盤面の時刻だけ + ターン数・質問回数・証拠の個数には使いません。等幅で書かれていれば作中の時刻という規則を、 + 七画面すべてで守ります。 +
+
+ 四 朱は告発だけ + 取り消せない一手にしか出しません。結果画面で外れに赤を使わないのも同じ理由で、 + 当たったところにだけ白緑を置きます。 +
+
+ 五 箱を作らない + 角丸+枠+塗りの箱は増やさず、区切るのは罫線と表の升目だけ。 + 列を起こすときも枠で囲まず、地をわずかに沈めるか、罫線と字を起こすだけにします。 +
+
+
+ +
+
幅が足りないとき
+

1120px を切ったら、机を畳む

+

+ 表と会話を並べるには左524px+会話の適正な行長が要ります。これを割ったら二段組みをやめ、 + 表は上端の帯へ畳んで既存のスマホ版の作りに戻します。中途半端に縮めた表は、升目が潰れて + 「なんとなく色が付いた領域」になり、いちばん読ませたいものがいちばん先に読めなくなるので。 +

+
+ +
diff --git a/mocks/direction.html b/mocks/direction.html new file mode 100644 index 0000000..a12e7d2 --- /dev/null +++ b/mocks/direction.html @@ -0,0 +1,1123 @@ + + +AlibAI 時刻軸 + + + +
+ +
+
AlibAI / 画面デザイン案
+

時刻軸

+

+ この作品のシナリオは、ひとつ残らず時刻と物で名付けられています。「雨の古書店、十九時八分のレシート」。 + 事実データも 18:28 から 19:15 まで、分単位で刻まれている。 + それなのに今の画面で時刻が現れるのは、見出しの隅にある経過時間だけです。 + ——なら、時刻軸そのものを画面の背骨に据えます。 +

+
+
+ + + + + + +
+
+ 18:20 + 19:20 +
+
+
+ +
+
+

灯を落とした部屋の、墨と顔料

+

+ 地は青みの灰(slate)からへ寄せます。同じ暗さでも、青い暗がりは「ダークモード」に、 + 温かい暗がりは「インク」に見えます。そのうえで——強調色を置きません。 + 容疑者はそれぞれ自分の顔料を持ち、明度も彩度も揃えてあります。誰か一人だけが目立つことは、この画面では起こらない。 +

+
+
#141317
地。白が挟まる瞬間を作らない
+
生成り
#E9E4D9
本文。紙のほうの白
+
#8E8896
副次。説明・補足
+
錆浅葱
#5E8B8C
牧野千尋
+
藤鼠
#8079A6
黒田征司
+
蘇芳
#96556A
瀬名真琴
+
芥子
#B49A55
被害者・水野英治
+
#D2452E
告発だけ。全画面で一度きり
+
+
+ +
+
書体
+

明朝で語り、ゴシックで喋り、等幅で刻む

+

+ 役割を三つに割ります。地の文(事件の記録・題字)は明朝——本格ミステリの文庫はゴシックで組みません。 + 会話とUIはゴシック。そして時刻だけが等幅。 + 「等幅で書かれていたら、それは時刻」という規則をアプリ全体で守ります。ターン数や件数には使いません。 +

+
+
+
DISPLAY
+
雨の古書店、十九時八分のレシートShippori Mincho B1 700 / 事件名・題字・語りの本文
+
+
+
BODY
+
閉店後に店の奥へは入っていません。雨が強くて、軒下で少し待っていただけです。Zen Kaku Gothic New 400/500 / 供述・操作・ラベル
+
+
+
DATA
+
18:28 → 18:47 → 19:08Roboto Mono 400 tabular-nums / 時刻のみ
+
+
+
+ +
+
画面
+

七つの画面と、六つを貫く一本

+

+ 時刻軸は同じ一本が縮尺だけ変えて現れます。聞き込みでは細い帯、告発では画面いっぱい、結果では答え合わせの物差し。 + 最後の一枚——設定だけは貫きません。ターン数や往復は回数であって時刻ではないので、 + 同じ帯を流用すると軸の意味がその場で薄まります。 + 枠で囲った箱は作りません(既存方針どおり)。区切るのは罫線だけです。 +

+ +
+ + +
+
+ 一 事件を選ぶ + 種別・題字・簡易情報を縦に積み、行そのものが押す場所。題字が全幅を使えるので、ほとんどが一行で収まります。 + 種別(要 meta.caseType 新設)は殺人を避けたい人のための軸で、種別順に並べ、同じ値が続くあいだは繰り返しません。 + 舞台ジャンルは実データの 34 本中 24 本が同じ値なので落とし、代わりに事件の時間帯を等幅で置いています。 +
+
+
+

AlibAI

+
聞き込みで、犯人を指し示す
+
種別順 34件
+
+
+ 殺人 + 雨の古書店、十九時八分のレシート + 18:20–19:20 3人 ★★ 約10分 +
+
+ 崖崩れの時計博物館、十一分の狂い + 20:40–21:30 4人 ★★★ 約15分 +
+
+ 暴風の水族館、午前零時の給餌灯 + 23:30–00:20 4人 ★★★ 約15分 +
+
+ 盗難 + 最終電車と遅延証明書 + 23:10–00:05 3人 ★★ 約10分 +
+
+ 失踪 + 霧の連絡船、乗客名簿の一人 + 05:40–07:10 4人 ★★★ 約15分 +
+
+
+
+
+ + +
+
+ 二 事件の記録 + 既存のせり上がる演出はそのまま。組み方を明朝・行間2.5にして、読み物として読ませます。 +
+
+
+
記録 0001
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。

+

事件に関わるのは三人です。店員の牧野千尋、常連の収集家・黒田征司、向かいの喫茶店主・瀬名真琴。

+
+
+
+
+
+ + +
+
+ 三 概要 + 記録を読み終えて、聞き込みに入る前の一拍。まだ一本も刺さっていない時刻軸をここで見せます。 + 埋まっていく先を先に見せておくと、供述から時刻が立つ意味が最初から分かる。ここで最初の相手も選びます。 +
+
+
+

雨の古書店、
十九時八分のレシート

+ +
+
+ 18:20 + 19:20 + + + +
+
この一時間を、説明しきる
+
+ +
+
まず誰から話を聞くか
+
+
+ + 牧野千尋店員。生真面目で、話しはじめると止まらない +
+
+ + 黒田征司常連の収集家。初版本の商談に来ていた +
+
+ + 瀬名真琴向かいの喫茶店主。雨脚を見ていた +
+
+ + 水野英治青雨堂店主 + 被害者 +
+
+
+ +
+
手がかりの見え方
+
+ EasyNormalHardNo Hope +
+
場所と人物、それぞれの合計だけが見える
+
+ +
牧野千尋に聞き込みをする
+
事件の記録をもう一度読む
+
+
+
+ + +
+
+ 四 聞き込み + 人物列も切り替えも置かず、幅も高さも本文に返しました。ひとりとのやり取りだけが載る画面です。 + 相手を替えるときは概要に戻ります。埋まっていく時刻軸を挟んでから次を選ぶ、その間こそ考えどころなので。 + 供述から時刻が確定するたび、その人の顔料で目盛りが立ちます。淡い目盛りは未確定。 + 発言は交互とは限りません。ひとりが続けて喋るあいだは名前を一度しか出さず、左の縦罫だけが最後まで伸びます。 + 訊けそうなことは常設せず、ログの末尾に畳んでおきます。選べばそのまま質問が飛びます。 +
+
+
+
+ ← 概要に戻る + 4/12 08:41 +
+
牧野千尋
+
生真面目で、話しはじめると止まらない
+
+ +
+ 18:2019:20 + + + + + + + +
+ +
+
+ 探偵 +

郵便局に行った時刻を、もう一度

+
+
+ 牧野千尋 +

午後六時三十五分には店を出ています。発送があったので。

+

窓口の受付は午後七時八分でした。レシートも残っています。

+

……三十分以上かかる道のりでしたけど。

+

雨でしたから。

+
+ +
+ 訊けそうなこと + レシートを見せてもらえますか + 黒田さんとは話しましたか +
+
+ +
+
+ 何について訊く? + 訊く +
+
+
+
+ + +
+
+ 五 告発 + 同じ時刻軸を全幅まで拡大し、三人の在店を帯で重ねます。ただし時刻は答え欄ではありません。 + 考えるための場所として置き、名指しするのは誰が・どうやって・なぜの三つだけ。朱が出るのはここだけ。 +
+
+
+
← 概要に戻る
+

犯人を指し示す

+

誰が、どうやって、なぜ。

+ +
+
+ 牧野 + + +
+
+ 黒田 + + +
+
+ 瀬名 + + +
+
+ 18:2018:5019:20 +
+
+ +
犯人
+
+
牧野千尋
+
黒田征司
+
瀬名真琴
+
+ +
+ 殺害方法 +
どうやって殺したのか
+
+ +
+ 動機 +
なぜ殺したのか
+
+ +
この推理を提出する
+
+
+
+ + +
+
+ 六 結果 + 当たっていたか(判定)と、どう辿り着いたか(記録)を分けます。混ぜると、評価が推理の甘さのせいか + 回り道のせいか分からなくなるので。真相は時刻付きで並べ、外れに赤は使いません。 +
+
+
+
事件解決
+ +
判定
+
+
犯人牧野千尋 正解
+
殺害方法正解
+
動機惜しい
+
+ +
記録
+
+
解決タイム08:41
+
質問回数9回
+
発見した証拠6個
+
+ +
真相
+
+
18:28黒田が水野に、記録に残さない現金取引を持ちかける
+
18:37水野がすり替えに気づき、牧野を問い詰める
+
18:50牧野が店の奥で水野を襲う
+
19:08牧野が郵便窓口で小包を発送する
+
+
+
+
+ + +
+
+ 七 設定 + 物語の外にある唯一の画面なので、ここだけは何も演出しません。時刻軸も引きません—— + ターン数や往復は回数であって盤面の時刻ではなく、同じ帯で描けば軸は「なんとなく量を表す装置」に落ちます。 + 数字を等幅にしないのも同じ理由(決めごと三)。 + 提供元を選ぶまでモデルは触れず、枠を地に沈めて押せないことを示します。既定のままなら送信にも載らないので、 + サーバは配備時の設定のまま動きます。 + 三つの数字は掛け算で効くので、積とモデルの呼び出し回数を文で返します。ここを出さないと、 + 3 と入れた値が黙って 2 に削られる理由(clampLimits)が誰にも分かりません。 +
+
+
+
+
← 事件を選ぶ
+

設定

+

この端末にだけ保存されます。サーバには残らず、あなたのプレイにだけ効きます。

+
+ +
+
使うモデル
+ +
+
+
会話
+
NPCの受け答えと、探偵が組み立てる質問
+
+
+
+ 提供元 + Anthropic +
+
+ モデル + Claude Sonnet 5 +
+
+
+ +
+
+
判定
+
証拠の開示と、推理の採点
+
+
+
+ 提供元 + 既定のまま +
+
+ モデル + 既定のまま +
+
+
+ +

Google は APIキーが未設定のため選べません。

+
+ +
+
進行
+

新しく始める事件から効きます。進行中のものは変わりません。

+ +
+
+ ターン数 + 5 +
+
+ 1ターンの質問 + 1 +
+
+ 1話題の往復 + 3 +
+
+ +

+ 5ターン × 1問 = 全部で5問(上限20問)。
+ 1話題ごとに、モデルを7回呼びます。 +

+
+
+
+
+ +
+
+ +
+
動き
+

十の動きと、その役目

+

+ 動きは状態が変わったことを言うためにだけ使います。雰囲気のために足さない。 + 合図の類は 0.4 秒前後で終わり、長く動くのは読ませるもの(記録のせり上げ・タイプ送り)だけです。 + 動きが苦手な人には出さず、そのとき見えるのは動いた後の姿——切っても中身が消えないように、要素は完成形で書いてあります。 + 各枠の「もう一度」で繰り返せます。 +

+
+ +
+
一 目盛りが立つ
+
+
+ + + + + 19:08 +
+
+

供述から時刻が確定した瞬間。軸の下から伸び上がり、遅れて時刻が出ます。この作品でいちばん大事な出来事なので、強めに動くのはここだけ。

+
+ +
+
二 疑問
+
+
+ + + + +
+
+

辻褄の合わない供述。目盛りは立とうとして、立ちきらずに淡いまま残ります。question mark は出しますが、色は容疑者の顔料のまま——画面が先に誰かを疑わないために。

+
+ +
+
三 ひらめき
+
+
+ + + + +
+
+

離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。

+
+ +
+
四 新事実発見
+
+
+ 窓口の受付は午後七時八分でした。レシートも残っています。 + + 新事実 + 牧野は午後六時三十五分に店を出たと述べた + +
+
+

記録は聞き込み中に画面上にないので、増えたことを帯で被せて知らせます。箱にはせず、二本の罫線と薄い覆いだけ。 + 操作は塞がず 2.6 秒で引きます。祝いはしません——増えたのは事実であって手柄ではないので。

+
+ +
+
五 発話が続く
+
+
+ 牧野千尋 + 午後六時三十五分には店を出ています。 + 窓口の受付は午後七時八分でした。 + ……雨でしたから。 +
+
+

ひとりが続けて喋るとき、一行ずつ置いていきます。間は 0.5 秒。読み終わる前に次が来ると急かされるので、詰めすぎない。

+
+ +
+
六 タイプ送り
+
+

+
+

一字ずつ送る読み上げ。65 ミリ秒/字は、既にアプリで動いている TypewriterBriefing と同じ値です。

+
+ +
+
七 記録のせり上げ
+
+
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。

+
+ +
+
+

記録を下から流します。既存の briefing-crawl と同じ作りで、移動距離は本文の高さから決まるので、長い記録ほど自然に長く流れます。下端は地に溶かして、切れ目を作らない。

+
+ +
+
八 入り込む
+
+
+
+ 雨の古書店、十九時八分のレシート + + + + + + 牧野千尋 +
+ +
+
+

記録を読み終えて、その場に入る敷居。墨の中から浮かび上がり、寄りが戻って焦点が合います。 + 0.76 秒。強く動かすのはこの一度だけで、他の九つは合図に徹します。既存の screen-enter は + 1.03 倍で体感が無いので、ここに寄せた分だけ他から引きました。

+
+ +
+
九 選択肢が開く
+
+
+ 訊けそうなこと + + レシートを見せてもらえますか + 黒田さんとは話しましたか + +
+
+

畳んであるものが開くので、高さそのものを動かします。中身を透かせるだけだと、下の入力欄が飛んで見えるため。

+
+ +
+
十 判定が出る
+
+
+ 犯人牧野千尋 正解 + 殺害方法正解 + 動機惜しい +
+
+

上から順に 0.2 秒ずつ。一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になります。

+
+ +
+
+ +
+
決めごと
+

この案が守る四つ

+
+
+ +
+

強調色を持たない

+

今の琥珀は「未質問」「残り件数」「捜査メモ」「推理ボタン」「提出ボタン」の五つを同時に指していて、結果として何も指していません。 + 代わりに容疑者へ顔料を配り、明度を揃える。画面があなたより先に誰かを怪しむことがなくなります

+
+
+
+ +
+

朱は、告発の一度だけ

+

全画面を通して #D2452E が現れるのは、犯人を指す操作と、そこへ向かう入口だけ。 + 取り消せない一手だからこそ、他のどこにも同じ色を置きません。結果の外れにも使いません

+
+
+
+ +
+

等幅なら、それは盤面の時刻

+

ターン数・件数・正誤は等幅にしません。数字の形そのものが「これは時刻だ」と言う状態を作ります。 + ただし会話の中の時刻は地の文のまま。喋っている途中で書体が変わると、読みがそこで切れるので。 + 等幅は時刻軸・時間帯・解決タイムのような、システムが刻んだ時刻に限ります。

+
+
+
+ +
+

箱は増やさない

+

既存方針どおり、角丸+枠+塗りの箱は作らず罫線で区切ります。時刻軸も箱ではなく、 + 二本の罫線のあいだの帯として置く。新しく足すのは色と縦線一本だけです。

+
+
+
+
+ +
+ + diff --git a/src/client/components/AlibiChart.tsx b/src/client/components/AlibiChart.tsx index 1bf62f2..6463f43 100644 --- a/src/client/components/AlibiChart.tsx +++ b/src/client/components/AlibiChart.tsx @@ -72,6 +72,13 @@ const format = (minutes: number): string => const PX_PER_MIN = 10 const GUTTER = 46 +/** + * 噛み合わない区間の線を引き終わる時刻(`draw` の 180ms 遅れ+520ms)。 + * 両端の丸と札はこれを待ってから置く。線と同時に出すと、 + * 線が伸びきる前に端の印だけが宙に浮く。 + */ +const CLASH_SETTLED_MS = '700ms' + /** * アリバイ表。 * @@ -214,24 +221,47 @@ export const AlibiChart = ({ const top = (toMinutes(s.from) - from) * PX_PER_MIN const h = (toMinutes(s.to) - toMinutes(s.from)) * PX_PER_MIN const solid = s.kind === 'solid' - // 突き合わせのときは、聞き取った線を細い破線にして実際の線の左へ寄せる。 - const claimClass = - truth === undefined - ? 'absolute left-[18px] w-px border-l border-dashed border-l-current opacity-55' - : 'absolute left-[13px] w-px border-l border-dashed border-l-current opacity-45' + // 裏付けの取れた線として描くのは、突き合わせに入っていないときだけ。 + // 結末の突き合わせでは、聞き取った線は実際の線の左へ細い破線で寄せる。 + const heard = !solid || truth !== undefined + + /* + * 枠と帯を分ける。帯は現れるときに上端から伸びるので(pin-rise / waver)、 + * 在所と時刻を同じ要素に入れると、伸びるあいだ字が縦に潰れる。 + * 枠は動かさず位置と淡さだけを持ち、動くのは中の帯だけにする。 + */ + const frameClass = heard + ? truth === undefined + ? 'absolute left-[18px] w-px opacity-55' + : 'absolute left-[13px] w-px opacity-45' + : // 会話で指している一本だけ、線も目盛りも太らせる。 + s.fix !== undefined && s.fix === litFix + ? 'absolute left-[16px] w-[5px]' + : 'absolute left-[17px] w-[3px]' + + const barClass = heard + ? 'border-l border-dashed border-l-current' + : s.fix !== undefined && s.fix === litFix + ? `${HUE[p.hue].bar} before:absolute before:top-0 before:-left-[5px] before:h-[2px] before:w-[15px] before:bg-current before:content-['']` + : `${HUE[p.hue].bar} before:absolute before:top-0 before:-left-[4px] before:h-px before:w-[11px] before:bg-current before:content-['']` + return ( + {/* + 増えた線だけが一度だけ動く。key は who と from で決まるので、 + 既に立っている線は積み直しても再生されない。 + 裏の取れた線は伸び上がり(一 目盛りが立つ)、申告だけの線は + 行き過ぎて戻り、淡いまま残る(二 疑問)。 + */} + {/* 幅は実寸で持たせる——親は 3px しかないので、max-width にすると 親に合わせて一文字ずつ縦に折れる。 @@ -239,7 +269,7 @@ export const AlibiChart = ({ */} {truth !== undefined ? null : ( @@ -247,7 +277,11 @@ export const AlibiChart = ({ )} {s.fix === undefined || truth !== undefined ? null : ( - + // 時刻は帯より遅れて出す。先に線が立ち、それから時刻が添う。 + {s.fix} )} @@ -275,8 +309,10 @@ export const AlibiChart = ({ )} {clash === undefined ? null : ( + // 離れた二つの供述が噛み合わないと分かった瞬間(三 ひらめき)。 + // 線が左から引かれ、引き終わってから両端の丸と札が置かれる。光らせず、繋ぐ。 {/* 両端の丸。どこからどこまでが噛み合っていないのかを、線だけでなく端でも示す。 */} - - - + + + {clash.label} diff --git a/src/client/components/ChatLog.stories.tsx b/src/client/components/ChatLog.stories.tsx new file mode 100644 index 0000000..f5f5218 --- /dev/null +++ b/src/client/components/ChatLog.stories.tsx @@ -0,0 +1,119 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { useEffect, useState } from 'react' +import { ChatLog } from '@/client/components/ChatLog' +import type { ChatTurn } from '@/client/hooks/useInterrogation' + +/* + * 会話の見え方と、文が置かれていく間。 + * + * 見たいのは静止した姿ではなく速さのほうなので、返答が一字ずつ届くところまで作る。 + * 実際の SSE と同じで、届くのは字、画面に出るのは文——ChatLog は書きかけの一文を + * 伏せ、出来上がった順に 0.8 秒の間を置いて通す。 + */ + +const ASKED_AT = 1_700_000_000_000 + +const SPEAKER = '牧野千尋' +const ASKER = '灰城アキラ' + +/** 相手の顔料。0 は浅葱。 */ +const SPEAKER_INDEX = 0 + +const TOPIC = '事件のあった時間について訊いて' +const QUESTION = '午後六時半から七時過ぎまで、どちらにいらっしゃいましたか。' + +/* + * 返答は句点で四文。モデルが速いと二文がほぼ同時に着地するので、 + * ここでは 1 字 25 ミリ秒——読むより速く届く側に寄せてある。 + * それでも画面に出る間隔は 0.8 秒より詰まらない、というのが見どころ。 + */ +const REPLY = + '午後六時三十五分には店を出ています。郵便局へ行くところでした。窓口の受付は午後七時八分でしたよ。……ええ、雨でしたから、少し手間取りました。' + +const CHAR_INTERVAL_MS = 25 + +const turnsUpTo = (count: number): ChatTurn[] => [ + { id: 't1', role: 'topic', text: TOPIC, askedAt: ASKED_AT }, + { id: 'u1', role: 'user', text: QUESTION, askedAt: ASKED_AT }, + { id: 'a1', role: 'assistant', text: REPLY.slice(0, count), askedAt: ASKED_AT }, +] + +const Streaming = () => { + const [count, setCount] = useState(0) + // 積み直しの合図。ChatLog は間の消化ぶんを内側に持つので、作り直さないと戻らない。 + const [take, setTake] = useState(0) + + useEffect(() => { + if (count >= REPLY.length) { + return + } + + const timer = setTimeout(() => setCount(count + 1), CHAR_INTERVAL_MS) + + return () => clearTimeout(timer) + }, [count]) + + return ( +
+
+ + {count} / {REPLY.length} 字が到着 + + +
+ +
+ +
+
+ ) +} + +const Settled = () => ( +
+ +
+) + +const meta: Meta = { + title: 'Parts/会話', + component: ChatLog, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +/** 返答が流れてくる。書きかけの一文は出ず、出来た一文が 0.8 秒ごとに置かれる。 */ +export const 流れてくる: Story = { render: () => } + +/** 読み直したとき。書き終わった会話は間を置かず、そのまま全部出る。 */ +export const 読み直し: Story = { render: () => } diff --git a/src/client/components/ChatLog.tsx b/src/client/components/ChatLog.tsx index fe02932..e9f91e8 100644 --- a/src/client/components/ChatLog.tsx +++ b/src/client/components/ChatLog.tsx @@ -1,5 +1,7 @@ import { edgeOf, inkOf } from '@/client/components/CharacterAvatar' import type { ChatTurn } from '@/client/hooks/useInterrogation' +import { usePacedReveal } from '@/client/hooks/usePacedReveal' +import { settledSentences } from '@/client/lib/paragraphs' type Props = { turns: ChatTurn[] @@ -33,40 +35,34 @@ type Item = | { kind: 'block'; id: string; role: 'user' | 'assistant'; lines: Line[] } /** - * ひとつの発言を、空行の位置で行に割る。 + * ひとつの発言を、画面に置く一文ずつに割る。 * - * NPCは息継ぎのある喋り方をするよう指示されていて(`@/server/game/rules`)、 - * 塊のあいだが空行で来る。割るのをここに置いてあるのは、流れてくる最中の返答も - * 読み直した履歴も同じ1本のテキストだから。積む側で割ると、リロードを挟んだ - * 前後で行の分かれ方が変わる。 - * - * 改行が続いても区切りは1つと数える。塊のあいだを空行1つで区切るか2つで区切るかは - * モデルの気分で揺れるが、読み手にとっては同じ「間」でしかない。 + * 割るのをここに置いてあるのは、流れてくる最中の返答も読み直した履歴も同じ1本の + * テキストだから。積む側で割ると、リロードを挟んだ前後で行の分かれ方が変わる。 */ -const splitLines = (text: string): string[] => - text - .split(/\n+/) - .map((line) => line.trim()) - .filter((line) => line.length > 0) - -const linesOf = (turn: ChatTurn): Line[] => - splitLines(turn.text).map((text, index) => ({ id: `${turn.id}:${index}`, text })) +const linesOf = (turn: ChatTurn, streaming: boolean): Line[] => + settledSentences(turn.text, streaming).map((text, index) => ({ + id: `${turn.id}:${index}`, + text, + })) /** * 続けて喋った分をひと塊にまとめる。 * * 表示のための純関数なので、ここに通信も状態も持たせない。 */ -export const groupTurns = (turns: ChatTurn[]): Item[] => { +export const groupTurns = (turns: ChatTurn[], awaiting: boolean): Item[] => { const items: Item[] = [] + const lastIndex = turns.length - 1 - for (const turn of turns) { + for (const [index, turn] of turns.entries()) { if (turn.role === 'topic') { items.push({ kind: 'topic', id: turn.id, text: turn.text }) continue } - const lines = linesOf(turn) + // 流れている最中なのは末尾の返答だけ。それより前は書き終わっている。 + const lines = linesOf(turn, awaiting && index === lastIndex && turn.role === 'assistant') // 返答待ちのあいだ積まれている空の行は、中身が届くまで置かない。 // 最初の一文字が改行だったときも同じ扱いにしたいので、割った結果で見る。 @@ -87,7 +83,37 @@ export const groupTurns = (turns: ChatTurn[]): Item[] => { return items } +/** + * 出してよい行数まで切り詰める。 + * + * 話題の区切りは行として数えない。プレイヤー自身が投げたものなので、 + * 本人に向かって間を置いて出す意味がない。 + */ +const capItems = (items: Item[], limit: number): Item[] => + items.reduce<{ left: number; kept: Item[] }>( + (acc, item) => { + if (item.kind === 'topic') { + return { left: acc.left, kept: [...acc.kept, item] } + } + + const lines = item.lines.slice(0, acc.left) + + return { + left: acc.left - lines.length, + // 行が残らない塊は落とす。名前だけが立つと、口を開けたまま黙って見える。 + kept: lines.length === 0 ? acc.kept : [...acc.kept, { ...item, lines }], + } + }, + { left: limit, kept: [] }, + ).kept + export const ChatLog = ({ turns, speakerName, speakerIndex, askerName, awaiting }: Props) => { + const items = groupTurns(turns, awaiting) + const shown = usePacedReveal( + items.reduce((count, item) => (item.kind === 'topic' ? count : count + item.lines.length), 0), + awaiting, + ) + /* * 点を出すのは「送ったが、まだ一文字も返ってきていない」あいだだけ。 * @@ -99,11 +125,11 @@ export const ChatLog = ({ turns, speakerName, speakerIndex, askerName, awaiting const showTyping = awaiting && last !== undefined && - (last.role === 'topic' || (last.role === 'assistant' && last.text.length === 0)) + (last.role === 'topic' || (last.role === 'assistant' && linesOf(last, true).length === 0)) return ( <> - {groupTurns(turns).map((item) => { + {capItems(items, shown).map((item) => { if (item.kind === 'topic') { return (
@@ -134,7 +160,7 @@ export const ChatLog = ({ turns, speakerName, speakerIndex, askerName, awaiting {item.lines.map((line) => (

diff --git a/src/client/components/Motions.stories.tsx b/src/client/components/Motions.stories.tsx new file mode 100644 index 0000000..2a85378 --- /dev/null +++ b/src/client/components/Motions.stories.tsx @@ -0,0 +1,347 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { type ReactNode, useEffect, useState } from 'react' +import { characterCount, crawlDurationSeconds, visibleText } from '@/client/lib/briefing-mode' + +/* + * 「十の動きと、その役目」の突き合わせ台。 + * + * 動きは状態が変わったことを言うためにだけ使う、という決めごとを目で確かめる場所なので、 + * 十枚を同じ大きさの舞台に並べる。隣と高さが違うと、動きの大きさを比べられない。 + * + * キーフレームは src/client/index.css にあるものだけを使う。ここで新しく足すと、 + * カタログにはあるがアプリには無い動きが生まれ、突き合わせの意味が消える。 + */ + +/** 舞台の高さ。全枚で揃える。 */ +const STAGE = 'h-28' + +/* + * 遅延を style で渡すのは、Tailwind の [animation-delay:*] が効かないため。 + * index.css の .line-in などはレイヤーの外にあり、@layer utilities より強い。 + * animation ショートハンドが delay ごと上書きするので、インラインで越えるしかない。 + */ +const delay = (ms: number) => ({ animationDelay: `${ms}ms` }) + +type MotionProps = { + /** 漢数字。演出案の並び順をそのまま持ってくる。 */ + index: string + name: string + why: ReactNode + children: ReactNode +} + +const Motion = ({ index, name, why, children }: MotionProps) => { + // key を差し替えて舞台を作り直す。CSS アニメーションは要素が生まれ直したときにだけ走る。 + const [take, setTake] = useState(0) + + return ( +

+
+ + {index} {name} + + +
+
+ {children} +
+

{why}

+
+ ) +} + +/** 説明文のなかで実装の名前を指すときだけ等幅にする。 */ +const Code = ({ children }: { children: string }) => ( + {children} +) + +const Rail = ({ children }: { children: ReactNode }) => ( +
+ + {children} +
+) + +const TYPED = '午後七時十五分、店主の水野英治が死亡しているのが見つかりました。' + +/** 65ミリ秒/字。TypewriterBriefing と同じ速さでないと、突き合わせにならない。 */ +const CHAR_INTERVAL_MS = 65 + +const Typing = ({ text }: { text: string }) => { + const [count, setCount] = useState(0) + + const total = characterCount(text) + + // 出し切ったら止める。カタログは十枚が同時に生きているので、 + // 終わった動きが裏で回り続けないようにする。 + useEffect(() => { + if (count >= total) { + return + } + + const timer = setTimeout(() => setCount(count + 1), CHAR_INTERVAL_MS) + + return () => clearTimeout(timer) + }, [count, total]) + + return ( +

+ {visibleText(text, count)} +

+ ) +} + +const CRAWL = [ + '——事件の記録を読み上げます。', + '午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。', +] + +const MotionCatalogue = () => ( +
+
+ 動き +

+ 十の動きと、その役目 +

+

+ 動きは状態が変わったことを言うため + にだけ使います。雰囲気のために足さない。 合図の類は 0.4 + 秒前後で終わり、長く動くのは読ませるもの(記録のせり上げ・タイプ送り)だけです。 + 動きが苦手な人には出さず、そのとき見えるのは動いた後の姿——切っても中身が消えないように、要素は完成形で書いてあります。 + 各枠の「もう一度」で繰り返せます。 +

+
+ +
+ + + + + + {/* + * 横中央寄せは transform で書く。Tailwind v4 の -translate-x-1/2 は + * 独立した translate プロパティなので、transform を動かす at-in と二重に効いて + * 動いているあいだだけ倍ずれる。 + */} + + 19:08 + + + + + + + + + + ? + + + + + + + + + + + + + +
+ + 窓口の受付は午後七時八分でした。レシートも残っています。 + + + 新事実 + + 牧野は午後六時三十五分に店を出たと述べた + + +
+
+ + +
+ 牧野千尋 + + 午後六時三十五分には店を出ています。 + + + 窓口の受付は午後七時八分でした。 + + + ……雨でしたから。 + +
+
+ + + 一字ずつ送る読み上げ。65 ミリ秒/字は、既にアプリで動いている{' '} + TypewriterBriefing と同じ値です。 + + } + > + + + + + 記録を下から流します。既存の briefing-crawl{' '} + と同じ作りで、移動距離は本文の高さから決まるので、長い記録ほど自然に長く流れます。下端は地に溶かして、切れ目を作らない。 + + } + > +
+ {/* + * briefing-crawl は舞台ではなく画面の高さ(70dvh 下)から始まる。 + * 本番と同じ動きをこの小さな舞台で見るために、始まりが舞台の下端に + * 重なる位置まで、要素そのものを持ち上げてある。 + */} +
+ {CRAWL.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+ +
+
+ + + 記録を読み終えて、その場に入る敷居。墨の中から浮かび上がり、寄りが戻って焦点が合います。 + 0.76 秒。強く動かすのはこの一度だけで、他の九つは合図に徹します。既存の{' '} + screen-enter は 1.03 + 倍で体感が無いので、ここに寄せた分だけ他から引きました。 + + } + > +
+
+ + 雨の古書店、十九時八分のレシート + + + + + + + 牧野千尋 +
+ +
+
+ + +
+ + 訊けそうなこと + + + + + レシートを見せてもらえますか + + + 黒田さんとは話しましたか + + + +
+
+ + +
+ + 犯人 + + 牧野千尋 正解 + + + + 殺害方法 + 正解 + + + 動機 + 惜しい + +
+
+
+
+) + +const meta: Meta = { + title: 'Parts/十の動き', + component: MotionCatalogue, +} + +export default meta + +type Story = StoryObj + +export const 十の動き: Story = {} diff --git a/src/client/hooks/usePacedReveal.ts b/src/client/hooks/usePacedReveal.ts new file mode 100644 index 0000000..6341db8 --- /dev/null +++ b/src/client/hooks/usePacedReveal.ts @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react' + +/** + * 文と文のあいだに置く間。 + * + * 返答は文の単位で届くが、モデルが速いと二文がほぼ同時に着地して、 + * 読んでいる最中に次が積まれる。届いた順はそのままに、通す間隔にだけ下限を設ける。 + */ +const REVEAL_INTERVAL_MS = 800 + +type State = { shown: number; at: number; armed: boolean } + +/** + * 積まれた行を、間を置いて一つずつ通す。返すのは「今いくつまで出してよいか」。 + * + * 間を置くのは `live`(返答が流れている)あいだに増えた分だけ。履歴の読み込みや + * 画面の復帰で一気に増えたときは全部そのまま出す——読み直しに間は要らない。 + * 流れ終わっても、溜まっているぶんは間を置いて出し切る。最後の数文だけ + * まとめて落ちてくると、そこで急に置いていかれるので。 + */ +export const usePacedReveal = (total: number, live: boolean): number => { + const [state, setState] = useState({ shown: total, at: 0, armed: false }) + + useEffect(() => { + // 訊いた瞬間から数え始める。ここで初めて「間を置く相手」になる。 + if (live && !state.armed) { + setState({ shown: state.shown, at: state.at, armed: true }) + + return + } + + if (!state.armed) { + if (state.shown !== total) { + setState({ shown: total, at: state.at, armed: false }) + } + + return + } + + if (state.shown >= total) { + if (!live) { + setState({ shown: state.shown, at: state.at, armed: false }) + } + + return + } + + /* + * 待ち時間は「前の一文を出した時刻」から数える。新しい文が届くたびに + * 測り直すと、速く喋られたときに間がいつまでも満了せず、何も出なくなる。 + */ + const wait = Math.max(0, state.at + REVEAL_INTERVAL_MS - Date.now()) + const timer = setTimeout( + () => setState({ shown: state.shown + 1, at: Date.now(), armed: true }), + wait, + ) + + return () => clearTimeout(timer) + }, [total, live, state]) + + return state.shown +} diff --git a/src/client/index.css b/src/client/index.css index dc7de1e..d3bb662 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -128,6 +128,200 @@ body { } } +/* + * 発話が一行ずつ置かれる。 + * + * 聞き込みの返答はこれで出す。一字ずつ送ると読む前に目が字を追いはじめるので、 + * 文が出来上がってから、その一文をまとめて置く。5px の浮きは「今これが来た」と + * 言うだけの幅で、読む位置を動かさない。 + */ +@keyframes line-in { + from { + opacity: 0; + transform: translateY(5px); + } +} + +.line-in { + animation: line-in 360ms ease-out both; +} + +/* + * 目盛りが立つ。 + * + * 供述から時刻が確定した瞬間。軸から伸び上がり、遅れて時刻が出る。 + * この作品でいちばん大事な出来事なので、合図のなかで強く動くのはここだけ。 + */ +@keyframes pin-rise { + from { + transform: scaleY(0); + } +} + +/* + * 伸びる向きは呼び出し側が決める(Tailwind の origin-bottom / origin-top など)。 + * ここで焼き込むと、横倒しの帯と縦のアリバイ表で同じ動きが使えない。 + */ +.pin-rise { + animation: pin-rise 420ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} + +/* + * 目盛りに添う時刻。 + * + * translateX(-50%) は目盛りの真上に置くための位置合わせで、動きの一部ではない。 + * キーフレームの側にも書いておかないと、動いているあいだだけ横へずれる。 + */ +@keyframes at-in { + from { + opacity: 0; + transform: translate(-50%, 5px); + } +} + +.at-in { + animation: at-in 360ms 150ms ease-out both; +} + +/* + * 立ちきらない目盛り。 + * + * 辻褄の合わない供述。伸び上がろうとして、行き過ぎて、戻って、淡いまま残る。 + * 色は容疑者の顔料のままにする——画面がプレイヤーより先に誰かを疑わないために。 + */ +@keyframes waver { + 0% { + transform: scaleY(0); + } + 45% { + transform: scaleY(1.18); + } + 72% { + transform: scaleY(0.88); + } +} + +.waver { + animation: waver 900ms ease-in-out both; +} + +/* + * 離れた二つの証言が噛み合う。 + * + * 二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びる。光らせず、繋ぐ。 + */ +@keyframes draw { + from { + transform: scaleX(0); + } +} + +.draw { + animation: draw 520ms 180ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} + +@keyframes pin-lift { + 55% { + transform: scaleY(1.5); + } +} + +.pin-lift { + animation: pin-lift 620ms 180ms ease-out both; +} + +/* + * 新事実を帯で知らせる。 + * + * 記録は聞き込み中に画面上にないので、増えたことは被せて伝えるしかない。 + * 箱にはせず、二本の罫線と薄い覆いだけ。操作は塞がず 2.6 秒で引く。 + * 祝わない——増えたのは事実であって、手柄ではないので。 + */ +@keyframes band { + 0% { + opacity: 0; + transform: translateY(-50%) scaleY(0.86); + } + 11% { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + 84% { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + 100% { + opacity: 0; + transform: translateY(-50%) scaleY(1); + } +} + +.band { + animation: band 2600ms both; +} + +/* + * 畳んであるものが開く。 + * + * 高さそのものを動かす(grid-template-rows の 0fr → 1fr)。中身を透かせるだけだと、 + * 下にある入力欄が動かないまま文字だけ現れて、飛んで見える。 + */ +@keyframes fold-open { + from { + grid-template-rows: 0fr; + } +} + +.fold-open { + animation: fold-open 360ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} + +/* + * 判定が上から順に出る。 + * + * 一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になる。 + * 行ごとの遅延は使う側で 0.2 秒ずつずらす。 + */ +@keyframes row-in { + from { + opacity: 0; + transform: translateY(6px); + } +} + +.row-in { + animation: row-in 400ms ease-out both; +} + +/* + * 事件の記録を読み終えて、その場に入る敷居。 + * + * 墨の中から浮かび上がり、寄りが戻って焦点が合う。強く動かすのはこの一度だけで、 + * 他の動きは合図に徹する。screen-enter(1.03倍)はほぼ体感が無いので、 + * 敷居を越えるところだけこちらを使う。 + */ +@keyframes dive { + from { + opacity: 0; + transform: scale(1.16); + filter: blur(8px); + } +} + +.dive { + animation: dive 760ms cubic-bezier(0.16, 1, 0.3, 1) both; +} + +@keyframes unveil { + from { + opacity: 1; + } +} + +.unveil { + animation: unveil 560ms ease-out both; +} + /* * ターンの切り替わり。 * @@ -159,8 +353,25 @@ body { animation: turn-announce 1600ms ease-out both; } +/* + * 動きが苦手な人には出さない。演出のために体調を崩させる理由はない。 + * + * どの動きも「動いた後の姿」を素の状態として書いてあるので、切っても完成形が残る。 + * 新しい動きを足したら、ここにも足す。 + */ @media (prefers-reduced-motion: reduce) { - .turn-announce { + .turn-announce, + .line-in, + .pin-rise, + .at-in, + .waver, + .draw, + .pin-lift, + .band, + .fold-open, + .row-in, + .dive, + .unveil { animation: none; } } diff --git a/src/client/lib/paragraphs.ts b/src/client/lib/paragraphs.ts index 14d74e0..a69c2ad 100644 --- a/src/client/lib/paragraphs.ts +++ b/src/client/lib/paragraphs.ts @@ -46,6 +46,31 @@ const chunkParagraph = (paragraph: string): string[] => { return chunks } +/** 文が書き終わっているか。閉じ括弧は句点の後ろに付いてくるので、そこまで見る。 */ +const isComplete = (sentence: string): boolean => /[。!?][」』)】]*$/.test(sentence) + +/** + * 流れてくる発言を、画面に置ける一文の並びに変える。 + * + * 聞き込みの返答は一字ずつ届くが、届いたそばから出すと読むより先に目が字を追いはじめ、 + * 追いつくための時間になる。文が出来上がるまで待って、出来た一文をまとめて置く。 + * 置く側は現れた瞬間だけ淡く浮かせる(`line-in`)。 + * + * `streaming` が false なら全部返す。書き終わった発言は、句点で終わらない喋り方 + * (「……」で切るなど)でも、そこで待たせる理由がない。 + */ +export const settledSentences = (text: string, streaming: boolean): string[] => { + const sentences = text + .split(/\n+/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .flatMap(splitSentences) + + const last = sentences[sentences.length - 1] + + return streaming && last !== undefined && !isComplete(last) ? sentences.slice(0, -1) : sentences +} + /** * ブリーフィング本文を、画面に出す段落の並びに変える。 * diff --git a/src/client/lib/settings-store.ts b/src/client/lib/settings-store.ts index ab8ba02..799516b 100644 --- a/src/client/lib/settings-store.ts +++ b/src/client/lib/settings-store.ts @@ -44,9 +44,14 @@ const STORAGE_KEY = 'alibai:settings' */ export const DEFAULT_SETTINGS: Settings = { llm: {}, + /* + * limits には llm のような「未選択」が無く、常にリクエストへ載る。 + * つまりここの数値がサーバの既定(env の MAX_TURNS など)を必ず上書きするので、 + * 片方だけ動かしても効かない。変えるときは両方を揃える。 + */ limits: { - maxTurns: 5, - questionsPerTurn: 1, + maxTurns: 15, + questionsPerTurn: 2, exchangesPerTopic: EXCHANGES_PER_TOPIC, }, } diff --git a/src/client/screens/InterrogationScreen.stories.tsx b/src/client/screens/InterrogationScreen.stories.tsx index 5c5efa6..564a3a5 100644 --- a/src/client/screens/InterrogationScreen.stories.tsx +++ b/src/client/screens/InterrogationScreen.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite' +import { useEffect, useState } from 'react' import type { AlibiSegment } from '@/client/components/AlibiChart' import { type ChatTurn, @@ -160,6 +161,56 @@ const Harness = ({ ) } +/** 線が一本増えるまでの間。立ち上がり(0.42秒)を見届けてから次が来る速さ。 */ +const SEGMENT_INTERVAL_MS = 1200 + +/** + * 線が一本ずつ増えていくところ。 + * + * 聞き込みが進むと表がどう動くかを見るための story で、実際の進行とは繋がっていない + * (サーバはまだ時刻付きの在所を返さない)。増えた線だけが一度動く、という + * AlibiChart の作りをここで確かめる。 + */ +const Growing = () => { + const [count, setCount] = useState(0) + const [take, setTake] = useState(0) + + useEffect(() => { + if (count >= SEGMENTS_LAST.length) { + return + } + + const timer = setTimeout(() => setCount(count + 1), SEGMENT_INTERVAL_MS) + + return () => clearTimeout(timer) + }, [count]) + + const done = count >= SEGMENTS_LAST.length + + return ( + <> + + + + ) +} + const meta: Meta = { title: 'Screens/INT 聞き込み', component: InterrogationScreen, @@ -195,3 +246,6 @@ export const LastTurn: Story = { /> ), } + +/** 線が増えていくところ。裏の取れた線は伸び上がり、申告だけの線は揺れて淡く残る。 */ +export const 時刻表が埋まる: Story = { render: () => } diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index 13ea1c8..56123d4 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -6,8 +6,10 @@ import { FloorPlanMap } from '@/client/components/FloorPlan' import { TurnAnnounce } from '@/client/components/TurnAnnounce' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/client/components/ui/dialog' import type { ChatTurn, UseInterrogation } from '@/client/hooks/useInterrogation' +import { usePacedReveal } from '@/client/hooks/usePacedReveal' import { fetchSessionState } from '@/client/lib/api' import { formatSeconds } from '@/client/lib/format' +import { settledSentences } from '@/client/lib/paragraphs' import type { ScenarioDetail, SessionState } from '@/client/lib/schemas' /** @@ -196,13 +198,27 @@ const buildBlocks = ( characters: ScenarioDetail['characters'], conversations: Record, askerName: string, + /** 今まさに返答が流れてきている相手。書きかけの一文を伏せるのに要る。 */ + askingCharacterId: string | undefined, ): Block[] => { - const said = characters.flatMap((character, index) => - turnsOf(conversations, character.id) - .map((turn, seq) => ({ turn, seq, index, name: character.name })) - // 話題はプレイヤーの指示であって発言ではない。探偵が投げた質問のほうが残る。 - .filter(({ turn }) => turn.role !== 'topic' && turn.text.length > 0), - ) + const said = characters.flatMap((character, index) => { + const turns = turnsOf(conversations, character.id) + // 流れている最中なのは、訊いている相手の末尾の返答だけ。 + const streamingSeq = character.id === askingCharacterId ? turns.length - 1 : -1 + + return ( + turns + .map((turn, seq) => ({ + turn, + seq, + index, + name: character.name, + streaming: seq === streamingSeq && turn.role === 'assistant', + })) + // 話題はプレイヤーの指示であって発言ではない。探偵が投げた質問のほうが残る。 + .filter(({ turn }) => turn.role !== 'topic' && turn.text.length > 0) + ) + }) const ordered = [...said].sort((a, b) => a.turn.askedAt === b.turn.askedAt ? a.seq - b.seq : a.turn.askedAt - b.turn.askedAt, @@ -211,21 +227,52 @@ const buildBlocks = ( for (const item of ordered) { const who = item.turn.role === 'user' ? -1 : item.index - const line = { id: `${item.index}:${item.turn.id}`, text: item.turn.text } + const id = `${item.index}:${item.turn.id}` + const lines = settledSentences(item.turn.text, item.streaming).map((text, at) => ({ + id: `${id}:${at}`, + text, + })) + + // 一文目が出来上がるまでは何も置かない。名前だけ先に出ると、 + // 誰かが口を開いたまま黙っているように見える。 + if (lines.length === 0) { + continue + } + const last = blocks[blocks.length - 1] // 同じ人が続けて喋るあいだ、名前は一度きり。縦罫だけが最後まで伸びる。 if (last !== undefined && last.who === who) { - last.lines.push(line) + last.lines.push(...lines) continue } - blocks.push({ id: line.id, who, name: who === -1 ? askerName : item.name, lines: [line] }) + blocks.push({ id, who, name: who === -1 ? askerName : item.name, lines }) } return blocks } +/** + * 出してよい行数まで塊を切り詰める。 + * + * 塊ごとではなく通しで数えるので、探偵の質問と相手の一文目のあいだにも間が入る。 + * 行が一つも残らない塊は落とす——名前だけが立って、口を開けたまま黙っているように + * 見えるのを避けるため。 + */ +const capLines = (blocks: Block[], limit: number): Block[] => + blocks.reduce<{ left: number; kept: Block[] }>( + (acc, block) => { + const lines = block.lines.slice(0, acc.left) + + return { + left: acc.left - lines.length, + kept: lines.length === 0 ? acc.kept : [...acc.kept, { ...block, lines }], + } + }, + { left: limit, kept: [] }, + ).kept + /* ---- 端末の時刻軸 ---- */ type Pin = { id: string; left: string; surface: string; solid: boolean } @@ -437,7 +484,12 @@ export const InterrogationScreen = ({ ]), ) - const blocks = buildBlocks(scenario.characters, conversations, askerName) + const said = buildBlocks(scenario.characters, conversations, askerName, askingCharacterId) + const shown = usePacedReveal( + said.reduce((count, block) => count + block.lines.length, 0), + isAsking, + ) + const blocks = capLines(said, shown) const timeWindow = scenario.timeWindow const pins = timeWindow === null @@ -686,7 +738,7 @@ export const InterrogationScreen = ({ {block.lines.map((line) => (

diff --git a/src/server/env.ts b/src/server/env.ts index 63973cd..9209468 100644 --- a/src/server/env.ts +++ b/src/server/env.ts @@ -86,9 +86,9 @@ const schema = z.object({ GOOGLE_GENERATIVE_AI_BASE_URL: optionalString, /** 1プレイで使えるターン数。使い切ると質問できなくなり、推理に進む。 */ - MAX_TURNS: z.coerce.number().int().positive().default(5), + MAX_TURNS: z.coerce.number().int().positive().default(15), /** 1ターンに投げられる質問数。 */ - QUESTIONS_PER_TURN: z.coerce.number().int().positive().default(1), + QUESTIONS_PER_TURN: z.coerce.number().int().positive().default(2), /** 1ウィンドウあたりに許すLLM呼び出し回数。 */ /* diff --git a/src/server/read/scenarios.ts b/src/server/read/scenarios.ts index 790a7a1..5c0a1df 100644 --- a/src/server/read/scenarios.ts +++ b/src/server/read/scenarios.ts @@ -47,6 +47,14 @@ export const normalizePublicIntroduction = (value: string): string => { : trimmed } +/** + * characters.id はシナリオ投入時に独立に採番される UUID。 + * UUID の辞書順を表示順として使えば、作者が characters 配列へ書いた順序(犯人を + * 最初に設計しがちな LLM の癖)を公開 UI へ持ち込まず、同じ DB の間は順序も安定する。 + */ +export const sortCharactersById = (characters: T[]): T[] => + [...characters].sort((left, right) => left.id.localeCompare(right.id)) + /** 公開シナリオの一覧。読みは多いが滅多に書き換わらないので KV から返す。 */ export const listScenarios = (kv: KVNamespace, db: Db): Promise => loadPublishedScenarios(kv, db) @@ -98,19 +106,21 @@ export const findScenarioDetail = async ( return undefined } - const characterRows = ( - await db - .select({ - id: characters.id, - name: characters.name, - publicIntroduction: characters.publicIntroduction, - }) - .from(characters) - .where(eq(characters.scenarioId, scenarioId)) - ).map((character) => ({ - ...character, - publicIntroduction: normalizePublicIntroduction(character.publicIntroduction), - })) + const characterRows = sortCharactersById( + ( + await db + .select({ + id: characters.id, + name: characters.name, + publicIntroduction: characters.publicIntroduction, + }) + .from(characters) + .where(eq(characters.scenarioId, scenarioId)) + ).map((character) => ({ + ...character, + publicIntroduction: normalizePublicIntroduction(character.publicIntroduction), + })), + ) /* 図面はここで読み替えてから返す。 diff --git a/src/shared/turns.ts b/src/shared/turns.ts index bcfa37d..44d6eff 100644 --- a/src/shared/turns.ts +++ b/src/shared/turns.ts @@ -43,14 +43,20 @@ export const EXCHANGES_PER_TOPIC = 3 * 3つとも自由に上げられると天井そのものが消える。 * * 積の上限を別に持つのは、`maxTurns` と `questionsPerTurn` を両方上限まで上げられると - * 質問回数が 30 まで伸びるため。10分で遊ぶゲームとして、そこまでは要らない。 + * 質問回数が 45 まで伸びるため。事件に登場するのは3〜5人なので、そこまで訊けると + * 全員に片端から聞く余裕が生まれ、絞って訊くことがゲームでなくなる。 */ export const LIMIT_CEILINGS = { - maxTurns: 10, + maxTurns: 15, questionsPerTurn: 3, exchangesPerTopic: 5, - /** maxTurns × questionsPerTurn の上限。 */ - totalQuestions: 20, + /** + * maxTurns × questionsPerTurn の上限。 + * + * 20 から 30 へ引き上げてある。3人の相手に20問だと、矛盾を見つけてから + * それを突く問いに手が回らないまま終わる——一巡目で持ち札を使い切ってしまう。 + */ + totalQuestions: 30, } export type SessionLimits = { diff --git a/wrangler.jsonc b/wrangler.jsonc index ceba857..e4e02c1 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -76,8 +76,8 @@ "LLM_ACTOR_PROVIDER": "openai", "LLM_JUDGE_PROVIDER": "openai", "LLM_AUTHOR_PROVIDER": "openai", - "MAX_TURNS": "5", - "QUESTIONS_PER_TURN": "1", + "MAX_TURNS": "15", + "QUESTIONS_PER_TURN": "2", // 数えるのはモデル呼び出しの回数。1つの話題で最大 2×往復数+1 回消費する。 "RATE_LIMIT_MAX_CALLS": "420", "RATE_LIMIT_WINDOW_SECONDS": "3600", From 5e5e83fb13c89b9926e7da198a8cf05d1a33a947 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:38:03 +0000 Subject: [PATCH 03/51] feat(docs): add effects motion catalog mock and embed it in storyboard Adds mocks/effects.html cataloging the ten UI motions with replay controls, wires it into mocks/storyboard.html as a new section, and includes a reference screenshot. Co-Authored-By: Claude --- effects-page.png | Bin 0 -> 236567 bytes mocks/effects.html | 355 ++++++++++++++++++++++++++++++++++++++++++ mocks/storyboard.html | 21 +++ 3 files changed, 376 insertions(+) create mode 100644 effects-page.png create mode 100644 mocks/effects.html diff --git a/effects-page.png b/effects-page.png new file mode 100644 index 0000000000000000000000000000000000000000..872235ab766e5ed6b82c01a55fccbd2f43823a4e GIT binary patch literal 236567 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

C!{X&C_pE;hj9uN}NB` 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

Y{`2 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 literal 0 HcmV?d00001 diff --git a/mocks/effects.html b/mocks/effects.html new file mode 100644 index 0000000..6cd40a4 --- /dev/null +++ b/mocks/effects.html @@ -0,0 +1,355 @@ + + + + +AlibAI — 動きの一覧 + + + + + + +

+

十の動きと、その役目

+

+ 動きは状態が変わったことを言うためにだけ使う。雰囲気のために足さない。 + 合図の類は 0.4 秒前後で終わり、長く動くのは読ませるもの(記録のせり上げ・タイプ送り)だけ。 + 動きが苦手な人には出さず、そのとき見えるのは動いた後の姿——切っても中身が消えないように、要素は完成形で書いてある。 + 各枠の「もう一度」で繰り返せる。 +

+
+ +
+
一 目盛りが立つ
+
+
+ + + + + 19:08 +
+
+

供述から時刻が確定した瞬間。軸の下から伸び上がり、遅れて時刻が出ます。この作品でいちばん大事な出来事なので、強めに動くのはここだけ。

+
+ +
+
二 疑問
+
+
+ + + + +
+
+

辻褄の合わない供述。目盛りは立とうとして、立ちきらずに淡いまま残ります。question mark は出しますが、色は容疑者の顔料のまま——画面が先に誰かを疑わないために。

+
+ +
+
三 ひらめき
+
+
+ + + + +
+
+

離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。

+
+ +
+
四 新事実発見
+
+
+ 窓口の受付は午後七時八分でした。レシートも残っています。 + + 新事実 + 牧野は午後六時三十五分に店を出たと述べた + +
+
+

記録は聞き込み中に画面上にないので、増えたことを帯で被せて知らせます。箱にはせず、二本の罫線と薄い覆いだけ。 + 操作は塞がず 2.6 秒で引きます。祝いはしません——増えたのは事実であって手柄ではないので。

+
+ +
+
五 発話が続く
+
+
+ 牧野千尋 + 午後六時三十五分には店を出ています。 + 窓口の受付は午後七時八分でした。 + ……雨でしたから。 +
+
+

ひとりが続けて喋るとき、一行ずつ置いていきます。間は 0.5 秒。読み終わる前に次が来ると急かされるので、詰めすぎない。

+
+ +
+
六 タイプ送り
+
+

+
+

一字ずつ送る読み上げ。65 ミリ秒/字は、既にアプリで動いている TypewriterBriefing と同じ値です。

+
+ +
+
七 記録のせり上げ
+
+
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。

+
+ +
+
+

記録を下から流します。既存の briefing-crawl と同じ作りで、移動距離は本文の高さから決まるので、長い記録ほど自然に長く流れます。下端は地に溶かして、切れ目を作らない。

+
+ +
+
八 入り込む
+
+
+
+ 雨の古書店、十九時八分のレシート + + + + + + 牧野千尋 +
+ +
+
+

記録を読み終えて、その場に入る敷居。墨の中から浮かび上がり、寄りが戻って焦点が合います。 + 0.76 秒。強く動かすのはこの一度だけで、他の九つは合図に徹します。既存の screen-enter は + 1.03 倍で体感が無いので、ここに寄せた分だけ他から引きました。

+
+ +
+
九 選択肢が開く
+
+
+ 訊けそうなこと + + レシートを見せてもらえますか + 黒田さんとは話しましたか + +
+
+

畳んであるものが開くので、高さそのものを動かします。中身を透かせるだけだと、下の入力欄が飛んで見えるため。

+
+ +
+
十 判定が出る
+
+
+ 犯人牧野千尋 正解 + 殺害方法正解 + 動機惜しい +
+
+

上から順に 0.2 秒ずつ。一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になります。

+
+ +
+
+ + + diff --git a/mocks/storyboard.html b/mocks/storyboard.html index 0e3969d..9466b8e 100644 --- a/mocks/storyboard.html +++ b/mocks/storyboard.html @@ -87,6 +87,12 @@ .paper.desk iframe { width: 1440px; height: 900px; } .paper.phone { width: 195px; height: 422px; } .paper.phone iframe { width: 390px; height: 844px; } + /* + 動きの一覧だけは縮めない。画面の見え方ではなく速さと大きさを見る枠なので、 + 50% に縮めると 5px の浮きが 2.5px になり、比べたいものが消える。 + */ + .paper.flat { width: 100%; height: 660px; } + .paper.flat iframe { width: 100%; height: 660px; transform: none; } @@ -303,6 +309,21 @@

ALI_SET設定

+ +
+

ALI_FX動き

+

+ 画面をまたいで使う十の動き。枠の中で もう一度 を押すと再生する。 + 実装は src/client/index.css のキーフレームと Storybook の + Parts/十の動き にあり、この枠はその原典(mocks/effects.html)を読んでいる。 + 十のうちアプリで実際に動いているのは五・六・七と、画面の入り・ターンの変わり目だけで、 + 残りはまだ部品に当たっていない。 +

+
+
十の動きと、その役目
+
+
+
diff --git a/mocks/desktop/case-overview.html b/mocks/desktop/case-overview.html index f6c987e..5387b05 100644 --- a/mocks/desktop/case-overview.html +++ b/mocks/desktop/case-overview.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + -
+
← 事件を選ぶ
diff --git a/mocks/desktop/detective.html b/mocks/desktop/detective.html index 12c4d27..3058063 100644 --- a/mocks/desktop/detective.html +++ b/mocks/desktop/detective.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + -
+
← シナリオを選び直す diff --git a/mocks/desktop/interrogation.html b/mocks/desktop/interrogation.html index c6e6c6b..59dc414 100644 --- a/mocks/desktop/interrogation.html +++ b/mocks/desktop/interrogation.html @@ -10,11 +10,14 @@ rel="stylesheet" /> + -
+ +
+
@@ -42,6 +45,16 @@
+ +
+ 新事実 + 牧野は午後六時三十五分に店を出たと述べた +
+
@@ -98,6 +111,70 @@ .join(''), ) + /* + * 一 目盛りが立つ/二 疑問。 + * + * 塗りを内側の帯へ移してから伸ばす。字を含む .bar ごと拡大すると、 + * 伸びているあいだ在所と時刻が縦に潰れる。 + * 裏の取れた線は伸び上がり、申告だけの線は行き過ぎて戻り、淡いまま残る。 + */ + Array.prototype.forEach.call(document.querySelectorAll('#chart .bar'), function (bar) { + var solid = bar.classList.contains('solid') + var fill = document.createElement('span') + fill.className = 'fill origin-top ' + (solid ? 'pin-rise' : 'waver') + fill.style.background = bar.style.background + bar.style.background = '' + bar.insertBefore(fill, bar.firstChild) + + // 時刻は帯より遅れて添う。先に線が立ち、それから時刻が出る。 + var fix = bar.querySelector('.fix') + if (fix) { + fix.classList.add('line-in') + fix.style.animationDelay = '150ms' + } + }) + + /* + * 三 ひらめき。噛み合わない区間へ線が左から引かれる。 + * 両端の丸と札は引き終わり(180ms 待って 520ms)を待ってから置く。 + * 線が伸びきる前に端の印だけが宙に浮くのを避けるため。 + */ + var clash = document.querySelector('#chart .clash') + if (clash) { + clash.classList.add('draw', 'origin-left') + Array.prototype.forEach.call(clash.children, function (mark) { + mark.classList.add('line-in') + mark.style.animationDelay = '700ms' + }) + } + + /* + * 五 発話が続く。いま届いた返答だけ、一文ずつ置いていく。間は 0.8 秒。 + * 読み返している古い塊まで動かすと、画面に入るたび全部が波打つ。 + */ + var turns = document.querySelectorAll('#log .turn') + var newest = turns[turns.length - 1] + if (newest) { + Array.prototype.forEach.call(newest.querySelectorAll('.txt'), function (line, index) { + line.classList.add('line-in') + line.style.animationDelay = 100 + index * 800 + 'ms' + }) + } + + /* + * 九 選択肢が開く。高さそのものを動かすので、grid の行を 0fr から開く。 + * 中身を透かせるだけだと、下の入力欄が動かないまま文字だけ現れて飛んで見える。 + */ + var fold = document.createElement('span') + fold.className = 'fold fold-open' + var inner = document.createElement('span') + inner.className = 'fold-inner' + Array.prototype.forEach.call(document.querySelectorAll('#hint .q'), function (q) { + inner.appendChild(q) + }) + fold.appendChild(inner) + document.getElementById('hint').appendChild(fold) + // 列見出しを押すと、その人に相手を替える。 Array.prototype.forEach.call(document.querySelectorAll('[data-who]'), function (el) { el.addEventListener('click', function () { diff --git a/mocks/desktop/result.html b/mocks/desktop/result.html index 3854967..c9a212e 100644 --- a/mocks/desktop/result.html +++ b/mocks/desktop/result.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + -
+

AlibAI聞き込みで、犯人を指し示す

diff --git a/mocks/desktop/settings.html b/mocks/desktop/settings.html index d5ceb62..94a95b3 100644 --- a/mocks/desktop/settings.html +++ b/mocks/desktop/settings.html @@ -10,6 +10,7 @@ rel="stylesheet" /> + - - -
-

十の動きと、その役目

-

- 動きは状態が変わったことを言うためにだけ使う。雰囲気のために足さない。 - 合図の類は 0.4 秒前後で終わり、長く動くのは読ませるもの(記録のせり上げ・タイプ送り)だけ。 - 動きが苦手な人には出さず、そのとき見えるのは動いた後の姿——切っても中身が消えないように、要素は完成形で書いてある。 - 各枠の「もう一度」で繰り返せる。 -

-
- -
-
一 目盛りが立つ
-
-
- - - - - 19:08 -
-
-

供述から時刻が確定した瞬間。軸の下から伸び上がり、遅れて時刻が出ます。この作品でいちばん大事な出来事なので、強めに動くのはここだけ。

-
- -
-
二 疑問
-
-
- - - - -
-
-

辻褄の合わない供述。目盛りは立とうとして、立ちきらずに淡いまま残ります。question mark は出しますが、色は容疑者の顔料のまま——画面が先に誰かを疑わないために。

-
- -
-
三 ひらめき
-
-
- - - - -
-
-

離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。

-
- -
-
四 新事実発見
-
-
- 窓口の受付は午後七時八分でした。レシートも残っています。 - - 新事実 - 牧野は午後六時三十五分に店を出たと述べた - -
-
-

記録は聞き込み中に画面上にないので、増えたことを帯で被せて知らせます。箱にはせず、二本の罫線と薄い覆いだけ。 - 操作は塞がず 2.6 秒で引きます。祝いはしません——増えたのは事実であって手柄ではないので。

-
- -
-
五 発話が続く
-
-
- 牧野千尋 - 午後六時三十五分には店を出ています。 - 窓口の受付は午後七時八分でした。 - ……雨でしたから。 -
-
-

ひとりが続けて喋るとき、一行ずつ置いていきます。間は 0.5 秒。読み終わる前に次が来ると急かされるので、詰めすぎない。

-
- -
-
六 タイプ送り
-
-

-
-

一字ずつ送る読み上げ。65 ミリ秒/字は、既にアプリで動いている TypewriterBriefing と同じ値です。

-
- -
-
七 記録のせり上げ
-
-
-
-

——事件の記録を読み上げます。

-

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。

-
- -
-
-

記録を下から流します。既存の briefing-crawl と同じ作りで、移動距離は本文の高さから決まるので、長い記録ほど自然に長く流れます。下端は地に溶かして、切れ目を作らない。

-
- -
-
八 入り込む
-
-
-
- 雨の古書店、十九時八分のレシート - - - - - - 牧野千尋 -
- -
-
-

記録を読み終えて、その場に入る敷居。墨の中から浮かび上がり、寄りが戻って焦点が合います。 - 0.76 秒。強く動かすのはこの一度だけで、他の九つは合図に徹します。既存の screen-enter は - 1.03 倍で体感が無いので、ここに寄せた分だけ他から引きました。

-
- -
-
九 選択肢が開く
-
-
- 訊けそうなこと - - レシートを見せてもらえますか - 黒田さんとは話しましたか - -
-
-

畳んであるものが開くので、高さそのものを動かします。中身を透かせるだけだと、下の入力欄が飛んで見えるため。

-
- -
-
十 判定が出る
-
-
- 犯人牧野千尋 正解 - 殺害方法正解 - 動機惜しい -
-
-

上から順に 0.2 秒ずつ。一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になります。

-
- -
-
- - - diff --git a/mocks/effects/_effects.css b/mocks/effects/_effects.css new file mode 100644 index 0000000..57fe931 --- /dev/null +++ b/mocks/effects/_effects.css @@ -0,0 +1,527 @@ +/* + 十の動きだけを抜き出した台。原典は direction.html の「動き」の節で、 + CSS と組みはそこから写している。片方だけ直すとカタログが二枚に割れるので、 + 値を変えるときは direction.html と src/client/index.css も一緒に直す。 +*/ +:root { + --sumi: #141317; + --sumi-2: #1c1a21; + --sumi-3: #232028; + --keisen: #302c39; + --kinari: #e9e4d9; + --nezumi: #8e8896; + --nezumi-dim: #625d6c; + + --asagi: #5e8b8c; + --asagi-t: #8ab5b5; + --fuji: #8079a6; + --fuji-t: #ada6cf; + --karashi: #b49a55; + --karashi-t: #d6bd77; + --suou: #96556a; + --suou-t: #c68698; + + --shu: #d2452e; + --byakuroku: #7fa88a; + + --mincho: "Shippori Mincho B1", "Hiragino Mincho ProN", "Yu Mincho", serif; + --gothic: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", sans-serif; + --mono: "Roboto Mono", ui-monospace, monospace; +} +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--sumi); + color: var(--kinari); + font-family: var(--gothic); + font-size: 15px; + line-height: 1.8; + -webkit-font-smoothing: antialiased; +} +/* ================= 動き ================= */ +.motions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(258px, 1fr)); + gap: 1px; + background: var(--keisen); + border: 1px solid var(--keisen); +} +.motion { + background: var(--sumi); + padding: 15px 18px 18px; + display: flex; + flex-direction: column; + gap: 10px; +} +.mhead { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} +.mnm { + font-family: var(--mincho); + font-size: 15px; + font-weight: 500; + letter-spacing: 0.05em; +} +.replay { + font-family: var(--gothic); + font-size: 10px; + letter-spacing: 0.16em; + color: var(--nezumi-dim); + background: none; + border: 1px solid var(--keisen); + padding: 3px 9px; + cursor: pointer; + flex: none; +} +.replay:hover { + color: var(--kinari); + border-color: var(--nezumi-dim); +} +.replay:focus-visible { + outline: 1px solid var(--kinari); + outline-offset: 2px; +} +/* 舞台の高さは全枚で同じに保つ。一枚でも違うと隣と揃わない。 */ +.stage { + height: 112px; + display: flex; + flex-direction: column; + justify-content: center; +} +.mwhy { + margin: 0; + font-size: 11.5px; + line-height: 1.7; + color: var(--nezumi); +} + +/* 一 目盛りが立つ */ +.d-rail { + position: relative; + height: 42px; +} +.d-rail .line { + position: absolute; + left: 0; + right: 0; + top: 28px; + height: 1px; + background: var(--keisen); +} +.d-rail .pin { + position: absolute; + top: 16px; + width: 2px; + height: 13px; + transform-origin: 50% 100%; +} +.d-rail .at { + position: absolute; + top: 0; + transform: translateX(-50%); + color: var(--asagi-t); + font-family: var(--mono); + font-variant-numeric: tabular-nums; + font-size: 11px; +} +.play .d-rail .pin.fresh { + animation: pin-rise 420ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} +.play .d-rail .at { + animation: at-in 360ms 150ms ease-out both; +} +@keyframes pin-rise { + from { + transform: scaleY(0); + } +} +@keyframes at-in { + from { + opacity: 0; + transform: translate(-50%, 5px); + } +} + +/* 五 発話が続く */ +.d-turn { + display: flex; + flex-direction: column; + gap: 5px; + padding-left: 10px; + border-left: 1px solid var(--asagi); +} +.d-turn .who { + font-size: 10px; + letter-spacing: 0.1em; + color: var(--asagi-t); +} +.d-turn .l { + font-size: 12px; + line-height: 1.7; +} +.play .d-turn .l { + animation: line-in 360ms ease-out both; +} +.play .d-turn .l:nth-child(2) { + animation-delay: 100ms; +} +.play .d-turn .l:nth-child(3) { + animation-delay: 620ms; +} +.play .d-turn .l:nth-child(4) { + animation-delay: 1140ms; +} +@keyframes line-in { + from { + opacity: 0; + transform: translateY(5px); + } +} + +/* 九 選択肢が開く */ +.d-hint { + display: flex; + flex-direction: column; + border-top: 1px solid var(--keisen); +} +.d-hint .head { + display: flex; + justify-content: space-between; + padding: 6px 0; + font-size: 11px; + color: var(--nezumi-dim); +} +.d-hint .fold { + display: grid; + grid-template-rows: 1fr; +} +.d-hint .inner { + overflow: hidden; + display: flex; + flex-direction: column; +} +.d-hint .q { + padding: 6px 0; + border-top: 1px solid var(--keisen); + font-size: 11.5px; + color: var(--nezumi); +} +.play .d-hint .fold { + animation: fold-open 360ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} +@keyframes fold-open { + from { + grid-template-rows: 0fr; + } +} + +/* 十 判定が出る */ +.d-rows { + display: flex; + flex-direction: column; + border-top: 1px solid var(--keisen); +} +.d-rows .r { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 6px 0; + border-bottom: 1px solid var(--keisen); + font-size: 12px; +} +.d-rows .k { + color: var(--nezumi); +} +.d-rows em { + font-style: normal; + color: var(--byakuroku); +} +.play .d-rows .r { + animation: row-in 400ms ease-out both; +} +.play .d-rows .r:nth-child(2) { + animation-delay: 200ms; +} +.play .d-rows .r:nth-child(3) { + animation-delay: 400ms; +} +@keyframes row-in { + from { + opacity: 0; + transform: translateY(6px); + } +} + +/* 二 疑問 — 立ちきらない目盛り */ +.d-rail .pin.hollow { + opacity: 0.34; +} +.d-rail .qm { + position: absolute; + top: 1px; + transform: translateX(-50%); + font-size: 13px; + color: var(--suou-t); +} +.play .doubt .pin.wobble { + animation: waver 900ms ease-in-out both; +} +.play .doubt .qm { + animation: at-in 360ms 260ms ease-out both; +} +@keyframes waver { + 0% { + transform: scaleY(0); + } + 45% { + transform: scaleY(1.18); + } + 72% { + transform: scaleY(0.88); + } +} + +/* 三 ひらめき — 離れた二本が繋がる */ +.d-rail .link { + position: absolute; + top: 11px; + height: 1px; + background: var(--kinari); + transform-origin: 0 50%; +} +.play .spark .link { + animation: draw 520ms 180ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +} +.play .spark .pin { + animation: pin-lift 620ms 180ms ease-out both; +} +@keyframes pin-lift { + 55% { + transform: scaleY(1.5); + } +} + +/* 新事実発見 — 記録は聞き込み中に画面上にないので、帯で被せて知らせる。 + 箱は作らず、二本の罫線と薄い覆いだけ。操作は塞がず、2.6 秒で引く。 */ +.d-over { + position: relative; + height: 112px; + display: flex; + align-items: center; + overflow: hidden; +} +.d-over .bg { + font-size: 12px; + line-height: 1.9; + color: var(--nezumi-dim); +} +.d-over .band { + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + gap: 3px; + padding: 9px 0; + border-top: 1px solid var(--asagi); + border-bottom: 1px solid var(--asagi); + background: rgba(20, 19, 23, 0.93); +} +.d-over .k { + font-family: var(--mono); + font-size: 9.5px; + letter-spacing: 0.24em; + color: var(--asagi-t); +} +.d-over .v { + font-size: 12px; + line-height: 1.6; +} +.play .d-over .band { + animation: band 2600ms both; +} +@keyframes band { + 0% { + opacity: 0; + transform: translateY(-50%) scaleY(0.86); + } + 11% { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + 84% { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + 100% { + opacity: 0; + transform: translateY(-50%) scaleY(1); + } +} +@keyframes draw { + from { + transform: scaleX(0); + } +} + +/* プロローグのせり上げ — 既存 briefing-crawl と同じ考え方 */ +.d-crawl { + position: relative; + height: 112px; + overflow: hidden; +} +.d-crawl .inner { + font-family: var(--mincho); + font-size: 12px; + line-height: 2.2; + letter-spacing: 0.04em; +} +.d-crawl .inner p { + margin: 0 0 14px; +} +.d-crawl .veil { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 34px; + background: linear-gradient(to bottom, transparent, var(--sumi)); +} +.play .d-crawl .inner { + animation: crawl 9s linear both; +} +@keyframes crawl { + from { + transform: translateY(112px); + } + to { + transform: translateY(-100%); + } +} + +/* タイプ送り — アプリ実装と同じ 65ms/字 */ +.d-type { + margin: 0; + font-family: var(--mincho); + font-size: 12.5px; + line-height: 2; + letter-spacing: 0.04em; +} +.play .d-type span { + animation: type-in 1ms both; + animation-delay: calc(var(--i) * 65ms); +} +@keyframes type-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 入り込む — 敷居を越える一度だけ。墨の中から浮かび上がり、焦点が合う。 + 既存 screen-enter は 1.03 倍でほぼ体感できないので、ここだけ強く寄せる。 */ +.d-dive { + position: relative; + height: 112px; + overflow: hidden; + display: flex; + align-items: center; +} +.d-dive .inner { + display: flex; + flex-direction: column; + gap: 13px; + width: 100%; +} +.d-dive .ttl { + font-family: var(--mincho); + font-size: 15px; + letter-spacing: 0.05em; +} +.d-dive .bar { + position: relative; + height: 1px; + background: var(--keisen); +} +.d-dive .bar i { + position: absolute; + top: -6px; + width: 2px; + height: 13px; +} +.d-dive .who { + font-size: 12px; + color: var(--asagi-t); +} +.d-dive .veil { + position: absolute; + inset: 0; + background: var(--sumi); + opacity: 0; +} +.play .d-dive .inner { + animation: dive 760ms cubic-bezier(0.16, 1, 0.3, 1) both; +} +.play .d-dive .veil { + animation: unveil 560ms ease-out both; +} +@keyframes dive { + from { + opacity: 0; + transform: scale(1.16); + filter: blur(8px); + } +} +@keyframes unveil { + from { + opacity: 1; + } +} + +/* 動きが苦手な人には出さない。演出のために体調を崩させる理由はない。 + 要素は動いた後の姿で書いてあるので、切っても完成形が残る。 */ +@media (prefers-reduced-motion: reduce) { + .play .d-rail .pin.fresh, + .play .d-rail .at, + .play .d-turn .l, + .play .d-hint .fold, + .play .d-rows .r, + .play .doubt .pin.wobble, + .play .doubt .qm, + .play .spark .link, + .play .spark .pin, + .play .d-over .band, + .play .d-crawl .inner, + .play .d-type span, + .play .d-dive .inner, + .play .d-dive .veil { + animation: none; + } +} + +/* + * 一枚に一つずつ載せる頁。枠の外側に余白を置かないのは、この頁をそのまま + * ストーリーボードの升目へ嵌めるため。余白は嵌めた側が持つ。 + * + * .motions より後ろに置く。前に置くと、あとから来る低い詳細度の .motions が + * 勝ってしまい、升目の指定が効かない。 + */ +.sheet.one { + padding: 0; +} + +.sheet.one .motions { + grid-template-columns: 1fr; + border: 0; + background: none; +} diff --git a/mocks/effects/_effects.js b/mocks/effects/_effects.js new file mode 100644 index 0000000..1393c97 --- /dev/null +++ b/mocks/effects/_effects.js @@ -0,0 +1,32 @@ +// タイプ送りは一字ずつ遅らせる。順番だけ CSS に渡して、時間の決定は CSS 側に残す。 +for (const el of document.querySelectorAll('[data-type]')) { + const chars = [...el.dataset.type] + el.replaceChildren( + ...chars.map((ch, i) => { + const s = document.createElement('span') + s.textContent = ch + s.style.setProperty('--i', String(i)) + return s + }), + ) +} + +for (const m of document.querySelectorAll('.motion')) { + // クラスを外して強制的に再計算させないと、同じアニメーションは再生し直されない。 + const play = () => { + m.classList.remove('play') + void m.offsetWidth + m.classList.add('play') + } + m.querySelector('.replay').addEventListener('click', play) + new IntersectionObserver( + (entries, obs) => { + for (const e of entries) + if (e.isIntersecting) { + play() + obs.unobserve(e.target) + } + }, + { threshold: 0.55 }, + ).observe(m) +} diff --git a/mocks/effects/band.html b/mocks/effects/band.html new file mode 100644 index 0000000..85d05d9 --- /dev/null +++ b/mocks/effects/band.html @@ -0,0 +1,32 @@ + + + + +AlibAI — 四 新事実発見 + + + + + + +
+
+
+
四 新事実発見
+
+
+ 窓口の受付は午後七時八分でした。レシートも残っています。 + + 新事実 + 牧野は午後六時三十五分に店を出たと述べた + +
+
+

記録は聞き込み中に画面上にないので、増えたことを帯で被せて知らせます。箱にはせず、二本の罫線と薄い覆いだけ。 + 操作は塞がず 2.6 秒で引きます。祝いはしません——増えたのは事実であって手柄ではないので。

+
+
+
+ + + diff --git a/mocks/effects/crawl.html b/mocks/effects/crawl.html new file mode 100644 index 0000000..912a0fe --- /dev/null +++ b/mocks/effects/crawl.html @@ -0,0 +1,31 @@ + + + + +AlibAI — 七 記録のせり上げ + + + + + + +
+
+
+
七 記録のせり上げ
+
+
+
+

——事件の記録を読み上げます。

+

午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。

+
+ +
+
+

記録を下から流します。既存の briefing-crawl と同じ作りで、移動距離は本文の高さから決まるので、長い記録ほど自然に長く流れます。下端は地に溶かして、切れ目を作らない。

+
+
+
+ + + diff --git a/mocks/effects/dive.html b/mocks/effects/dive.html new file mode 100644 index 0000000..966a544 --- /dev/null +++ b/mocks/effects/dive.html @@ -0,0 +1,38 @@ + + + + +AlibAI — 八 入り込む + + + + + + +
+
+
+
八 入り込む
+
+
+
+ 雨の古書店、十九時八分のレシート + + + + + + 牧野千尋 +
+ +
+
+

記録を読み終えて、その場に入る敷居。墨の中から浮かび上がり、寄りが戻って焦点が合います。 + 0.76 秒。強く動かすのはこの一度だけで、他の九つは合図に徹します。既存の screen-enter は + 1.03 倍で体感が無いので、ここに寄せた分だけ他から引きました。

+
+
+
+ + + diff --git a/mocks/effects/doubt.html b/mocks/effects/doubt.html new file mode 100644 index 0000000..8ac4871 --- /dev/null +++ b/mocks/effects/doubt.html @@ -0,0 +1,30 @@ + + + + +AlibAI — 二 疑問 + + + + + + +
+
+
+
二 疑問
+
+
+ + + + +
+
+

辻褄の合わない供述。目盛りは立とうとして、立ちきらずに淡いまま残ります。question mark は出しますが、色は容疑者の顔料のまま——画面が先に誰かを疑わないために。

+
+
+
+ + + diff --git a/mocks/effects/fold.html b/mocks/effects/fold.html new file mode 100644 index 0000000..774b258 --- /dev/null +++ b/mocks/effects/fold.html @@ -0,0 +1,31 @@ + + + + +AlibAI — 九 選択肢が開く + + + + + + +
+
+
+
九 選択肢が開く
+
+
+ 訊けそうなこと + + レシートを見せてもらえますか + 黒田さんとは話しましたか + +
+
+

畳んであるものが開くので、高さそのものを動かします。中身を透かせるだけだと、下の入力欄が飛んで見えるため。

+
+
+
+ + + diff --git a/mocks/effects/line-in.html b/mocks/effects/line-in.html new file mode 100644 index 0000000..5fb55e1 --- /dev/null +++ b/mocks/effects/line-in.html @@ -0,0 +1,30 @@ + + + + +AlibAI — 五 発話が続く + + + + + + +
+
+
+
五 発話が続く
+
+
+ 牧野千尋 + 午後六時三十五分には店を出ています。 + 窓口の受付は午後七時八分でした。 + ……雨でしたから。 +
+
+

ひとりが続けて喋るとき、一行ずつ置いていきます。間は 0.5 秒。読み終わる前に次が来ると急かされるので、詰めすぎない。

+
+
+
+ + + diff --git a/mocks/effects/pin-rise.html b/mocks/effects/pin-rise.html new file mode 100644 index 0000000..0af8a3b --- /dev/null +++ b/mocks/effects/pin-rise.html @@ -0,0 +1,31 @@ + + + + +AlibAI — 一 目盛りが立つ + + + + + + +
+
+
+
一 目盛りが立つ
+
+
+ + + + + 19:08 +
+
+

供述から時刻が確定した瞬間。軸の下から伸び上がり、遅れて時刻が出ます。この作品でいちばん大事な出来事なので、強めに動くのはここだけ。

+
+
+
+ + + diff --git a/mocks/effects/rows.html b/mocks/effects/rows.html new file mode 100644 index 0000000..836bb87 --- /dev/null +++ b/mocks/effects/rows.html @@ -0,0 +1,29 @@ + + + + +AlibAI — 十 判定が出る + + + + + + +
+
+
+
十 判定が出る
+
+
+ 犯人牧野千尋 正解 + 殺害方法正解 + 動機惜しい +
+
+

上から順に 0.2 秒ずつ。一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になります。

+
+
+
+ + + diff --git a/mocks/effects/spark.html b/mocks/effects/spark.html new file mode 100644 index 0000000..078cae9 --- /dev/null +++ b/mocks/effects/spark.html @@ -0,0 +1,30 @@ + + + + +AlibAI — 三 ひらめき + + + + + + +
+
+
+
三 ひらめき
+
+
+ + + + +
+
+

離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。

+
+
+
+ + + diff --git a/mocks/effects/typing.html b/mocks/effects/typing.html new file mode 100644 index 0000000..26599da --- /dev/null +++ b/mocks/effects/typing.html @@ -0,0 +1,25 @@ + + + + +AlibAI — 六 タイプ送り + + + + + + +
+
+
+
六 タイプ送り
+
+

+
+

一字ずつ送る読み上げ。65 ミリ秒/字は、既にアプリで動いている TypewriterBriefing と同じ値です。

+
+
+
+ + + diff --git a/mocks/mobile/_phone.css b/mocks/mobile/_phone.css index f0afb98..868426a 100644 --- a/mocks/mobile/_phone.css +++ b/mocks/mobile/_phone.css @@ -385,6 +385,8 @@ textarea { /* 最新の発話を下端に置く。交互とは限らないので、上は溢れるに任せる。 */ .log { + /* 新事実の帯を会話へ被せるための基準。位置取りだけで、並びは変わらない。 */ + position: relative; flex: 1; overflow: hidden; padding: 12px; @@ -394,6 +396,50 @@ textarea { gap: 15px; } +/* + * 新事実の帯。聞き込みのあいだ記録は画面上にないので、増えたことは被せて知らせる。 + * 箱は作らず、二本の罫線と薄い覆いだけ。操作を塞がないので pointer-events は殺す。 + */ +.newfact { + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + gap: 3px; + padding: 9px 12px; + border-top: 1px solid var(--asagi); + border-bottom: 1px solid var(--asagi); + background: rgba(20, 19, 23, 0.93); + pointer-events: none; +} +.newfact .k { + font-family: var(--mono); + font-size: 9.5px; + letter-spacing: 0.24em; + color: var(--asagi-fg); +} +.newfact .v { + font-size: 12px; + line-height: 1.6; +} + +/* + * 訊けそうなことの開閉。高さそのものを動かすので、中身は grid の行に入れる。 + * 透かせるだけだと、下の入力欄が動かないまま文字だけ現れて飛んで見える。 + */ +.hint .fold { + display: grid; + grid-template-rows: 1fr; +} +.hint .fold .inner { + overflow: hidden; + display: flex; + flex-direction: column; +} + /* 発話の塊。同じ人が続けて喋るあいだ、名前は一度きりしか出さない。 塊の左に立つ縦罫がその人の顔料で、どこまでが一人の言葉かを示す。 */ .turn { diff --git a/mocks/mobile/accusation.html b/mocks/mobile/accusation.html index 9efa3fa..80819cd 100644 --- a/mocks/mobile/accusation.html +++ b/mocks/mobile/accusation.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
← 聞き込みに戻る diff --git a/mocks/mobile/briefing.html b/mocks/mobile/briefing.html index 6333497..c0a43f8 100644 --- a/mocks/mobile/briefing.html +++ b/mocks/mobile/briefing.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
@@ -41,15 +42,31 @@ ;(function () { var C = Mock.case document.getElementById('no').textContent = '記録 ' + C.no - Mock.readOut( - document.getElementById('crawl'), - C.brief - .map(function (p) { - return '

' + p + '

' - }) - .join(''), - { scroller: document.querySelector('.p-brief') }, - ) + + var crawl = document.getElementById('crawl') + var body = C.brief + .map(function (p) { + return '

' + p + '

' + }) + .join('') + + /* + 記録の見せ方は二通り。既定は 六 タイプ送りで、読む速さをプレイヤーが握る。 + #brief=crawl にすると 七 記録のせり上げ——全文が下から流れる、映画の導入。 + 実装も同じ二択を設定で切り替える(src/client/lib/briefing-mode.ts)。 + */ + if (Mock.params.brief === 'crawl') { + crawl.innerHTML = body + crawl.classList.add('briefing-crawl') + /* + 尺は実装だと本文の長さから決まる。ここは静止画としても見る枚なので、 + 流れ切って空の画面が残らないよう、長めに取って途中で写るようにしてある。 + */ + crawl.style.animationDuration = '60s' + } else { + Mock.readOut(crawl, body, { scroller: document.querySelector('.p-brief') }) + } + Mock.ready() })() diff --git a/mocks/mobile/case-overview.html b/mocks/mobile/case-overview.html index 5de7634..6448b30 100644 --- a/mocks/mobile/case-overview.html +++ b/mocks/mobile/case-overview.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
← 事件を選ぶ diff --git a/mocks/mobile/detective.html b/mocks/mobile/detective.html index e058142..6c1b2ee 100644 --- a/mocks/mobile/detective.html +++ b/mocks/mobile/detective.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
diff --git a/mocks/mobile/interrogation.html b/mocks/mobile/interrogation.html index 8c9a35d..4adc63c 100644 --- a/mocks/mobile/interrogation.html +++ b/mocks/mobile/interrogation.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+ +
← 概要に戻る @@ -77,18 +79,68 @@ document.getElementById('who').style.color = 'var(--' + me.hue + Mock.lit + ')' document.getElementById('pers').textContent = me.persona - document.getElementById('rail').innerHTML = Mock.rail({ segments: st.segments }) + var rail = document.getElementById('rail') + rail.innerHTML = Mock.rail({ segments: st.segments }) + + /* + 一 目盛りが立つ/二 疑問。 + 裏付けの取れた印は軸から伸び上がり、申告だけの印は行き過ぎて戻り、淡いまま残る。 + 印は中に字を持たないので、拡大をそのまま当ててよい。 + */ + Array.prototype.forEach.call(rail.querySelectorAll('.pin'), function (pin, i) { + pin.classList.add(pin.classList.contains('hollow') ? 'waver' : 'pin-rise', 'origin-bottom') + pin.style.animationDelay = 90 * i + 'ms' + }) + + // 最後に裏付けが取れた一本。印が立ち終わってから添う。 + var now = rail.querySelector('.now') + if (now) { + now.classList.add('pin-rise', 'origin-bottom') + now.style.animationDelay = 90 * st.segments.length + 'ms' + } // 訊けそうなことはログの末尾に畳んである。会話はその上に積む。 var hint = document.getElementById('hint') - document.getElementById('log').insertAdjacentHTML('afterbegin', Mock.log(st.log.slice(-8))) + var log = document.getElementById('log') + log.insertAdjacentHTML('afterbegin', Mock.log(st.log.slice(-8))) + + /* + 九 選択肢が開く。畳んであるものが開くので、高さそのものを動かす。 + grid の行を 0fr から起こすため、質問はひとつの器に入れてから包む。 + */ hint.insertAdjacentHTML( 'beforeend', - st.hints - .map(function (q) { - return '' + Mock.esc(q) + '' - }) - .join(''), + '' + + st.hints + .map(function (q) { + return '' + Mock.esc(q) + '' + }) + .join('') + + '', + ) + + /* + 五 発話が続く。一文ずつ置いていく。間は 0.8 秒——読み終わる前に次が来ると急かされる。 + 直近の三行だけを動かす。前から積まれている行まで動かすと、 + 画面を開くたびに会話全体が湧き上がって見え、どれが新しいのか分からなくなる。 + */ + var lines = Array.prototype.slice.call(log.querySelectorAll('.turn .txt')).slice(-3) + lines.forEach(function (line, i) { + line.classList.add('line-in') + line.style.animationDelay = 800 * i + 'ms' + }) + + /* + 四 新事実発見。増えたのは事実であって手柄ではないので、祝わない。 + 実装では 2.6 秒で引くが、モックは静止画としても見るので出したままにする + (引くところは mocks/effects/band.html で見る)。 + */ + log.insertAdjacentHTML( + 'beforeend', + '' + + '新事実' + + '牧野は午後六時三十五分に店を出たと述べた' + + '', ) Mock.ready() diff --git a/mocks/mobile/result.html b/mocks/mobile/result.html index 0f9cc4b..ce296cb 100644 --- a/mocks/mobile/result.html +++ b/mocks/mobile/result.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
@@ -95,6 +96,18 @@ }) .join('') + /* + 十 判定が出る。上から順に 0.2 秒ずつ。 + 一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になる。 + */ + Array.prototype.forEach.call( + document.querySelectorAll('#judge .r'), + function (row, i) { + row.classList.add('row-in') + row.style.animationDelay = 200 * i + 'ms' + }, + ) + document.getElementById('record').innerHTML = [ [win ? '解決タイム' : 'かかった時間', '' + Mock.clock(Mock.total) + ''], ['質問回数', Mock.total + '回'], diff --git a/mocks/mobile/scenario-select.html b/mocks/mobile/scenario-select.html index d880337..cbac941 100644 --- a/mocks/mobile/scenario-select.html +++ b/mocks/mobile/scenario-select.html @@ -10,6 +10,7 @@ href="https://fonts.googleapis.com/css2?family=Shippori+Mincho+B1:wght@500;700&family=Zen+Kaku+Gothic+New:wght@400;500&family=Roboto+Mono:wght@400&display=swap" rel="stylesheet" /> + -
+

AlibAI

聞き込みで、犯人を指し示す

diff --git a/mocks/mobile/settings.html b/mocks/mobile/settings.html index 572c2e3..b640d58 100644 --- a/mocks/mobile/settings.html +++ b/mocks/mobile/settings.html @@ -11,6 +11,7 @@ rel="stylesheet" /> + -
+
← 事件を選ぶ diff --git a/mocks/storyboard.html b/mocks/storyboard.html index 9466b8e..a790680 100644 --- a/mocks/storyboard.html +++ b/mocks/storyboard.html @@ -88,11 +88,13 @@ .paper.phone { width: 195px; height: 422px; } .paper.phone iframe { width: 390px; height: 844px; } /* - 動きの一覧だけは縮めない。画面の見え方ではなく速さと大きさを見る枠なので、 - 50% に縮めると 5px の浮きが 2.5px になり、比べたいものが消える。 + 動きは一つずつ別の枚に分けてあり、ここでは升目に嵌めて並べる。 + 縮めないのは、画面の見え方ではなく速さと大きさを見る枠だから—— + 50% にすると 5px の浮きが 2.5px になり、比べたいものが消える。 */ - .paper.flat { width: 100%; height: 660px; } - .paper.flat iframe { width: 100%; height: 660px; transform: none; } + .fxgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1px; background: var(--keisen); border: 1px solid var(--keisen); } + .fxgrid .paper { width: auto; height: 300px; border: 0; } + .fxgrid iframe { width: 100%; height: 300px; transform: none; } @@ -315,13 +317,24 @@

ALI_FX動き

画面をまたいで使う十の動き。枠の中で もう一度 を押すと再生する。 実装は src/client/index.css のキーフレームと Storybook の - Parts/十の動き にあり、この枠はその原典(mocks/effects.html)を読んでいる。 - 十のうちアプリで実際に動いているのは五・六・七と、画面の入り・ターンの変わり目だけで、 - 残りはまだ部品に当たっていない。 + Parts/十の動き にあり、この升目は mocks/effects/*.html を一枚ずつ読んでいる。 + クラス名はモックと実装で同じにしてあるので、ここで当てた場所がそのまま実装で当てる場所になる + (モック側の語彙は mocks/_motion.css)。

十の動きと、その役目
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/components/NewFactBand.tsx b/src/client/components/NewFactBand.tsx new file mode 100644 index 0000000..14bb67f --- /dev/null +++ b/src/client/components/NewFactBand.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' + +type Props = { + /** 増えたことを伝える一行。証拠の名前か、掴んだ手掛かりの見出し。 */ + text: string +} + +/** 帯を出しておく時間。`band` のキーフレームの尺と揃える。 */ +const VISIBLE_MS = 2600 + +/** + * 新しく分かったことを、会話の上に帯で被せて知らせる。 + * + * 事件の記録は聞き込み中に画面上に無いので、増えたことは被せて伝えるしかない。 + * 箱にはせず、二本の罫線と薄い覆いだけ。操作は塞がない(pointer-events-none) + * ——テンポよく次を訊きたい人の指を、演出で止める理由がない。 + * + * 祝わない。増えたのは事実であって、手柄ではないので。 + * + * 出し直しは呼び出し側が key を差し替えて行う(TurnAnnounce と同じ約束)。 + */ +export const NewFactBand = ({ text }: Props) => { + const [visible, setVisible] = useState(true) + + useEffect(() => { + const timer = setTimeout(() => setVisible(false), VISIBLE_MS) + + return () => clearTimeout(timer) + }, []) + + if (!visible) { + return null + } + + return ( +
+
+ 新事実 + {text} +
+
+ ) +} diff --git a/src/client/hooks/useInterrogation.ts b/src/client/hooks/useInterrogation.ts index f2d9f86..55340a1 100644 --- a/src/client/hooks/useInterrogation.ts +++ b/src/client/hooks/useInterrogation.ts @@ -1,7 +1,13 @@ import { useState } from 'react' import { askTopic, describeError } from '@/client/lib/api' import { mergeById } from '@/client/lib/merge-by-id' -import type { Discovery, Hint, RevelationCard, TurnState } from '@/client/lib/schemas' +import type { + AlibiSegmentData, + Discovery, + Hint, + RevelationCard, + TurnState, +} from '@/client/lib/schemas' import { advanceTurn } from '@/shared/turns' export type ChatTurn = { @@ -36,6 +42,8 @@ export type InterrogationSeed = { revelations: RevelationCard[] /** 未発見のものの残り件数。難易度が許した粒度までしか入っていない。 */ hint: Hint + /** 時刻表に引ける線。掴んだ手掛かりから引ける分だけがサーバから届く。 */ + alibiSegments: AlibiSegmentData[] questionCount: number turn: TurnState | undefined } @@ -58,6 +66,11 @@ export const useInterrogation = (seed: InterrogationSeed) => { * 定期取得したセッション状態で丸ごと差し替える。 */ const [hint, setHint] = useState(seed.hint) + /** + * 時刻表の線。判定のたびに「その時点で引ける全件」が届くので、足さずに置き換える。 + * こちらで積むと、サーバが引き直した結果(線の終わりが縮むなど)を取りこぼす。 + */ + const [alibiSegments, setAlibiSegments] = useState(seed.alibiSegments) const [questionCount, setQuestionCount] = useState(seed.questionCount) /** * ターンの進行。正典はサーバ側(DOの質問回数から導かれる)で、 @@ -141,6 +154,7 @@ export const useInterrogation = (seed: InterrogationSeed) => { ...prev, [params.characterId]: judgement.suggestedQuestions, })) + setAlibiSegments(judgement.alibiSegments) setQuestionCount(judgement.questionCount) setTurn(judgement.turn) }, @@ -161,6 +175,8 @@ export const useInterrogation = (seed: InterrogationSeed) => { revelations, hint, setHint, + alibiSegments, + setAlibiSegments, questionCount, askingCharacterId, error, diff --git a/src/client/lib/schemas.ts b/src/client/lib/schemas.ts index b9ea775..f699cd6 100644 --- a/src/client/lib/schemas.ts +++ b/src/client/lib/schemas.ts @@ -97,6 +97,23 @@ export const revelationCardSchema = z.object({ }), }) +/** + * 時刻表に引く線。 + * + * 形は src/client/components/AlibiChart.tsx の `AlibiSegment` と揃えてある + * ——あちらが表示の正典なので、ここは受け取る形を写しているだけ。 + * サーバは発見済みの手掛かりから引ける分しか返さないので、聞き込みが進むほど増える。 + */ +export const alibiSegmentSchema = z.object({ + who: z.string().nonempty(), + from: z.string().nonempty(), + to: z.string().nonempty(), + kind: z.enum(['solid', 'claim']), + // 空を許す(在所の分かっていない出来事がある)。上限は表の列幅に収まる長さ。 + place: z.string().max(60), + fix: z.string().nonempty().optional(), +}) + export const sessionStateSchema = z.object({ sessionId: z.uuid(), scenarioId: z.uuid(), @@ -112,6 +129,8 @@ export const sessionStateSchema = z.object({ finished: z.boolean(), discoveries: z.array(discoverySchema), revelations: z.array(revelationCardSchema), + /** 既定を空にしてあるのは、この機能より前のサーバが返さないため。 */ + alibiSegments: z.array(alibiSegmentSchema).default([]), turn: turnStateSchema, }) @@ -121,6 +140,8 @@ export const judgementSchema = z.object({ revealedRevelations: z.array(revelationCardSchema), contradictionPointedOut: z.boolean(), suggestedQuestions: z.array(z.string().nonempty()), + /** 増えた分ではなく、その時点で引ける線すべて。表はこれで置き換える。 */ + alibiSegments: z.array(alibiSegmentSchema).default([]), questionCount: z.number().int(), turn: turnStateSchema, }) @@ -253,6 +274,7 @@ export type ScenarioDetail = z.infer export type CreateSessionResponse = z.infer export type Discovery = z.infer export type RevelationCard = z.infer +export type AlibiSegmentData = z.infer export type TurnState = z.infer export type SessionState = z.infer export type Judgement = z.infer diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index 56123d4..f1650cc 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -3,6 +3,7 @@ import { AlibiChart, type AlibiPerson, type AlibiSegment } from '@/client/compon import { CaseNoteDialog } from '@/client/components/CaseNote' import { edgeOf, inkOf, surfaceOf } from '@/client/components/CharacterAvatar' import { FloorPlanMap } from '@/client/components/FloorPlan' +import { NewFactBand } from '@/client/components/NewFactBand' import { TurnAnnounce } from '@/client/components/TurnAnnounce' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/client/components/ui/dialog' import type { ChatTurn, UseInterrogation } from '@/client/hooks/useInterrogation' @@ -334,6 +335,7 @@ export const InterrogationScreen = ({ conversations, suggestedQuestions, discoveries, + revelations, hint, setHint, askingCharacterId, @@ -348,6 +350,12 @@ export const InterrogationScreen = ({ const [serverState, setServerState] = useState(undefined) // 訊けそうなことは畳める。既定は開いたまま——次の一手が見えているほうが手が止まらない。 const [hintsOpen, setHintsOpen] = useState(true) + /* + * 帯に出す新事実。画面に入った時点で持っているぶんは出さない—— + * 戻ってくるたびに既知の手掛かりを知らされても、何も増えていない。 + */ + const seenFacts = useRef({ discoveries: discoveries.length, revelations: revelations.length }) + const [newFact, setNewFact] = useState<{ key: number; text: string } | undefined>(undefined) const [noteOpen, setNoteOpen] = useState(false) // 見取り図は常時出さない。会話の領域を削るほうが痛いので、見たいときだけ開く。 const [mapOpen, setMapOpen] = useState(false) @@ -364,6 +372,31 @@ export const InterrogationScreen = ({ 0, ) + /* + * 増えた手掛かりを一つだけ帯に出す。二つ以上増えた回でも重ねない—— + * 読み終える前に次が来ると、どちらも読めないまま消える。 + * 掴んだ手掛かりを証拠より先に採るのは、あちらのほうが words になっているため。 + */ + useEffect(() => { + const grewRevelation = revelations.length > seenFacts.current.revelations + const grewDiscovery = discoveries.length > seenFacts.current.discoveries + const lastRevelation = revelations[revelations.length - 1] + const lastDiscovery = discoveries[discoveries.length - 1] + + seenFacts.current = { discoveries: discoveries.length, revelations: revelations.length } + + const text = + grewRevelation && lastRevelation !== undefined + ? lastRevelation.title + : grewDiscovery && lastDiscovery !== undefined + ? lastDiscovery.label + : undefined + + if (text !== undefined) { + setNewFact({ key: discoveries.length + revelations.length, text }) + } + }, [discoveries, revelations]) + /* * 最新の発話を下端に置く。 * @@ -571,7 +604,9 @@ export const InterrogationScreen = ({ ) return ( -
+ // 記録を読み終えて、その場に入る敷居。強く動かすのは全画面を通してここ一度だけで、 + // 他の動きは合図に徹する。他の画面は screen-enter(1.03倍)のまま。 +
{/* 上部バーは題字と計器だけ。机の面をできるだけ広く残す。 */}
@@ -774,6 +809,8 @@ export const InterrogationScreen = ({ aria-hidden="true" className="pointer-events-none absolute inset-x-0 top-0 hidden h-[54px] bg-gradient-to-b from-sumi to-transparent lg:block" /> + + {newFact !== undefined && }
{error !== undefined &&

{error}

} @@ -790,17 +827,29 @@ export const InterrogationScreen = ({ 訊けそうなこと - {hintsOpen && - suggestionsToShow.map((suggestion) => ( - - ))} + {/* + 開くときは高さそのものを動かす。中身を透かせるだけだと、 + 下の入力欄が動かないまま文字だけ現れて、飛んで見える。 + 閉じるときは動かさない——自分で畳んだものが、ゆっくり閉じるのを + 待たされる理由がない。畳んだ先を DOM に残さないので、 + 見えない選択肢へタブで入ってしまうこともない。 + */} + {hintsOpen && ( +
+
+ {suggestionsToShow.map((suggestion) => ( + + ))} +
+
+ )}
)} diff --git a/src/client/screens/ResultScreen.tsx b/src/client/screens/ResultScreen.tsx index fe68fb0..98b99bf 100644 --- a/src/client/screens/ResultScreen.tsx +++ b/src/client/screens/ResultScreen.tsx @@ -116,9 +116,24 @@ const footOf = (main: boolean): string => ? 'flex-1 border border-nezumi p-[11px] text-center font-mincho text-[13px] text-kinari tracking-[0.16em]' : 'flex-1 border border-keisen p-[11px] text-center text-[13px] text-nezumi' -/** 判定・記録の1行。項目名と値を罫線で区切って並べるだけにする。 */ -const Row = ({ label, children }: { label: string; children: ReactNode }) => ( -
+/** + * 上から順に出すときの刻み。 + * + * 一度に出すと読む順が決まらず、遅すぎると結果を待たされている気分になる。 + */ +const ROW_STAGGER_MS = 200 + +/** + * 判定・記録の1行。項目名と値を罫線で区切って並べるだけにする。 + * + * `at` は上から何番目か。渡さなければ他と一緒に出る——順に読ませたいのは + * 判定の三行だけで、記録は表として一度に見えたほうが早い。 + */ +const Row = ({ label, at = 0, children }: { label: string; at?: number; children: ReactNode }) => ( +
{label} {children}
@@ -266,14 +281,14 @@ export const ResultScreen = ({ accuseResult, board, onRetry, onRestart }: Props) */}
- + {correct ? `${truth.culpritName} ` : ''} - + - + {result.motiveCorrect ? ( ) : ( diff --git a/src/client/stories/fixtures.ts b/src/client/stories/fixtures.ts index 6405514..bc6c800 100644 --- a/src/client/stories/fixtures.ts +++ b/src/client/stories/fixtures.ts @@ -182,6 +182,8 @@ export const INTERROGATION_SEED: InterrogationSeed = { ], revelations: [], hint: { mode: 'normal', places: 3, people: 4 }, + // 線は story ごとに違うものを見たいので、種には持たせず画面へ直に渡す。 + alibiSegments: [], questionCount: 3, turn: { turn: 4, diff --git a/src/routes/sessions/$sessionId/index.tsx b/src/routes/sessions/$sessionId/index.tsx index 3b2384e..d3baaee 100644 --- a/src/routes/sessions/$sessionId/index.tsx +++ b/src/routes/sessions/$sessionId/index.tsx @@ -28,6 +28,8 @@ function Interrogation() { sessionId={state.sessionId} detectiveName={state.detectiveName} interrogation={interrogation} + // 刻限と食い違いはまだサーバから出ていないので、線だけを渡す。 + alibi={{ segments: interrogation.alibiSegments }} onAccuse={() => navigate({ to: '/sessions/$sessionId/accuse', params: { sessionId: state.sessionId } }) } diff --git a/src/routes/sessions/$sessionId/route.tsx b/src/routes/sessions/$sessionId/route.tsx index b693c4b..ad5f177 100644 --- a/src/routes/sessions/$sessionId/route.tsx +++ b/src/routes/sessions/$sessionId/route.tsx @@ -37,6 +37,7 @@ function PlaySession() { discoveries: state.discoveries, revelations: state.revelations, hint: state.hint, + alibiSegments: state.alibiSegments, questionCount: state.questionCount, turn: state.turn, }) diff --git a/src/server/game/alibi.ts b/src/server/game/alibi.ts new file mode 100644 index 0000000..aefa98a --- /dev/null +++ b/src/server/game/alibi.ts @@ -0,0 +1,119 @@ +import type { TimelineEvent } from '~/db/timeline-event' + +/** + * 時刻表に引く線。 + * + * 形は src/client/components/AlibiChart.tsx の `AlibiSegment` と揃えてある。 + * あちらが表示の正典なので、ここは同じ形で吐くことだけを守る。 + */ +export type AlibiSegment = { + who: string + from: string + to: string + kind: 'solid' | 'claim' + place: string + fix?: string +} + +/** プレイヤーが掴んだ手掛かり。どちらも authoring のローカル fact ID を指している。 */ +export type RevealedClues = { + /** 発見済み revelation。subject が event ならその出来事を直に指す。 */ + revelations: { subjectType: string; subjectId: string; relatedFacts: string[] }[] + /** 発見済み証拠が裏付ける事実。 */ + evidenceSupports: string[] +} + +const MINUTES = /^(\d{2}):(\d{2})$/ + +const minutesOf = (at: string): number => { + const matched = MINUTES.exec(at) + + return matched === null ? 0 : Number(matched[1]) * 60 + Number(matched[2]) +} + +/** + * どの出来事までプレイヤーが辿り着いたか。 + * + * 鍵は二つ。revelation が出来事そのものを名指ししているか(subject.type === 'event')、 + * あるいは掴んだ手掛かりが、その出来事を構成する事実に触れているか。 + * 後者を入れないと線がほとんど増えない——出来事を直に名指しする revelation は + * 43本の事件を通しても30件ほどしか無く、多くの手掛かりは事実の側から刺さる。 + * + * 真相を丸ごと返さないための関門はここ一つきり。ここを緩めると、 + * 時刻表が最初から埋まった状態で始まる。 + */ +export const knownEventIds = (events: TimelineEvent[], clues: RevealedClues): Set => { + const namedEventIds = new Set( + clues.revelations + .filter((revelation) => revelation.subjectType === 'event') + .map((revelation) => revelation.subjectId), + ) + + const knownFacts = new Set([ + ...clues.revelations.flatMap((revelation) => revelation.relatedFacts), + ...clues.evidenceSupports, + ]) + + return new Set( + events + .filter( + (event) => namedEventIds.has(event.id) || event.facts.some((fact) => knownFacts.has(fact)), + ) + .map((event) => event.id), + ) +} + +/** + * 掴んだ出来事から、人ごとの在所の線を引く。 + * + * 線の終わりは「その人について次に分かっている出来事」まで。分かっていない出来事は + * 終端に使わない——使えば、まだ知らないはずの時刻が線の長さとして漏れる。 + * 結果として、聞き込みが進むほど長い線が短く割れていく。知るほど像が細かくなる、 + * という進み方がそのまま盤面に出る。 + * + * 最後の線は事件の幕切れ(`end`)まで伸ばす。そこで打ち切らないと、 + * 最後に分かった一点が幅ゼロの線になって画面から消える。 + */ +export const alibiSegmentsOf = (params: { + events: TimelineEvent[] + clues: RevealedClues + /** 時刻軸の右端(`HH:mm`)。scenarios.timeEnd をそのまま渡す。 */ + end: string +}): AlibiSegment[] => { + const known = knownEventIds(params.events, params.clues) + const ordered = params.events + .filter((event) => known.has(event.id)) + .toSorted((a, b) => minutesOf(a.at) - minutesOf(b.at)) + + const whoIds = new Set(ordered.flatMap((event) => event.participants)) + + return [...whoIds].flatMap((who) => { + const mine = ordered.filter((event) => event.participants.includes(who)) + + return mine.flatMap((event, index) => { + const next = mine[index + 1] + const to = next === undefined ? params.end : next.at + + // 幕切れより後ろの出来事は線にならない。軸の外なので描く場所が無い。 + if (minutesOf(to) <= minutesOf(event.at)) { + return [] + } + + return [ + { + who, + from: event.at, + to, + kind: event.kind, + place: event.place, + /* + 裏付けのある線だけ、始まりの時刻を留めた印を持つ。 + 申告だけの線に時刻を焼くと、本人が言っただけの時刻が + 記録で確定した時刻と同じ顔で並ぶ。 + */ + ...(event.kind === 'solid' ? { fix: event.at } : {}), + }, + ] + }) + }) +} diff --git a/src/server/routes/sessions.ts b/src/server/routes/sessions.ts index 88b5d2e..e9174c6 100644 --- a/src/server/routes/sessions.ts +++ b/src/server/routes/sessions.ts @@ -13,6 +13,7 @@ import { import type { Db } from '@/server/db/client' import { createDb } from '@/server/db/client' import type { Bindings, Env } from '@/server/env' +import { type AlibiSegment, alibiSegmentsOf } from '@/server/game/alibi' import { remainingHints } from '@/server/game/hints' import { acceptRevealedRevelationIds } from '@/server/game/revelations' import { GAME_RULES } from '@/server/game/rules' @@ -146,6 +147,77 @@ const loadTruth = async (db: Db, scenarioId: string): Promise } } +/** + * 時刻表に引ける線を、発見済みの手掛かりから組み立てる。 + * + * 真相そのもの(timeline_events)は読むが、返すのはプレイヤーが辿り着いた分だけ。 + * 絞り込みは alibiSegmentsOf に閉じているので、ここは材料を揃えるだけにする。 + * + * 時刻軸の右端が無いシナリオでは何も返さない。端が無いと線の終わりを決められず、 + * 描いても軸の外へ流れる。 + */ +const loadAlibiSegments = async ( + db: Db, + scenarioId: string, + discoveredEvidenceIds: string[], + discoveredRevelationIds: string[], +): Promise => { + const [truthRows, scenarioRows] = await Promise.all([ + db + .select({ timelineEvents: scenarioTruths.timelineEvents }) + .from(scenarioTruths) + .where(eq(scenarioTruths.scenarioId, scenarioId)) + .limit(1), + db + .select({ timeEnd: scenarios.timeEnd }) + .from(scenarios) + .where(eq(scenarios.id, scenarioId)) + .limit(1), + ]) + + const events = truthRows[0]?.timelineEvents + const end = scenarioRows[0]?.timeEnd + + if (events === undefined || events.length === 0 || end === null || end === undefined) { + return [] + } + + const [evidenceRows, revelationRows] = await Promise.all([ + discoveredEvidenceIds.length === 0 + ? Promise.resolve([]) + : db + .select({ supports: evidences.supports }) + .from(evidences) + .where( + and(eq(evidences.scenarioId, scenarioId), inArray(evidences.id, discoveredEvidenceIds)), + ), + discoveredRevelationIds.length === 0 + ? Promise.resolve([]) + : db + .select({ + subjectType: revelations.subjectType, + subjectId: revelations.subjectId, + relatedFacts: revelations.relatedFacts, + }) + .from(revelations) + .where( + and( + eq(revelations.scenarioId, scenarioId), + inArray(revelations.id, discoveredRevelationIds), + ), + ), + ]) + + return alibiSegmentsOf({ + events, + end, + clues: { + revelations: revelationRows, + evidenceSupports: evidenceRows.flatMap((row) => row.supports), + }, + }) +} + /** * 設定が無いときの拠りどころ。デプロイ設定がそのまま既定になる。 * @@ -356,6 +428,13 @@ sessionRoutes.get('/api/sessions/:id', validateSessionId, withEnv, async (c) => 発見済みは必ず DO の snapshot から取る。discoveries / revelation_discoveries の 行から数えると、DO への記録は成功して DB の insert が落ちた回でずれる。 */ + const alibiSegments = await loadAlibiSegments( + db, + scenarioId, + snapshot.discoveredEvidenceIds, + snapshot.discoveredRevelationIds, + ) + const subjects = await loadHintSubjects(c.env.SCENARIO_CACHE, db, scenarioId) const hint = remainingHints({ mode: meta.mode, @@ -386,6 +465,7 @@ sessionRoutes.get('/api/sessions/:id', validateSessionId, withEnv, async (c) => category: row.category, subject: { type: row.subjectType, id: row.subjectId }, })), + alibiSegments, turn: turnStateOf(snapshot.questionCount, limits.maxTurns, limits.questionsPerTurn), }) }) @@ -847,6 +927,18 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { const questionCount = persisted.questionCount === undefined ? snapshot.questionCount : persisted.questionCount + /* + 線は毎回まとめて返す。増えた分だけを送ると、受け取る側が積み上げを持つことになり、 + リロードで組み直した表と食い違う。表の側は key で見分けるので、 + 既に立っている線が送り直されても動き直さない。 + */ + const alibiSegments = await loadAlibiSegments( + db, + scenarioId, + snapshot.discoveredEvidenceIds, + snapshot.discoveredRevelationIds, + ) + await stream.writeSSE({ event: 'judgement', data: JSON.stringify({ @@ -860,6 +952,7 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { })), contradictionPointedOut: judgement.contradictionPointedOut, suggestedQuestions: judgement.suggestedQuestions, + alibiSegments, questionCount, turn: turnStateOf(questionCount, limits.maxTurns, limits.questionsPerTurn), }), From 2dca06f1d7b6ab79a52c1b2d64ab51b7a1c527b1 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:19:45 +0000 Subject: [PATCH 05/51] docs(scenario-authoring): document how timeline, evidence, and revelations drive the alibi table Explain that `kind` on facts decides solid vs. dashed lines, that `participants`/`location` on timeline events are required for a row to appear, and that `supports`/`relatedFacts` are what make discovered clues draw new segments. Co-Authored-By: Claude --- docs/scenario-authoring.md | 41 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/scenario-authoring.md b/docs/scenario-authoring.md index 6047f0c..a6bcf72 100644 --- a/docs/scenario-authoring.md +++ b/docs/scenario-authoring.md @@ -108,6 +108,13 @@ facts: - `kind` は `observation` / `physical` / `testimony` / `motive` / `truth` / `other`。 - `secret: true` はプレイヤーへ直接公開してはいけない事実の印。 +**`kind` はアリバイ表の線の種類を決めます。** 出来事を構成する事実に `physical`(物証)か +`observation`(第三者が見たこと)が一つでも混じれば**実線**(裏付けあり)、`testimony` などだけなら +**破線**(本人の申告のみ)になります。本人が黙っても残るものが物証と目撃だから、という区別です。 + +`kind` を省略すると、その事実は裏付けとして数えられません。**全件に書いてください。** +一件でも落とすと、本当は物証で固まっている時間帯が破線で出て、プレイヤーが読み違えます。 + 同じ事実を人物ごとに文章でコピーしてはいけません。時刻を一箇所直したときに他が古いまま残ります。 --- @@ -118,8 +125,8 @@ facts: timeline: - id: fukagawa-leaves at: "19:15" # "HH:mm" で統一する - location: corridor # 省略可 - participants: [fukagawa, kiryu] # characters[].id、省略可 + location: 廊下 # アリバイ表に出る在所 + participants: [fukagawa, kiryu] # characters[].id facts: [fukagawa-left-1915] # 1件以上、必須 description: 深川が電話のため一時的に席を外す。桐生が廊下でこれを見ている。 ``` @@ -128,6 +135,26 @@ timeline: - `description` は**結末画面にそのまま並ぶ一文**なので、読み物として書く。省くと `facts` を ` / ` で機械連結したものが出て不格好になります。全イベントに書いてください。 - 被害者は `characters` に居ないので `participants` には書けません。 +### timeline はアリバイ表になる + +聞き込みの画面には、縦に時刻・横に人が並ぶ**アリバイ表**があります。プレイヤーが手掛かりを +掴むたび、そこへ線が一本ずつ増えていきます。線を作っているのがこの `timeline` です。 + +- **`participants` が、誰の列に線が引かれるかを決めます。** 空にすると、その出来事は表に出ません。 + その時刻にその場に居た人を全員書いてください。 +- **`location` が、線に添う在所の文字になります。**「郵便窓口」「裏の路地」のような**短い名詞句**にする。 + 列の幅は狭く、長い句は入りません。**8文字を超えないこと。** `description` の文をそのまま入れてはいけません。 +- 線の種類(実線か破線か)は `facts` の `kind` から決まります(§5 参照)。 +- 線の終わりは「その人について**次に分かっている**出来事」までです。まだ発見されていない出来事は + 終端に使われないので、知らない時刻が線の長さとして漏れることはありません。最後の一本だけは + 事件の幕切れまで伸びます。 +- したがって、**一人あたりの出来事が一つだけだと、幕切れまで伸びる大雑把な一本にしかなりません。** + 在所が変わる節目を一人につき三つ前後は置いてください。手掛かりを掴むたびに長い線が短く割れ、 + 知るほど像が細かくなる——その手応えが、出来事の数から生まれます。 + +スキーマ上は `location` も `participants` も省略できますが、省略した出来事は表に出ないか、 +在所が空欄の線になります。**新しく書くシナリオでは必ず両方を入れてください。** + --- ## 7. `characters` — 話を聞ける人物 @@ -234,6 +261,9 @@ evidences: - **`reveal.condition` に改行を入れてはいけません。** 判定役のLLMへ1件1行で渡すので、行が割れると証拠が判定不能になります。 - `sources` の `location` は見取り図の部屋 ID。`floorPlan` が `null` なら `location` は使えません。 - `contradicts` は `"lie:"` の形式のみ。実在する嘘だけを指せます。自由文は書けません。 +- **`supports` はアリバイ表の鍵でもあります。** この証拠が裏付ける事実が `timeline` のどれかの + 出来事に含まれていれば、その証拠を掴んだ時点で表に線が引かれます。空にすると、 + 証拠を掴んでも表が動きません。裏付けている事実を必ず書いてください。 ### `revelations` — 解禁されて初めて見える情報 @@ -260,6 +290,10 @@ revelations: - `subject.type` は `character`(人物ID)/ `location`(部屋ID)/ `event`(timeline の ID)。 - `requires` で解禁の順番を作れます。**前提を辿って必ず「前提なし」に行き着くこと。** 循環すると検証で落ちます。 - `revealCondition` も**改行禁止**です。 +- **`subject.type: event` と `relatedFacts` は、どちらもアリバイ表の鍵です。** 出来事を直に名指しするか、 + その出来事を構成する事実に触れていれば、掴んだ時点で表に線が引かれます。 + 聞き込みで在所や時刻が分かる類の情報には、**必ず `relatedFacts` を書いてください。** + ここが空だと、プレイヤーが真相に近づいても表が動かず、何が分かったのか目に見えません。 --- @@ -383,6 +417,9 @@ quality: - **ミスリードを1つ入れてよい。** ただし**それ自体で完結させ、犯人には繋げない**こと。追いかけた末に何も無いのが良いミスリードで、犯人に半分繋がっているものは単に分かりにくいだけです。 - **全員に隠し事を持たせる。** 犯人だけが秘密を持っていると、秘密の有無が答えになってしまいます。 - **証拠には複数の入口を用意する。** 一人にしか聞けない証拠だけで組むと、その人物への質問を思いつかなかったプレイヤーが詰みます。 +- **表が動くかを確かめる。** 手掛かりを一つ掴むごとにアリバイ表へ線が増えるのが、この作品の手応えの中心です。 + `timeline` の各出来事に `participants` と `location` が入っているか、証拠と啓示に `supports` / + `relatedFacts` が入っているかを見てください。ここが空だと、推理が進んでいるのに画面が何も変わりません。 --- From 990db8d2eaa71c77f41c1e7da1ed110c5f66eb0c Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:52:50 +0000 Subject: [PATCH 06/51] feat(scenario): add victim findings and investigability to scenario schema Introduce victim cause-of-death, findings, and found location fields, and derive an investigable flag so the client can decide whether the body can be examined without consulting the truth table. Co-Authored-By: Claude --- db/compile-scenario.ts | 20 +- db/migrations/0010_victim-findings.sql | 5 + db/migrations/meta/0010_snapshot.json | 1136 ++++++++++++++++++++++++ db/migrations/meta/_journal.json | 7 + db/scenario-definition.ts | 112 ++- db/schema.ts | 28 + db/victim-finding.ts | 39 + src/client/lib/schemas.ts | 11 +- src/client/stories/fixtures.ts | 8 +- src/server/cache/scenario.ts | 22 +- src/server/game/revelations.ts | 9 +- src/server/read/scenarios.ts | 27 +- 12 files changed, 1400 insertions(+), 24 deletions(-) create mode 100644 db/migrations/0010_victim-findings.sql create mode 100644 db/migrations/meta/0010_snapshot.json create mode 100644 db/victim-finding.ts diff --git a/db/compile-scenario.ts b/db/compile-scenario.ts index ad800fd..5e1d52b 100644 --- a/db/compile-scenario.ts +++ b/db/compile-scenario.ts @@ -264,6 +264,16 @@ const compileDefinition = ( */ const window = timeWindowOf(definition.timeline) + const victim = definition.victim + + /* + 遺体を調べられる事件かどうかを、ここで公開側へ焼いておく。 + 所見も死因も無いなら調べても何も出ないので、聞き込みの相手に並べない。 + 画面はこの一つだけを見れば決められる——真相のテーブルを覗きに行かずに済む。 + */ + const investigable = + victim !== undefined && (victim.findings.length > 0 || victim.causeOfDeath !== undefined) + return { scenario: { id: scenarioId, @@ -274,8 +284,11 @@ const compileDefinition = ( category: definition.meta.category, timeStart: window === undefined ? null : window.start, timeEnd: window === undefined ? null : window.end, - victimName: definition.victim === undefined ? null : definition.victim.name, - victimIntroduction: definition.victim === undefined ? null : definition.victim.introduction, + victimName: victim === undefined ? null : victim.name, + victimIntroduction: victim === undefined ? null : victim.introduction, + victimFoundAt: victim === undefined || victim.foundAt === undefined ? null : victim.foundAt, + victimFoundIn: victim === undefined || victim.foundIn === undefined ? null : victim.foundIn, + victimInvestigable: investigable, isPublished: options.isPublished, difficulty: definition.meta.difficulty, estimatedMinutes: definition.meta.estimatedMinutes, @@ -291,6 +304,9 @@ const compileDefinition = ( motive: definition.solution.motive, timeline, timelineEvents, + victimCauseOfDeath: + victim === undefined || victim.causeOfDeath === undefined ? null : victim.causeOfDeath, + victimFindings: victim === undefined ? [] : victim.findings, secretKeywords: definition.solution.secretKeywords, }, } diff --git a/db/migrations/0010_victim-findings.sql b/db/migrations/0010_victim-findings.sql new file mode 100644 index 0000000..d8fe7dd --- /dev/null +++ b/db/migrations/0010_victim-findings.sql @@ -0,0 +1,5 @@ +ALTER TABLE `scenario_truths` ADD `victim_cause_of_death` text;--> statement-breakpoint +ALTER TABLE `scenario_truths` ADD `victim_findings` text DEFAULT '[]' NOT NULL;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `victim_found_at` text;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `victim_found_in` text;--> statement-breakpoint +ALTER TABLE `scenarios` ADD `victim_investigable` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/db/migrations/meta/0010_snapshot.json b/db/migrations/meta/0010_snapshot.json new file mode 100644 index 0000000..72ca8ae --- /dev/null +++ b/db/migrations/meta/0010_snapshot.json @@ -0,0 +1,1136 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b2985582-8bb1-4034-8076-0d8e93048e81", + "prevId": "07734c4f-2636-42dd-8648-e6efba562aac", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 9f37dce..fc6ca11 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1788194839194, "tag": "0009_alibi-timeline-events", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1788216627374, + "tag": "0010_victim-findings", + "breakpoints": true } ] } \ No newline at end of file diff --git a/db/scenario-definition.ts b/db/scenario-definition.ts index ef4b1fa..e967c76 100644 --- a/db/scenario-definition.ts +++ b/db/scenario-definition.ts @@ -22,10 +22,49 @@ export const scenarioMetaSchema = z.object({ tags: z.array(nonemptyTextSchema.max(50)).default([]), }) +/** + * 被害者を指すときの ID。 + * + * 被害者は `characters` に居ないので固有のIDを持たない。証拠や啓示の出どころとして + * 名指しするときだけ、この決め打ちの文字列を使う。指す先は一人しか居ないので、 + * 照合すべき一覧も無い。 + */ +export const VICTIM_ID = 'victim' + +/** + * 遺体と現場から分かること。 + * + * 1件1文。人物の心情や動機の解釈は書かない——あれは `revelations` の仕事で、 + * ここに混ぜると「遺体を見ただけで動機が分かる」ことになってしまう。 + * ここに書けるのは、その場で目にできるものだけ。 + */ +export const scenarioVictimFindingSchema = z.object({ + id: localIdSchema, + statement: nonemptyTextSchema, + /** 段階的に見せたいときだけ。形は revelation の解禁前提と同じ。 */ + requires: z + .object({ + revelations: z.array(localIdSchema).default([]), + evidences: z.array(localIdSchema).default([]), + }) + .default({ revelations: [], evidences: [] }), +}) + export const scenarioVictimSchema = z.object({ name: nonemptyTextSchema.max(50), /** 肩書きひとつぶんの短い紹介。「青雨堂店主」のように、役割が分かれば足りる。 */ introduction: nonemptyTextSchema.max(60), + /* + * ここから下は、遺体を調べて初めて画面に出るもの。 + * すべて省略可にしてあるのは、この機能より前に書かれたシナリオを落とさないため。 + * 一つも無いシナリオでは、被害者は聞き込みの相手に並ばない。 + */ + /** 発見時刻。`HH:mm` で、timeline と同じ書き方。 */ + foundAt: timelineAtSchema.optional(), + /** 発見場所。アリバイ表の在所と同じく、列に収まる短い名詞句。 */ + foundIn: nonemptyTextSchema.max(20).optional(), + causeOfDeath: nonemptyTextSchema.max(100).optional(), + findings: z.array(scenarioVictimFindingSchema).default([]), }) export const scenarioFactSchema = z.object({ @@ -83,7 +122,8 @@ export const scenarioCharacterSchema = z.object({ }) export const scenarioRevelationSourceSchema = z.object({ - type: z.enum(['character', 'location']), + // victim のとき id は VICTIM_ID 固定。指す先が一人しか居ないので照合先の一覧を持たない。 + type: z.enum(['character', 'location', 'victim']), id: localIdSchema, revealCondition: nonemptyTextSchema, requires: z @@ -123,7 +163,7 @@ export const scenarioRevelationSchema = z.object({ * 難易度モードの「この人にあと N 件」を数えるのに使う。 */ export const scenarioEvidenceSourceSchema = z.object({ - type: z.enum(['character', 'location']), + type: z.enum(['character', 'location', 'victim']), id: localIdSchema, }) @@ -236,6 +276,29 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin scenario.floorPlan === null ? [] : scenario.floorPlan.rooms.map((room) => room.id), ) + /** + * 出どころが実在するか。駄目なら理由を返す。 + * + * 被害者だけは照合すべき一覧を持たない(事件に一人しか居ない)ので、 + * 「その事件に被害者が居るか」と「決め打ちのIDか」の二点だけを見る。 + */ + const sourceIssue = (source: { type: string; id: string }): string | undefined => { + if (source.type === 'victim') { + if (scenario.victim === undefined) { + return '被害者の居ない事件で type: victim は使えません。' + } + + return source.id === VICTIM_ID + ? undefined + : `type: victim の id は「${VICTIM_ID}」で固定です。` + } + + const exists = + source.type === 'character' ? characterIds.has(source.id) : locationIds.has(source.id) + + return exists ? undefined : `存在しない ${source.type}「${source.id}」を参照しています。` + } + const addDuplicateIssues = ( ids: string[], namespace: string, @@ -276,6 +339,37 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin (index) => ['revelations', index, 'id'], ) + const findings = scenario.victim === undefined ? [] : scenario.victim.findings + + addDuplicateIssues( + findings.map((finding) => finding.id), + 'finding', + (index) => ['victim', 'findings', index, 'id'], + ) + + // 所見の解禁前提。証拠や啓示を書き換えたときに、遺体側だけ古い ID が残るのを防ぐ。 + findings.forEach((finding, findingIndex) => { + finding.requires.evidences.forEach((evidenceId, at) => { + if (!evidenceIds.has(evidenceId)) { + ctx.addIssue({ + code: 'custom', + path: ['victim', 'findings', findingIndex, 'requires', 'evidences', at], + message: `存在しない evidence「${evidenceId}」を参照しています。`, + }) + } + }) + + finding.requires.revelations.forEach((revelationId, at) => { + if (!revelationIds.has(revelationId)) { + ctx.addIssue({ + code: 'custom', + path: ['victim', 'findings', findingIndex, 'requires', 'revelations', at], + message: `存在しない revelation「${revelationId}」を参照しています。`, + }) + } + }) + }) + const lieEntries = scenario.characters.flatMap((character, characterIndex) => character.lies.map((lie, lieIndex) => ({ characterIndex, lie, lieIndex })), ) @@ -413,14 +507,13 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin }) revelation.sources.forEach((source, sourceIndex) => { - const sourceExists = - source.type === 'character' ? characterIds.has(source.id) : locationIds.has(source.id) + const issue = sourceIssue(source) - if (!sourceExists) { + if (issue !== undefined) { ctx.addIssue({ code: 'custom', path: ['revelations', revelationIndex, 'sources', sourceIndex, 'id'], - message: `存在しない ${source.type}「${source.id}」を参照しています。`, + message: issue, }) } @@ -515,14 +608,13 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin // 場所IDは見取り図の部屋IDと文字列で一致しているだけなので、 // 片方を書き換えた瞬間に証拠がどこにも紐づかなくなる。ここで落とす。 evidence.sources.forEach((source, sourceIndex) => { - const sourceExists = - source.type === 'character' ? characterIds.has(source.id) : locationIds.has(source.id) + const issue = sourceIssue(source) - if (!sourceExists) { + if (issue !== undefined) { ctx.addIssue({ code: 'custom', path: ['evidences', evidenceIndex, 'sources', sourceIndex, 'id'], - message: `存在しない ${source.type}「${source.id}」を参照しています。`, + message: issue, }) } }) diff --git a/db/schema.ts b/db/schema.ts index 47ddf91..0091c6b 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -4,6 +4,7 @@ import type { Detective } from './detective' import type { FloorPlanInput } from './floor-plan' import type { ScenarioEvidenceSource, ScenarioRevelationSource } from './scenario-definition' import type { TimelineEvent } from './timeline-event' +import type { VictimFinding } from './victim-finding' /** * プレイヤーが演じる探偵の形と検証は db/detective.ts が正典。 @@ -83,6 +84,20 @@ export const scenarios = sqliteTable('scenarios', { */ victimName: text('victim_name'), victimIntroduction: text('victim_introduction'), + /** + * 発見時刻と発見場所。事件の記録が既に語っている情報なので公開側に置く。 + * 伏せても意味が無いし、伏せると時刻表の被害者の列に何も置けなくなる。 + */ + victimFoundAt: text('victim_found_at'), + victimFoundIn: text('victim_found_in'), + /** + * 遺体を調べられる事件か。 + * + * 所見そのものは真相側にあるが、聞き込みの相手に被害者を並べるかどうかは + * 公開側だけを読んで決めたい(画面のために真相のテーブルへ触りたくない)。 + * だからここに焼く。所見も死因も無いシナリオでは false。 + */ + victimInvestigable: integer('victim_investigable', { mode: 'boolean' }).notNull().default(false), authorId: text('author_id'), isPublished: integer('is_published', { mode: 'boolean' }).notNull().default(false), difficulty: integer('difficulty').notNull().default(3), @@ -127,6 +142,19 @@ export const scenarioTruths = sqliteTable('scenario_truths', { .$type() .notNull() .default([]), + /** + * 死因と、遺体・現場から分かること。 + * + * 真相側に置くのは、これが「調べて初めて分かる」ものだから。発見時刻と発見場所は + * 事件の記録が既に語っているので公開側(scenarios)に置いてある。 + * + * 既定が空なのは、この列より前に焼かれたシナリオ行があるため。 + */ + victimCauseOfDeath: text('victim_cause_of_death'), + victimFindings: text('victim_findings', { mode: 'json' }) + .$type() + .notNull() + .default([]), /** 出力フィルタが漏洩検知に使う秘匿キーワード */ secretKeywords: text('secret_keywords', { mode: 'json' }).$type().notNull(), }) diff --git a/db/victim-finding.ts b/db/victim-finding.ts new file mode 100644 index 0000000..3ae4a54 --- /dev/null +++ b/db/victim-finding.ts @@ -0,0 +1,39 @@ +import { z } from 'zod' + +/** + * 遺体と現場から分かること、を焼いた形。 + * + * authoring 側(`db/scenario-definition.ts` の `scenarioVictimFindingSchema`)と + * 同じ形をそのまま持つ。潰さないのは、解禁の前提を実行時に評価する必要があるため + * ——文章に均してしまうと「まだ見せてはいけない所見」を選り分けられなくなる。 + */ +export const victimFindingSchema = z.object({ + id: z.string().nonempty(), + statement: z.string().nonempty(), + requires: z.object({ + revelations: z.array(z.string().nonempty()), + evidences: z.array(z.string().nonempty()), + }), +}) + +export type VictimFinding = z.infer + +/** + * いま見せてよい所見だけを選ぶ。 + * + * 前提を満たしていない所見は伏せる。伏せた所見の存在も伝えない—— + * 「まだ何かある」と分かってしまうと、条件を満たす前から答えの形が見えてしまう。 + */ +export const availableFindings = ( + findings: VictimFinding[], + discovered: { evidenceIds: string[]; revelationIds: string[] }, +): VictimFinding[] => { + const evidences = new Set(discovered.evidenceIds) + const revelations = new Set(discovered.revelationIds) + + return findings.filter( + (finding) => + finding.requires.evidences.every((id) => evidences.has(id)) && + finding.requires.revelations.every((id) => revelations.has(id)), + ) +} diff --git a/src/client/lib/schemas.ts b/src/client/lib/schemas.ts index f699cd6..f59551a 100644 --- a/src/client/lib/schemas.ts +++ b/src/client/lib/schemas.ts @@ -54,7 +54,16 @@ export const scenarioDetailSchema = scenarioSummarySchema.omit({ characterCount: * 亡くなった人。characters には入らない——あちらは聞き込みの相手の一覧なので、 * 混ぜると話しかけられる列に死者が並ぶ。 */ - victim: z.object({ name: z.string().nonempty(), introduction: z.string().nonempty() }).nullable(), + victim: z + .object({ + name: z.string().nonempty(), + introduction: z.string().nonempty(), + foundAt: z.string().nonempty().nullable(), + foundIn: z.string().nonempty().nullable(), + /** 遺体を調べられる事件か。false なら聞き込みの相手に並べない。 */ + investigable: z.boolean(), + }) + .nullable(), characters: z.array(characterSchema), }) diff --git a/src/client/stories/fixtures.ts b/src/client/stories/fixtures.ts index bc6c800..099070c 100644 --- a/src/client/stories/fixtures.ts +++ b/src/client/stories/fixtures.ts @@ -34,7 +34,13 @@ export const SCENARIO: ScenarioDetail = { ].join('\n\n'), floorPlan: null, timeWindow: { start: '18:20', end: '19:20' }, - victim: { name: '水野英治', introduction: '青雨堂店主。初版本の商談を抱えていた' }, + victim: { + name: '水野英治', + introduction: '青雨堂店主。初版本の商談を抱えていた', + foundAt: '19:15', + foundIn: '店の奥', + investigable: true, + }, characters: [ { id: MAKINO, name: '牧野千尋', publicIntroduction: '店員。書誌と発送手順には強い' }, { id: KURODA, name: '黒田征司', publicIntroduction: '収集家。初版本の商談に来ていた' }, diff --git a/src/server/cache/scenario.ts b/src/server/cache/scenario.ts index fb1a8e7..9168031 100644 --- a/src/server/cache/scenario.ts +++ b/src/server/cache/scenario.ts @@ -1,6 +1,6 @@ import { count, eq } from 'drizzle-orm' import type { Db } from '@/server/db/client' -import type { HintItem } from '@/server/game/hints' +import type { HintItem, HintSource } from '@/server/game/hints' import { eligibleRevelationCandidates, type RevelationCandidate, @@ -278,14 +278,24 @@ export const loadHintSubjects = async ( ]) const plan = scenarioRows[0] + + /* + * 残り件数を数えるとき、被害者は人物として扱う。 + * + * 画面でも聴く相手の並びに一人分として出るので、そこだけ別の枠で数えると + * 「人にあと2件」と出ているのに遺体から3件目が出てくる、という食い違いになる。 + * 解禁の判定(RevelationSourceType)では victim のまま扱うので、混ざるのはここだけ。 + */ + const asHintSource = (source: { type: string; id: string }): HintSource => + source.type === 'location' + ? { type: 'location', id: source.id } + : { type: 'character', id: source.id } + const subjects: HintSubjects = { // revelation の source は解禁条件と前提条件も持っているが、数えるのに要るのは行き先だけ。 items: [ - ...revelationRows.map((row) => ({ - id: row.id, - sources: row.sources.map((source) => ({ type: source.type, id: source.id })), - })), - ...evidenceRows.map((row) => ({ id: row.id, sources: row.sources })), + ...revelationRows.map((row) => ({ id: row.id, sources: row.sources.map(asHintSource) })), + ...evidenceRows.map((row) => ({ id: row.id, sources: row.sources.map(asHintSource) })), ], roomIds: plan === undefined || plan.floorPlan === null diff --git a/src/server/game/revelations.ts b/src/server/game/revelations.ts index a886a12..c3f36ff 100644 --- a/src/server/game/revelations.ts +++ b/src/server/game/revelations.ts @@ -1,4 +1,11 @@ -export type RevelationSourceType = 'character' | 'location' +/** + * 手掛かりの出どころ。 + * + * victim は遺体と現場。喋らない相手なので会話では出てこないが、解禁の判定は + * 人物と同じ仕組みに乗せる——出どころが増えただけ、という扱いにしておくと、 + * 判定役にも作者にも新しい規則を覚えさせずに済む。 + */ +export type RevelationSourceType = 'character' | 'location' | 'victim' export type RevelationSource = { type: RevelationSourceType diff --git a/src/server/read/scenarios.ts b/src/server/read/scenarios.ts index 5c0a1df..0efdc60 100644 --- a/src/server/read/scenarios.ts +++ b/src/server/read/scenarios.ts @@ -23,8 +23,20 @@ export type ScenarioDetail = { briefing: string /** 事件が動いていた時間の幅。軸を引けないシナリオもあるので null あり。 */ timeWindow: { start: string; end: string } | null - /** 亡くなった人。聞き込みの相手ではないので characters とは別に返す。 */ - victim: { name: string; introduction: string } | null + /** + * 亡くなった人。聞き込みの相手ではないので characters とは別に返す。 + * + * `investigable` は「遺体を調べられる事件か」。所見も死因も無いシナリオでは false で、 + * 画面はこれを見て聞き込みの相手に並べるかどうかを決める。所見そのものは真相側にあり、 + * ここには出てこない——調べて初めて分かるものなので。 + */ + victim: { + name: string + introduction: string + foundAt: string | null + foundIn: string | null + investigable: boolean + } | null /** 既定値を埋めたあとの形。列そのものの型(入力側)ではない。 */ floorPlan: FloorPlan | null difficulty: number @@ -93,6 +105,9 @@ export const findScenarioDetail = async ( timeEnd: scenarios.timeEnd, victimName: scenarios.victimName, victimIntroduction: scenarios.victimIntroduction, + victimFoundAt: scenarios.victimFoundAt, + victimFoundIn: scenarios.victimFoundIn, + victimInvestigable: scenarios.victimInvestigable, difficulty: scenarios.difficulty, estimatedMinutes: scenarios.estimatedMinutes, }) @@ -146,7 +161,13 @@ export const findScenarioDetail = async ( victim: scenario.victimName === null || scenario.victimIntroduction === null ? null - : { name: scenario.victimName, introduction: scenario.victimIntroduction }, + : { + name: scenario.victimName, + introduction: scenario.victimIntroduction, + foundAt: scenario.victimFoundAt, + foundIn: scenario.victimFoundIn, + investigable: scenario.victimInvestigable, + }, characters: characterRows, } } From 171f5f2f28b59f365cd3184bd4cad18f29fd9f42 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:23:42 +0000 Subject: [PATCH 07/51] feat(examination): let players examine the victim during interrogation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a victim examination flow: compiled scenarios remap victim findings' evidence/revelation requirements to their scenario uuids so discovery gates actually resolve, a new server-side sheet builder exposes only unlocked findings without leaking that hidden ones exist, and the interrogation screen adds the victim as a special subject ("所見") alongside characters when the case allows examining the body. Co-Authored-By: Claude --- __tests__/db/compile-scenario.test.ts | 29 ++- __tests__/db/scenario-definition.test.ts | 62 +++++ __tests__/server/examination.test.ts | 117 +++++++++ db/compile-scenario.ts | 17 +- db/scenarios/tsukimisou.yaml | 30 +++ docs/scenario-authoring.md | 44 ++++ src/client/lib/schemas.ts | 11 +- src/client/screens/InterrogationScreen.tsx | 64 +++-- src/server/game/examination.ts | 63 +++++ src/server/game/rules.ts | 19 ++ src/server/llm/examiner.ts | 63 +++++ src/server/routes/sessions.ts | 266 +++++++++++++++------ src/shared/turns.ts | 9 + 13 files changed, 700 insertions(+), 94 deletions(-) create mode 100644 __tests__/server/examination.test.ts create mode 100644 src/server/game/examination.ts create mode 100644 src/server/llm/examiner.ts diff --git a/__tests__/db/compile-scenario.test.ts b/__tests__/db/compile-scenario.test.ts index e3fdd3c..52d7f92 100644 --- a/__tests__/db/compile-scenario.test.ts +++ b/__tests__/db/compile-scenario.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test' import { compileScenario } from '~/db/compile-scenario' import { parseFloorPlan } from '~/db/floor-plan' import { TSUKIMISOU_PLAN } from '~/db/floor-plans/tsukimisou' -import type { ScenarioDefinitionInput } from '~/db/scenario-definition' +import { type ScenarioDefinitionInput, VICTIM_ID } from '~/db/scenario-definition' import { loadScenarioYaml } from '~/db/scenario-file' const TSUKIMISOU_SCENARIO = await loadScenarioYaml('tsukimisou') @@ -151,7 +151,7 @@ describe('compileScenario: 月見荘のコンパイル', () => { expect(compiled.scenario.isPublished).toBe(true) expect(compiled.scenario.title).toBe('月見荘、十七回忌の夜') expect(compiled.characters).toHaveLength(3) - expect(compiled.evidences).toHaveLength(6) + expect(compiled.evidences).toHaveLength(7) expect(compiled.revelations).toHaveLength(2) }) @@ -221,6 +221,11 @@ describe('compileScenario: プロンプトを壊さない不変条件', () => { if (source.type === 'character') { expect(characterIds.has(source.id)).toBe(true) expect(roomIds.has(source.id)).toBe(false) + } else if (source.type === 'victim') { + // 被害者は一人しか居ないので採番しない。決め打ちのIDのまま焼かれる。 + expect(source.id).toBe(VICTIM_ID) + expect(characterIds.has(source.id)).toBe(false) + expect(roomIds.has(source.id)).toBe(false) } else { expect(roomIds.has(source.id)).toBe(true) expect(characterIds.has(source.id)).toBe(false) @@ -501,3 +506,23 @@ describe('compileScenario: 被害者', () => { expect(result.scenario.timeEnd).not.toBeNull() }) }) + +describe('被害者の所見', () => { + test('解禁の前提は uuid へ採番される', () => { + // ローカルIDのまま焼くと、DOが持つ uuid と突き合わない=前提が永久に満たされない。 + const evidenceIds = new Set(compiled.evidences.map((evidence) => evidence.id)) + const required = compiled.truth.victimFindings.flatMap((finding) => finding.requires.evidences) + + expect(required.length).toBeGreaterThan(0) + + for (const id of required) { + expect(evidenceIds.has(id)).toBe(true) + } + }) + + test('遺体を調べられる事件として焼かれる', () => { + expect(compiled.scenario.victimInvestigable).toBe(true) + expect(compiled.scenario.victimFoundAt).toBe('20:30') + expect(compiled.truth.victimCauseOfDeath).not.toBeNull() + }) +}) diff --git a/__tests__/db/scenario-definition.test.ts b/__tests__/db/scenario-definition.test.ts index 27aa427..cf34466 100644 --- a/__tests__/db/scenario-definition.test.ts +++ b/__tests__/db/scenario-definition.test.ts @@ -15,6 +15,7 @@ import { scenarioSecretSchema, scenarioSolutionSchema, scenarioTimelineEventSchema, + VICTIM_ID, } from '~/db/scenario-definition' const validScenario: ScenarioDefinition = { @@ -1474,3 +1475,64 @@ describe('ScenarioDefinitionSchema: evidence の sources', () => { expect(parsed.success ? requiredAt(parsed.data.evidences, 0).sources : undefined).toEqual([]) }) }) + +describe('被害者を出どころにする', () => { + test('type: victim は id が victim なら通る', () => { + const scenario = makeScenario() + scenario.victim = { name: '被害者', introduction: '館の主', findings: [] } + const evidence = scenario.evidences[0] + + if (evidence === undefined) { + throw new Error('この試験は証拠が1件以上ある前提で組んである。') + } + + evidence.sources = [{ type: 'victim', id: VICTIM_ID }] + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(true) + }) + + test('被害者の居ない事件では使えない', () => { + const scenario = makeScenario() + scenario.victim = undefined + const evidence = scenario.evidences[0] + + if (evidence === undefined) { + throw new Error('この試験は証拠が1件以上ある前提で組んである。') + } + + evidence.sources = [{ type: 'victim', id: VICTIM_ID }] + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(false) + }) + + test('id は victim で固定', () => { + const scenario = makeScenario() + scenario.victim = { name: '被害者', introduction: '館の主', findings: [] } + const evidence = scenario.evidences[0] + + if (evidence === undefined) { + throw new Error('この試験は証拠が1件以上ある前提で組んである。') + } + + evidence.sources = [{ type: 'victim', id: 'ryoko' }] + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(false) + }) + + test('所見の解禁前提は実在する証拠しか指せない', () => { + const scenario = makeScenario() + scenario.victim = { + name: '被害者', + introduction: '館の主', + findings: [ + { + id: 'draft', + statement: '草案が伏せてある。', + requires: { revelations: [], evidences: ['no-such-evidence'] }, + }, + ], + } + + expect(ScenarioDefinitionSchema.safeParse(scenario).success).toBe(false) + }) +}) diff --git a/__tests__/server/examination.test.ts b/__tests__/server/examination.test.ts new file mode 100644 index 0000000..30ecb8a --- /dev/null +++ b/__tests__/server/examination.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, test } from 'bun:test' +import { buildVictimSheet, type VictimRecord } from '@/server/game/examination' +import { availableFindings, type VictimFinding } from '~/db/victim-finding' + +const finding = ( + id: string, + statement: string, + requires?: VictimFinding['requires'], +): VictimFinding => ({ + id, + statement, + requires: requires === undefined ? { revelations: [], evidences: [] } : requires, +}) + +const NOTHING = { evidenceIds: [], revelationIds: [] } + +const record = (overrides: Partial): VictimRecord => ({ + name: '高瀬涼子', + introduction: '月見荘女将', + briefing: '——事件の記録を読み上げます。', + foundAt: '20:30', + foundIn: '書斎', + causeOfDeath: null, + findings: [], + ...overrides, +}) + +describe('availableFindings', () => { + test('前提のない所見はそのまま出る', () => { + const findings = [finding('a', '争った跡が無い。')] + + expect(availableFindings(findings, NOTHING)).toHaveLength(1) + }) + + test('前提を満たしていない所見は落ちる', () => { + const findings = [ + finding('a', '争った跡が無い。'), + finding('b', '草案が伏せてある。', { revelations: [], evidences: ['will-record'] }), + ] + + expect(availableFindings(findings, NOTHING).map((item) => item.id)).toEqual(['a']) + }) + + test('前提を満たせば出る', () => { + const findings = [ + finding('b', '草案が伏せてある。', { revelations: [], evidences: ['will-record'] }), + ] + const discovered = { evidenceIds: ['will-record'], revelationIds: [] } + + expect(availableFindings(findings, discovered).map((item) => item.id)).toEqual(['b']) + }) + + test('前提が複数あるときは全部揃って初めて出る', () => { + const findings = [ + finding('b', '草案が伏せてある。', { revelations: ['motive'], evidences: ['will-record'] }), + ] + + expect( + availableFindings(findings, { evidenceIds: ['will-record'], revelationIds: [] }), + ).toEqual([]) + }) +}) + +describe('buildVictimSheet', () => { + test('所見も死因も無ければ組まない', () => { + // 空のシートを渡すと、モデルが埋めようとして所見を作りはじめる。 + expect(buildVictimSheet(record({}), NOTHING)).toBeUndefined() + }) + + test('死因だけでも組む', () => { + const sheet = buildVictimSheet(record({ causeOfDeath: '中毒死' }), NOTHING) + + expect(sheet).toContain('中毒死') + expect(sheet).toContain('20:30') + expect(sheet).toContain('書斎') + }) + + test('伏せた所見は本文に現れない', () => { + const sheet = buildVictimSheet( + record({ + causeOfDeath: '中毒死', + findings: [ + finding('a', '争った跡が無い。'), + finding('b', '草案が伏せてある。', { revelations: [], evidences: ['will-record'] }), + ], + }), + NOTHING, + ) + + expect(sheet).toContain('争った跡が無い。') + expect(sheet).not.toContain('草案') + }) + + test('伏せた所見があること自体も漏らさない', () => { + // 「まだ何かある」と分かると、前提を満たす前に答えの形が見えてしまう。 + const sheet = buildVictimSheet( + record({ + causeOfDeath: '中毒死', + findings: [finding('b', '草案。', { revelations: [], evidences: ['will-record'] })], + }), + NOTHING, + ) + + expect(sheet).not.toContain('will-record') + expect(sheet).not.toMatch(/非公開|伏せ|まだ見せ/) + }) + + test('分かっていないことは行ごと出さない', () => { + const sheet = buildVictimSheet( + record({ foundAt: null, foundIn: null, findings: [finding('a', '争った跡が無い。')] }), + NOTHING, + ) + + expect(sheet).not.toContain('発見時刻') + expect(sheet).not.toContain('発見場所') + }) +}) diff --git a/db/compile-scenario.ts b/db/compile-scenario.ts index 5e1d52b..41bb4f2 100644 --- a/db/compile-scenario.ts +++ b/db/compile-scenario.ts @@ -306,7 +306,22 @@ const compileDefinition = ( timelineEvents, victimCauseOfDeath: victim === undefined || victim.causeOfDeath === undefined ? null : victim.causeOfDeath, - victimFindings: victim === undefined ? [] : victim.findings, + /* + 所見の解禁前提も採番する。DO が持っている発見済みのIDは uuid なので、 + authoring のローカルIDのまま焼くと、前提が永久に満たされない所見になる。 + (所見自身の id は他から参照されないので、そのまま残す。) + */ + victimFindings: + victim === undefined + ? [] + : victim.findings.map((finding) => ({ + id: finding.id, + statement: finding.statement, + requires: { + revelations: finding.requires.revelations.map(revelationUuid), + evidences: finding.requires.evidences.map(evidenceUuid), + }, + })), secretKeywords: definition.solution.secretKeywords, }, } diff --git a/db/scenarios/tsukimisou.yaml b/db/scenarios/tsukimisou.yaml index 74898cf..319cf03 100644 --- a/db/scenarios/tsukimisou.yaml +++ b/db/scenarios/tsukimisou.yaml @@ -34,6 +34,23 @@ meta: victim: name: 高瀬涼子 introduction: 老舗旅館「月見荘」女将 + foundAt: "20:30" + foundIn: 書斎 + causeOfDeath: 植物性の毒物による中毒死 + # 遺体と現場から目にできることだけを書く。誰がやったかの解釈は書かない。 + findings: + - id: no-struggle + statement: 争った跡が無い。着衣も髪も乱れておらず、文机の上も片付いたままになっている。 + - id: numbness-signs + statement: 唇のまわりと指先に、しびれが出たときの跡が残っている。 + - id: single-glass + statement: 文机に、飲みかけのグラスが一つだけ置かれている。誰かと酌み交わした跡は無い。 + # 後継者指定を知ってから読むと意味が変わる草案。順序を作るために前提を置く。 + - id: heir-draft + statement: 硯箱の下に、書き直しかけの遺言書の草案が伏せてある。後継者の項に線が引かれ、余白に書き込みがある。 + requires: + evidences: + - will-record briefing: |- ——事件の記録を読み上げます。 @@ -566,6 +583,19 @@ evidences: supports: - mizuki-named-heir contradicts: [] + # 遺体を調べて初めて出てくる一件。動機へ繋がる手掛かりを、人の口以外にも一つ置く。 + - id: will-draft + label: 書き直しかけの遺言書の草案 + description: 後継者の項に線が引かれ、余白に書き込みがある。日付はごく最近。 + reveal: + mode: conversation + condition: プレイヤーが遺体または書斎の文机まわりを調べ、探偵が硯箱の下の草案に触れたら開示する。 + sources: + - type: victim + id: victim + supports: + - ryoko-reconsidering-heir + contradicts: [] - id: brandy-bottle label: 書斎に残されたブランデーの瓶とグラス description: 瓶とグラスの双方から毒物の反応が出ている。 diff --git a/docs/scenario-authoring.md b/docs/scenario-authoring.md index a6bcf72..3659803 100644 --- a/docs/scenario-authoring.md +++ b/docs/scenario-authoring.md @@ -43,6 +43,7 @@ id: some-case-name # ^[a-z0-9][a-z0-9-]{2,63}$ meta: {} briefing: "" floorPlan: null # または見取り図(§9) +victim: {} # 省略可。殺人以外の事件では書かない(§7) facts: [] # 1件以上 timeline: [] # 1件以上 characters: [] # 2人以上 @@ -161,6 +162,49 @@ timeline: 2人以上、3人前後が扱いやすい。**被害者は入れません。** プレイヤーが会話できるのは容疑者と証言者だけです。 +### `victim` — 話を聞けない相手 + +被害者は `characters` に入れず、`victim` に置きます。喋りませんが、**遺体と現場は調べられます**。 + +```yaml +victim: + name: 高瀬涼子 + introduction: 老舗旅館「月見荘」女将 # 肩書きひとつぶん、60文字まで + foundAt: "20:30" # 発見時刻。timeline と同じ書き方 + foundIn: 書斎 # 発見場所。20文字まで + causeOfDeath: 植物性の毒物による中毒死 + findings: # 調べて分かること。1件1文 + - id: single-glass + statement: 文机に、飲みかけのグラスが一つだけ置かれている。誰かと酌み交わした跡は無い。 + - id: heir-draft + statement: 硯箱の下に、書き直しかけの遺言書の草案が伏せてある。 + requires: # 省略可。段階的に見せたいときだけ + evidences: [will-record] +``` + +- **`findings` には、その場で目にできるものだけを書く。** 人物の心情や動機の解釈は書かない + ——あれは `revelations` の仕事で、ここに混ぜると「遺体を見ただけで動機が分かる」ことになります。 +- 誰がやったかを書かない。見えたものだけを置き、繋ぐのはプレイヤーの仕事です。 +- `findings` も `causeOfDeath` も無ければ、被害者は聞き込みの相手に並びません(調べても何も出ないので)。 +- 遺体を調べるのも**質問1回ぶん**を消費します。人に訊くか現場を見るかの配分がそのままゲームになります。 + +### `sources: { type: victim }` + +証拠と啓示の出どころに、人物・場所と並んで**遺体**を指定できます。`id` は `victim` で固定です。 + +```yaml +evidences: + - id: will-draft + label: 書き直しかけの遺言書の草案 + sources: + - type: victim + id: victim +``` + +**動機に繋がる手掛かりを、最低ひとつは遺体の側に置いてください。** 動機が人の口だけに紐づいていると、 +その一人に質問が向かなかったプレイヤーは「なぜ殺されたのか」が分からないまま告発に進むことになります。 +遺体は逃げも黙秘もしない、最後の情報源です。 + ```yaml characters: - id: fukagawa diff --git a/src/client/lib/schemas.ts b/src/client/lib/schemas.ts index f59551a..433e802 100644 --- a/src/client/lib/schemas.ts +++ b/src/client/lib/schemas.ts @@ -3,6 +3,7 @@ import { detectiveSchema } from '~/db/detective' import { floorPlanSchema } from '~/db/floor-plan' import { gameModeSchema, hintSchema } from '~/db/game-mode' import { llmProviderSchema, settableLlmRoleSchema } from '~/db/llm-catalog' +import { VICTIM_ID } from '~/db/scenario-definition' /** * サーバのレスポンスは fetch の時点では unknown。 @@ -227,11 +228,19 @@ export const historyExchangeSchema = z.object({ topic: z.string().nonempty().nullable(), }) +/** + * 話しかけた相手のID。 + * + * 登場人物は uuid だが、被害者だけは決め打ちの `victim`(採番する先が一人しか無い)。 + * ここを uuid で縛ると、遺体を調べたセッションが復元できずに画面ごと落ちる。 + */ +const subjectIdSchema = z.union([z.uuid(), z.literal(VICTIM_ID)]) + export const sessionHistorySchema = z.object({ sessionId: z.uuid(), histories: z.array( z.object({ - characterId: z.uuid(), + characterId: subjectIdSchema, exchanges: z.array(historyExchangeSchema), }), ), diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index f1650cc..4a66bbb 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -12,6 +12,7 @@ import { fetchSessionState } from '@/client/lib/api' import { formatSeconds } from '@/client/lib/format' import { settledSentences } from '@/client/lib/paragraphs' import type { ScenarioDetail, SessionState } from '@/client/lib/schemas' +import { VICTIM_ID } from '~/db/scenario-definition' /** * アリバイ表に立てる線。 @@ -196,7 +197,8 @@ type Block = { id: string; who: number; name: string; lines: { id: string; text: * 相手を切り替えても読んでいた場所が消えない。 */ const buildBlocks = ( - characters: ScenarioDetail['characters'], + /** 話題を投げられる相手。被害者を含むので ScenarioDetail['characters'] より広い。 */ + characters: { id: string; logName: string }[], conversations: Record, askerName: string, /** 今まさに返答が流れてきている相手。書きかけの一文を伏せるのに要る。 */ @@ -213,7 +215,7 @@ const buildBlocks = ( turn, seq, index, - name: character.name, + name: character.logName, streaming: seq === streamingSeq && turn.role === 'assistant', })) // 話題はプレイヤーの指示であって発言ではない。探偵が投げた質問のほうが残る。 @@ -442,10 +444,40 @@ export const InterrogationScreen = ({ setInputText('') } - const activeIndex = scenario.characters.findIndex( - (character) => character.id === activeCharacterId, - ) - const activeCharacter = scenario.characters[activeIndex] + /* + * 話題を投げられる相手。遺体を調べられる事件では、被害者もここに並ぶ。 + * + * 顔料の添字はアリバイ表の列と揃える(被害者は登場人物の次)。ずらすと、 + * 表の列と会話の縦罫が違う色になって、同じ相手だと分からなくなる。 + * + * ログに出す名前だけは被害者を「所見」にする。喋ったのではなく探偵が見たものなので、 + * 名前を出すと死者が証言しているように読める。 + */ + const subjects = [ + ...scenario.characters.map((character, index) => ({ + id: character.id, + name: character.name, + logName: character.name, + introduction: character.publicIntroduction, + index, + })), + ...(scenario.victim === null || !scenario.victim.investigable + ? [] + : [ + { + id: VICTIM_ID, + name: scenario.victim.name, + logName: '所見', + introduction: `被害者・${scenario.victim.introduction}`, + index: scenario.characters.length, + }, + ]), + ] + + const activeIndex = subjects.findIndex((subject) => subject.id === activeCharacterId) + const activeCharacter = subjects[activeIndex] + /** 遺体を調べているあいだ。訊くのではなく見るので、文言が変わる。 */ + const examining = activeCharacterId === VICTIM_ID const activeSuggestions = suggestedQuestions[activeCharacterId] const suggestionsToShow = activeSuggestions === undefined ? [] : activeSuggestions const isAsking = askingCharacterId !== undefined @@ -517,7 +549,7 @@ export const InterrogationScreen = ({ ]), ) - const said = buildBlocks(scenario.characters, conversations, askerName, askingCharacterId) + const said = buildBlocks(subjects, conversations, askerName, askingCharacterId) const shown = usePacedReveal( said.reduce((count, block) => count + block.lines.length, 0), isAsking, @@ -583,22 +615,22 @@ export const InterrogationScreen = ({ 押せる見出しを持たないので、名前の隣に控えめに並べておく。 */}

- {activeCharacter === undefined ? '' : activeCharacter.publicIntroduction} + {activeCharacter === undefined ? '' : activeCharacter.introduction}

) @@ -892,7 +924,7 @@ export const InterrogationScreen = ({ } }} maxLength={500} - placeholder="何について訊く?" + placeholder={examining ? '何を調べる?' : '何について訊く?'} aria-label="訊きたいこと" disabled={isAsking} className="min-w-0 flex-1 border-keisen border-b bg-transparent px-0.5 py-[7px] text-[12px] outline-none placeholder:text-nezumi-dim focus-visible:border-nezumi-dim disabled:opacity-40 lg:py-[9px] lg:text-[13.5px]" @@ -903,7 +935,7 @@ export const InterrogationScreen = ({ disabled={isAsking || inputText.trim().length === 0} className="shrink-0 border border-keisen px-3.5 py-[7px] text-[12px] hover:border-nezumi-dim disabled:opacity-40 lg:px-[22px] lg:py-2 lg:text-[13px]" > - {isAsking ? '…' : '訊く'} + {isAsking ? '…' : examining ? '調べる' : '訊く'}
)} diff --git a/src/server/game/examination.ts b/src/server/game/examination.ts new file mode 100644 index 0000000..6ab3768 --- /dev/null +++ b/src/server/game/examination.ts @@ -0,0 +1,63 @@ +import { availableFindings, type VictimFinding } from '~/db/victim-finding' + +/** + * 検分の材料。 + * + * 真相のテーブルから引いた値をそのまま受ける。キャッシュ層(`@/server/cache/scenario`)へ + * 置かないのは、あちらが scenario_truths に触らない約束で作られているため。 + */ +export type VictimRecord = { + name: string + introduction: string + /** 事件の公開記録。探偵が既に読んでいる前提の背景として渡す。 */ + briefing: string + foundAt: string | null + foundIn: string | null + causeOfDeath: string | null + findings: VictimFinding[] +} + +export type DiscoveredIds = { + evidenceIds: string[] + revelationIds: string[] +} + +const line = (label: string, value: string | null): string => + value === null ? '' : `- ${label}: ${value}` + +/** + * 検分の材料を、そのまま読める形に組む。 + * + * 前提を満たしていない所見は入れない。伏せた所見があること自体も書かない + * ——「まだ何かある」と分かると、条件を満たす前に答えの形が見えてしまう。 + * + * 何も出せるものが無ければ undefined を返す。そのときは検分そのものを断る + * (空のシートを渡すと、モデルは埋めようとして所見を作りはじめる)。 + */ +export const buildVictimSheet = ( + record: VictimRecord, + discovered: DiscoveredIds, +): string | undefined => { + const findings = availableFindings(record.findings, discovered) + + if (findings.length === 0 && record.causeOfDeath === null) { + return undefined + } + + const facts = [ + line('発見時刻', record.foundAt), + line('発見場所', record.foundIn), + line('死因', record.causeOfDeath), + ].filter((text) => text.length > 0) + + return `# ${record.name}(被害者・${record.introduction}) + +## 事件の公開記録 +${record.briefing} + +## 検分で分かっていること +${facts.length === 0 ? '- (まだ何も分かっていない)' : facts.join('\n')} + +## 遺体と現場の所見 +${findings.map((finding) => `- ${finding.statement}`).join('\n')}` +} diff --git a/src/server/game/rules.ts b/src/server/game/rules.ts index cd7ccf9..f387aaf 100644 --- a/src/server/game/rules.ts +++ b/src/server/game/rules.ts @@ -14,3 +14,22 @@ export const GAME_RULES = `あなたはマーダーミステリーの登場人 - プレイヤーはスマホで10分ほど遊ぶ。返答は短く、会話のテンポを保つ。 - 息継ぎのある喋り方にする。返答は1〜3個の塊に分け、塊のあいだを空行で区切る。塊はそれぞれ1〜2文。 - 地の文やナレーションを書かない。その人物の発話だけを返す。` + +/** + * 遺体と現場を調べたときの語り口。 + * + * GAME_RULES とは別に置く。あちらは「人物として喋る」ための決まりで、 + * 相手が喋らないこの経路では前提から違う。プレフィックスの先頭に来るものでもあるので、 + * 混ぜるとキャッシュの効き方まで巻き添えになる。 + * + * 要は一つだけ——**書かれていない所見を作らないこと**。ここで創作されると、 + * 存在しない証拠が真相の顔をして画面に出る。言い換えは許すが、足すのは許さない。 + */ +export const EXAMINATION_RULES = `あなたは探偵が遺体と現場を検分した結果を書き起こす。 + +- **与えられた所見だけを書く。書かれていないことは、推測でも補足でも足さない。** +- 所見に無いことを訊かれたら「そこからは何も分からない」と正直に書く。取り繕わない。 +- 犯人が誰か、動機が何かを断定しない。見えたものだけを書き、解釈はプレイヤーに残す。 +- 探偵が自分の目で見た記録として書く。遺体に喋らせない。 +- プレイヤーはスマホで10分ほど遊ぶ。短く、淡々と。 +- 1〜3個の塊に分け、塊のあいだを空行で区切る。塊はそれぞれ1〜2文。` diff --git a/src/server/llm/examiner.ts b/src/server/llm/examiner.ts new file mode 100644 index 0000000..e17d291 --- /dev/null +++ b/src/server/llm/examiner.ts @@ -0,0 +1,63 @@ +import { type ModelMessage, streamText } from 'ai' +import type { Env } from '@/server/env' +import { buildDetectiveBlock } from '@/server/llm/detective' +import { cacheHint, type LlmChoice, resolveModel } from '@/server/llm/provider' +import type { Detective } from '~/db/detective' + +export type ExaminationContext = { + env: Env + choice: LlmChoice + /** 検分の語り口の決まり(`EXAMINATION_RULES`)。会話中変わらないので先頭に置く。 */ + examinationRules: string + /** 被害者の検分シート。いま見せてよい所見だけが入っている。 */ + victimSheet: string + detective: Detective | undefined + /** 遺体についてのこれまでの検分。他の人物との会話は混ぜない。 */ + history: ModelMessage[] + /** 探偵が何を調べようとしているか。 */ + utterance: string +} + +const buildDetectiveMessages = (detective: Detective | undefined): ModelMessage[] => + detective === undefined ? [] : [{ role: 'system', content: buildDetectiveBlock(detective) }] + +/** + * 遺体と現場の検分を書き起こす。 + * + * 作りは `streamNpcReply` と同じ(プレフィックスを二段に分けてキャッシュを効かせる)。 + * 違うのは相手が喋らないことだけで、渡すのが人物像ではなく所見のシートになる。 + * + * ここは**言い換えだけをさせる経路**で、所見そのものはシナリオが決めている。 + * モデルに与えていない所見は出力にも現れない、という保証をシートの側で作っておくこと + * (`buildVictimSheet` が前提を満たさない所見を落とす)。 + */ +export const streamExamination = ({ + env, + choice, + examinationRules, + victimSheet, + detective, + history, + utterance, +}: ExaminationContext) => + streamText({ + model: resolveModel(env, choice), + // 理由は streamNpcReply と同じ。ブロックごとにキャッシュの区切りを打つため。 + allowSystemInMessages: true, + messages: [ + { + role: 'system', + content: examinationRules, + providerOptions: cacheHint(choice), + }, + { + role: 'system', + content: victimSheet, + providerOptions: cacheHint(choice), + }, + ...buildDetectiveMessages(detective), + ...history, + { role: 'user', content: utterance }, + ], + maxOutputTokens: 1024, + }) diff --git a/src/server/routes/sessions.ts b/src/server/routes/sessions.ts index e9174c6..4d29f63 100644 --- a/src/server/routes/sessions.ts +++ b/src/server/routes/sessions.ts @@ -14,12 +14,14 @@ import type { Db } from '@/server/db/client' import { createDb } from '@/server/db/client' import type { Bindings, Env } from '@/server/env' import { type AlibiSegment, alibiSegmentsOf } from '@/server/game/alibi' +import { buildVictimSheet, type DiscoveredIds } from '@/server/game/examination' import { remainingHints } from '@/server/game/hints' import { acceptRevealedRevelationIds } from '@/server/game/revelations' -import { GAME_RULES } from '@/server/game/rules' +import { EXAMINATION_RULES, GAME_RULES } from '@/server/game/rules' import { scoreSession } from '@/server/game/scoring' import { streamNpcReply } from '@/server/llm/actor' import { gradeDeduction } from '@/server/llm/deduction' +import { streamExamination } from '@/server/llm/examiner' import { createFilterState, FALLBACK_REPLY, feedChunk, finalizeFilter } from '@/server/llm/filter' import { generateQuestion, type TopicExchange } from '@/server/llm/interviewer' import { judgeTurn } from '@/server/llm/judge' @@ -28,6 +30,7 @@ import { toUsageRow } from '@/server/llm/usage' import { withEnv } from '@/server/middleware/env' import { clampLimits, + EXAMINATION_MODEL_CALLS, EXCHANGES_PER_TOPIC, modelCallsPerTopic, type SessionLimits, @@ -38,6 +41,7 @@ import { import { type Detective, detectiveSchema } from '~/db/detective' import { type GameMode, gameModeOf, gameModeSchema } from '~/db/game-mode' import { llmOverridesSchema } from '~/db/llm-catalog' +import { VICTIM_ID } from '~/db/scenario-definition' import { characters, type DeductionRecord, @@ -147,6 +151,91 @@ const loadTruth = async (db: Db, scenarioId: string): Promise } } +/** 話題を投げる相手。人物でも遺体でも、この形まで均せば以降は同じ扱いでいい。 */ +type Subject = { name: string; sheet: string } + +const loadCharacterSubject = async ( + kv: KVNamespace, + db: Db, + characterId: string, +): Promise => { + const [sheet, rows] = await Promise.all([ + loadCharacterSheet(kv, db, characterId), + // 探偵に渡してよいのは名前だけ。キャラクターシートは訊かれる側の持ち物なので、 + // 質問を組み立てる側には見せない(`@/server/llm/interviewer`)。 + db + .select({ name: characters.name }) + .from(characters) + .where(eq(characters.id, characterId)) + .limit(1), + ]) + + const row = rows[0] + + return sheet === undefined || row === undefined ? undefined : { name: row.name, sheet } +} + +/** + * 遺体の検分シート。 + * + * 人物と違ってキャッシュに置けない。見せてよい所見が発見状況で変わるので、 + * 焼いた瞬間の姿を使い回すと、まだ満たしていない前提の所見まで出てしまう。 + */ +const loadVictimSubject = async ( + db: Db, + scenarioId: string, + discovered: DiscoveredIds, +): Promise => { + const [scenarioRows, truthRows] = await Promise.all([ + db + .select({ + name: scenarios.victimName, + introduction: scenarios.victimIntroduction, + briefing: scenarios.briefing, + foundAt: scenarios.victimFoundAt, + foundIn: scenarios.victimFoundIn, + }) + .from(scenarios) + .where(eq(scenarios.id, scenarioId)) + .limit(1), + db + .select({ + causeOfDeath: scenarioTruths.victimCauseOfDeath, + findings: scenarioTruths.victimFindings, + }) + .from(scenarioTruths) + .where(eq(scenarioTruths.scenarioId, scenarioId)) + .limit(1), + ]) + + const scenarioRow = scenarioRows[0] + const truthRow = truthRows[0] + + if ( + scenarioRow === undefined || + truthRow === undefined || + scenarioRow.name === null || + scenarioRow.introduction === null + ) { + return undefined + } + + const sheet = buildVictimSheet( + { + name: scenarioRow.name, + introduction: scenarioRow.introduction, + briefing: scenarioRow.briefing, + foundAt: scenarioRow.foundAt, + foundIn: scenarioRow.foundIn, + causeOfDeath: truthRow.causeOfDeath, + findings: truthRow.findings, + }, + discovered, + ) + + return sheet === undefined ? undefined : { name: scenarioRow.name, sheet } +} + /** * 時刻表に引ける線を、発見済みの手掛かりから組み立てる。 * @@ -472,7 +561,11 @@ sessionRoutes.get('/api/sessions/:id', validateSessionId, withEnv, async (c) => const askSchema = z.object({ sessionId: z.uuid(), - characterId: z.uuid(), + /** + * 話題を投げる相手。登場人物は uuid だが、遺体だけは決め打ちの `victim` + * (採番する先が一人しか居ない)。ここを uuid で縛ると検分そのものが通らない。 + */ + characterId: z.union([z.uuid(), z.literal(VICTIM_ID)]), /** * プレイヤーが指定する話題。「アリバイについて」「被害者との関係を」のような指示で、 * 実際にNPCへ投げる質問は探偵役のモデルがここから組み立てる。 @@ -617,6 +710,13 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { const scenarioId = meta.scenarioId + /* + 遺体の検分は、相手が喋らないぶんだけ経路が違う。 + 分かれるのは「渡すシート」と「往復するかどうか」の二点だけで、 + ターンの消費・記録・判定はそのあと合流する。 + */ + const examining = askInput.characterId === VICTIM_ID + // 進行中のセッションはDOが正典。ターンを使い切っていたらここで断る。 // クライアント側の残り回数表示だけに任せると、リクエストを直接投げれば // 何回でも聞けてしまい、制限が演出にしかならない。 @@ -650,7 +750,7 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { const verdict = await limiter.consume( env.RATE_LIMIT_MAX_CALLS, env.RATE_LIMIT_WINDOW_SECONDS, - modelCallsPerTopic(limits.exchangesPerTopic), + examining ? EXAMINATION_MODEL_CALLS : modelCallsPerTopic(limits.exchangesPerTopic), ) if (!verdict.allowed) { @@ -661,35 +761,26 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { return c.json({ error: 'no turns left', turn: turnsBefore }, 409) } - const characterSheet = await loadCharacterSheet(c.env.SCENARIO_CACHE, db, askInput.characterId) - - if (characterSheet === undefined) { - return c.json({ error: 'character not found' }, 404) - } - - // 履歴・探偵・秘匿キーワード・名前は互いに独立なので直列に待つ理由がない。 - // 履歴はこのNPCとの分だけで、他NPCの会話は混ざらない。 - const [history, detective, secretKeywords, characterRows] = await Promise.all([ + // 履歴・探偵・秘匿キーワード・相手は互いに独立なので直列に待つ理由がない。 + // 履歴はこの相手との分だけで、他の相手との会話は混ざらない。 + const [history, detective, secretKeywords, subject] = await Promise.all([ session.getHistory(askInput.characterId), session.getDetective(), loadSecretKeywords(db, scenarioId), - // 探偵に渡してよいのは名前だけ。キャラクターシートは訊かれる側の持ち物なので、 - // 質問を組み立てる側には見せない(`@/server/llm/interviewer`)。 - db - .select({ name: characters.name }) - .from(characters) - .where(eq(characters.id, askInput.characterId)) - .limit(1), + examining + ? loadVictimSubject(db, scenarioId, { + evidenceIds: before.discoveredEvidenceIds, + revelationIds: before.discoveredRevelationIds, + }) + : loadCharacterSubject(c.env.SCENARIO_CACHE, db, askInput.characterId), ]) - const characterRow = characterRows[0] - - if (characterRow === undefined) { - return c.json({ error: 'character not found' }, 404) + if (subject === undefined) { + // 検分の場合、見せられる所見が一つも無いときもここへ来る。 + // 空のシートを渡すとモデルが埋めようとして所見を作りはじめるので、断るほうを選ぶ。 + return c.json({ error: examining ? 'nothing to examine' : 'character not found' }, 404) } - const characterName = characterRow.name - return streamSSE(c, async (stream) => { const collected: { exchanges: TopicExchange[] @@ -697,15 +788,21 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { blocked: boolean } = { exchanges: [], usages: [], blocked: false } - // 往復の上限はセッションに固定された値。ここが1つの話題のコストを決める。 - const rounds = Array.from({ length: limits.exchangesPerTopic }, (_value, index) => index) + /* + 往復の上限はセッションに固定された値。ここが1つの話題のコストを決める。 + 検分だけは1回きり——相手が喋らないので、答えを受けて掘り下げる相手が居ない。 + */ + const rounds = Array.from( + { length: examining ? 1 : limits.exchangesPerTopic }, + (_value, index) => index, + ) for (const _round of rounds) { const interviewer = await generateQuestion({ env, choice: choices.actor, detective, - characterName, + characterName: subject.name, topic: askInput.topic, exchanges: collected.exchanges, }) @@ -730,23 +827,33 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { await stream.writeSSE({ event: 'question', data: interviewer.question }) - const result = streamNpcReply({ - env, - choice: choices.actor, - gameRules: GAME_RULES, - characterSheet, - detective, - // この話題でここまでに交わしたぶんも履歴に足す。DOへ書くのは話題が - // 終わってからなので、途中の往復はここで持ち回るしかない。 - history: [ - ...history, - ...collected.exchanges.flatMap((exchange): ModelMessage[] => [ - { role: 'user', content: exchange.question }, - { role: 'assistant', content: exchange.answer }, - ]), - ], - utterance: interviewer.question, - }) + const result = examining + ? streamExamination({ + env, + choice: choices.actor, + examinationRules: EXAMINATION_RULES, + victimSheet: subject.sheet, + detective, + history, + utterance: interviewer.question, + }) + : streamNpcReply({ + env, + choice: choices.actor, + gameRules: GAME_RULES, + characterSheet: subject.sheet, + detective, + // この話題でここまでに交わしたぶんも履歴に足す。DOへ書くのは話題が + // 終わってからなので、途中の往復はここで持ち回るしかない。 + history: [ + ...history, + ...collected.exchanges.flatMap((exchange): ModelMessage[] => [ + { role: 'user', content: exchange.question }, + { role: 'assistant', content: exchange.answer }, + ]), + ], + utterance: interviewer.question, + }) const streamed = await streamFilteredReply(stream, result.textStream, secretKeywords) @@ -812,34 +919,43 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { collected.exchanges, ) - // 会話ログとコストは別のテーブルへ、同時に書く。ここは Judge の手前なので、 - // 直列にすると往復ぶんだけ判定の開始が遅れる。 - // - // 話題そのものも1行として残す。探偵の質問だけを並べると、プレイヤーが - // 何を指示したのかが後から辿れない。 + /* + 会話ログとコストは別のテーブルへ、同時に書く。ここは Judge の手前なので、 + 直列にすると往復ぶんだけ判定の開始が遅れる。 + + 話題そのものも1行として残す。探偵の質問だけを並べると、プレイヤーが + 何を指示したのかが後から辿れない。 + + 検分だけは messages に残さない。あの列は characters への外部キーを持っていて、 + 被害者は characters に居ないため入らない。読み返しに使うのは DO の側なので + (履歴の復元も判定もそちらを見る)、プレイには影響しない。 + あの表は後から分析するための控えなので、そこだけ欠けることになる。 + */ await Promise.all([ - db.insert(messages).values([ - { - sessionId, - characterId: askInput.characterId, - role: 'topic', - content: askInput.topic, - }, - ...collected.exchanges.flatMap((exchange) => [ - { - sessionId, - characterId: askInput.characterId, - role: 'user', - content: exchange.question, - }, - { - sessionId, - characterId: askInput.characterId, - role: 'assistant', - content: exchange.answer, - }, - ]), - ]), + examining + ? Promise.resolve() + : db.insert(messages).values([ + { + sessionId, + characterId: askInput.characterId, + role: 'topic', + content: askInput.topic, + }, + ...collected.exchanges.flatMap((exchange) => [ + { + sessionId, + characterId: askInput.characterId, + role: 'user', + content: exchange.question, + }, + { + sessionId, + characterId: askInput.characterId, + role: 'assistant', + content: exchange.answer, + }, + ]), + ]), db.insert(llmUsages).values(collected.usages), ]) } catch (error) { @@ -853,7 +969,7 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { const [rubric, revelationCandidates] = await Promise.all([ loadJudgeRubric(c.env.SCENARIO_CACHE, db, scenarioId), loadEligibleRevelationCandidates(c.env.SCENARIO_CACHE, db, scenarioId, { - source: { type: 'character', id: askInput.characterId }, + source: { type: examining ? 'victim' : 'character', id: askInput.characterId }, discoveredEvidenceIds: current.discoveredEvidenceIds, discoveredRevelationIds: current.discoveredRevelationIds, }), @@ -1204,7 +1320,9 @@ sessionRoutes.get('/api/sessions/:id/history', validateSessionId, async (c) => { .where(eq(characters.scenarioId, scenarioId)) const session = c.env.PLAY_SESSION.get(c.env.PLAY_SESSION.idFromName(sessionId)) - const histories = await session.getHistories(characterRows.map((row) => row.id)) + // 遺体の検分も同じ入れ物に積まれている。ここへ加えないと、リロードした瞬間に + // 検分の記録だけが画面から消える。持っていないセッションでは空で返るだけ。 + const histories = await session.getHistories([...characterRows.map((row) => row.id), VICTIM_ID]) return c.json({ sessionId, histories }) }) diff --git a/src/shared/turns.ts b/src/shared/turns.ts index 44d6eff..187ec31 100644 --- a/src/shared/turns.ts +++ b/src/shared/turns.ts @@ -111,6 +111,15 @@ export const clampLimits = ( */ export const modelCallsPerTopic = (exchangesPerTopic: number): number => 2 * exchangesPerTopic + 1 +/** + * 遺体を検分するときのモデル呼び出しの回数。 + * + * 何を調べるかを組み立てて1回、所見を書き起こして1回、判定で1回。 + * 相手が喋らないので往復しない——聞き込み(既定7回)より軽い。 + * ターンは同じく1問ぶん使うが、予算の減り方はこの重みで数える。 + */ +export const EXAMINATION_MODEL_CALLS = 3 + export type TurnState = { /** 何ターン目か。1始まり。使い切ったあとは最終ターンの番号で止まる。 */ turn: number From a250b7815f9444f25bc438ea4c81cb10e2c689bb Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:29:42 +0000 Subject: [PATCH 08/51] feat(interrogation): carry chosen target from case prep into interrogation Pass the character (or victim) picked on the overview screen through the route search param so interrogation resumes with that target instead of always starting from the top of the character list. Co-Authored-By: Claude --- src/client/screens/CaseOverviewScreen.tsx | 45 +++++++++++++++---- src/client/screens/InterrogationScreen.tsx | 26 +++++++---- src/routes/scenarios/$scenarioId/overview.tsx | 4 +- src/routes/sessions/$sessionId/index.tsx | 3 ++ src/routes/sessions/$sessionId/route.tsx | 15 +++++++ 5 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/client/screens/CaseOverviewScreen.tsx b/src/client/screens/CaseOverviewScreen.tsx index e6328ec..e5622dd 100644 --- a/src/client/screens/CaseOverviewScreen.tsx +++ b/src/client/screens/CaseOverviewScreen.tsx @@ -19,6 +19,7 @@ import { activeDetective, loadDetectiveStore, toDetective } from '@/client/lib/d import { loadGameMode } from '@/client/lib/game-mode-store' import type { CreateSessionResponse, ScenarioDetail } from '@/client/lib/schemas' import { railSpanMinutes } from '@/client/lib/time-rail' +import { VICTIM_ID } from '~/db/scenario-definition' /** 節の見出し。等幅なのは書式であって時刻ではないので、値には使わない。 */ const LEGEND = @@ -75,7 +76,11 @@ type Props = { * それまでの聞き込みが宙に浮く。 */ activeSessionId?: string - onStart: (session: CreateSessionResponse) => void + /** + * 支度で選んだ相手を添えて渡す。ここで選んだのに聞き込みが別の人から始まると、 + * 名簿を眺めただけの画面になってしまう。 + */ + onStart: (session: CreateSessionResponse, firstTarget: string | undefined) => void onResume: () => void onGiveUp: () => void onBack: () => void @@ -131,7 +136,19 @@ export const CaseOverviewScreen = ({ */ const [firstTarget, setFirstTarget] = useState(scenario.characters[0]?.id) - const chosen = scenario.characters.find((character) => character.id === firstTarget) + /** + * 遺体を調べられる事件か。 + * + * 調べられるなら、被害者も「まず誰から」の一人として選べる。所見も死因も無い事件では + * 押せないままにする——押せるのに何も出ない相手を並べるより、押せないほうが正直。 + */ + const investigable = scenario.victim?.investigable === true + + const chosen = + scenario.victim !== null && firstTarget === VICTIM_ID + ? { name: scenario.victim.name, examine: true } + : scenario.characters.find((character) => character.id === firstTarget) + const paragraphs = paragraphsOf(scenario.briefing) /** 表の列。聞き込みの相手に亡くなった人を継ぎ足す——事件の時間を説明するのは四人ぶん。 */ @@ -158,7 +175,7 @@ export const CaseOverviewScreen = ({ setError(undefined) createSession(scenario.id, detective, mode) - .then(onStart) + .then((session) => onStart(session, firstTarget)) .catch((err: unknown) => { setError(describeError(err)) setStarting(false) @@ -171,7 +188,9 @@ export const CaseOverviewScreen = ({ ? '準備中…' : chosen === undefined ? '聞き込みを始める' - : `${chosen.name}に聞き込みをする` + : 'examine' in chosen + ? `${chosen.name}を調べる` + : `${chosen.name}に聞き込みをする` return (
@@ -343,17 +362,25 @@ export const CaseOverviewScreen = ({ {/* 亡くなった人も同じ列に並べる。別枠にすると、事件のあいだ誰がその場に - いたのかという一覧が二つに割れる。話しかけられないことは右端のラベルと、 - 押せないことで足りている。 + いたのかという一覧が二つに割れる。 + + 喋らないが、遺体と現場は調べられる。調べられる事件では選べるようにして、 + 右端のラベルを「調べる」に替える——押せる相手なのか、名簿の上で分かるように。 */} {scenario.victim !== null && (
  • diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index 4a66bbb..ca37686 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -35,6 +35,8 @@ type Props = { /** このセッションで名乗った探偵の名前。名乗らずに始めたなら null。 */ detectiveName: string | null interrogation: UseInterrogation + /** 支度で選んだ「まず誰から」。会話が始まっていればそちらが優先される。 */ + firstTarget?: string alibi?: Alibi onAccuse: () => void /** 聞き込みを切り上げて事件の一覧へ戻る。セッションはサーバに残るが、ここからは辿れなくなる。 */ @@ -75,18 +77,17 @@ const turnsOf = (conversations: Record, characterId: string) * 画面のいちばん下に映っている発言と、下の入力欄が向いている相手がずれる。 */ const lastSpokenId = ( - characters: ScenarioDetail['characters'], + /** 話題を投げられる相手。遺体も含むので、検分の途中で開き直しても戻ってこられる。 */ + subjectIds: string[], conversations: Record, fallback: string, ): string => - characters.reduce( - (best, character) => { - const turns = turnsOf(conversations, character.id) + subjectIds.reduce( + (best, id) => { + const turns = turnsOf(conversations, id) const last = turns[turns.length - 1] - return last !== undefined && last.askedAt >= best.at - ? { id: character.id, at: last.askedAt } - : best + return last !== undefined && last.askedAt >= best.at ? { id, at: last.askedAt } : best }, { id: fallback, at: -1 }, ).id @@ -316,6 +317,7 @@ export const InterrogationScreen = ({ sessionId, detectiveName, interrogation, + firstTarget, alibi = EMPTY_ALIBI, onAccuse, onLeave, @@ -346,7 +348,15 @@ export const InterrogationScreen = ({ } = interrogation const [activeCharacterId, setActiveCharacterId] = useState(() => - lastSpokenId(scenario.characters, conversations, firstCharacter.id), + /* + * まだ一度も話していなければ、支度で選んだ相手から始める。 + * 一度でも話していれば、そちらが優先——読んでいた場所と入力欄の向きを合わせる。 + */ + lastSpokenId( + [...scenario.characters.map((character) => character.id), VICTIM_ID], + conversations, + firstTarget === undefined ? firstCharacter.id : firstTarget, + ), ) const [inputText, setInputText] = useState('') const [serverState, setServerState] = useState(undefined) diff --git a/src/routes/scenarios/$scenarioId/overview.tsx b/src/routes/scenarios/$scenarioId/overview.tsx index b34c6b3..7954bfb 100644 --- a/src/routes/scenarios/$scenarioId/overview.tsx +++ b/src/routes/scenarios/$scenarioId/overview.tsx @@ -38,10 +38,12 @@ function CaseOverview() { activeSessionId={session} // replace で入るのは、戻るボタンで「聞き込みを始める」に着地させないため。 // あれをもう一度押すと、別のセッションが立って計時がやり直しになる。 - onStart={(started) => + onStart={(started, firstTarget) => navigate({ to: '/sessions/$sessionId', params: { sessionId: started.sessionId }, + // 支度で選んだ相手を連れていく。渡さないと、聞き込みは常に名簿の先頭から始まる。 + search: firstTarget === undefined ? {} : { first: firstTarget }, replace: true, }) } diff --git a/src/routes/sessions/$sessionId/index.tsx b/src/routes/sessions/$sessionId/index.tsx index d3baaee..eadfab6 100644 --- a/src/routes/sessions/$sessionId/index.tsx +++ b/src/routes/sessions/$sessionId/index.tsx @@ -11,6 +11,7 @@ export const Route = createFileRoute('/sessions/$sessionId/')({ function Interrogation() { const { scenario, state } = layout.useLoaderData() + const { first } = layout.useSearch() const interrogation = useInterrogationContext() const navigate = useNavigate() @@ -28,6 +29,8 @@ function Interrogation() { sessionId={state.sessionId} detectiveName={state.detectiveName} interrogation={interrogation} + // 支度で選んだ相手。会話が始まっていればそちらが優先される。 + firstTarget={first} // 刻限と食い違いはまだサーバから出ていないので、線だけを渡す。 alibi={{ segments: interrogation.alibiSegments }} onAccuse={() => diff --git a/src/routes/sessions/$sessionId/route.tsx b/src/routes/sessions/$sessionId/route.tsx index ad5f177..c23744c 100644 --- a/src/routes/sessions/$sessionId/route.tsx +++ b/src/routes/sessions/$sessionId/route.tsx @@ -1,9 +1,19 @@ import { createFileRoute, Outlet } from '@tanstack/react-router' +import { z } from 'zod' import { InterrogationProvider } from '@/client/hooks/InterrogationContext' import { useInterrogation } from '@/client/hooks/useInterrogation' import { fetchScenarioDetail, fetchSessionHistory, fetchSessionState } from '@/client/lib/api' import { restoreConversations } from '@/client/lib/restore' +/** + * 支度で選んだ「まず誰から」。付いていなければ、最後に話した相手から再開する。 + * + * uuid で縛らないのは、遺体(`victim`)も選べるため。壊れた値が来ても弾かず、 + * 画面側で存在しない相手として無視する——URLを手で書き換えた人にエラー画面を出す + * ほどのことではない。 + */ +const searchSchema = z.object({ first: z.string().nonempty().optional() }) + /** * 1プレイぶんのレイアウト(聞き込み・推理・リザルト)。 * @@ -17,6 +27,11 @@ import { restoreConversations } from '@/client/lib/restore' */ export const Route = createFileRoute('/sessions/$sessionId')({ ssr: false, + validateSearch: (search) => { + const parsed = searchSchema.safeParse(search) + + return parsed.success ? parsed.data : {} + }, loader: async ({ params }) => { const state = await fetchSessionState(params.sessionId) // 記録とシナリオは互いに依存しないので並べて取る。 From b8354a2a87b9d5fb0aeb381bf7bc07b7bd34190c Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:39:45 +0000 Subject: [PATCH 09/51] feat(examination): give the detective an independent inquiry step for examinations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Examining the victim previously reused the interview question-generator, which produced lines addressed to the deceased ("涼子さん、〜はありますか?"). Add composeExaminationFocus/EXAMINATION_INTENT_RULES so examinations get a self-directed "what to check next" line instead of a question, and route sessions.ts to it when the target is under examination. Co-Authored-By: Claude --- src/server/game/rules.ts | 28 ++++++++++++--- src/server/llm/examiner.ts | 64 +++++++++++++++++++++++++++++++++-- src/server/routes/sessions.ts | 30 +++++++++++----- 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/src/server/game/rules.ts b/src/server/game/rules.ts index f387aaf..e709ecf 100644 --- a/src/server/game/rules.ts +++ b/src/server/game/rules.ts @@ -25,11 +25,29 @@ export const GAME_RULES = `あなたはマーダーミステリーの登場人 * 要は一つだけ——**書かれていない所見を作らないこと**。ここで創作されると、 * 存在しない証拠が真相の顔をして画面に出る。言い換えは許すが、足すのは許さない。 */ -export const EXAMINATION_RULES = `あなたは探偵が遺体と現場を検分した結果を書き起こす。 +export const EXAMINATION_RULES = `あなたは探偵で、遺体と現場を自分の目で検分している。見て取ったことを書く。 -- **与えられた所見だけを書く。書かれていないことは、推測でも補足でも足さない。** -- 所見に無いことを訊かれたら「そこからは何も分からない」と正直に書く。取り繕わない。 -- 犯人が誰か、動機が何かを断定しない。見えたものだけを書き、解釈はプレイヤーに残す。 -- 探偵が自分の目で見た記録として書く。遺体に喋らせない。 +- **与えられた所見だけを事実として書く。書かれていないものを、見つけたことにしない。** +- **書類の引き写しにしない。**「〜と記録されている」「記録によれば」とは書かない。 + いま自分がその場で見て取ったこととして書く。 +- 見たものを書いたら、最後に一言だけ、そこから何が言えそうかを**自分への問いの形で**添えてよい。 + 「となると、毒物だろうか?」のように。**断定はしない。犯人や動機には踏み込まない。** +- 所見に無いことについては「そこからは分からない」と正直に書く。取り繕わない。 +- 遺体に喋らせない。誰かへの問いかけにしない。相手は亡くなっている。 - プレイヤーはスマホで10分ほど遊ぶ。短く、淡々と。 - 1〜3個の塊に分け、塊のあいだを空行で区切る。塊はそれぞれ1〜2文。` + +/** + * 何を確かめに行くか、という探偵の一手。 + * + * 聞き込みの「質問」に当たる位置に置くが、問いかける相手が居ないので質問にしない。 + * ここを INTERVIEWER_RULES に任せると「涼子さん、〜はありますか?」と + * 死者に話しかける画になる。独り言として書かせる。 + */ +export const EXAMINATION_INTENT_RULES = `あなたはマーダーミステリーの探偵で、これから遺体と現場を検分する。 + +- 依頼人から渡された「調べどころ」を受けて、**これから何を確かめるつもりか**を書く。 +- **誰かへの問いかけにしない。質問文を書かない。**相手は亡くなっている。 +- 自分に向けた独り言として書く。「〜を調べてみよう」「〜はどうだろうか」のように。 +- **1文で書く。**同じことを言い換えて二度書かない。地の文の説明や前置きも書かない。 +- まだ確かめていないことを、分かっている前提で書かない。` diff --git a/src/server/llm/examiner.ts b/src/server/llm/examiner.ts index e17d291..a27b8aa 100644 --- a/src/server/llm/examiner.ts +++ b/src/server/llm/examiner.ts @@ -1,9 +1,69 @@ -import { type ModelMessage, streamText } from 'ai' +import { + generateText, + type LanguageModelUsage, + type ModelMessage, + type ProviderMetadata, + streamText, +} from 'ai' import type { Env } from '@/server/env' -import { buildDetectiveBlock } from '@/server/llm/detective' +import { EXAMINATION_INTENT_RULES } from '@/server/game/rules' +import { buildDetectiveBlock, buildDetectiveSelfBlock } from '@/server/llm/detective' +import type { TopicExchange } from '@/server/llm/interviewer' import { cacheHint, type LlmChoice, resolveModel } from '@/server/llm/provider' import type { Detective } from '~/db/detective' +export type ExaminationFocusResult = { + /** これから何を確かめるか。モデルが何も返さなければ空文字。 */ + focus: string + usage: LanguageModelUsage + providerMetadata: ProviderMetadata | undefined + model: string +} + +/** + * 何を確かめに行くかを一言にする。 + * + * 聞き込みの `generateQuestion` と同じ位置に立つが、別の関数にしてある。 + * あちらは「目の前の人物へ質問を投げる」ための役で、そのまま使うと + * 「涼子さん、〜はありますか?」と死者に話しかけることになる。 + */ +export const composeExaminationFocus = async (params: { + env: Env + choice: LlmChoice + detective: Detective | undefined + /** プレイヤーが指定した調べどころ。 */ + topic: string + /** ここまでの検分。同じ場所を二度調べさせないために渡す。 */ + exchanges: TopicExchange[] +}): Promise => { + // 調べどころはプレイヤー由来の文字列なので、必ず user ロールに閉じ込める。 + const result = await generateText({ + model: resolveModel(params.env, params.choice), + system: + params.detective === undefined + ? EXAMINATION_INTENT_RULES + : `${EXAMINATION_INTENT_RULES}\n\n${buildDetectiveSelfBlock(params.detective)}`, + messages: [ + { + role: 'user', + content: `次の調べどころを確かめてください。\n\n調べどころ: ${params.topic}`, + }, + ...params.exchanges.flatMap((exchange): ModelMessage[] => [ + { role: 'assistant', content: exchange.question }, + { role: 'user', content: `検分の結果: ${exchange.answer}` }, + ]), + ], + maxOutputTokens: 256, + }) + + return { + focus: result.text.trim(), + usage: result.usage, + providerMetadata: result.providerMetadata, + model: result.response.modelId, + } +} + export type ExaminationContext = { env: Env choice: LlmChoice diff --git a/src/server/routes/sessions.ts b/src/server/routes/sessions.ts index 4d29f63..60865f4 100644 --- a/src/server/routes/sessions.ts +++ b/src/server/routes/sessions.ts @@ -21,7 +21,7 @@ import { EXAMINATION_RULES, GAME_RULES } from '@/server/game/rules' import { scoreSession } from '@/server/game/scoring' import { streamNpcReply } from '@/server/llm/actor' import { gradeDeduction } from '@/server/llm/deduction' -import { streamExamination } from '@/server/llm/examiner' +import { composeExaminationFocus, streamExamination } from '@/server/llm/examiner' import { createFilterState, FALLBACK_REPLY, feedChunk, finalizeFilter } from '@/server/llm/filter' import { generateQuestion, type TopicExchange } from '@/server/llm/interviewer' import { judgeTurn } from '@/server/llm/judge' @@ -798,14 +798,26 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { ) for (const _round of rounds) { - const interviewer = await generateQuestion({ - env, - choice: choices.actor, - detective, - characterName: subject.name, - topic: askInput.topic, - exchanges: collected.exchanges, - }) + /* + 探偵の一手。聞き込みなら相手への質問、検分なら「何を確かめるか」の独り言。 + 検分でも質問を作らせると「涼子さん、〜はありますか?」と死者に話しかける画になる。 + */ + const interviewer = examining + ? await composeExaminationFocus({ + env, + choice: choices.actor, + detective, + topic: askInput.topic, + exchanges: collected.exchanges, + }).then((result) => ({ ...result, question: result.focus })) + : await generateQuestion({ + env, + choice: choices.actor, + detective, + characterName: subject.name, + topic: askInput.topic, + exchanges: collected.exchanges, + }) collected.usages.push( toUsageRow({ From d35e5cdd2ad5ae9b568d0f7df0b8a971002b601f Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:43:48 +0000 Subject: [PATCH 10/51] feat(interrogation): disable victim column when case is not investigable Add a `pickable` flag to AlibiChart so the victim's column header only becomes clickable when the scenario allows examining them, and wire the interrogation screen's victim role label and target-switching through it. Co-Authored-By: Claude --- src/client/components/AlibiChart.tsx | 15 +++++++++++---- src/client/screens/InterrogationScreen.tsx | 12 ++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/client/components/AlibiChart.tsx b/src/client/components/AlibiChart.tsx index 6463f43..ee7d649 100644 --- a/src/client/components/AlibiChart.tsx +++ b/src/client/components/AlibiChart.tsx @@ -8,6 +8,8 @@ export type AlibiPerson = { hue: Hue /** 結果の画面で、当てられた犯人にだけ白緑を載せる。 */ roleSolved?: boolean + /** 列見出しから話しかけられるか。既定は押せる。話しかけられない相手だけ false を渡す。 */ + pickable?: boolean } export type AlibiSegment = { @@ -113,13 +115,18 @@ export const AlibiChart = ({
    {people.map((p) => { const on = p.key === activeKey - // 押せる列見出しは button で。span に onClick を載せるとキーボードから触れない。 - const Tag = onPick === undefined ? 'div' : 'button' + /* + * 押せる列見出しは button で。span に onClick を載せるとキーボードから触れない。 + * 話しかけられない相手(調べられない被害者)は押せる形にしない—— + * 押せるのに何も起きない列があると、押し方を間違えたのだと思わせてしまう。 + */ + const pickable = onPick !== undefined && p.pickable !== false + const Tag = pickable ? 'button' : 'div' return ( onPick(p.key)} + type={pickable ? 'button' : undefined} + onClick={pickable && onPick !== undefined ? () => onPick(p.key) : undefined} className={`border-b pb-[7px] pl-[10px] text-left ${HUE[p.hue].text} ${ on ? 'border-b-current' : 'border-b-keisen' }`} diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index ca37686..a1702cb 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -541,10 +541,13 @@ export const InterrogationScreen = ({ ? [] : [ { - key: 'victim', + key: VICTIM_ID, name: scenario.victim.name, - role: '被害者', + role: + scenario.victim.investigable && activeCharacterId === VICTIM_ID ? '検分中' : '被害者', hue: hueOf(scenario.characters.length), + // 調べられない事件では、この列だけ押せる形にしない。 + pickable: scenario.victim.investigable, }, ]), ] @@ -714,6 +717,11 @@ export const InterrogationScreen = ({ span={{ from: timeWindow.start, to: timeWindow.end }} deadline={alibi.deadline} activeKey={activeCharacterId} + /* + 列見出しから相手を替える。名前の隣の小さな並びだけだと、 + 表の上に相手が居るのに押せず、切り替えの口が見つからない。 + */ + onPick={setActiveCharacterId} clash={alibi.clash} /> )} From 92f1240367d945b2440c453c0db9c52074349ac4 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:50:26 +0000 Subject: [PATCH 11/51] feat(interrogation): keep re-chosen target when resuming a case Carries the target picked from the character list back into the interrogation screen when resuming, and consumes the `first` search param once so navigating away and back doesn't snap the conversation back to it. Co-Authored-By: Claude --- src/client/screens/CaseOverviewScreen.tsx | 9 +++++++-- src/client/screens/InterrogationScreen.tsx | 19 ++++++++++++------- src/routes/scenarios/$scenarioId/overview.tsx | 8 ++++++-- src/routes/sessions/$sessionId/index.tsx | 18 +++++++++++++++++- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/client/screens/CaseOverviewScreen.tsx b/src/client/screens/CaseOverviewScreen.tsx index e5622dd..d37d403 100644 --- a/src/client/screens/CaseOverviewScreen.tsx +++ b/src/client/screens/CaseOverviewScreen.tsx @@ -81,7 +81,8 @@ type Props = { * 名簿を眺めただけの画面になってしまう。 */ onStart: (session: CreateSessionResponse, firstTarget: string | undefined) => void - onResume: () => void + /** 戻るときも、名簿で選び直した相手を連れていく。 */ + onResume: (firstTarget: string | undefined) => void onGiveUp: () => void onBack: () => void } @@ -405,7 +406,11 @@ export const CaseOverviewScreen = ({ {error !== undefined &&

    {error}

    }
    -
    diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index a1702cb..a5b7d0a 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -347,16 +347,21 @@ export const InterrogationScreen = ({ ask, } = interrogation + /** 話題を投げられる相手のID。支度から渡された相手が実在するかの判定に使う。 */ + const subjectIds = [ + ...scenario.characters.map((character) => character.id), + ...(scenario.victim?.investigable === true ? [VICTIM_ID] : []), + ] + const [activeCharacterId, setActiveCharacterId] = useState(() => /* - * まだ一度も話していなければ、支度で選んだ相手から始める。 - * 一度でも話していれば、そちらが優先——読んでいた場所と入力欄の向きを合わせる。 + * 支度で選び直した相手が渡っていれば、それを優先する。 + * 「聞き込みに戻る」で名簿から選び直したのに、前に話していた相手が開くと、 + * 選び直した意味が無い。渡っていなければ、最後に話した相手から再開する。 */ - lastSpokenId( - [...scenario.characters.map((character) => character.id), VICTIM_ID], - conversations, - firstTarget === undefined ? firstCharacter.id : firstTarget, - ), + firstTarget !== undefined && subjectIds.includes(firstTarget) + ? firstTarget + : lastSpokenId(subjectIds, conversations, firstCharacter.id), ) const [inputText, setInputText] = useState('') const [serverState, setServerState] = useState(undefined) diff --git a/src/routes/scenarios/$scenarioId/overview.tsx b/src/routes/scenarios/$scenarioId/overview.tsx index 7954bfb..36bc0b6 100644 --- a/src/routes/scenarios/$scenarioId/overview.tsx +++ b/src/routes/scenarios/$scenarioId/overview.tsx @@ -47,9 +47,13 @@ function CaseOverview() { replace: true, }) } - onResume={() => { + onResume={(firstTarget) => { if (session !== undefined) { - navigate({ to: '/sessions/$sessionId', params: { sessionId: session } }) + navigate({ + to: '/sessions/$sessionId', + params: { sessionId: session }, + search: firstTarget === undefined ? {} : { first: firstTarget }, + }) } }} onGiveUp={() => navigate({ to: '/' })} diff --git a/src/routes/sessions/$sessionId/index.tsx b/src/routes/sessions/$sessionId/index.tsx index eadfab6..9a91334 100644 --- a/src/routes/sessions/$sessionId/index.tsx +++ b/src/routes/sessions/$sessionId/index.tsx @@ -1,4 +1,5 @@ import { createFileRoute, getRouteApi, Navigate, useNavigate } from '@tanstack/react-router' +import { useEffect, useState } from 'react' import { useInterrogationContext } from '@/client/hooks/InterrogationContext' import { InterrogationScreen } from '@/client/screens/InterrogationScreen' @@ -15,6 +16,21 @@ function Interrogation() { const interrogation = useInterrogationContext() const navigate = useNavigate() + /* + * 支度で選んだ相手は一度だけ使う。 + * + * URLに残したままにすると、推理の画面へ行って戻るたびにこの相手へ引き戻され、 + * 聞き込みの途中で替えた相手が巻き戻る。最初の描画で受け取ってから、 + * クエリだけを静かに落とす(履歴に残さないので、戻るボタンの挙動も変わらない)。 + */ + const [initialTarget] = useState(first) + + useEffect(() => { + if (first !== undefined) { + navigate({ to: '.', search: {}, replace: true }) + } + }, [first, navigate]) + // 終わった事件は聞き込みに戻れない(推理を出した後に戻るを押した場合など)。 // 開いたままにすると、答え合わせが済んだ相手に質問を投げられてしまう。 if (state.finished) { @@ -30,7 +46,7 @@ function Interrogation() { detectiveName={state.detectiveName} interrogation={interrogation} // 支度で選んだ相手。会話が始まっていればそちらが優先される。 - firstTarget={first} + firstTarget={initialTarget} // 刻限と食い違いはまだサーバから出ていないので、線だけを渡す。 alibi={{ segments: interrogation.alibiSegments }} onAccuse={() => From 3b1092099bd67be8603cd49c99bdf3e1fcc0defe Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 00:10:16 +0000 Subject: [PATCH 12/51] fix(alibi): make alibi chart scroll instead of overflow on tall cases Wrap AlibiChart in a scrollable container across all four screens that render it, so long time windows scroll within the frame at their real 1min/10px scale instead of shrinking the tick spacing or overflowing the viewport. Co-Authored-By: Claude --- src/client/screens/AccusationScreen.tsx | 15 +++++++----- src/client/screens/CaseOverviewScreen.tsx | 13 ++++++---- src/client/screens/InterrogationScreen.tsx | 28 ++++++++++++++-------- src/client/screens/ResultScreen.tsx | 17 +++++++------ 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/client/screens/AccusationScreen.tsx b/src/client/screens/AccusationScreen.tsx index 6768865..9a4960b 100644 --- a/src/client/screens/AccusationScreen.tsx +++ b/src/client/screens/AccusationScreen.tsx @@ -199,12 +199,15 @@ export const AccusationScreen = ({
    - + {/* 事件の幅が長いと表は画面より背が高くなる。縮めずにここで送る(聞き込みと同じ)。 */} +
    + +
    {/* 線の意味はここで一度だけ言う。表の中に註を足すと、供述より註が目立つ。 */}
    diff --git a/src/client/screens/CaseOverviewScreen.tsx b/src/client/screens/CaseOverviewScreen.tsx index d37d403..2e8460f 100644 --- a/src/client/screens/CaseOverviewScreen.tsx +++ b/src/client/screens/CaseOverviewScreen.tsx @@ -244,11 +244,14 @@ export const CaseOverviewScreen = ({
    - + {/* 事件の幅が長いと表は画面より背が高くなる。縮めずにここで送る(聞き込みと同じ)。 */} +
    + +
    {/* 線の意味は表のそばに置く。色は顔料に使い切っているので、 diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index a5b7d0a..17f2096 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -715,20 +715,28 @@ export const InterrogationScreen = ({ )}
    + {/* + 表は1分10pxの実寸で描く(AlibiChart の PX_PER_MIN)。事件の幅が長いほど背が伸び、 + 二時間近い事件では画面に収まらない。縮めると目盛りの間隔が事件ごとに変わって + 「同じ長さの線=同じ長さの時間」が崩れるので、縮めずにここで送る。 + 凡例と資料への入口は送らない——下端に据えておきたいものなので、枠の外に置く。 + */} {timeWindow === null ? null : ( - + + onPick={setActiveCharacterId} + clash={alibi.clash} + /> +
    )}
    diff --git a/src/client/screens/ResultScreen.tsx b/src/client/screens/ResultScreen.tsx index 98b99bf..77c9ef2 100644 --- a/src/client/screens/ResultScreen.tsx +++ b/src/client/screens/ResultScreen.tsx @@ -234,13 +234,16 @@ export const ResultScreen = ({ accuseResult, board, onRetry, onRestart }: Props) 解決したときだけ、申告と実際を重ねて見せる。迷宮入りで実線を引くと、 そこに真相がまるごと描かれてしまう——伏せているのは文章だけではない。 */} - + {/* 事件の幅が長いと表は画面より背が高くなる。縮めずにここで送る(聞き込みと同じ)。 */} +
    + +
    {solved ? ( Date: Tue, 1 Sep 2026 03:19:47 +0000 Subject: [PATCH 13/51] feat(db): add victim examination data and current-authoring migration for all scenarios Add victim.foundAt/foundIn/causeOfDeath/findings and victim-sourced motive clues to every scenario, backed by migration 0011 and a new test enforcing the current-authoring guide. Co-Authored-By: Claude --- __tests__/db/compile-scenario.test.ts | 2 +- .../db/scenario-current-authoring.test.ts | 85 ++ ...011_scenario-current-authoring-upgrade.sql | 257 ++++ db/migrations/meta/0011_snapshot.json | 1136 +++++++++++++++++ db/migrations/meta/_journal.json | 7 + .../avalanche-lodge-phantom-room.yaml | 21 +- .../avalanche-monastery-bell-window.yaml | 19 +- .../blizzard-lodge-seat-score-alibi.yaml | 21 +- db/scenarios/cave-lab-locator-cart.yaml | 21 +- .../coldcase-lodge-borrowed-memory.yaml | 19 +- .../deepsea-habitat-batch-signature.yaml | 20 +- db/scenarios/ferry-fog-passenger-count.yaml | 22 +- db/scenarios/festival-lantern-blackout.yaml | 23 +- .../flood-archive-self-locking-vault.yaml | 19 +- .../flood-courthouse-unlogged-door.yaml | 21 +- db/scenarios/gallery-blue-frame.yaml | 23 +- .../generation-ship-staggered-dawn.yaml | 19 +- db/scenarios/glasshouse-dawn-irrigation.yaml | 21 +- db/scenarios/ink-stained-contract.yaml | 22 +- ...landslide-clock-museum-eleven-minutes.yaml | 21 +- .../landslide-hotel-frosted-silhouette.yaml | 20 +- .../last-train-delay-certificate.yaml | 21 +- db/scenarios/mars-outpost-delayed-reply.yaml | 19 +- db/scenarios/midnight-radio-rerun.yaml | 23 +- db/scenarios/polar-station-shared-clock.yaml | 19 +- .../quarantine-clinic-borrowed-badge.yaml | 18 +- db/scenarios/rainy-bookstore-receipt.yaml | 22 +- .../shanghai-warehouse-carbon-copy.yaml | 18 +- db/scenarios/snow-gallery-erased-tracks.yaml | 20 +- db/scenarios/snow-observatory-last-photo.yaml | 22 +- .../snowbound-farm-morning-chores.yaml | 21 +- .../snowbound-gallery-wrong-crime-scene.yaml | 20 +- .../snowbound-studio-roomtone-loop.yaml | 21 +- db/scenarios/snowbound-theater-auto-cues.yaml | 21 +- db/scenarios/storm-aquarium-feeding-lamp.yaml | 20 +- .../storm-hydropower-rising-walkway.yaml | 21 +- db/scenarios/storm-island-scheduled-mail.yaml | 21 +- .../storm-lighthouse-relief-slate.yaml | 20 +- .../storm-mountain-inn-echoed-cane.yaml | 19 +- .../storm-planetarium-reflected-witness.yaml | 18 +- .../storm-research-vessel-missing-launch.yaml | 21 +- .../storm-seminar-echoed-testimony.yaml | 21 +- .../thunder-cablecar-occupied-cabin.yaml | 21 +- db/scenarios/tsukimisou.yaml | 2 +- ...typhoon-datacenter-maintenance-window.yaml | 21 +- .../typhoon-offshore-drone-route.yaml | 21 +- .../venetian-lazaretto-sunset-bell.yaml | 19 +- .../victorian-underground-last-telegram.yaml | 19 +- mocks/_case.js | 123 +- mocks/_mock.js | 9 +- src/client/stories/fixtures.ts | 24 +- 51 files changed, 2359 insertions(+), 145 deletions(-) create mode 100644 __tests__/db/scenario-current-authoring.test.ts create mode 100644 db/migrations/0011_scenario-current-authoring-upgrade.sql create mode 100644 db/migrations/meta/0011_snapshot.json diff --git a/__tests__/db/compile-scenario.test.ts b/__tests__/db/compile-scenario.test.ts index 52d7f92..0fdf8a3 100644 --- a/__tests__/db/compile-scenario.test.ts +++ b/__tests__/db/compile-scenario.test.ts @@ -149,7 +149,7 @@ describe('compileScenario: 月見荘のコンパイル', () => { test('公開状態と件数', () => { expect(compiled.scenario.isPublished).toBe(true) - expect(compiled.scenario.title).toBe('月見荘、十七回忌の夜') + expect(compiled.scenario.title).toBe('十七回忌、月見荘にて') expect(compiled.characters).toHaveLength(3) expect(compiled.evidences).toHaveLength(7) expect(compiled.revelations).toHaveLength(2) diff --git a/__tests__/db/scenario-current-authoring.test.ts b/__tests__/db/scenario-current-authoring.test.ts new file mode 100644 index 0000000..2bea2ce --- /dev/null +++ b/__tests__/db/scenario-current-authoring.test.ts @@ -0,0 +1,85 @@ +import { readdir } from 'node:fs/promises' +import path from 'node:path' +import YAML from 'yaml' +import { ScenarioDefinitionSchema } from '../../db/scenario-definition' + +const SCENARIO_DIR = path.resolve(import.meta.dir, '../../db/scenarios') + +const scenarioFiles = async (): Promise => + (await readdir(SCENARIO_DIR)).filter((name) => name.endsWith('.yaml')).sort() + +const scenarios = async () => { + const loaded = [] + + for (const file of await scenarioFiles()) { + const source = await Bun.file(path.join(SCENARIO_DIR, file)).text() + const parsed = ScenarioDefinitionSchema.safeParse(YAML.parse(source)) + if (!parsed.success) throw new Error(`${file}: invalid scenario schema`) + loaded.push({ file, scenario: parsed.data }) + } + + return loaded +} + +describe('scenario current authoring guide', () => { + test('殺人事件の victim は遺体・現場を調べられる情報を持つ', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + const victim = scenario.victim + if (victim === undefined) { + violations.push(`${file}: victim missing`) + continue + } + + if (victim.foundAt === undefined) violations.push(`${file}: victim.foundAt`) + if (victim.foundIn === undefined) violations.push(`${file}: victim.foundIn`) + if (victim.causeOfDeath === undefined) violations.push(`${file}: victim.causeOfDeath`) + if (victim.findings.length < 2) violations.push(`${file}: victim.findings < 2`) + } + + expect(violations).toEqual([]) + }) + + test('動機へ繋がる手掛かりが最低1つ victim source から取れる', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + const motiveFacts = new Set( + scenario.facts.filter((fact) => fact.kind === 'motive').map((fact) => fact.id), + ) + const evidenceHasMotiveClue = scenario.evidences.some( + (evidence) => + evidence.sources.some((source) => source.type === 'victim') && + evidence.supports.some((factId) => motiveFacts.has(factId)), + ) + const revelationHasMotiveClue = scenario.revelations.some( + (revelation) => + revelation.category === 'motive' && + revelation.sources.some((source) => source.type === 'victim'), + ) + + if (!evidenceHasMotiveClue && !revelationHasMotiveClue) { + violations.push(`${file}: no victim-sourced motive clue`) + } + } + + expect(violations).toEqual([]) + }) + + test('timeline はアリバイ表向けの短い在所を持つ', async () => { + const violations: string[] = [] + + for (const { file, scenario } of await scenarios()) { + for (const event of scenario.timeline) { + if (event.location === undefined) { + violations.push(`${file}: timeline:${event.id}: location missing`) + } else if ([...event.location].length > 8) { + violations.push(`${file}: timeline:${event.id}: location too long`) + } + } + } + + expect(violations).toEqual([]) + }) +}) diff --git a/db/migrations/0011_scenario-current-authoring-upgrade.sql b/db/migrations/0011_scenario-current-authoring-upgrade.sql new file mode 100644 index 0000000..9ab4a7f --- /dev/null +++ b/db/migrations/0011_scenario-current-authoring-upgrade.sql @@ -0,0 +1,257 @@ +UPDATE scenarios SET victim_found_at = '21:45', victim_found_in = '支配人室', victim_investigable = 1 WHERE victim_name = '早瀬隆司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'audit-warning', 'at', '21:05', 'place', '山荘内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["hayase-audit-next-morning","natsume-cash-shortage"]'), 'kind', 'claim'), json_object('id', 'fake-message', 'at', '21:16', 'place', 'フロント', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["fake-message-left","no-214-call","paper-from-night-pad"]'), 'kind', 'solid'), json_object('id', 'ask-oda', 'at', '21:24', 'place', '客室棟', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["natsume-asked-oda"]'), 'kind', 'solid'), json_object('id', 'management-corridor', 'at', '21:27', 'place', '管理廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["oda-saw-natsume-office-side"]'), 'kind', 'solid'), json_object('id', 'hayase-death', 'at', '21:30', 'place', '支配人室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1)), 'facts', json('["hayase-death-2130","natsume-killed-hayase"]'), 'kind', 'claim'), json_object('id', 'ask-fujino', 'at', '21:31', 'place', '厨房前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '藤野修平' LIMIT 1)), 'facts', json('["natsume-asked-fujino"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:45', 'place', '支配人室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '藤野修平' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '夏目悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND name = '小田真紀' LIMIT 1)), 'facts', json('["body-found-2145"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"早瀬隆司は支配人室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「翌朝の会計監査メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '夏目か小田に早瀬が翌朝予定していた会計確認について尋ねたら開示する。または遺体・現場を調べ、「翌朝の会計監査メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '早瀬隆司' LIMIT 1) AND label = '翌朝の会計監査メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '資料整理室', victim_investigable = 1 WHERE victim_name = '高瀬静一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'billing-confrontation', 'at', '20:48', 'place', '修道院内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1)), 'facts', json('["repair-overbilling","mizuki-overbilled","takase-confronted-mizuki"]'), 'kind', 'claim'), json_object('id', 'takase-death', 'at', '21:05', 'place', '資料整理室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1)), 'facts', json('["mizuki-killed-takase"]'), 'kind', 'claim'), json_object('id', 'bell-rung', 'at', '21:20', 'place', '鐘楼', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '黒川玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["mizuki-rang-bell-remotely","all-heard-bell","no-one-saw-takase-bell"]'), 'kind', 'solid'), json_object('id', 'test-line-clue', 'at', '21:27', 'place', '聖歌席裏', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["temporary-test-line-exists","genda-thought-line-removed","test-line-tag-remained"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '資料整理室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '黒川玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '水城奈央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND name = '玄田修' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"高瀬静一は資料整理室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「修復費の追加請求一覧」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '水城に高瀬から追及された修復費について尋ねるか、玄田に問題になっていた工事項目を確認したら開示する。または遺体・現場を調べ、「修復費の追加請求一覧」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬静一' LIMIT 1) AND label = '修復費の追加請求一覧'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '書斎', victim_investigable = 1 WHERE victim_name = '塚本誠'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'fraud-discovered', 'at', '21:05', 'place', 'ロッジ内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["payment-fraud","tsukamoto-found-fraud"]'), 'kind', 'claim'), json_object('id', 'summon-note', 'at', '21:10', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1)), 'facts', json('["tsukamoto-summoned-katase"]'), 'kind', 'claim'), json_object('id', 'katase-leaves-game', 'at', '21:12', 'place', '談話室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["katase-left-2112","mamiya-substituted-blue","game-tracks-seats"]'), 'kind', 'solid'), json_object('id', 'tsukamoto-death', 'at', '21:18', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1)), 'facts', json('["katase-killed-tsukamoto"]'), 'kind', 'claim'), json_object('id', 'score-continues', 'at', '21:21', 'place', 'ロッジ内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["blue-score-continued","yoshioka-copied-score-later"]'), 'kind', 'solid'), json_object('id', 'katase-returns', 'at', '21:25', 'place', '談話室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1)), 'facts', json('["katase-returned-2125","todo-saw-katase-return"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '吉岡蓮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '片瀬真央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '藤堂凛' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND name = '間宮葵' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"塚本誠は書斎で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「外部講師費の精算書」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '片瀬か吉岡に事件前に塚本が確認していた精算書について尋ねたら開示する。または遺体・現場を調べ、「外部講師費の精算書」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '塚本誠' LIMIT 1) AND label = '外部講師費の精算書'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:27', victim_found_in = '地図解析室', victim_investigable = 1 WHERE victim_name = '岩代圭吾'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'tag-attached-cart', 'at', '20:58', 'place', '研究所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["locator-tag-removable","kagawa-tag-on-cart"]'), 'kind', 'solid'), json_object('id', 'cart-loop-start', 'at', '21:00', 'place', '測量区画', 'participants', json_array(), 'facts', json('["mapping-cart-auto-loop","tag-track-matches-cart"]'), 'kind', 'solid'), json_object('id', 'kagawa-leaves', 'at', '21:01', 'place', '測量区画', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-left-survey-zone"]'), 'kind', 'claim'), json_object('id', 'tono-sighting', 'at', '21:07', 'place', '連絡通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '遠野澪' LIMIT 1)), 'facts', json('["tono-saw-kagawa-2107"]'), 'kind', 'solid'), json_object('id', 'iwashiro-death', 'at', '21:11', 'place', '地図解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-killed-iwashiro"]'), 'kind', 'claim'), json_object('id', 'cart-loop-end', 'at', '21:15', 'place', '測量区画', 'participants', json_array(), 'facts', json('["mapping-cart-auto-loop","tag-track-matches-cart"]'), 'kind', 'solid'), json_object('id', 'tag-recovered', 'at', '21:17', 'place', '測量区画', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1)), 'facts', json('["kagawa-recovered-tag"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '21:27', 'place', '地図解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '新堂匠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '香川紗英' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '結城真' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND name = '遠野澪' LIMIT 1)), 'facts', json('["body-found-2127"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"岩代圭吾は地図解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「試料ラベルと測量座標の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '香川か新堂に岩代が事件直前に照合していた試料採取地点について尋ね、測量座標との不一致を追及したら開示する。または遺体・現場を調べ、「試料ラベルと測量座標の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '岩代圭吾' LIMIT 1) AND label = '試料ラベルと測量座標の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '23:10', victim_found_in = '書斎', victim_investigable = 1 WHERE victim_name = '野上修一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'fraud-confrontation', 'at', '21:50', 'place', '山荘内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-forged-expenses","nogami-found-megumi-fraud"]'), 'kind', 'claim'), json_object('id', 'nogami-death', 'at', '22:05', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-killed-nogami"]'), 'kind', 'claim'), json_object('id', 'maki-sees-megumi', 'at', '22:08', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["maki-saw-megumi-study"]'), 'kind', 'solid'), json_object('id', 'megumi-goes-room', 'at', '22:15', 'place', '自室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["megumi-originally-asleep-2215"]'), 'kind', 'claim'), json_object('id', 'false-fireplace-sighting', 'at', '22:30', 'place', '暖炉前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1)), 'facts', json('["maki-lied-fireplace","original-only-maki-claimed-sighting"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:10', 'place', '書斎', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '高瀬真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '藤村達也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND name = '倉田恵' LIMIT 1)), 'facts', json('["body-found-2310"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"野上修一は書斎で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「仕入れ帳の水増し」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '倉田に野上と事件直前に揉めた帳簿の内容を追及したら開示する。または遺体・現場を調べ、「仕入れ帳の水増し」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '野上修一' LIMIT 1) AND label = '仕入れ帳の水増し'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '00:22', victim_found_in = '資料室', victim_investigable = 1 WHERE victim_name = '篠宮亮'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'readings-start', 'at', '00:00', 'place', '制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["readings-automatic","signatures-batchable"]'), 'kind', 'solid'), json_object('id', 'sagisawa-leaves', 'at', '00:01', 'place', '制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-left-control"]'), 'kind', 'claim'), json_object('id', 'passage-sighting', 'at', '00:05', 'place', '中央通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '小日向茜' LIMIT 1)), 'facts', json('["kohinata-saw-sagisawa"]'), 'kind', 'solid'), json_object('id', 'shinomiya-death', 'at', '00:08', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-killed-shinomiya"]'), 'kind', 'claim'), json_object('id', 'sagisawa-return', 'at', '00:14', 'place', '制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-returned-control"]'), 'kind', 'claim'), json_object('id', 'batch-sign', 'at', '00:16', 'place', '居住区内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1)), 'facts', json('["sagisawa-batch-signed","one-signature-transaction"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '00:22', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鳴海俊' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '鷺沢怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '小日向茜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND name = '土岐誠' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"篠宮亮は資料室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「部品交換記録と実機番号の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '鷺沢か土岐に篠宮が事件直前に照合していた部品番号と点検表について尋ねたら開示する。または遺体・現場を調べ、「部品交換記録と実機番号の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '篠宮亮' LIMIT 1) AND label = '部品交換記録と実機番号の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '17:20', victim_found_in = '上部ラウンジ', victim_investigable = 1 WHERE victim_name = '柴田功'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'passenger-leaves', 'at', '16:35', 'place', '船内', 'participants', json_array(), 'facts', json('["passenger-left-before-departure","booked-passengers-42"]'), 'kind', 'solid'), json_object('id', 'shibata-warning', 'at', '16:48', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-skimmed-sales","shibata-found-shortage","shibata-warned-kanda"]'), 'kind', 'claim'), json_object('id', 'secret-interview', 'at', '16:50', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '藤原奈緒' LIMIT 1)), 'facts', json('["fujiwara-secret-interview"]'), 'kind', 'solid'), json_object('id', 'count-sheet-made', 'at', '16:58', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-wrote-count-sheet"]'), 'kind', 'claim'), json_object('id', 'stair-sighting', 'at', '17:03', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '荻原陸' LIMIT 1)), 'facts', json('["ogiwara-saw-kanda-1703"]'), 'kind', 'solid'), json_object('id', 'shibata-death', 'at', '17:06', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-killed-shibata-1706"]'), 'kind', 'claim'), json_object('id', 'kanda-returns', 'at', '17:11', 'place', '下部客室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1)), 'facts', json('["kanda-returned-lower-1711","count-sheet-says-42"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '17:20', 'place', '上部ラウンジ', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '荻原陸' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '神田美奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND name = '藤原奈緒' LIMIT 1)), 'facts', json('["body-found-1720"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"柴田功は上部ラウンジで倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「追加券の控えと売上帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '神田に柴田が監査していた帳簿の内容を尋ねるか、藤原に会社の数字で柴田が問題視していた点を尋ねたら開示する。または遺体・現場を調べ、「追加券の控えと売上帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '柴田功' LIMIT 1) AND label = '追加券の控えと売上帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:35', victim_found_in = '祭具倉庫', victim_investigable = 1 WHERE victim_name = '神谷宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kamiya-warning', 'at', '19:50', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["kamiya-warned-tozuka","kamiya-found-kickbacks"]'), 'kind', 'claim'), json_object('id', 'blackout-start', 'at', '20:18', 'place', '会場', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["blackout-started-2018","lighting-preset-ran"]'), 'kind', 'solid'), json_object('id', 'tozuka-leaves', 'at', '20:20', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-left-console-2020"]'), 'kind', 'claim'), json_object('id', 'aihara-sighting', 'at', '20:22', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["aihara-saw-tozuka-2022"]'), 'kind', 'solid'), json_object('id', 'kamiya-death', 'at', '20:23', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-killed-kamiya-2023","spare-key-borrowed"]'), 'kind', 'claim'), json_object('id', 'tozuka-returns', 'at', '20:25', 'place', '操作卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["tozuka-returned-2025"]'), 'kind', 'claim'), json_object('id', 'lights-return', 'at', '20:26', 'place', '会場', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1)), 'facts', json('["lights-restored-2026"]'), 'kind', 'solid'), json_object('id', 'key-check', 'at', '20:28', 'place', '境内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1)), 'facts', json('["makabe-checked-key-2028"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:35', 'place', '祭具倉庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '真壁聡' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '相原夏帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND name = '戸塚岳' LIMIT 1)), 'facts', json('["body-found-2035"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"神谷宗一は祭具倉庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「照明設備の発注帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '戸塚に設備費の内訳を尋ねるか、真壁に神谷が祭り前から確認していた帳簿について尋ねたら開示する。または遺体・現場を調べ、「照明設備の発注帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神谷宗一' LIMIT 1) AND label = '照明設備の発注帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '資料庫', victim_investigable = 1 WHERE victim_name = '今泉孝臣'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'vault-opened', 'at', '21:09', 'place', '希少資料庫', 'participants', json_array(), 'facts', json('["vault-key-only-opens","vault-self-locks","vault-open-2109","vault-stayed-open"]'), 'kind', 'solid'), json_object('id', 'yagami-enters', 'at', '21:14', 'place', '資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '久我遼' LIMIT 1)), 'facts', json('["yagami-entered-vault-2114","kuga-saw-yagami-2114"]'), 'kind', 'solid'), json_object('id', 'imaizumi-enters', 'at', '21:17', 'place', '資料庫', 'participants', json_array(), 'facts', json('["imaizumi-entered-vault-2117"]'), 'kind', 'solid'), json_object('id', 'imaizumi-death', 'at', '21:20', 'place', '希少資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1)), 'facts', json('["yagami-killed-imaizumi"]'), 'kind', 'claim'), json_object('id', 'yagami-exits', 'at', '21:22', 'place', '資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1)), 'facts', json('["yagami-left-vault-2122","vault-closed-2122"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '資料庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '戸塚誠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '八神琴子' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '久我遼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND name = '真島香苗' LIMIT 1)), 'facts', json('["key-found-on-imaizumi","body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"今泉孝臣は資料庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「修復処置記録と資料状態の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '八神か久我に今泉が事件直前に調べていた修復記録について尋ね、実資料との不一致を追及したら開示する。または遺体・現場を調べ、「修復処置記録と資料状態の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '今泉孝臣' LIMIT 1) AND label = '修復処置記録と資料状態の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:35', victim_found_in = '保存記録室', victim_investigable = 1 WHERE victim_name = '磯崎章'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'door-opened-for-maintenance', 'at', '19:50', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1)), 'facts', json('["door-held-open-1950","door-remained-open-2022","no-new-unlock"]'), 'kind', 'solid'), json_object('id', 'mitamura-warning', 'at', '20:00', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["mitamura-told-yabuki-open"]'), 'kind', 'solid'), json_object('id', 'yabuki-enters', 'at', '20:09', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["yabuki-entered-unlogged"]'), 'kind', 'claim'), json_object('id', 'isozaki-death', 'at', '20:12', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1)), 'facts', json('["isozaki-death-2012","yabuki-killed-isozaki"]'), 'kind', 'claim'), json_object('id', 'corridor-return', 'at', '20:16', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '木瀬亮' LIMIT 1)), 'facts', json('["kise-saw-yabuki-return-2016"]'), 'kind', 'solid'), json_object('id', 'door-closes', 'at', '20:22', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1)), 'facts', json('["door-remained-open-2022"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:35', 'place', '保存記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '三田村悟' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '矢吹梓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND name = '木瀬亮' LIMIT 1)), 'facts', json('["body-found-2035"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"磯崎章は保存記録室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「監督部署への報告草案」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '矢吹か三田村に磯崎が翌朝提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「監督部署への報告草案」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '磯崎章' LIMIT 1) AND label = '監督部署への報告草案'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '18:55', victim_found_in = '収蔵庫前', victim_investigable = 1 WHERE victim_name = '鳥羽薫'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'forgery-discovered', 'at', '18:25', 'place', '美術館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["toba-found-forgery"]'), 'kind', 'claim'), json_object('id', 'sakaki-confronted', 'at', '18:29', 'place', '美術館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["toba-confronted-sakaki"]'), 'kind', 'claim'), json_object('id', 'mido-bribe', 'at', '18:32', 'place', '美術館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '御堂和也' LIMIT 1)), 'facts', json('["mido-offered-bribe"]'), 'kind', 'claim'), json_object('id', 'uv-test-start', 'at', '18:35', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["uv-test-started"]'), 'kind', 'solid'), json_object('id', 'sakaki-leaves', 'at', '18:38', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["uv-lamp-off-1837","sakaki-left-restoration"]'), 'kind', 'solid'), json_object('id', 'enomoto-sighting', 'at', '18:44', 'place', '収蔵庫前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榎本駿' LIMIT 1)), 'facts', json('["enomoto-saw-sakaki-1844"]'), 'kind', 'solid'), json_object('id', 'toba-death', 'at', '18:46', 'place', '収蔵庫前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["sakaki-killed-toba-1846"]'), 'kind', 'claim'), json_object('id', 'sakaki-returns', 'at', '18:50', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1)), 'facts', json('["sakaki-returned-1850","uv-test-resumed"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '18:55', 'place', '収蔵庫前', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '御堂和也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榊玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND name = '榎本駿' LIMIT 1)), 'facts', json('["body-found-1855"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"鳥羽薫は収蔵庫前で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「鳥羽の額装検査メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '榊か御堂に鳥羽が事件前に作品の真贋を調べていなかったか尋ね、額装の違いへ話が及んだら開示する。または遺体・現場を調べ、「鳥羽の額装検査メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鳥羽薫' LIMIT 1) AND label = '鳥羽の額装検査メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '06:28', victim_found_in = '種子保管区', victim_investigable = 1 WHERE victim_name = 'ミラ・ヴォス'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'mira-confronts-sera', 'at', '05:45', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["sera-seed-diversion","mira-found-diversion","mira-would-audit"]'), 'kind', 'claim'), json_object('id', 'dario-sees-sera', 'at', '05:52', 'place', '種子保管区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ダリオ・ケイン' LIMIT 1)), 'facts', json('["dario-saw-sera-0552"]'), 'kind', 'solid'), json_object('id', 'mira-death', 'at', '05:57', 'place', '種子保管区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["sera-killed-mira"]'), 'kind', 'claim'), json_object('id', 'agriculture-six', 'at', '06:00', 'place', '農業区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1)), 'facts', json('["agriculture-dawn-0600-local","sera-claimed-after-six"]'), 'kind', 'solid'), json_object('id', 'medical-six', 'at', '06:20', 'place', '医療区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ユナ・パク' LIMIT 1)), 'facts', json('["medical-dawn-0600-local"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '06:28', 'place', '種子保管区', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'セラ・イワノフ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ダリオ・ケイン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND name = 'ユナ・パク' LIMIT 1)), 'facts', json('["body-found-0628"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"ミラ・ヴォスは種子保管区で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「希少種子の監査記録」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'セラかダリオにミラが事件直前に確認していた種子在庫を尋ねたら開示する。または遺体・現場を調べ、「希少種子の監査記録」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ミラ・ヴォス' LIMIT 1) AND label = '希少種子の監査記録'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '06:30', victim_found_in = '標本庫', victim_investigable = 1 WHERE victim_name = '木島祥子'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'confrontation', 'at', '05:45', 'place', '植物園内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["kijima-found-missing-tags","kijima-confronted-narahara"]'), 'kind', 'claim'), json_object('id', 'narahara-leaves', 'at', '05:52', 'place', '東温室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-left-east-house","manual-water-zero"]'), 'kind', 'solid'), json_object('id', 'auto-misting', 'at', '06:00', 'place', '東温室', 'participants', json_array(), 'facts', json('["auto-misting-ran-0600"]'), 'kind', 'solid'), json_object('id', 'corridor-sighting', 'at', '06:05', 'place', '標本庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '大西蒼太' LIMIT 1)), 'facts', json('["onishi-saw-narahara-0605"]'), 'kind', 'solid'), json_object('id', 'kijima-death', 'at', '06:08', 'place', '標本庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-killed-kijima-0608"]'), 'kind', 'claim'), json_object('id', 'narahara-returns', 'at', '06:15', 'place', '東温室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1)), 'facts', json('["narahara-returned-east-0615"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '06:30', 'place', '標本庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '水沢浩司' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '楢原彩' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND name = '大西蒼太' LIMIT 1)), 'facts', json('["body-found-0630"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"木島祥子は標本庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「木島の希少植物管理ノート」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '楢原に希少植物の管理札が足りない理由を尋ねるか、大西に木島が前日から調べていた記録を尋ねたら開示する。または遺体・現場を調べ、「木島の希少植物管理ノート」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '木島祥子' LIMIT 1) AND label = '木島の希少植物管理ノート'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:00', victim_found_in = '編集長室', victim_investigable = 1 WHERE victim_name = '石橋礼司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'royalty-confrontation', 'at', '20:25', 'place', '編集部内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-diverted-royalties","ishibashi-found-royalty-gap","ishibashi-warned-kawase"]'), 'kind', 'claim'), json_object('id', 'shido-argument', 'at', '20:31', 'place', '編集部内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '志堂透' LIMIT 1)), 'facts', json('["shido-argued-2031","shido-plagiarism"]'), 'kind', 'solid'), json_object('id', 'kawase-enters', 'at', '20:38', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-entered-2038"]'), 'kind', 'claim'), json_object('id', 'ishibashi-death', 'at', '20:41', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["kawase-killed-ishibashi-2041"]'), 'kind', 'claim'), json_object('id', 'kawase-leaves', 'at', '20:44', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '藤本圭' LIMIT 1)), 'facts', json('["fujimoto-saw-kawase-2044"]'), 'kind', 'solid'), json_object('id', 'contract-print', 'at', '20:47', 'place', '編集部', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["contract-printed-2047","kawase-forged-signature"]'), 'kind', 'solid'), json_object('id', 'claimed-signing', 'at', '20:50', 'place', '編集部内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1)), 'facts', json('["signed-contract-claims-2050"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '21:00', 'place', '編集長室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '藤本圭' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '川瀬梨奈' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND name = '志堂透' LIMIT 1)), 'facts', json('["body-found-2100"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"石橋礼司は編集長室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「海外版権の送金記録と支払帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '川瀬に石橋が調べていた版権収入について尋ねるか、藤本に石橋が事件前に確認していた帳簿を尋ねたら開示する。または遺体・現場を調べ、「海外版権の送金記録と支払帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '石橋礼司' LIMIT 1) AND label = '海外版権の送金記録と支払帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:42', victim_found_in = '修復室', victim_investigable = 1 WHERE victim_name = '倉橋宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'clock-offset-remains', 'at', '19:36', 'place', '館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '城戸篤' LIMIT 1)), 'facts', json('["master-clock-fast-eleven","gallery-clocks-follow-master","kido-caused-clock-offset"]'), 'kind', 'solid'), json_object('id', 'false-half-past-chime', 'at', '20:19', 'place', '館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '保科悠人' LIMIT 1)), 'facts', json('["half-past-chime-actual-2019","security-clock-accurate"]'), 'kind', 'solid'), json_object('id', 'shiba-kurahashi-talk', 'at', '20:20', 'place', '西回廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-spoke-kurahashi-2020"]'), 'kind', 'solid'), json_object('id', 'shiba-leaves-west', 'at', '20:22', 'place', '西回廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-left-west-corridor"]'), 'kind', 'claim'), json_object('id', 'asakura-sighting', 'at', '20:24', 'place', '北廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1)), 'facts', json('["asakura-saw-shiba-2024"]'), 'kind', 'solid'), json_object('id', 'kurahashi-death', 'at', '20:28', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-killed-kurahashi"]'), 'kind', 'claim'), json_object('id', 'shiba-return', 'at', '20:34', 'place', '展示準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1)), 'facts', json('["shiba-returned-gallery"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '20:42', 'place', '修復室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '保科悠人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '志波沙月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '朝倉真紀' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND name = '城戸篤' LIMIT 1)), 'facts', json('["body-found-2042"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"倉橋宗一は修復室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「倉橋の来歴照合メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '志波か保科に倉橋が事件直前に調べていた展示時計の来歴について尋ね、登録内容の不一致を追及したら開示する。または遺体・現場を調べ、「倉橋の来歴照合メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋宗一' LIMIT 1) AND label = '倉橋の来歴照合メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:00', victim_found_in = '執務室', victim_investigable = 1 WHERE victim_name = '長峰宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'invoice-confrontation', 'at', '20:52', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["renovation-shortage","ayase-falsified-invoices","nagamine-called-ayase"]'), 'kind', 'claim'), json_object('id', 'nagamine-death', 'at', '21:05', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["ayase-killed-nagamine"]'), 'kind', 'claim'), json_object('id', 'coat-missing', 'at', '21:18', 'place', 'フロント裏', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '星野結' LIMIT 1)), 'facts', json('["hoshino-saw-empty-coat-hook"]'), 'kind', 'solid'), json_object('id', 'silhouette-staged', 'at', '21:23', 'place', 'ホテル内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1)), 'facts', json('["ayase-staged-silhouette","desk-lamp-left-on","stand-feet-dust-mark"]'), 'kind', 'solid'), json_object('id', 'silhouette-seen', 'at', '21:30', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '神田理一' LIMIT 1)), 'facts', json('["kanda-saw-silhouette","kanda-did-not-hear-voice"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:00', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '星野結' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '綾瀬環' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND name = '神田理一' LIMIT 1)), 'facts', json('["body-found-2200","coat-returned-after-discovery"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"長峰宗一は執務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「水増しされた改装請求書」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '綾瀬に改装費の処理を尋ねるか、長峰が直前まで確認していた書類について追及したら開示する。または遺体・現場を調べ、「水増しされた改装請求書」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '長峰宗一' LIMIT 1) AND label = '水増しされた改装請求書'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:25', victim_found_in = '駅務室', victim_investigable = 1 WHERE victim_name = '藤崎正雄'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'refund-confrontation', 'at', '21:55', 'place', '駅構内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["arima-skimmed-refunds","fujisaki-found-refund-gap","fujisaki-warned-arima"]'), 'kind', 'claim'), json_object('id', 'arima-office-corridor', 'at', '22:05', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1)), 'facts', json('["arima-entered-office-2205","sawada-saw-arima-2205"]'), 'kind', 'solid'), json_object('id', 'fujisaki-death', 'at', '22:08', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["arima-killed-fujisaki-2208"]'), 'kind', 'claim'), json_object('id', 'arima-returns-platform', 'at', '22:12', 'place', 'ホーム', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1)), 'facts', json('["arima-left-office-2212","komori-saw-arima-2212"]'), 'kind', 'solid'), json_object('id', 'last-train-departs', 'at', '22:18', 'place', '駅構内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1)), 'facts', json('["last-train-delayed"]'), 'kind', 'solid'), json_object('id', 'certificates-print', 'at', '22:20', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1)), 'facts', json('["certificates-printed-2220"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:25', 'place', '駅務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '小森拓' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '有馬結衣' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND name = '沢田亮' LIMIT 1)), 'facts', json('["body-found-2225"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"藤崎正雄は駅務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「券売機返金処理と現金残高の差」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '有馬に藤崎が事件前に確認していた返金処理について尋ねるか、小森に駅長が帳簿を調べていた理由を尋ねたら開示する。または遺体・現場を調べ、「券売機返金処理と現金残高の差」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '藤崎正雄' LIMIT 1) AND label = '券売機返金処理と現金残高の差'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:50', victim_found_in = '解析室', victim_investigable = 1 WHERE victim_name = 'エレナ・ヴァルガ'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'noah-question', 'at', '21:18', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1)), 'facts', json('["noah-sent-question-2118"]'), 'kind', 'solid'), json_object('id', 'fraud-confrontation', 'at', '21:25', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1)), 'facts', json('["noah-budget-fraud","elena-found-fraud","elena-would-report"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '21:33', 'place', '連絡廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ルイス・オルテガ' LIMIT 1)), 'facts', json('["luis-saw-noah-corridor"]'), 'kind', 'solid'), json_object('id', 'elena-death', 'at', '21:36', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1)), 'facts', json('["noah-killed-elena"]'), 'kind', 'claim'), json_object('id', 'delayed-reply', 'at', '21:38', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1)), 'facts', json('["earth-reply-arrived-2138","noah-played-arrival-tone","earth-mars-delay"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:50', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ノア・チェン' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'アミラ・サイード' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND name = 'ルイス・オルテガ' LIMIT 1)), 'facts', json('["body-found-2150"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"エレナ・ヴァルガは解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「調達予算の監査ファイル」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'ノアかアミラにエレナが翌朝送ろうとしていた監査資料について尋ねたら開示する。または遺体・現場を調べ、「調達予算の監査ファイル」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エレナ・ヴァルガ' LIMIT 1) AND label = '調達予算の監査ファイル'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '00:12', victim_found_in = '第2ブース', victim_investigable = 1 WHERE victim_name = '大門修一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'opening-recording', 'at', '23:20', 'place', '局内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1)), 'facts', json('["opening-recorded-2320"]'), 'kind', 'claim'), json_object('id', 'natsume-meets-daimon', 'at', '23:38', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["natsume-secret-meeting"]'), 'kind', 'solid'), json_object('id', 'natsume-leaves', 'at', '23:43', 'place', 'ロビー', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["natsume-left-2343"]'), 'kind', 'solid'), json_object('id', 'minobe-enters', 'at', '23:46', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["minobe-entered-2346"]'), 'kind', 'claim'), json_object('id', 'daimon-death', 'at', '23:48', 'place', 'ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["daimon-died-2348"]'), 'kind', 'claim'), json_object('id', 'minobe-returns', 'at', '23:50', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1)), 'facts', json('["kuze-saw-minobe-2350"]'), 'kind', 'solid'), json_object('id', 'recording-queued', 'at', '23:53', 'place', '送出室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1)), 'facts', json('["minobe-queued-recording"]'), 'kind', 'claim'), json_object('id', 'opening-airs', 'at', '00:00', 'place', '局内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["recorded-opening-aired"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '00:12', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '久世直人' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '美濃部沙耶' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND name = '夏目亮介' LIMIT 1)), 'facts', json('["body-found-0012"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"大門修一は第2ブースで倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「大門が保存したスポンサー報告の比較メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '美濃部に制作費やスポンサー報告の不一致について尋ねるか、夏目に大門が最近スポンサー実績を調べていなかったか尋ねたら開示する。または遺体・現場を調べ、「大門が保存したスポンサー報告の比較メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '大門修一' LIMIT 1) AND label = '大門が保存したスポンサー報告の比較メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '23:10', victim_found_in = '解析室', victim_investigable = 1 WHERE victim_name = '牧瀬航'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'sync-failure', 'at', '21:50', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '樋口海' LIMIT 1)), 'facts', json('["central-clock-offset","higuchi-hid-sync-failure"]'), 'kind', 'solid'), json_object('id', 'false-alibi-window', 'at', '22:13', 'place', '基地内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1)), 'facts', json('["terminal-uses-central-clock","access-uses-central-clock","observation-log-central-clock","wall-clock-synced","four-times-not-independent"]'), 'kind', 'solid'), json_object('id', 'real-corridor-sighting', 'at', '22:20', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '沢渡直人' LIMIT 1)), 'facts', json('["handheld-clock-correct","sawatari-saw-shinomiya-real-2220","displayed-time-was-2227"]'), 'kind', 'solid'), json_object('id', 'makise-death', 'at', '22:23', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1)), 'facts', json('["makise-death-real-2223","shinomiya-killed-makise"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:10', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '樋口海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '篠宮怜' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND name = '沢渡直人' LIMIT 1)), 'facts', json('["body-found-2310"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"牧瀬航は解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「翌朝の研究会議議題」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '篠宮か樋口に翌朝の研究会議で牧瀬が扱う予定だった議題を尋ねたら開示する。または遺体・現場を調べ、「翌朝の研究会議議題」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '牧瀬航' LIMIT 1) AND label = '翌朝の研究会議議題'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:40', victim_found_in = '事務室', victim_investigable = 1 WHERE victim_name = '星名悟'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'badge-loan', 'at', '20:55', 'place', '診療所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1)), 'facts', json('["kuga-borrowed-badge","sera-approved-loan"]'), 'kind', 'claim'), json_object('id', 'hoshina-death', 'at', '21:08', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1)), 'facts', json('["hoshina-death-2108","sera-killed-hoshina"]'), 'kind', 'claim'), json_object('id', 'orange-passage', 'at', '21:18', 'place', '診療所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '相原誠' LIMIT 1)), 'facts', json('["orange-badge-passed-2118","aihara-saw-orange-suit","person-was-kuga-2118"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:40', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '相原誠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '世良美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND name = '久我夏樹' LIMIT 1)), 'facts', json('["body-found-2140"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"星名悟は事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「本部報告予定の確認メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '世良か久我に星名が翌朝予定していた本部報告の内容を尋ねたら開示する。または遺体・現場を調べ、「本部報告予定の確認メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '星名悟' LIMIT 1) AND label = '本部報告予定の確認メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '19:15', victim_found_in = '店奥', victim_investigable = 1 WHERE victim_name = '水野英治'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kuroda-offer', 'at', '18:28', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1)), 'facts', json('["kuroda-secret-offer"]'), 'kind', 'claim'), json_object('id', 'swap-discovered', 'at', '18:37', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["mizuno-discovered-swap"]'), 'kind', 'claim'), json_object('id', 'kuroda-leaves', 'at', '18:42', 'place', '軒下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1)), 'facts', json('["kuroda-left-1842"]'), 'kind', 'solid'), json_object('id', 'kuroda-sighting', 'at', '18:47', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '黒田征司' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["kuroda-saw-makino-1847"]'), 'kind', 'solid'), json_object('id', 'mizuno-death', 'at', '18:50', 'place', '店奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["makino-killed-mizuno-1850"]'), 'kind', 'claim'), json_object('id', 'makino-departs', 'at', '18:56', 'place', '店内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '瀬名真琴' LIMIT 1)), 'facts', json('["makino-left-1856","sena-saw-makino-leave"]'), 'kind', 'solid'), json_object('id', 'parcel-posted', 'at', '19:08', 'place', '郵便窓口', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '牧野千尋' LIMIT 1)), 'facts', json('["post-receipt-1908"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '19:15', 'place', '店奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND name = '瀬名真琴' LIMIT 1)), 'facts', json('["body-found-1915"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"水野英治は店奥で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「店頭に残った複製本」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '牧野か黒田に事件当日の高価な初版本について詳しく尋ね、真贋に疑問が出たら開示する。または遺体・現場を調べ、「店頭に残った複製本」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '水野英治' LIMIT 1) AND label = '店頭に残った複製本'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:20', victim_found_in = '事務室', victim_investigable = 1 WHERE victim_name = '周文海'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'blank-form-signed', 'at', '21:35', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1)), 'facts', json('["zhou-signed-blank-form"]'), 'kind', 'claim'), json_object('id', 'chen-sees-form', 'at', '21:40', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1)), 'facts', json('["chen-saw-blank-time"]'), 'kind', 'solid'), json_object('id', 'zhou-death', 'at', '21:52', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1)), 'facts', json('["lin-killed-zhou","lin-smuggled-silk","zhou-found-smuggling"]'), 'kind', 'claim'), json_object('id', 'time-added', 'at', '22:05', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1)), 'facts', json('["lin-added-2205","top-sheet-time-added-later"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:20', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '林雪梅' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '陳伯安' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND name = '王世傑' LIMIT 1)), 'facts', json('["body-found-2220"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"周文海は事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「絹荷の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '林か陳に周が事件前に調べていた荷の不一致を尋ねたら開示する。または遺体・現場を調べ、「絹荷の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '周文海' LIMIT 1) AND label = '絹荷の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:50', victim_found_in = '館内', victim_investigable = 1 WHERE victim_name = '青沼卓'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'snowfall-begins', 'at', '20:10', 'place', '母屋', 'participants', json_array(), 'facts', json('["snow-started-2010"]'), 'kind', 'solid'), json_object('id', 'kurata-crosses', 'at', '20:17', 'place', '離れ展示室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1)), 'facts', json('["kurata-crossed-2017"]'), 'kind', 'claim'), json_object('id', 'aoonuma-death', 'at', '20:20', 'place', '離れ展示室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1)), 'facts', json('["aoonuma-death-2020","kurata-killed-aoonuma"]'), 'kind', 'claim'), json_object('id', 'path-cleared', 'at', '20:26', 'place', '屋外通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["anzai-cleared-path-2026","snow-covered-after-clearing"]'), 'kind', 'solid'), json_object('id', 'kurata-back-main', 'at', '20:31', 'place', '母屋裏口', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["anzai-saw-kurata-2031"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:50', 'place', '館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '江波涼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '倉田真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND name = '安西雄' LIMIT 1)), 'facts', json('["body-found-2050","no-tracks-at-discovery"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"青沼卓は館内で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「来歴記録の修正履歴」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '倉田か江波に青沼が直前まで確認していた作品来歴について尋ねたら開示する。または遺体・現場を調べ、「来歴記録の修正履歴」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '青沼卓' LIMIT 1) AND label = '来歴記録の修正履歴'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:15', victim_found_in = '資料室', victim_investigable = 1 WHERE victim_name = '神崎遼'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'kanzaki-warning', 'at', '21:40', 'place', '観測所内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["kanzaki-found-fabrication","kanzaki-would-retract"]'), 'kind', 'claim'), json_object('id', 'kurose-data-room', 'at', '21:48', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '黒瀬俊介' LIMIT 1)), 'facts', json('["kurose-entered-data-room","kurose-copied-data"]'), 'kind', 'solid'), json_object('id', 'interval-start', 'at', '21:50', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["camera-interval-started","camera-kept-shooting"]'), 'kind', 'solid'), json_object('id', 'hiyama-leaves-roof', 'at', '21:54', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-left-roof-2154"]'), 'kind', 'claim'), json_object('id', 'muroi-sighting', 'at', '21:57', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '室井邦彦' LIMIT 1)), 'facts', json('["muroi-saw-hiyama-2157"]'), 'kind', 'solid'), json_object('id', 'kanzaki-death', 'at', '22:00', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-killed-kanzaki-2200"]'), 'kind', 'claim'), json_object('id', 'hiyama-returns', 'at', '22:06', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1)), 'facts', json('["hiyama-returned-roof-2206"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '22:15', 'place', '資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '黒瀬俊介' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '日山澪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND name = '室井邦彦' LIMIT 1)), 'facts', json('["body-found-2215"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"神崎遼は資料室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「神崎の観測データ検証メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '日山か黒瀬に神崎が直前まで調べていたデータについて尋ね、不正補正の可能性を追及したら開示する。または遺体・現場を調べ、「神崎の観測データ検証メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '神崎遼' LIMIT 1) AND label = '神崎の観測データ検証メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '2026-01-16T07:10:00+09:00', victim_found_in = '事務室', victim_investigable = 1 WHERE victim_name = '佐久間隆志'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'shortage-discovered', 'at', '21:35', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1)), 'facts', json('["audit-shortage-found"]'), 'kind', 'claim'), json_object('id', 'confrontation', 'at', '21:50', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["sakuma-confronted-fuyuki","fuyuki-diverted-sales"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '22:06', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '久世圭太' LIMIT 1)), 'facts', json('["kuze-saw-fuyuki-office-side"]'), 'kind', 'solid'), json_object('id', 'sakuma-death', 'at', '22:10', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["fuyuki-killed-sakuma","fuyuki-left-victim-coat"]'), 'kind', 'claim'), json_object('id', 'staged-chores', 'at', '05:35', 'place', '農園内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1)), 'facts', json('["fuyuki-did-morning-chores","feed-board-magnet-moved","fuyuki-boots-wet-straw"]'), 'kind', 'solid'), json_object('id', 'kitchen-light-seen', 'at', '05:48', 'place', '農園内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1)), 'facts', json('["kitchen-light-on","nozomi-assumed-uncle-awake"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '07:10', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '佐久間希' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '冬木紬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND name = '久世圭太' LIMIT 1)), 'facts', json('["body-found-0710"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"佐久間隆志は事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「出荷伝票の不足メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '希か冬木に前夜の帳簿確認と不足伝票について尋ね、経理上の問題を追及したら開示する。または遺体・現場を調べ、「出荷伝票の不足メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '佐久間隆志' LIMIT 1) AND label = '出荷伝票の不足メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:05', victim_found_in = '保存庫', victim_investigable = 1 WHERE victim_name = '荻原直哉'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'report-confrontation', 'at', '21:02', 'place', '額装作業室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["forged-restoration-report","ogiwara-found-forgery","ogiwara-called-kosaka"]'), 'kind', 'claim'), json_object('id', 'ogiwara-death', 'at', '21:10', 'place', '額装作業室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-killed-ogiwara-framing","framing-paper-fibers"]'), 'kind', 'solid'), json_object('id', 'transfer-to-vault', 'at', '21:18', 'place', '保存庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-moved-ogiwara","cart-used-after-cleaning"]'), 'kind', 'solid'), json_object('id', 'kosaka-exits-vault', 'at', '21:23', 'place', '搬送廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["kosaka-left-vault-before-lock","kosaka-apron-dust"]'), 'kind', 'solid'), json_object('id', 'vault-seals', 'at', '21:30', 'place', '保存庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '多田圭' LIMIT 1)), 'facts', json('["vault-night-mode","framing-room-open-before-2130","vault-closed-2130"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:05', 'place', '保存庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '朝倉凪' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '多田圭' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND name = '香坂澪' LIMIT 1)), 'facts', json('["body-found-2205"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"荻原直哉は保存庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「修復工程と材料在庫の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '香坂に事件前に荻原から指摘された修復報告を尋ねるか、朝倉に材料在庫の不一致を確認したら開示する。または遺体・現場を調べ、「修復工程と材料在庫の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '荻原直哉' LIMIT 1) AND label = '修復工程と材料在庫の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '23:18', victim_found_in = '編集室', victim_investigable = 1 WHERE victim_name = '冬木圭介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'roomtone-loop-made', 'at', '22:46', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-made-roomtone-loop"]'), 'kind', 'solid'), json_object('id', 'loop-recording-starts', 'at', '22:50', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["loop-routed-to-recorder","recording-repeats-identically"]'), 'kind', 'solid'), json_object('id', 'manabe-leaves', 'at', '22:53', 'place', '監視席', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-left-booth"]'), 'kind', 'claim'), json_object('id', 'shirase-hearing', 'at', '22:56', 'place', '第2ブース', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '白瀬環' LIMIT 1)), 'facts', json('["shirase-heard-loop"]'), 'kind', 'solid'), json_object('id', 'makimura-sighting', 'at', '22:58', 'place', '機材廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '牧村葉月' LIMIT 1)), 'facts', json('["makimura-saw-manabe"]'), 'kind', 'solid'), json_object('id', 'fuyuki-death', 'at', '23:02', 'place', '編集室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-killed-fuyuki"]'), 'kind', 'claim'), json_object('id', 'manabe-return', 'at', '23:08', 'place', '監視席', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1)), 'facts', json('["manabe-returned-booth"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '23:18', 'place', '編集室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '鷹野徹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '真鍋伊織' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '白瀬環' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND name = '牧村葉月' LIMIT 1)), 'facts', json('["body-found-2318"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"冬木圭介は編集室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「未公開音源の複製履歴」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '真鍋か鷹野に冬木が事件前に調べていた音源データの持ち出しについて尋ね、複製履歴を追及したら開示する。または遺体・現場を調べ、「未公開音源の複製履歴」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '冬木圭介' LIMIT 1) AND label = '未公開音源の複製履歴'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:36', victim_found_in = '演出控室', victim_investigable = 1 WHERE victim_name = '瀬尾雅人'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'auto-cues-set', 'at', '21:57', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["cue-console-auto-mode","kunieda-set-auto-cues"]'), 'kind', 'solid'), json_object('id', 'cues-start', 'at', '22:00', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1)), 'facts', json('["cues-ran-automatically","toba-saw-light-changes"]'), 'kind', 'solid'), json_object('id', 'kunieda-leaves', 'at', '22:03', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-left-console"]'), 'kind', 'claim'), json_object('id', 'sasai-sighting', 'at', '22:10', 'place', '演出控室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '笹井徹' LIMIT 1)), 'facts', json('["sasai-saw-kunieda-2210"]'), 'kind', 'solid'), json_object('id', 'seo-death', 'at', '22:14', 'place', '演出控室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-killed-seo"]'), 'kind', 'claim'), json_object('id', 'kunieda-return', 'at', '22:19', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1)), 'facts', json('["kunieda-returned-console"]'), 'kind', 'claim'), json_object('id', 'cues-end', 'at', '22:20', 'place', '調光卓', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1)), 'facts', json('["cues-ran-automatically"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:36', 'place', '演出控室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '柊真琴' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '国枝美冬' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '鳥羽香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND name = '笹井徹' LIMIT 1)), 'facts', json('["body-found-2236"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"瀬尾雅人は演出控室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「架空スタッフを含む残業費一覧」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '国枝か柊に瀬尾が事件前に問題視していた残業費請求について尋ね、スタッフ名の実在性を追及したら開示する。または遺体・現場を調べ、「架空スタッフを含む残業費一覧」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾雅人' LIMIT 1) AND label = '架空スタッフを含む残業費一覧'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '00:12', victim_found_in = '検疫準備室', victim_investigable = 1 WHERE victim_name = '江波慎吾'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'feeder-set', 'at', '23:51', 'place', '給餌台', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["feeder-auto-capable","feeding-lamp-motor-linked","morishita-set-auto-feeder"]'), 'kind', 'solid'), json_object('id', 'first-cycle', 'at', '23:55', 'place', '展示側', 'participants', json_array(), 'facts', json('["feeder-cycled"]'), 'kind', 'solid'), json_object('id', 'morishita-leaves', 'at', '23:57', 'place', '給餌台', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-left-station"]'), 'kind', 'claim'), json_object('id', 'sagara-sighting', 'at', '00:02', 'place', '検疫準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '相良芳江' LIMIT 1)), 'facts', json('["sagara-saw-morishita"]'), 'kind', 'solid'), json_object('id', 'enami-death', 'at', '00:05', 'place', '検疫準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-killed-enami"]'), 'kind', 'claim'), json_object('id', 'morishita-return', 'at', '00:09', 'place', '給餌台', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1)), 'facts', json('["morishita-returned"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '00:12', 'place', '検疫準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '御子柴徹' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '森下莉央' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '榊原直' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND name = '相良芳江' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"江波慎吾は検疫準備室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「飼育記録の版差分」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '森下か榊原に江波が事件直前に調べていた飼育記録について尋ね、改ざんの可能性を追及したら開示する。または遺体・現場を調べ、「飼育記録の版差分」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '江波慎吾' LIMIT 1) AND label = '飼育記録の版差分'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:10', victim_found_in = '旧制御室', victim_investigable = 1 WHERE victim_name = '峰岸達也'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'records-confrontation', 'at', '20:12', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-falsified-inspection","minegishi-found-falsification","minegishi-called-kido"]'), 'kind', 'claim'), json_object('id', 'walkway-still-open', 'at', '20:15', 'place', '保守歩廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["walkway-passable-2015","old-walkway-connects"]'), 'kind', 'solid'), json_object('id', 'kido-enters', 'at', '20:18', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-used-walkway"]'), 'kind', 'claim'), json_object('id', 'minegishi-death', 'at', '20:24', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-killed-minegishi"]'), 'kind', 'claim'), json_object('id', 'kido-leaves', 'at', '20:29', 'place', '保守歩廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1)), 'facts', json('["kido-left-before-rise","kido-boot-silt"]'), 'kind', 'solid'), json_object('id', 'water-cuts-route', 'at', '20:40', 'place', '保守歩廊', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["walkway-closed-2040","water-log-recorded-rise"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:10', 'place', '旧制御室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '西園寺悠' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '城戸真琴' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND name = '谷口航' LIMIT 1)), 'facts', json('["front-door-remained-locked","body-found-2110"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"峰岸達也は旧制御室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「未実施箇所の点検票」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '城戸か西園寺に事件当日の監査対象となった点検票について尋ねたら開示する。または遺体・現場を調べ、「未実施箇所の点検票」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '峰岸達也' LIMIT 1) AND label = '未実施箇所の点検票'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:02', victim_found_in = '執務室', victim_investigable = 1 WHERE victim_name = '榊原宗一'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'mail-drafted', 'at', '18:11', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["mail-drafted-1811"]'), 'kind', 'solid'), json_object('id', 'mail-scheduled', 'at', '18:12', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["mail-scheduled-2142","aizawa-knew-scheduled-mail"]'), 'kind', 'solid'), json_object('id', 'aizawa-enters-office', 'at', '21:24', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["aizawa-entered-office-2124"]'), 'kind', 'claim'), json_object('id', 'sakakibara-death', 'at', '21:28', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1)), 'facts', json('["aizawa-killed-sakakibara"]'), 'kind', 'claim'), json_object('id', 'horie-sighting', 'at', '21:29', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '堀江充' LIMIT 1)), 'facts', json('["horie-saw-aizawa-2129"]'), 'kind', 'solid'), json_object('id', 'aizawa-joins-hatori', 'at', '21:35', 'place', '講義室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '羽鳥栞' LIMIT 1)), 'facts', json('["aizawa-joined-hatori-2135","aizawa-with-hatori-until-2155"]'), 'kind', 'solid'), json_object('id', 'scheduled-mail-sends', 'at', '21:42', 'place', '執務室', 'participants', json_array(), 'facts', json('["mail-sent-automatically"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:02', 'place', '執務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '御影崇' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '相沢奈緒' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '羽鳥栞' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND name = '堀江充' LIMIT 1)), 'facts', json('["body-found-2202"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"榊原宗一は執務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「相沢の小口支出重複一覧」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '相沢か御影に榊原が夕方確認していた支出一覧を尋ね、重複と翌朝の監査予定を追及したら開示する。または遺体・現場を調べ、「相沢の小口支出重複一覧」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '榊原宗一' LIMIT 1) AND label = '相沢の小口支出重複一覧'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:50', victim_found_in = '西外扉', victim_investigable = 1 WHERE victim_name = '倉橋徹'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'west-seal', 'at', '18:40', 'place', '西外扉', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透' LIMIT 1)), 'facts', json('["west-gate-sealed"]'), 'kind', 'solid'), json_object('id', 'east-round', 'at', '19:25', 'place', '東側', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1)), 'facts', json('["torigoe-coat-wet-before"]'), 'kind', 'claim'), json_object('id', 'panel-reading', 'at', '20:12', 'place', '制御盤', 'participants', json_array(), 'facts', json('["slate-entry-2012","slate-written-after-round","torigoe-copied-panel-reading"]'), 'kind', 'solid'), json_object('id', 'stair-sighting', 'at', '20:21', 'place', '内階段', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '橋場圭' LIMIT 1)), 'facts', json('["hashiba-saw-torigoe-2021"]'), 'kind', 'solid'), json_object('id', 'kurahashi-death', 'at', '20:24', 'place', '灯台内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1)), 'facts', json('["kurahashi-death-2024","torigoe-killed-kurahashi"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '20:50', 'place', '西外扉', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '森下透' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '鳥越玲' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND name = '橋場圭' LIMIT 1)), 'facts', json('["body-found-2050","west-seal-intact"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"倉橋徹は西外扉で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「補給報告の在庫差メモ」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '鳥越か森下に翌日の補給報告と燃料在庫の差について尋ねたら開示する。または遺体・現場を調べ、「補給報告の在庫差メモ」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '倉橋徹' LIMIT 1) AND label = '補給報告の在庫差メモ'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:50', victim_found_in = '帳場奥', victim_investigable = 1 WHERE victim_name = '桐谷宗介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'supply-confrontation', 'at', '20:58', 'place', '山宿内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["supplier-kickback","kiritani-found-kickback","kiritani-confronted-akiwa"]'), 'kind', 'claim'), json_object('id', 'kiritani-death', 'at', '21:08', 'place', '帳場奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["akiwa-killed-kiritani","akiwa-took-cane"]'), 'kind', 'claim'), json_object('id', 'tapping-staged', 'at', '21:25', 'place', '旧階段', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '森崎透' LIMIT 1)), 'facts', json('["akiwa-made-tapping","morisaki-heard-taps","morisaki-heard-no-steps"]'), 'kind', 'solid'), json_object('id', 'cane-hidden', 'at', '21:31', 'place', '厨房脇', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1)), 'facts', json('["cane-found-kitchen","rail-fresh-marks"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:50', 'place', '帳場奥', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '榊原蓮' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '秋庭澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND name = '森崎透' LIMIT 1)), 'facts', json('["body-found-2150"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"桐谷宗介は帳場奥で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「不自然な仕入れ伝票」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '秋庭に仕入れ業者との関係を尋ねるか、桐谷が事件前に確認していた伝票について追及したら開示する。または遺体・現場を調べ、「不自然な仕入れ伝票」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '桐谷宗介' LIMIT 1) AND label = '不自然な仕入れ伝票'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:00', victim_found_in = '投影準備室', victim_investigable = 1 WHERE victim_name = '犬塚誠'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'inuzuka-death', 'at', '21:24', 'place', '投影準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1)), 'facts', json('["inuzuka-death-2124","onodera-killed-inuzuka"]'), 'kind', 'claim'), json_object('id', 'booth-lights-down', 'at', '21:30', 'place', '投影室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["booth-dark-2130","glass-reflects-dark-booth"]'), 'kind', 'solid'), json_object('id', 'reflected-sighting', 'at', '21:35', 'place', '天象館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["onodera-behind-matsuda-2135","matsuda-saw-white-figure","figure-was-reflection","onodera-endorsed-sighting"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:00', 'place', '投影準備室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '朝倉葉月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '小野寺莉香' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND name = '松田圭介' LIMIT 1)), 'facts', json('["body-found-2200"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"犬塚誠は投影準備室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「運営法人への予算報告草案」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '小野寺か朝倉に犬塚が翌朝運営法人へ提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「運営法人への予算報告草案」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '犬塚誠' LIMIT 1) AND label = '運営法人への予算報告草案'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:40', victim_found_in = '解析室', victim_investigable = 1 WHERE victim_name = '瀬尾俊'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'launch-loss', 'at', '19:42', 'place', '甲板', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["launch-lost-1942","kariya-saw-launch-loss"]'), 'kind', 'solid'), json_object('id', 'launch-note', 'at', '19:48', 'place', '甲板', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["launch-note-signed"]'), 'kind', 'solid'), json_object('id', 'seo-call', 'at', '19:56', 'place', '船内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '野々村岳' LIMIT 1)), 'facts', json('["seo-alive-1956","launch-gone-before-death"]'), 'kind', 'solid'), json_object('id', 'kariya-analysis', 'at', '20:14', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1)), 'facts', json('["kariya-entered-analysis-2014"]'), 'kind', 'claim'), json_object('id', 'seo-death', 'at', '20:18', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1)), 'facts', json('["seo-death-2018","kariya-killed-seo"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '20:22', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["mizuki-saw-kariya-2022"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:40', 'place', '解析室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '野々村岳' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '狩谷琴音' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND name = '水城明' LIMIT 1)), 'facts', json('["body-found-2040"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"瀬尾俊は解析室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「共同研究データの提出履歴」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '狩谷か野々村に瀬尾が直前まで確認していた研究データの提出経緯を尋ねたら開示する。または遺体・現場を調べ、「共同研究データの提出履歴」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '瀬尾俊' LIMIT 1) AND label = '共同研究データの提出履歴'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:45', victim_found_in = '西側事務室', victim_investigable = 1 WHERE victim_name = '鷺沢修'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'submission-confrontation', 'at', '20:58', 'place', '会館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1)), 'facts', json('["kuga-stole-results","sagisawa-found-submission","sagisawa-confronted-kuga"]'), 'kind', 'claim'), json_object('id', 'sagisawa-death', 'at', '21:10', 'place', '西側事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1)), 'facts', json('["kuga-killed-sagisawa"]'), 'kind', 'claim'), json_object('id', 'west-corridor-sighting', 'at', '21:16', 'place', '西側事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["shiraishi-saw-kuga-west"]'), 'kind', 'solid'), json_object('id', 'rumor-starts', 'at', '21:18', 'place', '東資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1)), 'facts', json('["kuga-claimed-saw-east","kuga-started-rumor","no-independent-east-sighting"]'), 'kind', 'claim'), json_object('id', 'rumor-repeated', 'at', '21:22', 'place', '会館内', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["naruse-learned-from-kuga","naruse-repeated-rumor","shiraishi-learned-from-naruse"]'), 'kind', 'solid'), json_object('id', 'east-room-still-unused', 'at', '21:40', 'place', '東資料室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["east-room-unused"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:45', 'place', '西側事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '成瀬灯' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '久我真帆' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND name = '白石慧' LIMIT 1)), 'facts', json('["body-found-2145"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"鷺沢修は西側事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「単独名義の投稿原稿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '久我か成瀬に事件直前の投稿原稿と鷺沢との対立について尋ねたら開示する。または遺体・現場を調べ、「単独名義の投稿原稿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '鷺沢修' LIMIT 1) AND label = '単独名義の投稿原稿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '21:38', victim_found_in = '運行事務室', victim_investigable = 1 WHERE victim_name = '高瀬修司'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'tool-case-placed', 'at', '21:09', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["cabin-display-weight-based","tool-case-heavy-enough","sudo-left-tool-case"]'), 'kind', 'solid'), json_object('id', 'occupancy-start', 'at', '21:10', 'place', '山頂駅', 'participants', json_array(), 'facts', json('["occupancy-display-on"]'), 'kind', 'solid'), json_object('id', 'sudo-leaves-cabin', 'at', '21:12', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-left-cabin-area"]'), 'kind', 'claim'), json_object('id', 'enomoto-sighting', 'at', '21:18', 'place', '職員通路', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '榎本澄' LIMIT 1)), 'facts', json('["enomoto-saw-sudo-2118"]'), 'kind', 'solid'), json_object('id', 'takase-death', 'at', '21:22', 'place', '運行事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-killed-takase"]'), 'kind', 'claim'), json_object('id', 'sudo-return', 'at', '21:28', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["sudo-returned-cabin"]'), 'kind', 'claim'), json_object('id', 'occupancy-end', 'at', '21:30', 'place', '非常搬器', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1)), 'facts', json('["occupancy-display-on"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '21:38', 'place', '運行事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '長峰礼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '須藤拓海' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '榎本澄' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND name = '折原壮' LIMIT 1)), 'facts', json('["body-found-2138"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"高瀬修司は運行事務室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「制動点検記録と作業履歴の不一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '須藤か長峰に高瀬が事件直前に確認していた制動点検について尋ね、記録と実作業の不一致を追及したら開示する。または遺体・現場を調べ、「制動点検記録と作業履歴の不一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬修司' LIMIT 1) AND label = '制動点検記録と作業履歴の不一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '20:30', victim_found_in = '書斎', victim_investigable = 1 WHERE victim_name = '高瀬涼子'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'dinner-start', 'at', '19:00', 'place', 'dining', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["dinner-started-1900"]'), 'kind', 'solid'), json_object('id', 'fukagawa-leaves', 'at', '19:15', 'place', 'corridor', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["fukagawa-left-1915","fukagawa-at-phone-booth"]'), 'kind', 'solid'), json_object('id', 'ryoko-to-study', 'at', '19:20', 'place', 'study', 'participants', json_array(), 'facts', json('["ryoko-moved-to-study-1920"]'), 'kind', 'solid'), json_object('id', 'kiryu-argument', 'at', '19:35', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["kiryu-argued-with-ryoko-1935"]'), 'kind', 'solid'), json_object('id', 'fukagawa-returns', 'at', '19:45', 'place', 'dining', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["fukagawa-returned-1945"]'), 'kind', 'solid'), json_object('id', 'mizuki-poisons', 'at', '19:50', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1)), 'facts', json('["mizuki-poisoned-brandy-1950","kiryu-passed-mizuki-1950","mizuki-took-aconite"]'), 'kind', 'solid'), json_object('id', 'mizuki-serves', 'at', '20:00', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1)), 'facts', json('["mizuki-carried-brandy-2000","brandy-was-poisoned"]'), 'kind', 'solid'), json_object('id', 'ryoko-drinks', 'at', '20:15', 'place', 'study', 'participants', json_array(), 'facts', json('["ryoko-drank-at-2015"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '20:30', 'place', 'study', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '早坂美月' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '桐生涼' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND name = '深川誠也' LIMIT 1)), 'facts', json('["death-found-2030"]'), 'kind', 'solid')), victim_cause_of_death = '植物性の毒物による中毒死', victim_findings = json('[{"id":"no-struggle","statement":"争った跡が無い。着衣も髪も乱れておらず、文机の上も片付いたままになっている。","requires":{"revelations":[],"evidences":[]}},{"id":"numbness-signs","statement":"唇のまわりと指先に、しびれが出たときの跡が残っている。","requires":{"revelations":[],"evidences":[]}},{"id":"single-glass","statement":"文机に、飲みかけのグラスが一つだけ置かれている。誰かと酌み交わした跡は無い。","requires":{"revelations":[],"evidences":[]}},{"id":"heir-draft","statement":"硯箱の下に、書き直しかけの遺言書の草案が伏せてある。後継者の項に線が引かれ、余白に書き込みがある。","requires":{"revelations":[],"evidences":["will-record"]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'プレイヤーが遺体または書斎の文机まわりを調べ、探偵が硯箱の下の草案に触れたら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1) AND label = '書き直しかけの遺言書の草案'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:15', victim_found_in = '予備部品庫', victim_investigable = 1 WHERE victim_name = '真田啓介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'schedule-created', 'at', '21:52', 'place', '監視室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-created-scheduled-job"]'), 'kind', 'solid'), json_object('id', 'door-propped', 'at', '21:55', 'place', '予備部品庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '波多野結' LIMIT 1)), 'facts', json('["parts-door-propped"]'), 'kind', 'solid'), json_object('id', 'kuze-leaves', 'at', '21:58', 'place', '監視室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-left-noc"]'), 'kind', 'claim'), json_object('id', 'scheduled-execution', 'at', '22:00', 'place', '監視室', 'participants', json_array(), 'facts', json('["scheduled-jobs-ran"]'), 'kind', 'solid'), json_object('id', 'corridor-sighting', 'at', '22:01', 'place', '東側廊下', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '真壁徹' LIMIT 1)), 'facts', json('["makabe-saw-kuze"]'), 'kind', 'solid'), json_object('id', 'sanada-death', 'at', '22:04', 'place', '予備部品庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-killed-sanada"]'), 'kind', 'claim'), json_object('id', 'kuze-return', 'at', '22:08', 'place', '監視室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1)), 'facts', json('["kuze-returned-noc"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '22:15', 'place', '予備部品庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '波多野結' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '久世秋穂' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '甲田修' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND name = '真壁徹' LIMIT 1)), 'facts', json('["body-found"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"真田啓介は予備部品庫で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「交換部品の請求書と在庫表」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '久世か波多野に真田が事件直前に照合していた保守費と在庫について尋ねたら開示する。または遺体・現場を調べ、「交換部品の請求書と在庫表」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '真田啓介' LIMIT 1) AND label = '交換部品の請求書と在庫表'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '01:29', victim_found_in = '会議室', victim_investigable = 1 WHERE victim_name = '芳賀俊介'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'route-loaded', 'at', '00:57', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-repeat-route-capable","hiiragi-loaded-auto-route"]'), 'kind', 'solid'), json_object('id', 'drone-launch', 'at', '01:00', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-flew-auto","route-has-no-manual-input"]'), 'kind', 'solid'), json_object('id', 'hiiragi-leaves-roof', 'at', '01:02', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-left-roof"]'), 'kind', 'claim'), json_object('id', 'saegusa-sighting', 'at', '01:08', 'place', '連絡階段', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '三枝千尋' LIMIT 1)), 'facts', json('["saegusa-saw-hiiragi-0108"]'), 'kind', 'solid'), json_object('id', 'haga-death', 'at', '01:12', 'place', '会議室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-killed-haga"]'), 'kind', 'claim'), json_object('id', 'hiiragi-return', 'at', '01:17', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["hiiragi-returned-roof"]'), 'kind', 'claim'), json_object('id', 'drone-lands', 'at', '01:18', 'place', '屋上', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1)), 'facts', json('["drone-flew-auto"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '01:29', 'place', '会議室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '国分透' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '柊木慧' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '三枝千尋' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND name = '波木恵' LIMIT 1)), 'facts', json('["body-found-0129"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"芳賀俊介は会議室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「過去点検写真との一致」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = '柊木か三枝に芳賀が事件直前に確認していた点検写真について尋ね、過去画像との一致を追及したら開示する。または遺体・現場を調べ、「過去点検写真との一致」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = '芳賀俊介' LIMIT 1) AND label = '過去点検写真との一致'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '19:10', victim_found_in = '記録室', victim_investigable = 1 WHERE victim_name = 'ロレンツォ・ヴァーレ'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'lorenzo-audit', 'at', '18:35', 'place', '検疫島', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-diverted-medicine","lorenzo-found-shortage","lorenzo-confronted-marta"]'), 'kind', 'claim'), json_object('id', 'corridor-sighting', 'at', '18:41', 'place', '記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ニコロ・フェッリ' LIMIT 1)), 'facts', json('["nicolo-saw-marta-before-bell"]'), 'kind', 'solid'), json_object('id', 'lorenzo-death', 'at', '18:44', 'place', '記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-killed-lorenzo"]'), 'kind', 'claim'), json_object('id', 'sunset-bell', 'at', '18:47', 'place', '鐘楼', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ピエトロ・サルヴィ' LIMIT 1)), 'facts', json('["bell-rang-1847","sunset-bell-not-fixed-hour"]'), 'kind', 'solid'), json_object('id', 'marta-returns', 'at', '18:49', 'place', '薬剤庫', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1)), 'facts', json('["marta-returned-after-bell"]'), 'kind', 'claim'), json_object('id', 'discovery', 'at', '19:10', 'place', '記録室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'マルタ・ベッリーニ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ニコロ・フェッリ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND name = 'ピエトロ・サルヴィ' LIMIT 1)), 'facts', json('["body-found-1910"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"ロレンツォ・ヴァーレは記録室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「不足した薬剤の帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'マルタかニコロに薬剤の不足とロレンツォの直前の調査を尋ねたら開示する。または遺体・現場を調べ、「不足した薬剤の帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'ロレンツォ・ヴァーレ' LIMIT 1) AND label = '不足した薬剤の帳簿'; +--> statement-breakpoint +UPDATE scenarios SET victim_found_at = '22:30', victim_found_in = '測量室', victim_investigable = 1 WHERE victim_name = 'エドワード・ヘイル'; +--> statement-breakpoint +UPDATE scenario_truths SET timeline_events = json_array(json_object('id', 'hale-confronts-bell', 'at', '21:35', 'place', '事務室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-bid-fraud","helale-confronted-bell"]'), 'kind', 'claim'), json_object('id', 'bell-enters-tunnel', 'at', '21:52', 'place', '連絡坑道', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-left-shaft-one"]'), 'kind', 'solid'), json_object('id', 'thomas-sees-bell', 'at', '21:58', 'place', '第2立坑', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'トーマス・リード' LIMIT 1)), 'facts', json('["thomas-saw-bell-tunnel"]'), 'kind', 'solid'), json_object('id', 'hale-death', 'at', '22:03', 'place', '測量室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1)), 'facts', json('["bell-killed-hale"]'), 'kind', 'claim'), json_object('id', 'false-telegram', 'at', '22:12', 'place', '第2立坑', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'クララ・ウェッブ' LIMIT 1)), 'facts', json('["bell-sent-telegram","telegram-received-2212"]'), 'kind', 'solid'), json_object('id', 'discovery', 'at', '22:30', 'place', '測量室', 'participants', json_array((SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'アーサー・ベル' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'クララ・ウェッブ' LIMIT 1), (SELECT id FROM characters WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND name = 'トーマス・リード' LIMIT 1)), 'facts', json('["body-found-2230"]'), 'kind', 'solid')), victim_cause_of_death = '事件性のある外傷による死亡', victim_findings = json('[{"id":"victim-state","statement":"エドワード・ヘイルは測量室で倒れており、その場で死亡が確認されている。","requires":{"revelations":[],"evidences":[]}},{"id":"victim-motive-material","statement":"遺体のそばには「水増しされた資材帳簿」に関わる資料が残されている。","requires":{"revelations":[],"evidences":[]}}]') WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1); +--> statement-breakpoint +UPDATE evidences SET reveal_condition = 'ベルにヘイルと直前に揉めた帳簿について追及するか、クララに二人の口論について尋ねたら開示する。または遺体・現場を調べ、「水増しされた資材帳簿」に関わる資料を確認したら開示する。', sources = CASE WHEN EXISTS (SELECT 1 FROM json_each(evidences.sources) WHERE json_extract(value, '$.type') = 'victim') THEN sources ELSE json_insert(sources, '$[#]', json('{"type":"victim","id":"victim"}')) END WHERE scenario_id = (SELECT id FROM scenarios WHERE victim_name = 'エドワード・ヘイル' LIMIT 1) AND label = '水増しされた資材帳簿'; diff --git a/db/migrations/meta/0011_snapshot.json b/db/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..13db70e --- /dev/null +++ b/db/migrations/meta/0011_snapshot.json @@ -0,0 +1,1136 @@ +{ + "id": "f50f059d-d1f3-4c47-a327-6f99ff50158f", + "prevId": "b2985582-8bb1-4034-8076-0d8e93048e81", + "version": "6", + "dialect": "sqlite", + "tables": { + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "columnsFrom": [ + "evidence_id" + ], + "tableTo": "evidences", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "columnsFrom": [ + "character_id" + ], + "tableTo": "characters", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "session_id" + ], + "tableTo": "play_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "columnsFrom": [ + "revelation_id" + ], + "tableTo": "revelations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index fc6ca11..ba64d80 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1788216627374, "tag": "0010_victim-findings", "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1788231785231, + "tag": "0011_scenario-current-authoring-upgrade", + "breakpoints": true } ] } \ No newline at end of file diff --git a/db/scenarios/avalanche-lodge-phantom-room.yaml b/db/scenarios/avalanche-lodge-phantom-room.yaml index bd089de..168736b 100644 --- a/db/scenarios/avalanche-lodge-phantom-room.yaml +++ b/db/scenarios/avalanche-lodge-phantom-room.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: avalanche-lodge-phantom-room meta: - title: 雪崩の山荘、白樺峰の夜 + title: 白樺峰に雪崩が落ちた夜 synopsis: "午後九時四十五分、雪崩で道路を失った白樺峰ホテルの支配人室で、支配人の早瀬隆司が死亡しているのが見つかりました。外部との電話も不通で、事件時刻に館内にいた従業員は三人だけです。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 早瀬隆司 introduction: 白樺峰ホテル支配人 + foundAt: 21:45 + foundIn: 支配人室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 早瀬隆司は支配人室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「翌朝の会計監査メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -93,36 +101,43 @@ timeline: participants: [natsume] facts: [hayase-audit-next-morning, natsume-cash-shortage] description: 早瀬が夏目に、翌朝の夜勤会計監査を告げる。 + location: 山荘内 - id: fake-message at: "21:16" participants: [natsume] facts: [fake-message-left, no-214-call, paper-from-night-pad] description: フロントに二一四号室の設備苦情を装う伝言が置かれる。 + location: フロント - id: ask-oda at: "21:24" participants: [natsume, oda] facts: [natsume-asked-oda] description: 夏目が小田に二一四号室の場所を尋ね、客室棟へ向かうように見せる。 + location: 客室棟 - id: management-corridor at: "21:27" participants: [natsume, oda] facts: [oda-saw-natsume-office-side] description: 小田が管理廊下から出てくる夏目を目撃する。 + location: 管理廊下 - id: hayase-death at: "21:30" participants: [natsume] facts: [hayase-death-2130, natsume-killed-hayase] description: 夏目が支配人室で早瀬を襲い、早瀬は死亡する。 + location: 支配人室 - id: ask-fujino at: "21:31" participants: [natsume, fujino] facts: [natsume-asked-fujino] description: 夏目が厨房前で藤野にも二一四号室の場所を尋ねる。 + location: 厨房前 - id: discovery at: "21:45" participants: [fujino, natsume, oda] facts: [body-found-2145] description: 藤野が支配人室で早瀬の死を発見する。 + location: 支配人室 characters: - id: natsume name: 夏目悠 @@ -308,12 +323,14 @@ evidences: description: 早瀬の予定表には、翌朝最初の業務として夏目の夜勤売上不足を確認する予定が記されている。 reveal: mode: conversation - condition: 夏目か小田に早瀬が翌朝予定していた会計確認について尋ねたら開示する。 + condition: 夏目か小田に早瀬が翌朝予定していた会計確認について尋ねたら開示する。または遺体・現場を調べ、「翌朝の会計監査メモ」に関わる資料を確認したら開示する。 sources: - type: character id: natsume - type: character id: oda + - type: victim + id: victim supports: [hayase-audit-next-morning, natsume-cash-shortage] contradicts: [] - id: linen-record diff --git a/db/scenarios/avalanche-monastery-bell-window.yaml b/db/scenarios/avalanche-monastery-bell-window.yaml index 007dce6..efc89eb 100644 --- a/db/scenarios/avalanche-monastery-bell-window.yaml +++ b/db/scenarios/avalanche-monastery-bell-window.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: avalanche-monastery-bell-window meta: - title: 雪崩の修道院、山上の三人 + title: 山上の修道院から誰も帰れない synopsis: "午後九時四十分、雪崩で麓への道を断たれた山上の修道院資料館で、館長・高瀬静一が死亡しているのが見つかりました。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 高瀬静一 introduction: 修道院資料館館長 + foundAt: 21:40 + foundIn: 資料整理室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 高瀬静一は資料整理室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「修復費の追加請求一覧」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -90,26 +98,31 @@ timeline: participants: [mizuki] facts: [repair-overbilling, mizuki-overbilled, takase-confronted-mizuki] description: 高瀬が水城に修復費の追加請求について説明を求める。 + location: 修道院内 - id: takase-death at: "21:05" participants: [mizuki] facts: [mizuki-killed-takase] description: 水城が資料整理室で高瀬を襲う。 + location: 資料整理室 - id: bell-rung at: "21:20" participants: [mizuki, kurokawa, genda] facts: [mizuki-rang-bell-remotely, all-heard-bell, no-one-saw-takase-bell] description: 九時二十分の鐘が鳴り、三人はいつもの高瀬の閉館合図だと思い込む。 + location: 鐘楼 - id: test-line-clue at: "21:27" participants: [genda] facts: [temporary-test-line-exists, genda-thought-line-removed, test-line-tag-remained] description: 玄田が聖歌席裏の点検口を通るが、残った工事タグを気に留めない。 + location: 聖歌席裏 - id: discovery at: "21:40" participants: [kurokawa, mizuki, genda] facts: [body-found-2140] description: 黒川が資料整理室で高瀬の死を発見する。 + location: 資料整理室 characters: - id: mizuki name: 水城奈央 @@ -293,12 +306,14 @@ evidences: description: 実施記録のない追加工事が水城の承認で請求され、高瀬がその項目へ印を付けている。 reveal: mode: conversation - condition: 水城に高瀬から追及された修復費について尋ねるか、玄田に問題になっていた工事項目を確認したら開示する。 + condition: 水城に高瀬から追及された修復費について尋ねるか、玄田に問題になっていた工事項目を確認したら開示する。または遺体・現場を調べ、「修復費の追加請求一覧」に関わる資料を確認したら開示する。 sources: - type: character id: mizuki - type: character id: genda + - type: victim + id: victim supports: [repair-overbilling, mizuki-overbilled, takase-confronted-mizuki] contradicts: ["lie:mizuki-no-overbilling"] - id: forbidden-photos diff --git a/db/scenarios/blizzard-lodge-seat-score-alibi.yaml b/db/scenarios/blizzard-lodge-seat-score-alibi.yaml index 761bb45..4b8fb88 100644 --- a/db/scenarios/blizzard-lodge-seat-score-alibi.yaml +++ b/db/scenarios/blizzard-lodge-seat-score-alibi.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: blizzard-lodge-seat-score-alibi meta: - title: 吹雪の研修ロッジ、四人の夜 + title: 四人だけの研修ロッジ synopsis: "午後九時四十分、吹雪で道路が閉鎖された研修ロッジで、運営責任者・塚本誠が書斎で死亡しているのが見つかりました。" category: クローズドサークル difficulty: 4 @@ -10,6 +10,14 @@ meta: victim: name: 塚本誠 introduction: 研修ロッジ運営責任者 + foundAt: 21:40 + foundIn: 書斎 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 塚本誠は書斎で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「外部講師費の精算書」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -101,36 +109,43 @@ timeline: participants: [katase, yoshioka] facts: [payment-fraud, tsukamoto-found-fraud] description: 塚本が講師費の精算書から架空計上を見つける。 + location: ロッジ内 - id: summon-note at: "21:10" participants: [katase] facts: [tsukamoto-summoned-katase] description: 塚本が片瀬へ書斎に来るようメモを渡す。 + location: 書斎 - id: katase-leaves-game at: "21:12" participants: [katase, mamiya, todo, yoshioka] facts: [katase-left-2112, mamiya-substituted-blue, game-tracks-seats] description: 片瀬が談話室を離れ、間宮が青席へ座ってゲームを続ける。 + location: 談話室 - id: tsukamoto-death at: "21:18" participants: [katase] facts: [katase-killed-tsukamoto] description: 片瀬が書斎で塚本を襲う。 + location: 書斎 - id: score-continues at: "21:21" participants: [mamiya, todo, yoshioka] facts: [blue-score-continued, yoshioka-copied-score-later] description: 片瀬不在のまま青席の得点が続き、吉岡が後から席単位で清書する。 + location: ロッジ内 - id: katase-returns at: "21:25" participants: [katase, todo, mamiya, yoshioka] facts: [katase-returned-2125, todo-saw-katase-return] description: 片瀬が談話室へ戻り、間宮から青席を引き継ぐ。 + location: 談話室 - id: discovery at: "21:40" participants: [yoshioka, katase, todo, mamiya] facts: [body-found-2140] description: 吉岡が書斎で塚本の死を発見する。 + location: 書斎 characters: - id: katase name: 片瀬真央 @@ -354,12 +369,14 @@ evidences: description: 実在しない追加講師枠が片瀬の処理で計上され、塚本が該当欄に確認印を付けている。 reveal: mode: conversation - condition: 片瀬か吉岡に事件前に塚本が確認していた精算書について尋ねたら開示する。 + condition: 片瀬か吉岡に事件前に塚本が確認していた精算書について尋ねたら開示する。または遺体・現場を調べ、「外部講師費の精算書」に関わる資料を確認したら開示する。 sources: - type: character id: katase - type: character id: yoshioka + - type: victim + id: victim supports: [payment-fraud, tsukamoto-found-fraud, tsukamoto-summoned-katase] contradicts: ["lie:katase-no-fake-fee"] - id: copied-material diff --git a/db/scenarios/cave-lab-locator-cart.yaml b/db/scenarios/cave-lab-locator-cart.yaml index bdf27e5..328736b 100644 --- a/db/scenarios/cave-lab-locator-cart.yaml +++ b/db/scenarios/cave-lab-locator-cart.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: cave-lab-locator-cart meta: - title: 崩落洞窟、地下研究所の夜 + title: 救助隊が来るまで synopsis: "午後九時二十七分、地下洞窟研究所の地図解析室で、調査主任の岩代圭吾が死亡しているのが見つかりました。午後八時四十分の落盤で唯一の坑道出口が塞がれ、救助隊が到着するまで外部との往来は不可能です。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 岩代圭吾 introduction: 地下洞窟研究所調査主任 + foundAt: 21:27 + foundIn: 地図解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 岩代圭吾は地図解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「試料ラベルと測量座標の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -100,41 +108,49 @@ timeline: participants: [kagawa] facts: [locator-tag-removable, kagawa-tag-on-cart] description: 香川が位置タグを作業ベストから外し、自動測量カートへ固定する。 + location: 研究所内 - id: cart-loop-start at: "21:00" participants: [] facts: [mapping-cart-auto-loop, tag-track-matches-cart] description: 自動測量カートが無人で校正ルートの周回を始め、香川の位置タグも同じ軌跡を記録する。 + location: 測量区画 - id: kagawa-leaves at: "21:01" participants: [kagawa] facts: [kagawa-left-survey-zone] description: 香川が位置タグをカートに残したまま測量区画を離れる。 + location: 測量区画 - id: tono-sighting at: "21:07" participants: [kagawa, tono] facts: [tono-saw-kagawa-2107] description: 遠野が連絡通路で、位置タグを付けていない香川を目撃する。 + location: 連絡通路 - id: iwashiro-death at: "21:11" participants: [kagawa] facts: [kagawa-killed-iwashiro] description: 香川が地図解析室で岩代を襲い、岩代は死亡する。 + location: 地図解析室 - id: cart-loop-end at: "21:15" participants: [] facts: [mapping-cart-auto-loop, tag-track-matches-cart] description: 自動測量カートが校正ルートの周回を終える。 + location: 測量区画 - id: tag-recovered at: "21:17" participants: [kagawa] facts: [kagawa-recovered-tag] description: 香川が測量区画へ戻り、自動カートから位置タグを回収する。 + location: 測量区画 - id: discovery at: "21:27" participants: [shindo, kagawa, yuki, tono] facts: [body-found-2127] description: 新堂が地図解析室で岩代の死を発見する。 + location: 地図解析室 characters: - id: kagawa @@ -332,10 +348,11 @@ evidences: description: 香川が深部採取として登録した試料のラベルが、実際の測量データでは浅い区画の座標と一致する。岩代の訂正予定も残る。 reveal: mode: conversation - condition: 香川か新堂に岩代が事件直前に照合していた試料採取地点について尋ね、測量座標との不一致を追及したら開示する。 + condition: 香川か新堂に岩代が事件直前に照合していた試料採取地点について尋ね、測量座標との不一致を追及したら開示する。または遺体・現場を調べ、「試料ラベルと測量座標の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: kagawa } - { type: character, id: shindo } + - { type: victim, id: victim } supports: [kagawa-forged-sample-origin, iwashiro-found-forgery, iwashiro-planned-retraction] contradicts: [] - id: shindo-private-case diff --git a/db/scenarios/coldcase-lodge-borrowed-memory.yaml b/db/scenarios/coldcase-lodge-borrowed-memory.yaml index 7914f4d..abf631e 100644 --- a/db/scenarios/coldcase-lodge-borrowed-memory.yaml +++ b/db/scenarios/coldcase-lodge-borrowed-memory.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: coldcase-lodge-borrowed-memory meta: - title: 1979年、雪山荘未解決事件 + title: 四十七年目の白樺館 synopsis: "2026年。47年前、雪崩で孤立した山荘「白樺館」で起きた未解決事件を再調査します。 1979年2月17日の夜、山荘主・野上修一が書斎で死亡しました。" category: 未解決事件再調査 difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 野上修一 introduction: 山荘「白樺館」主人 + foundAt: 23:10 + foundIn: 書斎 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 野上修一は書斎で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「仕入れ帳の水増し」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -86,31 +94,37 @@ timeline: participants: [megumi] facts: [megumi-forged-expenses, nogami-found-megumi-fraud] description: 野上が倉田の仕入れ代水増しを見抜き、翌朝警察へ相談すると告げる。 + location: 山荘内 - id: nogami-death at: "22:05" participants: [megumi] facts: [megumi-killed-nogami] description: 倉田が書斎で野上を襲う。 + location: 書斎 - id: maki-sees-megumi at: "22:08" participants: [maki, megumi] facts: [maki-saw-megumi-study] description: 真紀が書斎側の廊下から出てくる倉田を見かける。 + location: 書斎 - id: megumi-goes-room at: "22:15" participants: [megumi] facts: [megumi-originally-asleep-2215] description: 倉田が自室へ入り、その後は眠っていたと翌朝の供述で話す。 + location: 自室 - id: false-fireplace-sighting at: "22:30" participants: [maki] facts: [maki-lied-fireplace, original-only-maki-claimed-sighting] description: 真紀が自分の窃盗時間を隠すため、野上を暖炉前で見たという架空の目撃を後に申告する。 + location: 暖炉前 - id: discovery at: "23:10" participants: [maki, tatsuya, megumi] facts: [body-found-2310] description: 藤村が書斎で野上の死を発見する。 + location: 書斎 characters: - id: maki name: 高瀬真紀 @@ -299,9 +313,10 @@ evidences: description: 倉田の担当欄で仕入れ額が実際より増やされ、野上が事件当日に再確認の印を付けている。 reveal: mode: conversation - condition: 倉田に野上と事件直前に揉めた帳簿の内容を追及したら開示する。 + condition: 倉田に野上と事件直前に揉めた帳簿の内容を追及したら開示する。または遺体・現場を調べ、「仕入れ帳の水増し」に関わる資料を確認したら開示する。 sources: - { type: character, id: megumi } + - { type: victim, id: victim } supports: [megumi-forged-expenses, nogami-found-megumi-fraud] contradicts: ["lie:megumi-no-fraud"] solution: diff --git a/db/scenarios/deepsea-habitat-batch-signature.yaml b/db/scenarios/deepsea-habitat-batch-signature.yaml index e60c0e4..2ad5ae9 100644 --- a/db/scenarios/deepsea-habitat-batch-signature.yaml +++ b/db/scenarios/deepsea-habitat-batch-signature.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: deepsea-habitat-batch-signature meta: - title: 深海居住区、海底の四人 + title: 海底居住区アビス3 synopsis: "午前零時二十二分、海底居住実験区「アビス3」の資料室で、主任研究者の篠宮亮が死亡しているのが見つかりました。海上は暴風で、午後十一時四十分に支援船との昇降カプセル接続が解除されて以降、外部との往来はありません。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 篠宮亮 introduction: 海底居住実験区「アビス3」主任研究者 + foundAt: 00:22 + foundIn: 資料室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 篠宮亮は資料室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「部品交換記録と実機番号の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -100,36 +108,43 @@ timeline: participants: [sagisawa] facts: [readings-automatic, signatures-batchable] description: 生命維持点検画面が二分間隔のセンサー値を自動記録し始める。 + location: 制御室 - id: sagisawa-leaves at: "00:01" participants: [sagisawa] facts: [sagisawa-left-control] description: 鷺沢が制御室を離れる。 + location: 制御室 - id: passage-sighting at: "00:05" participants: [sagisawa, kohinata] facts: [kohinata-saw-sagisawa] description: 小日向が中央通路で鷺沢とすれ違う。 + location: 中央通路 - id: shinomiya-death at: "00:08" participants: [sagisawa] facts: [sagisawa-killed-shinomiya] description: 鷺沢が資料室で篠宮を襲い、篠宮は死亡する。 + location: 資料室 - id: sagisawa-return at: "00:14" participants: [sagisawa] facts: [sagisawa-returned-control] description: 鷺沢が制御室へ戻る。 + location: 制御室 - id: batch-sign at: "00:16" participants: [sagisawa] facts: [sagisawa-batch-signed, one-signature-transaction] description: 鷺沢が00時00分から00時12分までの点検行をまとめて確認し、一括で署名を付ける。 + location: 居住区内 - id: discovery at: "00:22" participants: [narumi, sagisawa, kohinata, toki] facts: [body-found] description: 鳴海が資料室で篠宮の死を発見する。 + location: 資料室 characters: - id: sagisawa name: 鷺沢怜 @@ -305,10 +320,11 @@ evidences: description: 鷺沢の点検表では交換済みの部品が、実際には古い個体番号のまま残っている。篠宮の調査メモには帰還後の報告予定がある。 reveal: mode: conversation - condition: 鷺沢か土岐に篠宮が事件直前に照合していた部品番号と点検表について尋ねたら開示する。 + condition: 鷺沢か土岐に篠宮が事件直前に照合していた部品番号と点検表について尋ねたら開示する。または遺体・現場を調べ、「部品交換記録と実機番号の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: sagisawa } - { type: character, id: toki } + - { type: victim, id: victim } supports: [sagisawa-falsified-tests, shinomiya-found-falsification, shinomiya-planned-report] contradicts: [] - id: narumi-sample diff --git a/db/scenarios/ferry-fog-passenger-count.yaml b/db/scenarios/ferry-fog-passenger-count.yaml index 32a7437..cb69916 100644 --- a/db/scenarios/ferry-fog-passenger-count.yaml +++ b/db/scenarios/ferry-fog-passenger-count.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: ferry-fog-passenger-count meta: - title: 霧のフェリー、航海の途中で + title: 霧の中を進む「しおかぜ」 synopsis: "午後五時二十分、離島航路のフェリー「しおかぜ」の上部ラウンジで、運航会社の監査役・柴田功が死亡しているのが見つかりました。船は濃霧のため減速して航行中で、出航後に乗り降りした者はいません。" category: 船上ミステリ difficulty: 3 @@ -10,6 +10,14 @@ meta: victim: name: 柴田功 introduction: 離島航路運航会社監査役 + foundAt: 17:20 + foundIn: 上部ラウンジ + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 柴田功は上部ラウンジで倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「追加券の控えと売上帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -88,41 +96,49 @@ timeline: participants: [] facts: [passenger-left-before-departure, booked-passengers-42] description: 予約客の一人が出航前に下船し、実際の乗客は41人になる。 + location: 船内 - id: shibata-warning at: "16:48" participants: [kanda] facts: [kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda] description: 柴田が神田の売上金抜き取りを指摘し、到着後の本社報告を告げる。 + location: 船内 - id: secret-interview at: "16:50" participants: [fujiwara] facts: [fujiwara-secret-interview] description: 藤原が上部ラウンジで柴田から内部資料を見せてもらう。 + location: 上部ラウンジ - id: count-sheet-made at: "16:58" participants: [kanda] facts: [kanda-wrote-count-sheet] description: 神田が実際の点呼をせず、予約名簿の42人という数字を点呼表へ写す。 + location: 船内 - id: stair-sighting at: "17:03" participants: [kanda, ogiwara] facts: [ogiwara-saw-kanda-1703] description: 荻原が上部ラウンジへ向かう階段で神田とすれ違う。 + location: 上部ラウンジ - id: shibata-death at: "17:06" participants: [kanda] facts: [kanda-killed-shibata-1706] description: 神田が上部ラウンジで柴田を襲い、柴田は死亡する。 + location: 上部ラウンジ - id: kanda-returns at: "17:11" participants: [kanda] facts: [kanda-returned-lower-1711, count-sheet-says-42] description: 神田が下部客室へ戻り、17時12分の時刻を点呼表へ記入する。 + location: 下部客室 - id: discovery at: "17:20" participants: [ogiwara, kanda, fujiwara] facts: [body-found-1720] description: 荻原が上部ラウンジで柴田の死を発見する。 + location: 上部ラウンジ characters: - id: kanda name: 神田美奈 @@ -275,12 +291,14 @@ evidences: description: 現金で販売された追加券の枚数に対し、帳簿へ記録された売上が継続的に少ない。 reveal: mode: conversation - condition: 神田に柴田が監査していた帳簿の内容を尋ねるか、藤原に会社の数字で柴田が問題視していた点を尋ねたら開示する。 + condition: 神田に柴田が監査していた帳簿の内容を尋ねるか、藤原に会社の数字で柴田が問題視していた点を尋ねたら開示する。または遺体・現場を調べ、「追加券の控えと売上帳簿」に関わる資料を確認したら開示する。 sources: - type: character id: kanda - type: character id: fujiwara + - type: victim + id: victim supports: [kanda-skimmed-sales, shibata-found-shortage, shibata-warned-kanda] contradicts: [] - id: interview-notes diff --git a/db/scenarios/festival-lantern-blackout.yaml b/db/scenarios/festival-lantern-blackout.yaml index 4e6765d..1e61995 100644 --- a/db/scenarios/festival-lantern-blackout.yaml +++ b/db/scenarios/festival-lantern-blackout.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: festival-lantern-blackout meta: - title: 夏祭り、祭具倉庫の夜 + title: 祭りが暗くなった八分間 synopsis: "午後八時三十五分、八坂野神社の夏祭りで、実行委員長の神谷宗一が祭具倉庫の中で死亡しているのが見つかりました。会場には多くの客がいましたが、事件の直前、午後八時十八分から八時二十六分まで提灯と舞台照明が一斉に消えるトラブルが起きています。" category: 祭りの夜 difficulty: 3 @@ -10,6 +10,14 @@ meta: victim: name: 神谷宗一 introduction: 八坂野神社夏祭り実行委員長 + foundAt: 20:35 + foundIn: 祭具倉庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 神谷宗一は祭具倉庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「照明設備の発注帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -92,46 +100,55 @@ timeline: participants: [tozuka] facts: [kamiya-warned-tozuka, kamiya-found-kickbacks] description: 神谷が戸塚に発注帳簿を示し、祭り終了後に不正を公表すると告げる。 + location: 事務室 - id: blackout-start at: "20:18" participants: [tozuka, makabe, aihara] facts: [blackout-started-2018, lighting-preset-ran] description: 会場の照明が一斉に消え、自動復旧シーケンスが始まる。 + location: 会場 - id: tozuka-leaves at: "20:20" participants: [tozuka] facts: [tozuka-left-console-2020] description: 戸塚が操作卓を離れ、暗い境内を祭具倉庫へ向かう。 + location: 祭具倉庫 - id: aihara-sighting at: "20:22" participants: [tozuka, aihara] facts: [aihara-saw-tozuka-2022] description: 相原が非常灯の下で祭具倉庫方向へ急ぐ戸塚を目撃する。 + location: 祭具倉庫 - id: kamiya-death at: "20:23" participants: [tozuka] facts: [tozuka-killed-kamiya-2023, spare-key-borrowed] description: 戸塚が祭具倉庫へ入り、神谷を襲う。 + location: 祭具倉庫 - id: tozuka-returns at: "20:25" participants: [tozuka] facts: [tozuka-returned-2025] description: 戸塚が照明操作卓へ戻る。 + location: 操作卓 - id: lights-return at: "20:26" participants: [tozuka, makabe, aihara] facts: [lights-restored-2026] description: 自動復旧シーケンスが完了し、会場の照明が戻る。 + location: 会場 - id: key-check at: "20:28" participants: [makabe] facts: [makabe-checked-key-2028] description: 真壁が予備鍵の箱を確認し、鍵が戻されているのを見る。 + location: 境内 - id: discovery at: "20:35" participants: [makabe, aihara, tozuka] facts: [body-found-2035] description: 真壁が祭具倉庫で神谷の死を発見する。 + location: 祭具倉庫 characters: - id: tozuka name: 戸塚岳 @@ -291,12 +308,14 @@ evidences: description: 同規模の設備と比べて発注額が不自然に高く、戸塚へ還流した謝礼を神谷が照合したメモが挟まれている。 reveal: mode: conversation - condition: 戸塚に設備費の内訳を尋ねるか、真壁に神谷が祭り前から確認していた帳簿について尋ねたら開示する。 + condition: 戸塚に設備費の内訳を尋ねるか、真壁に神谷が祭り前から確認していた帳簿について尋ねたら開示する。または遺体・現場を調べ、「照明設備の発注帳簿」に関わる資料を確認したら開示する。 sources: - type: character id: tozuka - type: character id: makabe + - type: victim + id: victim supports: [kamiya-found-kickbacks, kamiya-warned-tozuka] contradicts: [] - id: donation-shortfall diff --git a/db/scenarios/flood-archive-self-locking-vault.yaml b/db/scenarios/flood-archive-self-locking-vault.yaml index 970b19b..7e33c7d 100644 --- a/db/scenarios/flood-archive-self-locking-vault.yaml +++ b/db/scenarios/flood-archive-self-locking-vault.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: flood-archive-self-locking-vault meta: - title: 高潮の文書館、閉館後の夜 + title: 高潮警報、文書館閉鎖 synopsis: "午後九時四十分、海辺歴史文書館の希少資料庫で、館長の今泉孝臣が死亡しているのが見つかりました。高潮で防潮扉が閉じ、午後八時半以降に館外へ出入りした者はいません。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 今泉孝臣 introduction: 海辺歴史文書館館長 + foundAt: 21:40 + foundIn: 資料庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 今泉孝臣は資料庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「修復処置記録と資料状態の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -108,31 +116,37 @@ timeline: participants: [] facts: [vault-key-only-opens, vault-self-locks, vault-open-2109, vault-stayed-open] description: 今泉が鍵で希少資料庫を開け、資料搬入のため扉を開いたままにする。 + location: 希少資料庫 - id: yagami-enters at: "21:14" participants: [yagami, kuga] facts: [yagami-entered-vault-2114, kuga-saw-yagami-2114] description: 八神が開いたままの資料庫へ入り、久我がその姿を目撃する。 + location: 資料庫 - id: imaizumi-enters at: "21:17" participants: [] facts: [imaizumi-entered-vault-2117] description: 今泉が資料箱を持って資料庫へ入り、開錠鍵をポケットへ戻す。 + location: 資料庫 - id: imaizumi-death at: "21:20" participants: [yagami] facts: [yagami-killed-imaizumi] description: 八神が希少資料庫で今泉を襲い、今泉は死亡する。 + location: 希少資料庫 - id: yagami-exits at: "21:22" participants: [yagami] facts: [yagami-left-vault-2122, vault-closed-2122] description: 八神が資料庫から出て扉を閉め、自動施錠させる。 + location: 資料庫 - id: discovery at: "21:40" participants: [tozuka, yagami, kuga, majima] facts: [key-found-on-imaizumi, body-found-2140] description: 戸塚が予備開錠手順で資料庫を開け、今泉の死とポケットの鍵を確認する。 + location: 資料庫 characters: - id: yagami @@ -342,10 +356,11 @@ evidences: description: 八神の処置記録では完了したはずの工程が資料の実状態と合わず、今泉が翌朝の担当変更と外部委員提出を記している。 reveal: mode: conversation - condition: 八神か久我に今泉が事件直前に調べていた修復記録について尋ね、実資料との不一致を追及したら開示する。 + condition: 八神か久我に今泉が事件直前に調べていた修復記録について尋ね、実資料との不一致を追及したら開示する。または遺体・現場を調べ、「修復処置記録と資料状態の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: yagami } - { type: character, id: kuga } + - { type: victim, id: victim } supports: [yagami-forged-restoration-record, imaizumi-found-forgery, imaizumi-planned-review] contradicts: [] - id: kuga-damaged-document diff --git a/db/scenarios/flood-courthouse-unlogged-door.yaml b/db/scenarios/flood-courthouse-unlogged-door.yaml index 1ccd6d5..7cf1168 100644 --- a/db/scenarios/flood-courthouse-unlogged-door.yaml +++ b/db/scenarios/flood-courthouse-unlogged-door.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: flood-courthouse-unlogged-door meta: - title: 水没寸前の裁判所、資料棟の夜 + title: 河川氾濫、旧南央裁判所 synopsis: "午後八時三十五分、河川氾濫により周辺道路が閉鎖された旧南央裁判所の資料棟で、主任記録官の磯崎章が死亡しているのが見つかりました。資料棟に残っていたのは書記官の矢吹梓、設備担当の三田村悟、警備員の木瀬亮の三人だけです。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 磯崎章 introduction: 旧南央裁判所主任記録官 + foundAt: 20:35 + foundIn: 保存記録室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 磯崎章は保存記録室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「監督部署への報告草案」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -90,36 +98,43 @@ timeline: participants: [mitamura] facts: [door-held-open-1950, door-remained-open-2022, no-new-unlock] description: 設備点検の資材搬入のため、保存記録室の扉が開放状態になる。 + location: 保存記録室 - id: mitamura-warning at: "20:00" participants: [mitamura, yabuki] facts: [mitamura-told-yabuki-open] description: 三田村が矢吹に、保存記録室の扉が点検中で開いていることを伝える。 + location: 保存記録室 - id: yabuki-enters at: "20:09" participants: [yabuki] facts: [yabuki-entered-unlogged] description: 矢吹が職員証を使わず、開放中の保存記録室へ入る。 + location: 保存記録室 - id: isozaki-death at: "20:12" participants: [yabuki] facts: [isozaki-death-2012, yabuki-killed-isozaki] description: 矢吹が保存記録室で磯崎を襲い、磯崎は死亡する。 + location: 保存記録室 - id: corridor-return at: "20:16" participants: [yabuki, kise] facts: [kise-saw-yabuki-return-2016] description: 木瀬が保存記録室側の廊下から戻る矢吹を目撃する。 + location: 保存記録室 - id: door-closes at: "20:22" participants: [mitamura] facts: [door-remained-open-2022] description: 設備点検が終わり、保存記録室の扉が通常の施錠状態へ戻る。 + location: 保存記録室 - id: discovery at: "20:35" participants: [mitamura, yabuki, kise] facts: [body-found-2035] description: 三田村が保存記録室で磯崎の死を発見する。 + location: 保存記録室 characters: - id: yabuki name: 矢吹梓 @@ -290,12 +305,14 @@ evidences: description: 磯崎の端末に、矢吹による不適切な記録修正を翌朝報告するための草案が残っている。 reveal: mode: conversation - condition: 矢吹か三田村に磯崎が翌朝提出予定だった報告について尋ねたら開示する。 + condition: 矢吹か三田村に磯崎が翌朝提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「監督部署への報告草案」に関わる資料を確認したら開示する。 sources: - type: character id: yabuki - type: character id: mitamura + - type: victim + id: victim supports: [altered-file-discovered, report-next-morning] contradicts: [] - id: skipped-maintenance-item diff --git a/db/scenarios/gallery-blue-frame.yaml b/db/scenarios/gallery-blue-frame.yaml index fa121ce..4ce23c3 100644 --- a/db/scenarios/gallery-blue-frame.yaml +++ b/db/scenarios/gallery-blue-frame.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: gallery-blue-frame meta: - title: 青環美術館、内覧会の夜 + title: 内覧会は終わっていた synopsis: "午後六時五十五分、市立青環美術館の収蔵庫前で、主任学芸員の鳥羽薫が死亡しているのが見つかりました。その夜は翌日から始まる企画展の内覧会で、一般客が帰った後も数人の関係者が館内に残っていました。" category: 美術館ミステリ difficulty: 3 @@ -10,6 +10,14 @@ meta: victim: name: 鳥羽薫 introduction: 市立青環美術館主任学芸員 + foundAt: 18:55 + foundIn: 収蔵庫前 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 鳥羽薫は収蔵庫前で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「鳥羽の額装検査メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -93,46 +101,55 @@ timeline: participants: [sakaki] facts: [toba-found-forgery] description: 鳥羽が作品のすり替えに気づく。 + location: 美術館内 - id: sakaki-confronted at: "18:29" participants: [sakaki] facts: [toba-confronted-sakaki] description: 鳥羽が榊へ贋作の件を問いただし、翌朝の報告を告げる。 + location: 美術館内 - id: mido-bribe at: "18:32" participants: [mido] facts: [mido-offered-bribe] description: 御堂が作品評価を巡る謝礼を鳥羽へ持ちかけ、拒否される。 + location: 美術館内 - id: uv-test-start at: "18:35" participants: [sakaki] facts: [uv-test-started] description: 榊が修復室で紫外線検査装置を起動する。 + location: 修復室 - id: sakaki-leaves at: "18:38" participants: [sakaki] facts: [uv-lamp-off-1837, sakaki-left-restoration] description: 装置の電源が切れた後、榊が修復室を離れて収蔵庫側へ向かう。 + location: 修復室 - id: enomoto-sighting at: "18:44" participants: [sakaki, enomoto] facts: [enomoto-saw-sakaki-1844] description: 榎本が収蔵庫前の通路で榊を目撃する。 + location: 収蔵庫前 - id: toba-death at: "18:46" participants: [sakaki] facts: [sakaki-killed-toba-1846] description: 榊が収蔵庫前で鳥羽を襲い、鳥羽は死亡する。 + location: 収蔵庫前 - id: sakaki-returns at: "18:50" participants: [sakaki] facts: [sakaki-returned-1850, uv-test-resumed] description: 榊が修復室へ戻り、ほどなく紫外線検査を再開する。 + location: 修復室 - id: discovery at: "18:55" participants: [mido, sakaki, enomoto] facts: [body-found-1855] description: 御堂が収蔵庫前で鳥羽の死を発見する。 + location: 収蔵庫前 characters: - id: sakaki name: 榊玲 @@ -279,12 +296,14 @@ evidences: description: 展示予定作品の額装と絵具層が過去の記録と一致せず、鳥羽が「原画ではない可能性」と書き残している。 reveal: mode: conversation - condition: 榊か御堂に鳥羽が事件前に作品の真贋を調べていなかったか尋ね、額装の違いへ話が及んだら開示する。 + condition: 榊か御堂に鳥羽が事件前に作品の真贋を調べていなかったか尋ね、額装の違いへ話が及んだら開示する。または遺体・現場を調べ、「鳥羽の額装検査メモ」に関わる資料を確認したら開示する。 sources: - type: character id: sakaki - type: character id: mido + - type: victim + id: victim supports: [toba-found-forgery, sakaki-made-forgery] contradicts: [] - id: mido-envelope diff --git a/db/scenarios/generation-ship-staggered-dawn.yaml b/db/scenarios/generation-ship-staggered-dawn.yaml index f2496d5..4534825 100644 --- a/db/scenarios/generation-ship-staggered-dawn.yaml +++ b/db/scenarios/generation-ship-staggered-dawn.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: generation-ship-staggered-dawn meta: - title: 世代船アステリア、居住リングの夜 + title: 2312年、世代船アステリア synopsis: "2312年、恒星間世代船《アステリア》。外部航行中のため、居住リングから船外へ出ることはできません。 生態系主任ミラ・ヴォスが種子保管区で死亡しました。" category: SFクローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: ミラ・ヴォス introduction: 恒星間世代船《アステリア》生態系主任 + foundAt: 06:28 + foundIn: 種子保管区 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: ミラ・ヴォスは種子保管区で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「希少種子の監査記録」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -86,31 +94,37 @@ timeline: participants: [sera] facts: [sera-seed-diversion, mira-found-diversion, mira-would-audit] description: ミラが希少種子の流用をセラへ突きつけ、中央評議会への報告を告げる。 + location: 船内 - id: dario-sees-sera at: "05:52" participants: [sera, dario] facts: [dario-saw-sera-0552] description: ダリオが種子保管区へ向かう連絡路でセラを見かける。 + location: 種子保管区 - id: mira-death at: "05:57" participants: [sera] facts: [sera-killed-mira] description: セラが種子保管区でミラを襲う。 + location: 種子保管区 - id: agriculture-six at: "06:00" participants: [sera] facts: [agriculture-dawn-0600-local, sera-claimed-after-six] description: 船内標準時ではすでに06時00分だが、セラは農業区の人工06時05分を基準に自分の行動を説明する。 + location: 農業区 - id: medical-six at: "06:20" participants: [yuna] facts: [medical-dawn-0600-local] description: 医療区が人工06時00分を迎える。 + location: 医療区 - id: discovery at: "06:28" participants: [sera, dario, yuna] facts: [body-found-0628] description: ユナが種子保管区でミラの死を発見する。 + location: 種子保管区 characters: - id: sera name: セラ・イワノフ @@ -260,10 +274,11 @@ evidences: description: セラ管理の希少種子だけ在庫と割当先が合わず、ミラが中央評議会への報告を準備している。 reveal: mode: conversation - condition: セラかダリオにミラが事件直前に確認していた種子在庫を尋ねたら開示する。 + condition: セラかダリオにミラが事件直前に確認していた種子在庫を尋ねたら開示する。または遺体・現場を調べ、「希少種子の監査記録」に関わる資料を確認したら開示する。 sources: - { type: character, id: sera } - { type: character, id: dario } + - { type: victim, id: victim } supports: [sera-seed-diversion, mira-found-diversion, mira-would-audit] contradicts: ["lie:sera-no-diversion"] - id: yuna-private-supply diff --git a/db/scenarios/glasshouse-dawn-irrigation.yaml b/db/scenarios/glasshouse-dawn-irrigation.yaml index 7dcd449..0087f02 100644 --- a/db/scenarios/glasshouse-dawn-irrigation.yaml +++ b/db/scenarios/glasshouse-dawn-irrigation.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: glasshouse-dawn-irrigation meta: - title: 緑苑植物園、開園前の事件 + title: 開園前、標本庫にて synopsis: "午前六時三十分、市立緑苑植物園の標本庫で、主任研究員の木島祥子が死亡しているのが見つかりました。開園前の園内にいたのは、研究助手の楢原彩、支援企業の担当者・水沢浩司、園芸員の大西蒼太の三人です。" category: 植物園ミステリ difficulty: 2 @@ -10,6 +10,14 @@ meta: victim: name: 木島祥子 introduction: 市立緑苑植物園主任研究員 + foundAt: 06:30 + foundIn: 標本庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 木島祥子は標本庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「木島の希少植物管理ノート」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -90,36 +98,43 @@ timeline: participants: [narahara] facts: [kijima-found-missing-tags, kijima-confronted-narahara] description: 木島が希少植物の持ち出しを楢原へ問いただし、開園後の報告を告げる。 + location: 植物園内 - id: narahara-leaves at: "05:52" participants: [narahara] facts: [narahara-left-east-house, manual-water-zero] description: 楢原が東温室を離れる。手灌水用水道は使われていない。 + location: 東温室 - id: auto-misting at: "06:00" participants: [] facts: [auto-misting-ran-0600] description: 東温室で自動ミスト装置が予定どおり作動する。 + location: 東温室 - id: corridor-sighting at: "06:05" participants: [narahara, onishi] facts: [onishi-saw-narahara-0605] description: 大西が標本庫へ向かう通路で楢原を目撃する。 + location: 標本庫 - id: kijima-death at: "06:08" participants: [narahara] facts: [narahara-killed-kijima-0608] description: 楢原が標本庫で木島を襲い、木島は死亡する。 + location: 標本庫 - id: narahara-returns at: "06:15" participants: [narahara] facts: [narahara-returned-east-0615] description: 楢原が東温室へ戻る。 + location: 東温室 - id: discovery at: "06:30" participants: [mizusawa, narahara, onishi] facts: [body-found-0630] description: 水沢が標本庫で木島の死を発見する。 + location: 標本庫 characters: - id: narahara name: 楢原彩 @@ -272,12 +287,14 @@ evidences: description: 消えた挿し穂と楢原の担当日が一覧化され、管理札の欠落も記録されている。 reveal: mode: conversation - condition: 楢原に希少植物の管理札が足りない理由を尋ねるか、大西に木島が前日から調べていた記録を尋ねたら開示する。 + condition: 楢原に希少植物の管理札が足りない理由を尋ねるか、大西に木島が前日から調べていた記録を尋ねたら開示する。または遺体・現場を調べ、「木島の希少植物管理ノート」に関わる資料を確認したら開示する。 sources: - type: character id: narahara - type: character id: onishi + - type: victim + id: victim supports: [narahara-sold-cuttings, kijima-found-missing-tags, missing-cuttings-list] contradicts: [] - id: sponsor-email diff --git a/db/scenarios/ink-stained-contract.yaml b/db/scenarios/ink-stained-contract.yaml index d2fbbef..96b1b8a 100644 --- a/db/scenarios/ink-stained-contract.yaml +++ b/db/scenarios/ink-stained-contract.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: ink-stained-contract meta: - title: 青燈社、深夜の編集部 + title: 締切後の青燈社 synopsis: "午後九時、出版社「青燈社」の編集部で、編集長の石橋礼司が個室内で死亡しているのが見つかりました。夜遅くまで残っていたのは、著作権エージェントの川瀬梨奈、編集者の藤本圭、作家の志堂透の三人です。" category: 出版社ミステリ difficulty: 3 @@ -10,6 +10,14 @@ meta: victim: name: 石橋礼司 introduction: 出版社「青燈社」編集長 + foundAt: 21:00 + foundIn: 編集長室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 石橋礼司は編集長室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「海外版権の送金記録と支払帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -90,41 +98,49 @@ timeline: participants: [kawase] facts: [kawase-diverted-royalties, ishibashi-found-royalty-gap, ishibashi-warned-kawase] description: 石橋が川瀬の版権収入の不正を指摘し、翌朝の契約打ち切りと報告を告げる。 + location: 編集部内 - id: shido-argument at: "20:31" participants: [shido] facts: [shido-argued-2031, shido-plagiarism] description: 志堂が盗用疑惑を巡って石橋と口論する。 + location: 編集部内 - id: kawase-enters at: "20:38" participants: [kawase] facts: [kawase-entered-2038] description: 川瀬が編集長室へ入る。 + location: 編集長室 - id: ishibashi-death at: "20:41" participants: [kawase] facts: [kawase-killed-ishibashi-2041] description: 川瀬が石橋を襲い、石橋は編集長室で死亡する。 + location: 編集長室 - id: kawase-leaves at: "20:44" participants: [kawase, fujimoto] facts: [fujimoto-saw-kawase-2044] description: 藤本が編集長室前から離れる川瀬を目撃する。 + location: 編集長室 - id: contract-print at: "20:47" participants: [kawase] facts: [contract-printed-2047, kawase-forged-signature] description: 川瀬が契約書を印刷し、石橋が後から署名したように見える書類を作る。 + location: 編集部 - id: claimed-signing at: "20:50" participants: [kawase] facts: [signed-contract-claims-2050] description: 川瀬は後に、この時刻に石橋本人から署名をもらったと説明する。 + location: 編集部内 - id: discovery at: "21:00" participants: [fujimoto, kawase, shido] facts: [body-found-2100] description: 藤本が編集長室で石橋の死を発見する。 + location: 編集長室 characters: - id: kawase name: 川瀬梨奈 @@ -279,12 +295,14 @@ evidences: description: 海外出版社からの入金額に対して作家へ報告された金額が少なく、川瀬の管理口座へ差額が残っている。 reveal: mode: conversation - condition: 川瀬に石橋が調べていた版権収入について尋ねるか、藤本に石橋が事件前に確認していた帳簿を尋ねたら開示する。 + condition: 川瀬に石橋が調べていた版権収入について尋ねるか、藤本に石橋が事件前に確認していた帳簿を尋ねたら開示する。または遺体・現場を調べ、「海外版権の送金記録と支払帳簿」に関わる資料を確認したら開示する。 sources: - type: character id: kawase - type: character id: fujimoto + - type: victim + id: victim supports: [kawase-diverted-royalties, ishibashi-found-royalty-gap, ishibashi-warned-kawase] contradicts: [] - id: plagiarism-comparison diff --git a/db/scenarios/landslide-clock-museum-eleven-minutes.yaml b/db/scenarios/landslide-clock-museum-eleven-minutes.yaml index 249f8ef..506c478 100644 --- a/db/scenarios/landslide-clock-museum-eleven-minutes.yaml +++ b/db/scenarios/landslide-clock-museum-eleven-minutes.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: landslide-clock-museum-eleven-minutes meta: - title: 崩落の時計博物館、閉館後の夜 + title: 山道が崩れた時計博物館 synopsis: "午後八時四十二分、山間の私設時計博物館の修復室で、学芸員長の倉橋宗一が死亡しているのが見つかりました。午後七時すぎの土砂崩れで唯一の山道は通行不能となり、館内には倉橋を含め五人しか残っていませんでした。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 倉橋宗一 introduction: 私設時計博物館学芸員長 + foundAt: 20:42 + foundIn: 修復室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 倉橋宗一は修復室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「倉橋の来歴照合メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -102,41 +110,49 @@ timeline: participants: [kido] facts: [master-clock-fast-eleven, gallery-clocks-follow-master, kido-caused-clock-offset] description: 同期試験後、親時計と展示時計が正しい時刻より十一分進んだ状態で残る。 + location: 館内 - id: false-half-past-chime at: "20:19" participants: [shiba, asakura, hoshina] facts: [half-past-chime-actual-2019, security-clock-accurate] description: 館内時計が20時30分を示し、大時計が半時の鐘を鳴らす。実際の時刻は20時19分だった。 + location: 館内 - id: shiba-kurahashi-talk at: "20:20" participants: [shiba] facts: [shiba-spoke-kurahashi-2020] description: 志波が西回廊で倉橋と短く話す。 + location: 西回廊 - id: shiba-leaves-west at: "20:22" participants: [shiba] facts: [shiba-left-west-corridor] description: 志波が西回廊を離れ、修復室側へ向かう。 + location: 西回廊 - id: asakura-sighting at: "20:24" participants: [shiba, asakura] facts: [asakura-saw-shiba-2024] description: 朝倉が北廊下で志波を目撃する。 + location: 北廊下 - id: kurahashi-death at: "20:28" participants: [shiba] facts: [shiba-killed-kurahashi] description: 志波が修復室で倉橋を襲い、倉橋は死亡する。 + location: 修復室 - id: shiba-return at: "20:34" participants: [shiba] facts: [shiba-returned-gallery] description: 志波が展示準備室へ戻る。 + location: 展示準備室 - id: discovery at: "20:42" participants: [hoshina, shiba, asakura, kido] facts: [body-found-2042] description: 保科が修復室で倉橋の死を発見する。 + location: 修復室 characters: - id: shiba @@ -340,10 +356,11 @@ evidences: description: 志波が登録した著名工房の来歴と原資料が一致せず、倉橋が翌朝の展示撤去と理事会報告を予定していたことが分かる。 reveal: mode: conversation - condition: 志波か保科に倉橋が事件直前に調べていた展示時計の来歴について尋ね、登録内容の不一致を追及したら開示する。 + condition: 志波か保科に倉橋が事件直前に調べていた展示時計の来歴について尋ね、登録内容の不一致を追及したら開示する。または遺体・現場を調べ、「倉橋の来歴照合メモ」に関わる資料を確認したら開示する。 sources: - { type: character, id: shiba } - { type: character, id: hoshina } + - { type: victim, id: victim } supports: [shiba-faked-provenance, kurahashi-found-provenance-fraud, kurahashi-planned-withdrawal] contradicts: [] - id: hoshina-image-history diff --git a/db/scenarios/landslide-hotel-frosted-silhouette.yaml b/db/scenarios/landslide-hotel-frosted-silhouette.yaml index 15a0f0c..e09160c 100644 --- a/db/scenarios/landslide-hotel-frosted-silhouette.yaml +++ b/db/scenarios/landslide-hotel-frosted-silhouette.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: landslide-hotel-frosted-silhouette meta: - title: 崖上ホテル、孤立の夜 + title: 崖の上から帰れない synopsis: "午後十時、山腹の小さなホテルで支配人・長峰宗一が執務室内で死亡しているのが見つかりました。夕方からの豪雨で道路は土砂崩れに塞がれ、ホテルには外部から出入りできません。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 長峰宗一 introduction: 山腹のホテル支配人 + foundAt: 22:00 + foundIn: 執務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 長峰宗一は執務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「水増しされた改装請求書」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -91,31 +99,37 @@ timeline: participants: [ayase] facts: [renovation-shortage, ayase-falsified-invoices, nagamine-called-ayase] description: 長峰が綾瀬を執務室へ呼び、改装費の不自然な請求について説明を求める。 + location: 執務室 - id: nagamine-death at: "21:05" participants: [ayase] facts: [ayase-killed-nagamine] description: 綾瀬が執務室で長峰を襲う。 + location: 執務室 - id: coat-missing at: "21:18" participants: [hoshino] facts: [hoshino-saw-empty-coat-hook] description: 星野がフロント裏の衣類掛けから長峰の長い上着がなくなっていることに気づく。 + location: フロント裏 - id: silhouette-staged at: "21:23" participants: [ayase] facts: [ayase-staged-silhouette, desk-lamp-left-on, stand-feet-dust-mark] description: 綾瀬が上着と衣紋掛け、机上灯を使って磨りガラス越しの人影を作る。 + location: ホテル内 - id: silhouette-seen at: "21:30" participants: [kanda] facts: [kanda-saw-silhouette, kanda-did-not-hear-voice] description: 神田が廊下から人影を見て、長峰本人が執務室にいると思い込む。 + location: 執務室 - id: discovery at: "22:00" participants: [hoshino, ayase, kanda] facts: [body-found-2200, coat-returned-after-discovery] description: 星野が執務室で長峰の死を発見し、室内の衣紋掛けには長峰の上着が残されている。 + location: 執務室 characters: - id: ayase name: 綾瀬環 @@ -299,12 +313,14 @@ evidences: description: 長峰が印を付けた請求書には、綾瀬が処理した項目に説明できない差額がまとまっている。 reveal: mode: conversation - condition: 綾瀬に改装費の処理を尋ねるか、長峰が直前まで確認していた書類について追及したら開示する。 + condition: 綾瀬に改装費の処理を尋ねるか、長峰が直前まで確認していた書類について追及したら開示する。または遺体・現場を調べ、「水増しされた改装請求書」に関わる資料を確認したら開示する。 sources: - type: character id: ayase - type: character id: hoshino + - type: victim + id: victim supports: [renovation-shortage, ayase-falsified-invoices, nagamine-called-ayase] contradicts: ["lie:ayase-clean-invoices"] - id: manuscript-copy diff --git a/db/scenarios/last-train-delay-certificate.yaml b/db/scenarios/last-train-delay-certificate.yaml index 2ca4703..e9850ea 100644 --- a/db/scenarios/last-train-delay-certificate.yaml +++ b/db/scenarios/last-train-delay-certificate.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: last-train-delay-certificate meta: - title: 夕凪駅、終電後の事件 + title: 終電が八分遅れた夜 synopsis: "午後十時二十五分、郊外駅「夕凪駅」の駅務室で、駅長の藤崎正雄が死亡しているのが見つかりました。その夜は信号トラブルで終電が八分遅れ、ホームには普段より長く乗客が残っていました。" category: 駅ミステリ difficulty: 3 @@ -10,6 +10,14 @@ meta: victim: name: 藤崎正雄 introduction: 夕凪駅駅長 + foundAt: 22:25 + foundIn: 駅務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 藤崎正雄は駅務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「券売機返金処理と現金残高の差」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -89,36 +97,43 @@ timeline: participants: [arima] facts: [arima-skimmed-refunds, fujisaki-found-refund-gap, fujisaki-warned-arima] description: 藤崎が有馬の返金処理の不正を指摘し、終電後の本部報告を告げる。 + location: 駅構内 - id: arima-office-corridor at: "22:05" participants: [arima, sawada] facts: [arima-entered-office-2205, sawada-saw-arima-2205] description: 沢田が駅務室へ向かう有馬を目撃する。 + location: 駅務室 - id: fujisaki-death at: "22:08" participants: [arima] facts: [arima-killed-fujisaki-2208] description: 有馬が駅務室で藤崎を襲い、藤崎は死亡する。 + location: 駅務室 - id: arima-returns-platform at: "22:12" participants: [arima, komori] facts: [arima-left-office-2212, komori-saw-arima-2212] description: 小森が駅務室側からホームへ戻る有馬を目撃する。 + location: ホーム - id: last-train-departs at: "22:18" participants: [arima, sawada, komori] facts: [last-train-delayed] description: 八分遅れの終電が夕凪駅を出発する。 + location: 駅構内 - id: certificates-print at: "22:20" participants: [arima] facts: [certificates-printed-2220] description: 終電出発後、有馬が駅務室の専用プリンターで遅延証明書を一括印刷する。 + location: 駅務室 - id: discovery at: "22:25" participants: [komori, arima, sawada] facts: [body-found-2225] description: 小森が駅務室で藤崎の死を発見する。 + location: 駅務室 characters: - id: arima name: 有馬結衣 @@ -282,12 +297,14 @@ evidences: description: 少額返金が繰り返された日時と現金不足が一致し、有馬の担当時間帯へ集中している。 reveal: mode: conversation - condition: 有馬に藤崎が事件前に確認していた返金処理について尋ねるか、小森に駅長が帳簿を調べていた理由を尋ねたら開示する。 + condition: 有馬に藤崎が事件前に確認していた返金処理について尋ねるか、小森に駅長が帳簿を調べていた理由を尋ねたら開示する。または遺体・現場を調べ、「券売機返金処理と現金残高の差」に関わる資料を確認したら開示する。 sources: - type: character id: arima - type: character id: komori + - type: victim + id: victim supports: [arima-skimmed-refunds, fujisaki-found-refund-gap, fujisaki-warned-arima] contradicts: [] - id: kiosk-disposal-record diff --git a/db/scenarios/mars-outpost-delayed-reply.yaml b/db/scenarios/mars-outpost-delayed-reply.yaml index 12ccd9e..4395fdd 100644 --- a/db/scenarios/mars-outpost-delayed-reply.yaml +++ b/db/scenarios/mars-outpost-delayed-reply.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: mars-outpost-delayed-reply meta: - title: 火星基地、砂嵐の夜 + title: 火星、エリュシオン観測基地 synopsis: "2147年、火星エリュシオン観測基地。大規模な砂嵐で地表車両も通信中継ドローンも停止するなか、主任研究員エレナ・ヴァルガが解析室で死亡しました。" category: SFクローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: エレナ・ヴァルガ introduction: 火星エリュシオン観測基地主任研究員 + foundAt: 21:50 + foundIn: 解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: エレナ・ヴァルガは解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「調達予算の監査ファイル」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -84,31 +92,37 @@ timeline: participants: [noah, amira] facts: [noah-sent-question-2118] description: ノアが地球管制へ観測予算について質問を送る。 + location: 基地内 - id: fraud-confrontation at: "21:25" participants: [noah] facts: [noah-budget-fraud, elena-found-fraud, elena-would-report] description: エレナが予算流用をノアへ突きつけ、翌朝に地球本部へ報告すると告げる。 + location: 基地内 - id: corridor-sighting at: "21:33" participants: [noah, luis] facts: [luis-saw-noah-corridor] description: ルイスが解析室へ続く連絡廊下でノアを見かける。 + location: 連絡廊下 - id: elena-death at: "21:36" participants: [noah] facts: [noah-killed-elena] description: ノアが解析室でエレナを襲う。 + location: 解析室 - id: delayed-reply at: "21:38" participants: [noah, amira] facts: [earth-reply-arrived-2138, noah-played-arrival-tone, earth-mars-delay] description: 21時18分の質問への返答が地球から到着し、ノアはそれを事件時刻の会話記録として扱わせる。 + location: 基地内 - id: discovery at: "21:50" participants: [noah, amira, luis] facts: [body-found-2150] description: アミラが解析室でエレナの死を発見する。 + location: 解析室 characters: - id: noah name: ノア・チェン @@ -260,10 +274,11 @@ evidences: description: ノア管理の予算だけ用途が合わず、エレナが翌朝の地球送信用フォルダへ証拠をまとめている。 reveal: mode: conversation - condition: ノアかアミラにエレナが翌朝送ろうとしていた監査資料について尋ねたら開示する。 + condition: ノアかアミラにエレナが翌朝送ろうとしていた監査資料について尋ねたら開示する。または遺体・現場を調べ、「調達予算の監査ファイル」に関わる資料を確認したら開示する。 sources: - { type: character, id: noah } - { type: character, id: amira } + - { type: victim, id: victim } supports: [noah-budget-fraud, elena-found-fraud, elena-would-report] contradicts: ["lie:noah-no-fraud"] - id: private-bandwidth-log diff --git a/db/scenarios/midnight-radio-rerun.yaml b/db/scenarios/midnight-radio-rerun.yaml index 0239676..0570b22 100644 --- a/db/scenarios/midnight-radio-rerun.yaml +++ b/db/scenarios/midnight-radio-rerun.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: midnight-radio-rerun meta: - title: ラジオ局レイライン、深夜の事件 + title: 午前零時十二分、第二収録ブース synopsis: "午前零時十二分、FMラジオ局「レイライン」の第二収録ブースで、人気パーソナリティの大門修一が死亡しているのが見つかりました。局内に残っていたのは、番組プロデューサーの美濃部沙耶、音響技師の久世直人、スポンサー会社の担当者・夏目亮介の三人です。" category: 放送局ミステリ difficulty: 3 @@ -10,6 +10,14 @@ meta: victim: name: 大門修一 introduction: FMラジオ局「レイライン」パーソナリティ + foundAt: 00:12 + foundIn: 第2ブース + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 大門修一は第2ブースで倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「大門が保存したスポンサー報告の比較メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -89,46 +97,55 @@ timeline: participants: [minobe, kuze] facts: [opening-recorded-2320] description: 大門が午前零時に流す番組冒頭を事前収録する。 + location: 局内 - id: natsume-meets-daimon at: "23:38" participants: [natsume] facts: [natsume-secret-meeting] description: 夏目が第二収録ブースで大門とスポンサー契約を巡って口論する。 + location: 第2ブース - id: natsume-leaves at: "23:43" participants: [natsume] facts: [natsume-left-2343] description: 夏目が大門のもとを離れ、一階ロビーへ向かう。 + location: ロビー - id: minobe-enters at: "23:46" participants: [minobe] facts: [minobe-entered-2346] description: 美濃部が第二収録ブースへ入る。 + location: 第2ブース - id: daimon-death at: "23:48" participants: [minobe] facts: [daimon-died-2348] description: 美濃部が大門を襲い、大門はブース内で死亡する。 + location: ブース - id: minobe-returns at: "23:50" participants: [minobe, kuze] facts: [kuze-saw-minobe-2350] description: 久世が第二収録ブース側の廊下から戻る美濃部を目撃する。 + location: 第2ブース - id: recording-queued at: "23:53" participants: [minobe] facts: [minobe-queued-recording] description: 美濃部が事前収録音声を午前零時の自動送出枠へ登録する。 + location: 送出室 - id: opening-airs at: "00:00" participants: [kuze, minobe, natsume] facts: [recorded-opening-aired] description: 事前収録された大門の声が番組冒頭として放送される。 + location: 局内 - id: discovery at: "00:12" participants: [kuze, minobe, natsume] facts: [body-found-0012] description: 久世が第二収録ブースで大門の死を発見する。 + location: 第2ブース characters: - id: minobe name: 美濃部沙耶 @@ -284,12 +301,14 @@ evidences: description: 公式報告と実際の放送枠に差があり、美濃部の報告水増しを大門が確認していたことが分かる。 reveal: mode: conversation - condition: 美濃部に制作費やスポンサー報告の不一致について尋ねるか、夏目に大門が最近スポンサー実績を調べていなかったか尋ねたら開示する。 + condition: 美濃部に制作費やスポンサー報告の不一致について尋ねるか、夏目に大門が最近スポンサー実績を調べていなかったか尋ねたら開示する。または遺体・現場を調べ、「大門が保存したスポンサー報告の比較メモ」に関わる資料を確認したら開示する。 sources: - type: character id: minobe - type: character id: natsume + - type: victim + id: victim supports: [minobe-falsified-reports, daimon-confronted-minobe] contradicts: [] - id: deleted-demo-log diff --git a/db/scenarios/polar-station-shared-clock.yaml b/db/scenarios/polar-station-shared-clock.yaml index 61177a6..40e2cbc 100644 --- a/db/scenarios/polar-station-shared-clock.yaml +++ b/db/scenarios/polar-station-shared-clock.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: polar-station-shared-clock meta: - title: 白夜第六観測基地、吹雪の夜 + title: 白夜第六観測基地 synopsis: "午後十一時十分、吹雪で完全に孤立した白夜第六観測基地の解析室で、主任研究員の牧瀬航が死亡しているのが見つかりました。基地内にいたのは観測員の篠宮怜、通信担当の樋口海、整備担当の沢渡直人の三人だけです。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 牧瀬航 introduction: 白夜第六観測基地主任研究員 + foundAt: 23:10 + foundIn: 解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 牧瀬航は解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「翌朝の研究会議議題」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -96,26 +104,31 @@ timeline: participants: [higuchi] facts: [central-clock-offset, higuchi-hid-sync-failure] description: 時刻同期系に障害が起き、基地の中央時刻系が実際より七分進んだ状態になる。 + location: 基地内 - id: false-alibi-window at: "22:13" participants: [shinomiya] facts: [terminal-uses-central-clock, access-uses-central-clock, observation-log-central-clock, wall-clock-synced, four-times-not-independent] description: 中央時刻系のずれを共有する複数の記録が、篠宮の通信区画滞在を実際より七分遅い時刻として残す。 + location: 基地内 - id: real-corridor-sighting at: "22:20" participants: [shinomiya, sawatari] facts: [handheld-clock-correct, sawatari-saw-shinomiya-real-2220, displayed-time-was-2227] description: 沢渡が独立した携帯時計で22時20分を確認し、解析室側の通路にいる篠宮を目撃する。 + location: 解析室 - id: makise-death at: "22:23" participants: [shinomiya] facts: [makise-death-real-2223, shinomiya-killed-makise] description: 篠宮が解析室で牧瀬を襲い、牧瀬は死亡する。 + location: 解析室 - id: discovery at: "23:10" participants: [higuchi, shinomiya, sawatari] facts: [body-found-2310] description: 樋口が解析室で牧瀬の死を発見する。 + location: 解析室 characters: - id: shinomiya name: 篠宮怜 @@ -282,12 +295,14 @@ evidences: description: 牧瀬の会議メモに、篠宮によるデータ除外の妥当性を全員の前で確認する予定が記されている。 reveal: mode: conversation - condition: 篠宮か樋口に翌朝の研究会議で牧瀬が扱う予定だった議題を尋ねたら開示する。 + condition: 篠宮か樋口に翌朝の研究会議で牧瀬が扱う予定だった議題を尋ねたら開示する。または遺体・現場を調べ、「翌朝の研究会議議題」に関わる資料を確認したら開示する。 sources: - type: character id: shinomiya - type: character id: higuchi + - type: victim + id: victim supports: [data-manipulation-found, disclosure-next-morning] contradicts: [] - id: hidden-sync-delay diff --git a/db/scenarios/quarantine-clinic-borrowed-badge.yaml b/db/scenarios/quarantine-clinic-borrowed-badge.yaml index 135914b..1e27bec 100644 --- a/db/scenarios/quarantine-clinic-borrowed-badge.yaml +++ b/db/scenarios/quarantine-clinic-borrowed-badge.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: quarantine-clinic-borrowed-badge meta: - title: 白嶺診療所、隔離の夜 + title: 封鎖された白嶺診療所 synopsis: "午後九時四十分、感染対策のため一時閉鎖中の白嶺診療所で、事務長の星名悟が事務室内で死亡しているのが見つかりました。外扉は午後八時以降封鎖され、館内にいたのは看護主任の世良美冬、検査技師の久我夏樹、警備担当の相原誠の三人だけです。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 星名悟 introduction: 白嶺診療所事務長 + foundAt: 21:40 + foundIn: 事務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 星名悟は事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「本部報告予定の確認メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -92,21 +100,25 @@ timeline: participants: [sera, kuga] facts: [kuga-borrowed-badge, sera-approved-loan] description: 久我が星名の橙色バッジを借り、世良もその貸与を確認する。 + location: 診療所内 - id: hoshina-death at: "21:08" participants: [sera] facts: [hoshina-death-2108, sera-killed-hoshina] description: 世良が事務室で星名を襲い、星名は死亡する。 + location: 事務室 - id: orange-passage at: "21:18" participants: [kuga, aihara] facts: [orange-badge-passed-2118, aihara-saw-orange-suit, person-was-kuga-2118] description: 星名の橙色バッジを借りた久我が検査廊下を通り、相原がその姿を目撃する。 + location: 診療所内 - id: discovery at: "21:40" participants: [aihara, sera, kuga] facts: [body-found-2140] description: 相原が事務室で星名の死を発見する。 + location: 事務室 characters: - id: sera name: 世良美冬 @@ -287,12 +299,14 @@ evidences: description: 星名の予定表に、翌朝最初の案件として世良による同意書管理手順の独断変更を本部へ報告すると記されている。 reveal: mode: conversation - condition: 世良か久我に星名が翌朝予定していた本部報告の内容を尋ねたら開示する。 + condition: 世良か久我に星名が翌朝予定していた本部報告の内容を尋ねたら開示する。または遺体・現場を調べ、「本部報告予定の確認メモ」に関わる資料を確認したら開示する。 sources: - type: character id: sera - type: character id: kuga + - type: victim + id: victim supports: [sera-changed-consent-process, consent-audit-next-day] contradicts: [] - id: private-print-log diff --git a/db/scenarios/rainy-bookstore-receipt.yaml b/db/scenarios/rainy-bookstore-receipt.yaml index f537f49..d76471a 100644 --- a/db/scenarios/rainy-bookstore-receipt.yaml +++ b/db/scenarios/rainy-bookstore-receipt.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: rainy-bookstore-receipt meta: - title: 古書店青雨堂、雨の夜 + title: 青雨堂、閉店後の商談 synopsis: "午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。" category: 日常系本格 difficulty: 2 @@ -10,6 +10,14 @@ meta: victim: name: 水野英治 introduction: 青雨堂店主 + foundAt: 19:15 + foundIn: 店奥 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 水野英治は店奥で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「店頭に残った複製本」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -87,41 +95,49 @@ timeline: participants: [kuroda] facts: [kuroda-secret-offer] description: 黒田が水野に、記録へ残さない現金取引を持ちかける。 + location: 店内 - id: swap-discovered at: "18:37" participants: [makino] facts: [mizuno-discovered-swap] description: 水野が初版本のすり替えに気づき、牧野を問い詰める。 + location: 店内 - id: kuroda-leaves at: "18:42" participants: [kuroda] facts: [kuroda-left-1842] description: 黒田が店を出るが、激しい雨のため近くの軒下で雨宿りする。 + location: 軒下 - id: kuroda-sighting at: "18:47" participants: [kuroda, makino] facts: [kuroda-saw-makino-1847] description: 黒田が正面ガラス越しに、まだ店内にいる牧野を目撃する。 + location: 店内 - id: mizuno-death at: "18:50" participants: [makino] facts: [makino-killed-mizuno-1850] description: 牧野が店の奥で水野を襲い、水野は死亡する。 + location: 店奥 - id: makino-departs at: "18:56" participants: [makino, sena] facts: [makino-left-1856, sena-saw-makino-leave] description: 牧野が小包を持って店を出るところを、向かいの喫茶店にいた瀬名が見る。 + location: 店内 - id: parcel-posted at: "19:08" participants: [makino] facts: [post-receipt-1908] description: 牧野が郵便窓口で小包を発送し、時刻入りのレシートを受け取る。 + location: 郵便窓口 - id: discovery at: "19:15" participants: [sena] facts: [body-found-1915] description: 瀬名が青雨堂を訪ね、店の奥で水野の死を発見する。 + location: 店奥 characters: - id: makino name: 牧野千尋 @@ -273,12 +289,14 @@ evidences: description: 初版本として保管されていた本は精巧な複製で、在庫と発送を扱う者ならすり替えの機会があった。 reveal: mode: conversation - condition: 牧野か黒田に事件当日の高価な初版本について詳しく尋ね、真贋に疑問が出たら開示する。 + condition: 牧野か黒田に事件当日の高価な初版本について詳しく尋ね、真贋に疑問が出たら開示する。または遺体・現場を調べ、「店頭に残った複製本」に関わる資料を確認したら開示する。 sources: - type: character id: makino - type: character id: kuroda + - type: victim + id: victim supports: [makino-swapped-book, rare-book-arrived] contradicts: [] - id: secret-deal-note diff --git a/db/scenarios/shanghai-warehouse-carbon-copy.yaml b/db/scenarios/shanghai-warehouse-carbon-copy.yaml index f2aa6b1..b5f2b40 100644 --- a/db/scenarios/shanghai-warehouse-carbon-copy.yaml +++ b/db/scenarios/shanghai-warehouse-carbon-copy.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: shanghai-warehouse-carbon-copy meta: - title: 1928年上海、河岸倉庫の夜 + title: 1928年、上海河岸 synopsis: "1928年、上海。暴風雨で河岸の道路が封鎖された夜、輸出倉庫の事務室で貿易商・周文海が死亡しているのが見つかりました。" category: 歴史クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 周文海 introduction: 上海の貿易商 + foundAt: 22:20 + foundIn: 事務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 周文海は事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「絹荷の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -84,26 +92,31 @@ timeline: participants: [lin, chen] facts: [zhou-signed-blank-form] description: 周が翌朝の搬出用に、時刻欄が空白の伝票へ先に署名する。 + location: 事務室 - id: chen-sees-form at: "21:40" participants: [chen] facts: [chen-saw-blank-time] description: 陳が署名済み伝票を確認し、時刻欄がまだ空白なのを見る。 + location: 事務室 - id: zhou-death at: "21:52" participants: [lin] facts: [lin-killed-zhou, lin-smuggled-silk, zhou-found-smuggling] description: 林が帳簿外取引の発覚を恐れ、事務室で周を襲う。 + location: 事務室 - id: time-added at: "22:05" participants: [lin] facts: [lin-added-2205, top-sheet-time-added-later] description: 林が署名済み伝票の上紙だけに22時05分と書き足す。 + location: 事務室 - id: discovery at: "22:20" participants: [lin, chen, wang] facts: [body-found-2220] description: 王が事務室で周の死を発見する。 + location: 事務室 characters: - id: lin name: 林雪梅 @@ -253,10 +266,11 @@ evidences: description: 倉庫の実数と帳簿が合わず、周が林の担当欄に翌朝再検査の印を付けている。 reveal: mode: conversation - condition: 林か陳に周が事件前に調べていた荷の不一致を尋ねたら開示する。 + condition: 林か陳に周が事件前に調べていた荷の不一致を尋ねたら開示する。または遺体・現場を調べ、「絹荷の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: lin } - { type: character, id: chen } + - { type: victim, id: victim } supports: [lin-smuggled-silk, zhou-found-smuggling] contradicts: ["lie:lin-no-smuggling"] - id: broken-cargo-cache diff --git a/db/scenarios/snow-gallery-erased-tracks.yaml b/db/scenarios/snow-gallery-erased-tracks.yaml index 82c0277..9298a8d 100644 --- a/db/scenarios/snow-gallery-erased-tracks.yaml +++ b/db/scenarios/snow-gallery-erased-tracks.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: snow-gallery-erased-tracks meta: - title: 白庭彫刻館、雪の夜 + title: 雪は白庭彫刻館を閉ざした synopsis: "午後八時五十分、吹雪で山道が閉ざされた白庭彫刻館の離れ展示室で、館長の青沼卓が死亡しているのが見つかりました。館内に残っていたのは学芸員の倉田真帆、施設員の安西雄、彫刻家の江波涼の三人だけです。" category: クローズドサークル difficulty: 4 @@ -10,6 +10,14 @@ meta: victim: name: 青沼卓 introduction: 白庭彫刻館館長 + foundAt: 20:50 + foundIn: 館内 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 青沼卓は館内で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「来歴記録の修正履歴」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -87,31 +95,37 @@ timeline: participants: [] facts: [snow-started-2010] description: 母屋と離れの間の通路に雪が積もり始める。 + location: 母屋 - id: kurata-crosses at: "20:17" participants: [kurata] facts: [kurata-crossed-2017] description: 倉田が母屋から離れ展示室へ向かう。 + location: 離れ展示室 - id: aoonuma-death at: "20:20" participants: [kurata] facts: [aoonuma-death-2020, kurata-killed-aoonuma] description: 倉田が離れ展示室で青沼を襲い、青沼は死亡する。 + location: 離れ展示室 - id: path-cleared at: "20:26" participants: [anzai] facts: [anzai-cleared-path-2026, snow-covered-after-clearing] description: 安西が排水口確認のため屋外通路を一度掃き直し、それ以前の足跡も雪面ごと崩れる。 + location: 屋外通路 - id: kurata-back-main at: "20:31" participants: [kurata, anzai] facts: [anzai-saw-kurata-2031] description: 安西が母屋裏口付近にいる倉田を目撃する。 + location: 母屋裏口 - id: discovery at: "20:50" participants: [enami, kurata, anzai] facts: [body-found-2050, no-tracks-at-discovery] description: 江波が離れで青沼の死を発見し、通路には判別できる足跡が残っていない。 + location: 館内 characters: - id: kurata name: 倉田真帆 @@ -281,12 +295,14 @@ evidences: description: 倉田が担当した複数作品で、根拠資料と合わない来歴修正が行われ、青沼が翌日理事会へ報告する印を付けている。 reveal: mode: conversation - condition: 倉田か江波に青沼が直前まで確認していた作品来歴について尋ねたら開示する。 + condition: 倉田か江波に青沼が直前まで確認していた作品来歴について尋ねたら開示する。または遺体・現場を調べ、「来歴記録の修正履歴」に関わる資料を確認したら開示する。 sources: - type: character id: kurata - type: character id: enami + - type: victim + id: victim supports: [provenance-forgery-found, board-report-next-day] contradicts: [] - id: broken-snow-sensor diff --git a/db/scenarios/snow-observatory-last-photo.yaml b/db/scenarios/snow-observatory-last-photo.yaml index 65794dc..e4d552f 100644 --- a/db/scenarios/snow-observatory-last-photo.yaml +++ b/db/scenarios/snow-observatory-last-photo.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: snow-observatory-last-photo meta: - title: 北岳観測所、吹雪の夜 + title: 北岳観測所、吹雪の午後十時 synopsis: "午後十時十五分、北岳観測所の資料室で、所長の神崎遼が死亡しているのが見つかりました。外は激しい吹雪で、午後九時以降に天文台を出入りした者はいません。" category: クローズドサークル difficulty: 3 @@ -10,6 +10,14 @@ meta: victim: name: 神崎遼 introduction: 北岳観測所所長 + foundAt: 22:15 + foundIn: 資料室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 神崎遼は資料室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「神崎の観測データ検証メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -90,41 +98,49 @@ timeline: participants: [hiyama] facts: [kanzaki-found-fabrication, kanzaki-would-retract] description: 神崎が日山のデータ不正を指摘し、翌朝の論文撤回と報告を告げる。 + location: 観測所内 - id: kurose-data-room at: "21:48" participants: [kurose] facts: [kurose-entered-data-room, kurose-copied-data] description: 黒瀬が解析室へ入り、未公開データを無断で複製する。 + location: 解析室 - id: interval-start at: "21:50" participants: [hiyama] facts: [camera-interval-started, camera-kept-shooting] description: 日山が屋上のカメラを三分間隔の自動撮影に設定する。 + location: 屋上 - id: hiyama-leaves-roof at: "21:54" participants: [hiyama] facts: [hiyama-left-roof-2154] description: 日山がカメラを残したまま屋上を離れ、建物内へ戻る。 + location: 屋上 - id: muroi-sighting at: "21:57" participants: [hiyama, muroi] facts: [muroi-saw-hiyama-2157] description: 室井が資料室へ向かう廊下で日山とすれ違う。 + location: 資料室 - id: kanzaki-death at: "22:00" participants: [hiyama] facts: [hiyama-killed-kanzaki-2200] description: 日山が資料室で神崎を襲い、神崎は死亡する。 + location: 資料室 - id: hiyama-returns at: "22:06" participants: [hiyama] facts: [hiyama-returned-roof-2206] description: 日山が屋上へ戻り、自動撮影中のカメラのそばへ戻る。 + location: 屋上 - id: discovery at: "22:15" participants: [kurose, hiyama, muroi] facts: [body-found-2215] description: 黒瀬が資料室で神崎の死を発見する。 + location: 資料室 characters: - id: hiyama name: 日山澪 @@ -271,12 +287,14 @@ evidences: description: 日山の補正値だけが元データと合わず、翌朝の論文撤回を示す神崎の書き込みが残っている。 reveal: mode: conversation - condition: 日山か黒瀬に神崎が直前まで調べていたデータについて尋ね、不正補正の可能性を追及したら開示する。 + condition: 日山か黒瀬に神崎が直前まで調べていたデータについて尋ね、不正補正の可能性を追及したら開示する。または遺体・現場を調べ、「神崎の観測データ検証メモ」に関わる資料を確認したら開示する。 sources: - type: character id: hiyama - type: character id: kurose + - type: victim + id: victim supports: [hiyama-fabricated-data, kanzaki-found-fabrication, kanzaki-would-retract] contradicts: [] - id: data-room-access diff --git a/db/scenarios/snowbound-farm-morning-chores.yaml b/db/scenarios/snowbound-farm-morning-chores.yaml index 5c97ab9..6ead0c1 100644 --- a/db/scenarios/snowbound-farm-morning-chores.yaml +++ b/db/scenarios/snowbound-farm-morning-chores.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: snowbound-farm-morning-chores meta: - title: 高原農園、雪籠りの朝 + title: 朝七時、高原農園 synopsis: "午前七時十分、雪に閉ざされた高原農園の事務室で、経営者の佐久間隆志が死亡しているのが発見されました。夜半から猛吹雪となり、農園へ通じる一本道は完全に塞がれています。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 佐久間隆志 introduction: 高原農園経営者 + foundAt: 2026-01-16T07:10:00+09:00 + foundIn: 事務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 佐久間隆志は事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「出荷伝票の不足メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -91,36 +99,43 @@ timeline: participants: [fuyuki, nozomi] facts: [audit-shortage-found] description: 佐久間が夜の帳簿確認で出荷代金の不足に気づく。 + location: 事務室 - id: confrontation at: "2026-01-15T21:50:00+09:00" participants: [fuyuki] facts: [sakuma-confronted-fuyuki, fuyuki-diverted-sales] description: 佐久間が冬木に不足分を問い、翌朝に帳簿を再確認すると告げる。 + location: 事務室 - id: corridor-sighting at: "2026-01-15T22:06:00+09:00" participants: [fuyuki, kuze] facts: [kuze-saw-fuyuki-office-side] description: 久世が事務室側の廊下から戻ってくる冬木を見かける。 + location: 事務室 - id: sakuma-death at: "2026-01-15T22:10:00+09:00" participants: [fuyuki] facts: [fuyuki-killed-sakuma, fuyuki-left-victim-coat] description: 冬木が事務室で佐久間を襲い、その後に朝まで生きていたように見せる準備をする。 + location: 事務室 - id: staged-chores at: "2026-01-16T05:35:00+09:00" participants: [fuyuki] facts: [fuyuki-did-morning-chores, feed-board-magnet-moved, fuyuki-boots-wet-straw] description: 冬木が普段は佐久間が行う朝の家畜の世話を代わりに済ませる。 + location: 農園内 - id: kitchen-light-seen at: "2026-01-16T05:48:00+09:00" participants: [nozomi] facts: [kitchen-light-on, nozomi-assumed-uncle-awake] description: 希が台所の灯りを見て、叔父がいつも通り起きていると思い込む。 + location: 農園内 - id: discovery at: "2026-01-16T07:10:00+09:00" participants: [nozomi, fuyuki, kuze] facts: [body-found-0710] description: 希が事務室で佐久間の死を発見する。 + location: 事務室 characters: - id: fuyuki name: 冬木紬 @@ -304,12 +319,14 @@ evidences: description: 佐久間の手元には不足額と冬木担当分の伝票番号、翌朝再確認する旨のメモが残っている。 reveal: mode: conversation - condition: 希か冬木に前夜の帳簿確認と不足伝票について尋ね、経理上の問題を追及したら開示する。 + condition: 希か冬木に前夜の帳簿確認と不足伝票について尋ね、経理上の問題を追及したら開示する。または遺体・現場を調べ、「出荷伝票の不足メモ」に関わる資料を確認したら開示する。 sources: - type: character id: nozomi - type: character id: fuyuki + - type: victim + id: victim supports: [audit-shortage-found, fuyuki-diverted-sales, sakuma-confronted-fuyuki] contradicts: ["lie:fuyuki-no-account-problem"] - id: debt-note diff --git a/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml b/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml index 8b0c923..89922d7 100644 --- a/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml +++ b/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: snowbound-gallery-wrong-crime-scene meta: - title: 私設画廊白環館、雪の夜 + title: 白環館、雪の作品保存庫 synopsis: "午後十時五分、大雪で道路を断たれた私設画廊「白環館」で、館長・荻原直哉が作品保存庫内で死亡しているのが発見されました。" category: 不可能犯罪 difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 荻原直哉 introduction: 私設画廊「白環館」館長 + foundAt: 22:05 + foundIn: 保存庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 荻原直哉は保存庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「修復工程と材料在庫の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -94,31 +102,37 @@ timeline: participants: [kosaka] facts: [forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka] description: 荻原が香坂を額装作業室へ呼び、修復報告と材料在庫の不一致を問いただす。 + location: 額装作業室 - id: ogiwara-death at: "21:10" participants: [kosaka] facts: [kosaka-killed-ogiwara-framing, framing-paper-fibers] description: 香坂が額装作業室で荻原を襲う。 + location: 額装作業室 - id: transfer-to-vault at: "21:18" participants: [kosaka] facts: [kosaka-moved-ogiwara, cart-used-after-cleaning] description: 香坂が作品搬送台車を使い、荻原を保存庫へ移す。 + location: 保存庫 - id: kosaka-exits-vault at: "21:23" participants: [kosaka] facts: [kosaka-left-vault-before-lock, kosaka-apron-dust] description: 香坂が保存庫を離れ、夜間環境管理が始まる前に搬送廊下へ戻る。 + location: 搬送廊下 - id: vault-seals at: "21:30" participants: [tada] facts: [vault-night-mode, framing-room-open-before-2130, vault-closed-2130] description: 保存庫が夜間環境管理へ移行し、発見時の密室状態が成立する。 + location: 保存庫 - id: discovery at: "22:05" participants: [asakura, tada, kosaka] facts: [body-found-2205] description: 朝倉と多田が保存庫内で荻原の死を発見する。 + location: 保存庫 characters: - id: kosaka name: 香坂澪 @@ -317,12 +331,14 @@ evidences: description: 香坂の報告上は使用したことになっている材料が在庫から減っておらず、荻原が確認印を付けている。 reveal: mode: conversation - condition: 香坂に事件前に荻原から指摘された修復報告を尋ねるか、朝倉に材料在庫の不一致を確認したら開示する。 + condition: 香坂に事件前に荻原から指摘された修復報告を尋ねるか、朝倉に材料在庫の不一致を確認したら開示する。または遺体・現場を調べ、「修復工程と材料在庫の不一致」に関わる資料を確認したら開示する。 sources: - type: character id: kosaka - type: character id: asakura + - type: victim + id: victim supports: [forged-restoration-report, ogiwara-found-forgery, ogiwara-called-kosaka] contradicts: ["lie:kosaka-report-clean"] - id: private-loan diff --git a/db/scenarios/snowbound-studio-roomtone-loop.yaml b/db/scenarios/snowbound-studio-roomtone-loop.yaml index 271ab52..7432bad 100644 --- a/db/scenarios/snowbound-studio-roomtone-loop.yaml +++ b/db/scenarios/snowbound-studio-roomtone-loop.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: snowbound-studio-roomtone-loop meta: - title: 録音所ノース・レイク、吹雪の夜 + title: ノース・レイクの深夜録音 synopsis: "午後十一時十八分、山中の録音所「ノース・レイク」の編集室で、音楽監督の冬木圭介が死亡しているのが見つかりました。午後十時半から吹雪で道路が閉鎖され、館内には冬木を含め五人しかいませんでした。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 冬木圭介 introduction: 録音所「ノース・レイク」音楽監督 + foundAt: 23:18 + foundIn: 編集室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 冬木圭介は編集室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「未公開音源の複製履歴」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -101,41 +109,49 @@ timeline: participants: [manabe] facts: [manabe-made-roomtone-loop] description: 真鍋が第2ブースの四十七秒の室内音をループ素材として作成する。 + location: 第2ブース - id: loop-recording-starts at: "22:50" participants: [manabe] facts: [loop-routed-to-recorder, recording-repeats-identically] description: 第2ブースの録音機へループ素材が入力され、連続収録が始まる。 + location: 第2ブース - id: manabe-leaves at: "22:53" participants: [manabe] facts: [manabe-left-booth] description: 真鍋が録音を動かしたまま監視席を離れる。 + location: 監視席 - id: shirase-hearing at: "22:56" participants: [shirase] facts: [shirase-heard-loop] description: 白瀬が廊下から第2ブースの録音中らしい音を聞く。 + location: 第2ブース - id: makimura-sighting at: "22:58" participants: [manabe, makimura] facts: [makimura-saw-manabe] description: 牧村が編集室へ続く機材廊下で真鍋を目撃する。 + location: 機材廊下 - id: fuyuki-death at: "23:02" participants: [manabe] facts: [manabe-killed-fuyuki] description: 真鍋が編集室で冬木を襲い、冬木は死亡する。 + location: 編集室 - id: manabe-return at: "23:08" participants: [manabe] facts: [manabe-returned-booth] description: 真鍋が第2ブースの監視席へ戻る。 + location: 監視席 - id: discovery at: "23:18" participants: [takano, manabe, shirase, makimura] facts: [body-found-2318] description: 鷹野が編集室で冬木の死を発見する。 + location: 編集室 characters: - id: manabe @@ -337,10 +353,11 @@ evidences: description: 真鍋の作業端末から未公開マスターが外部媒体へ複製され、冬木が翌朝のアクセス停止を記したメモを残している。 reveal: mode: conversation - condition: 真鍋か鷹野に冬木が事件前に調べていた音源データの持ち出しについて尋ね、複製履歴を追及したら開示する。 + condition: 真鍋か鷹野に冬木が事件前に調べていた音源データの持ち出しについて尋ね、複製履歴を追及したら開示する。または遺体・現場を調べ、「未公開音源の複製履歴」に関わる資料を確認したら開示する。 sources: - { type: character, id: manabe } - { type: character, id: takano } + - { type: victim, id: victim } supports: [manabe-copied-masters, fuyuki-found-master-leak, fuyuki-planned-report] contradicts: [] - id: shirase-message diff --git a/db/scenarios/snowbound-theater-auto-cues.yaml b/db/scenarios/snowbound-theater-auto-cues.yaml index f74a26c..698e584 100644 --- a/db/scenarios/snowbound-theater-auto-cues.yaml +++ b/db/scenarios/snowbound-theater-auto-cues.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: snowbound-theater-auto-cues meta: - title: 小劇場白燕座、雪封じの夜 + title: 白燕座、雪の終演後 synopsis: "午後十時三十六分、市外れの小劇場「白燕座」の演出控室で、演出家の瀬尾雅人が死亡しているのが見つかりました。午後九時半から大雪で道路が封鎖され、劇場内に残っていたのは瀬尾を含め五人だけです。" category: クローズドサークル difficulty: 4 @@ -10,6 +10,14 @@ meta: victim: name: 瀬尾雅人 introduction: 小劇場「白燕座」演出家 + foundAt: 22:36 + foundIn: 演出控室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 瀬尾雅人は演出控室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「架空スタッフを含む残業費一覧」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -100,41 +108,49 @@ timeline: participants: [kunieda] facts: [cue-console-auto-mode, kunieda-set-auto-cues] description: 国枝が二十分間の照明キューを自動進行へ設定する。 + location: 調光卓 - id: cues-start at: "22:00" participants: [kunieda, toba] facts: [cues-ran-automatically, toba-saw-light-changes] description: 調光卓が自動進行を開始し、舞台照明が設定順に切り替わる。 + location: 調光卓 - id: kunieda-leaves at: "22:03" participants: [kunieda] facts: [kunieda-left-console] description: 国枝が照明キューを動かしたまま調光卓を離れる。 + location: 調光卓 - id: sasai-sighting at: "22:10" participants: [kunieda, sasai] facts: [sasai-saw-kunieda-2210] description: 笹井が演出控室へ続く舞台袖通路で国枝を目撃する。 + location: 演出控室 - id: seo-death at: "22:14" participants: [kunieda] facts: [kunieda-killed-seo] description: 国枝が演出控室で瀬尾を襲い、瀬尾は死亡する。 + location: 演出控室 - id: kunieda-return at: "22:19" participants: [kunieda] facts: [kunieda-returned-console] description: 国枝が調光卓へ戻る。 + location: 調光卓 - id: cues-end at: "22:20" participants: [kunieda, toba] facts: [cues-ran-automatically] description: 自動進行の照明キューが最後まで終了する。 + location: 調光卓 - id: discovery at: "22:36" participants: [hiiragi, kunieda, toba, sasai] facts: [body-found-2236] description: 柊が演出控室で瀬尾の死を発見する。 + location: 演出控室 characters: - id: kunieda @@ -329,10 +345,11 @@ evidences: description: 国枝の請求に実在しないスタッフ名が複数含まれ、瀬尾が翌朝の運営会社報告と進行担当変更を記している。 reveal: mode: conversation - condition: 国枝か柊に瀬尾が事件前に問題視していた残業費請求について尋ね、スタッフ名の実在性を追及したら開示する。 + condition: 国枝か柊に瀬尾が事件前に問題視していた残業費請求について尋ね、スタッフ名の実在性を追及したら開示する。または遺体・現場を調べ、「架空スタッフを含む残業費一覧」に関わる資料を確認したら開示する。 sources: - { type: character, id: kunieda } - { type: character, id: hiiragi } + - { type: victim, id: victim } supports: [kunieda-falsified-overtime, seo-found-false-overtime, seo-planned-report] contradicts: [] - id: script-leak-message diff --git a/db/scenarios/storm-aquarium-feeding-lamp.yaml b/db/scenarios/storm-aquarium-feeding-lamp.yaml index 0c1866b..f3fb85f 100644 --- a/db/scenarios/storm-aquarium-feeding-lamp.yaml +++ b/db/scenarios/storm-aquarium-feeding-lamp.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-aquarium-feeding-lamp meta: - title: 海浜水族館、高潮の夜 + title: 閉館後の海浜水族館 synopsis: "午前零時十二分、海浜水族館の検疫準備室で、飼育部長の江波慎吾が死亡しているのが見つかりました。高潮で正面道路は通行止めとなり、防潮シャッターも閉鎖。午後十一時半以降、館内へ出入りした者はいません。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 江波慎吾 introduction: 海浜水族館飼育部長 + foundAt: 00:12 + foundIn: 検疫準備室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 江波慎吾は検疫準備室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「飼育記録の版差分」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -100,36 +108,43 @@ timeline: participants: [morishita] facts: [feeder-auto-capable, feeding-lamp-motor-linked, morishita-set-auto-feeder] description: 森下が給餌装置を四分間隔の自動運転へ切り替える。 + location: 給餌台 - id: first-cycle at: "23:55" participants: [] facts: [feeder-cycled] description: 自動給餌装置が作動し、展示側から見える青い給餌灯が点く。 + location: 展示側 - id: morishita-leaves at: "23:57" participants: [morishita] facts: [morishita-left-station] description: 森下が給餌操作台を離れ、バックヤードへ向かう。 + location: 給餌台 - id: sagara-sighting at: "00:02" participants: [morishita, sagara] facts: [sagara-saw-morishita] description: 相良が検疫準備室へ続く通路で森下を目撃する。 + location: 検疫準備室 - id: enami-death at: "00:05" participants: [morishita] facts: [morishita-killed-enami] description: 森下が検疫準備室で江波を襲い、江波は死亡する。 + location: 検疫準備室 - id: morishita-return at: "00:09" participants: [morishita] facts: [morishita-returned] description: 森下が深海水槽の給餌操作台へ戻る。 + location: 給餌台 - id: discovery at: "00:12" participants: [mikoshiba, morishita, sakakibara, sagara] facts: [body-found] description: 御子柴が検疫準備室で江波の死を発見する。 + location: 検疫準備室 characters: - id: morishita name: 森下莉央 @@ -305,10 +320,11 @@ evidences: description: 森下の担当水槽だけ過去の記録と当日の記録で数値が不自然に変わり、江波が翌朝の報告予定を書き残している。 reveal: mode: conversation - condition: 森下か榊原に江波が事件直前に調べていた飼育記録について尋ね、改ざんの可能性を追及したら開示する。 + condition: 森下か榊原に江波が事件直前に調べていた飼育記録について尋ね、改ざんの可能性を追及したら開示する。または遺体・現場を調べ、「飼育記録の版差分」に関わる資料を確認したら開示する。 sources: - { type: character, id: morishita } - { type: character, id: sakakibara } + - { type: victim, id: victim } supports: [morishita-falsified-records, enami-found-fraud, enami-planned-report] contradicts: [] - id: stock-edit-history diff --git a/db/scenarios/storm-hydropower-rising-walkway.yaml b/db/scenarios/storm-hydropower-rising-walkway.yaml index ea82abb..fc2a3c1 100644 --- a/db/scenarios/storm-hydropower-rising-walkway.yaml +++ b/db/scenarios/storm-hydropower-rising-walkway.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-hydropower-rising-walkway meta: - title: 山中水力発電所、豪雨の夜 + title: 増水する山中発電所 synopsis: "午後九時十分、豪雨で孤立した山中の水力発電所。その旧制御室で所長・峰岸達也が死亡しているのが発見されました。" category: 不可能犯罪 difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 峰岸達也 introduction: 山中の水力発電所所長 + foundAt: 21:10 + foundIn: 旧制御室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 峰岸達也は旧制御室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「未実施箇所の点検票」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -94,36 +102,43 @@ timeline: participants: [kido] facts: [kido-falsified-inspection, minegishi-found-falsification, minegishi-called-kido] description: 峰岸が城戸を旧制御室へ呼び、未実施点検の記録について説明を求める。 + location: 旧制御室 - id: walkway-still-open at: "20:15" participants: [taniguchi] facts: [walkway-passable-2015, old-walkway-connects] description: 水位記録上、古い保守歩廊はまだ通行できる状態にある。 + location: 保守歩廊 - id: kido-enters at: "20:18" participants: [kido] facts: [kido-used-walkway] description: 城戸が正面扉を使わず、古い保守歩廊から旧制御室へ入る。 + location: 旧制御室 - id: minegishi-death at: "20:24" participants: [kido] facts: [kido-killed-minegishi] description: 城戸が旧制御室で峰岸を襲う。 + location: 旧制御室 - id: kido-leaves at: "20:29" participants: [kido] facts: [kido-left-before-rise, kido-boot-silt] description: 城戸が増水前に古い保守歩廊から戻る。 + location: 保守歩廊 - id: water-cuts-route at: "20:40" participants: [taniguchi] facts: [walkway-closed-2040, water-log-recorded-rise] description: 増水により古い保守歩廊が通行不能となり、発見時の「密室」が完成する。 + location: 保守歩廊 - id: discovery at: "21:10" participants: [saionji, kido, taniguchi] facts: [front-door-remained-locked, body-found-2110] description: 西園寺が旧制御室の正面扉を開けさせ、峰岸の死を発見する。 + location: 旧制御室 characters: - id: kido name: 城戸真琴 @@ -311,12 +326,14 @@ evidences: description: 実施済みの印がある項目に対応する現場記録がなく、峰岸が城戸の欄へ確認印を付けている。 reveal: mode: conversation - condition: 城戸か西園寺に事件当日の監査対象となった点検票について尋ねたら開示する。 + condition: 城戸か西園寺に事件当日の監査対象となった点検票について尋ねたら開示する。または遺体・現場を調べ、「未実施箇所の点検票」に関わる資料を確認したら開示する。 sources: - type: character id: kido - type: character id: saionji + - type: victim + id: victim supports: [kido-falsified-inspection, minegishi-found-falsification, minegishi-called-kido] contradicts: ["lie:kido-inspection-complete"] - id: leaked-draft diff --git a/db/scenarios/storm-island-scheduled-mail.yaml b/db/scenarios/storm-island-scheduled-mail.yaml index 2fc0a69..478d6f4 100644 --- a/db/scenarios/storm-island-scheduled-mail.yaml +++ b/db/scenarios/storm-island-scheduled-mail.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-island-scheduled-mail meta: - title: 研修島青凪荘、暴風の夜 + title: 船の来ない青凪荘 synopsis: "午後十時二分、沖合の研修施設「青凪荘」の執務室で、奨学財団理事の榊原宗一が死亡しているのが見つかりました。午後八時から暴風で船便が全て欠航し、島に残ったのは榊原を含め五人だけです。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 榊原宗一 introduction: 奨学財団理事 + foundAt: 22:02 + foundIn: 執務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 榊原宗一は執務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「相沢の小口支出重複一覧」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -102,41 +110,49 @@ timeline: participants: [aizawa] facts: [mail-drafted-1811] description: 榊原が翌朝の資料確認を求めるメール本文を作成する。相沢は近くで予定を確認している。 + location: 執務室 - id: mail-scheduled at: "18:12" participants: [aizawa] facts: [mail-scheduled-2142, aizawa-knew-scheduled-mail] description: 榊原がメールを21時42分の予定送信へ設定し、相沢もその設定を知る。 + location: 執務室 - id: aizawa-enters-office at: "21:24" participants: [aizawa] facts: [aizawa-entered-office-2124] description: 相沢が榊原の執務室へ入る。 + location: 執務室 - id: sakakibara-death at: "21:28" participants: [aizawa] facts: [aizawa-killed-sakakibara] description: 相沢が執務室で榊原を襲い、榊原は死亡する。 + location: 執務室 - id: horie-sighting at: "21:29" participants: [aizawa, horie] facts: [horie-saw-aizawa-2129] description: 堀江が執務室前の廊下から出てくる相沢を目撃する。 + location: 執務室 - id: aizawa-joins-hatori at: "21:35" participants: [aizawa, hatori] facts: [aizawa-joined-hatori-2135, aizawa-with-hatori-until-2155] description: 相沢が講義室で羽鳥と翌朝の研修準備を始める。 + location: 講義室 - id: scheduled-mail-sends at: "21:42" participants: [] facts: [mail-sent-automatically] description: 榊原が夕方に設定したメールが、端末への操作なしで自動送信される。 + location: 執務室 - id: discovery at: "22:02" participants: [mikage, aizawa, hatori, horie] facts: [body-found-2202] description: 御影が執務室で榊原の死を発見する。 + location: 執務室 characters: - id: aizawa @@ -342,10 +358,11 @@ evidences: description: 相沢が処理した複数の小口支出に不自然な重複があり、榊原が翌朝の監査提出と担当変更を記している。 reveal: mode: conversation - condition: 相沢か御影に榊原が夕方確認していた支出一覧を尋ね、重複と翌朝の監査予定を追及したら開示する。 + condition: 相沢か御影に榊原が夕方確認していた支出一覧を尋ね、重複と翌朝の監査予定を追及したら開示する。または遺体・現場を調べ、「相沢の小口支出重複一覧」に関わる資料を確認したら開示する。 sources: - { type: character, id: aizawa } - { type: character, id: mikage } + - { type: victim, id: victim } supports: [aizawa-diverted-funds, sakakibara-found-diversion, sakakibara-planned-audit] contradicts: [] - id: mikage-budget-sheet diff --git a/db/scenarios/storm-lighthouse-relief-slate.yaml b/db/scenarios/storm-lighthouse-relief-slate.yaml index f774b10..7c50460 100644 --- a/db/scenarios/storm-lighthouse-relief-slate.yaml +++ b/db/scenarios/storm-lighthouse-relief-slate.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-lighthouse-relief-slate meta: - title: 夕凪灯台、暴風の夜 + title: 補給船の来ない夕凪灯台 synopsis: "午後八時五十分、暴風で補給船の接岸が不可能となった夕凪灯台の整備室で、主任灯台守の倉橋徹が死亡しているのが見つかりました。島にいるのは当直員の鳥越玲、通信担当の橋場圭、補助員の森下透の三人だけです。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 倉橋徹 introduction: 夕凪灯台主任灯台守 + foundAt: 20:50 + foundIn: 西外扉 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 倉橋徹は西外扉で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「補給報告の在庫差メモ」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -91,31 +99,37 @@ timeline: participants: [torigoe, morishita] facts: [west-gate-sealed] description: 暴風警戒により西側外扉が封鎖される。 + location: 西外扉 - id: east-round at: "19:25" participants: [torigoe] facts: [torigoe-coat-wet-before] description: 鳥越が東側巡回を行い、外套が雨で濡れる。 + location: 東側 - id: panel-reading at: "20:12" participants: [] facts: [slate-entry-2012, slate-written-after-round, torigoe-copied-panel-reading] description: 西設備の自動計測値が制御盤に記録され、後に鳥越がその時刻を当直板へ転記する。 + location: 制御盤 - id: stair-sighting at: "20:21" participants: [torigoe, hashiba] facts: [hashiba-saw-torigoe-2021] description: 橋場が整備室近くの内階段で鳥越とすれ違う。 + location: 内階段 - id: kurahashi-death at: "20:24" participants: [torigoe] facts: [kurahashi-death-2024, torigoe-killed-kurahashi] description: 鳥越が整備室で倉橋を襲い、倉橋は死亡する。 + location: 灯台内 - id: discovery at: "20:50" participants: [morishita, torigoe, hashiba] facts: [body-found-2050, west-seal-intact] description: 森下が倉橋の死を発見し、西側外扉の封鎖も保たれていることが確認される。 + location: 西外扉 characters: - id: torigoe name: 鳥越玲 @@ -303,12 +317,14 @@ evidences: description: 倉橋の翌日提出予定資料に、鳥越の担当分だけ燃料在庫の差を本部へ報告する注記がある。 reveal: mode: conversation - condition: 鳥越か森下に翌日の補給報告と燃料在庫の差について尋ねたら開示する。 + condition: 鳥越か森下に翌日の補給報告と燃料在庫の差について尋ねたら開示する。または遺体・現場を調べ、「補給報告の在庫差メモ」に関わる資料を確認したら開示する。 sources: - type: character id: torigoe - type: character id: morishita + - type: victim + id: victim supports: [torigoe-hid-fuel-shortage, fuel-report-next-day] contradicts: [] - id: private-radio-log diff --git a/db/scenarios/storm-mountain-inn-echoed-cane.yaml b/db/scenarios/storm-mountain-inn-echoed-cane.yaml index bcd62a2..a70abbb 100644 --- a/db/scenarios/storm-mountain-inn-echoed-cane.yaml +++ b/db/scenarios/storm-mountain-inn-echoed-cane.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-mountain-inn-echoed-cane meta: - title: 山宿梢庵、暴風雨の夜 + title: 梢庵から出られない synopsis: "午後九時五十分、暴風雨で孤立した山宿「梢庵」で、主人・桐谷宗介が帳場奥の事務室で死亡しているのが見つかりました。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 桐谷宗介 introduction: 山宿「梢庵」主人 + foundAt: 21:50 + foundIn: 帳場奥 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 桐谷宗介は帳場奥で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「不自然な仕入れ伝票」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -94,26 +102,31 @@ timeline: participants: [akiwa] facts: [supplier-kickback, kiritani-found-kickback, kiritani-confronted-akiwa] description: 桐谷が秋庭に不自然な仕入れを問い、翌朝に契約を見直すと告げる。 + location: 山宿内 - id: kiritani-death at: "21:08" participants: [akiwa] facts: [akiwa-killed-kiritani, akiwa-took-cane] description: 秋庭が帳場奥で桐谷を襲い、愛用の杖を持ち出す。 + location: 帳場奥 - id: tapping-staged at: "21:25" participants: [akiwa, morisaki] facts: [akiwa-made-tapping, morisaki-heard-taps, morisaki-heard-no-steps] description: 秋庭が旧階段で杖の音を作り、森崎は二階廊下を桐谷が歩いていると思い込む。 + location: 旧階段 - id: cane-hidden at: "21:31" participants: [akiwa] facts: [cane-found-kitchen, rail-fresh-marks] description: 秋庭が杖を厨房脇の物入れへ戻し、旧階段には新しい打痕が残る。 + location: 厨房脇 - id: discovery at: "21:50" participants: [sakakibara, akiwa, morisaki] facts: [body-found-2150] description: 榊原が帳場奥で桐谷の死を発見する。 + location: 帳場奥 characters: - id: akiwa name: 秋庭澄 @@ -312,12 +325,14 @@ evidences: description: 同じ業者から相場より高い価格で仕入れた記録と、秋庭が処理した伝票がまとまっている。 reveal: mode: conversation - condition: 秋庭に仕入れ業者との関係を尋ねるか、桐谷が事件前に確認していた伝票について追及したら開示する。 + condition: 秋庭に仕入れ業者との関係を尋ねるか、桐谷が事件前に確認していた伝票について追及したら開示する。または遺体・現場を調べ、「不自然な仕入れ伝票」に関わる資料を確認したら開示する。 sources: - type: character id: akiwa - type: character id: morisaki + - type: victim + id: victim supports: [supplier-kickback, kiritani-found-kickback, kiritani-confronted-akiwa] contradicts: ["lie:akiwa-clean-supplies"] - id: unpaid-bill diff --git a/db/scenarios/storm-planetarium-reflected-witness.yaml b/db/scenarios/storm-planetarium-reflected-witness.yaml index a62f916..60a8588 100644 --- a/db/scenarios/storm-planetarium-reflected-witness.yaml +++ b/db/scenarios/storm-planetarium-reflected-witness.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-planetarium-reflected-witness meta: - title: 星見ヶ丘天象館、山頂の夜 + title: 星見ヶ丘、ロープウェイ停止 synopsis: "午後十時、暴風で山道とロープウェイが停止した星見ヶ丘天象館で、館長の犬塚誠が投影準備室内で死亡しているのが見つかりました。館内に残っていたのは展示担当の小野寺莉香、投影技師の松田圭介、売店責任者の朝倉葉月の三人だけです。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 犬塚誠 introduction: 星見ヶ丘天象館館長 + foundAt: 22:00 + foundIn: 投影準備室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 犬塚誠は投影準備室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「運営法人への予算報告草案」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -95,21 +103,25 @@ timeline: participants: [onodera] facts: [inuzuka-death-2124, onodera-killed-inuzuka] description: 小野寺が投影準備室で犬塚を襲い、犬塚は死亡する。 + location: 投影準備室 - id: booth-lights-down at: "21:30" participants: [matsuda] facts: [booth-dark-2130, glass-reflects-dark-booth] description: 機器点検のため投影室内の主照明が落とされ、観察ガラスが廊下側を反射しやすい状態になる。 + location: 投影室 - id: reflected-sighting at: "21:35" participants: [onodera, matsuda] facts: [onodera-behind-matsuda-2135, matsuda-saw-white-figure, figure-was-reflection, onodera-endorsed-sighting] description: 松田が観察ガラスに映った白い人物像を犬塚だと思い込み、小野寺がその誤認を訂正せず生存証言として補強する。 + location: 天象館内 - id: discovery at: "22:00" participants: [asakura, onodera, matsuda] facts: [body-found-2200] description: 朝倉が投影準備室で犬塚の死を発見する。 + location: 投影準備室 characters: - id: onodera name: 小野寺莉香 @@ -287,12 +299,14 @@ evidences: description: 犬塚の端末に、小野寺による正式承認のない予算振替を翌朝報告する草案が残っている。 reveal: mode: conversation - condition: 小野寺か朝倉に犬塚が翌朝運営法人へ提出予定だった報告について尋ねたら開示する。 + condition: 小野寺か朝倉に犬塚が翌朝運営法人へ提出予定だった報告について尋ねたら開示する。または遺体・現場を調べ、「運営法人への予算報告草案」に関わる資料を確認したら開示する。 sources: - type: character id: onodera - type: character id: asakura + - type: victim + id: victim supports: [exhibit-budget-found, budget-report-next-day] contradicts: [] - id: skipped-projection-test diff --git a/db/scenarios/storm-research-vessel-missing-launch.yaml b/db/scenarios/storm-research-vessel-missing-launch.yaml index b0d947e..dbfece0 100644 --- a/db/scenarios/storm-research-vessel-missing-launch.yaml +++ b/db/scenarios/storm-research-vessel-missing-launch.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-research-vessel-missing-launch meta: - title: 調査船みなも、暴風海域の夜 + title: 退避航行中の「みなも」 synopsis: "午後八時四十分、暴風海域で退避航行中の海洋調査船みなもで、主任研究者の瀬尾俊が解析室内で死亡しているのが見つかりました。荒天のため他船との接触はなく、船内にいた関係者は研究員の狩谷琴音、機関担当の水城明、通信担当の野々村岳の三人です。" category: クローズドサークル difficulty: 4 @@ -10,6 +10,14 @@ meta: victim: name: 瀬尾俊 introduction: 海洋調査船みなも主任研究者 + foundAt: 20:40 + foundIn: 解析室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 瀬尾俊は解析室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「共同研究データの提出履歴」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -91,36 +99,43 @@ timeline: participants: [kariya, mizuki] facts: [launch-lost-1942, kariya-saw-launch-loss] description: 暴風による甲板設備の異常が起き、小型作業艇が既に失われたことを狩谷と水城が確認する。 + location: 甲板 - id: launch-note at: "19:48" participants: [kariya, mizuki] facts: [launch-note-signed] description: 作業艇消失を含む甲板異常メモに狩谷と水城が署名する。 + location: 甲板 - id: seo-call at: "19:56" participants: [nonomura] facts: [seo-alive-1956, launch-gone-before-death] description: 野々村が瀬尾と船内通話を行い、作業艇消失後にも瀬尾が生存していたことが確認される。 + location: 船内 - id: kariya-analysis at: "20:14" participants: [kariya] facts: [kariya-entered-analysis-2014] description: 狩谷が解析室へ向かう。 + location: 解析室 - id: seo-death at: "20:18" participants: [kariya] facts: [seo-death-2018, kariya-killed-seo] description: 狩谷が解析室で瀬尾を襲い、瀬尾は死亡する。 + location: 解析室 - id: corridor-sighting at: "20:22" participants: [kariya, mizuki] facts: [mizuki-saw-kariya-2022] description: 水城が解析室側の通路から戻る狩谷を目撃する。 + location: 解析室 - id: discovery at: "20:40" participants: [nonomura, kariya, mizuki] facts: [body-found-2040] description: 野々村が解析室で瀬尾の死を発見する。 + location: 解析室 characters: - id: kariya name: 狩谷琴音 @@ -296,12 +311,14 @@ evidences: description: 狩谷が共同研究データを単独成果として外部提出しようとし、瀬尾がその経緯を問題視していたことが分かる。 reveal: mode: conversation - condition: 狩谷か野々村に瀬尾が直前まで確認していた研究データの提出経緯を尋ねたら開示する。 + condition: 狩谷か野々村に瀬尾が直前まで確認していた研究データの提出経緯を尋ねたら開示する。または遺体・現場を調べ、「共同研究データの提出履歴」に関わる資料を確認したら開示する。 sources: - type: character id: kariya - type: character id: nonomura + - type: victim + id: victim supports: [seo-found-copying, seo-report-next-port] contradicts: [] - id: delayed-maintenance diff --git a/db/scenarios/storm-seminar-echoed-testimony.yaml b/db/scenarios/storm-seminar-echoed-testimony.yaml index 9417013..c0dd9d4 100644 --- a/db/scenarios/storm-seminar-echoed-testimony.yaml +++ b/db/scenarios/storm-seminar-echoed-testimony.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: storm-seminar-echoed-testimony meta: - title: 山中研究会館、嵐の夜 + title: 道路封鎖、山中研究会館 synopsis: "午後九時四十五分、嵐で道路が閉鎖された山中の研究会館で、主催者・鷺沢修が西側の事務室で死亡しているのが見つかりました。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 鷺沢修 introduction: 山中の研究会館の研究会主催者 + foundAt: 21:45 + foundIn: 西側事務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 鷺沢修は西側事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「単独名義の投稿原稿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -94,36 +102,43 @@ timeline: participants: [kuga] facts: [kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga] description: 鷺沢が久我の無断投稿準備を知り、取り下げと共同研究からの除外を告げる。 + location: 会館内 - id: sagisawa-death at: "21:10" participants: [kuga] facts: [kuga-killed-sagisawa] description: 久我が西側事務室で鷺沢を襲う。 + location: 西側事務室 - id: west-corridor-sighting at: "21:16" participants: [kuga, shiraishi] facts: [shiraishi-saw-kuga-west] description: 白石が西側事務室へ続く廊下から出てくる久我を見かける。 + location: 西側事務室 - id: rumor-starts at: "21:18" participants: [kuga, naruse] facts: [kuga-claimed-saw-east, kuga-started-rumor, no-independent-east-sighting] description: 久我が成瀬に「鷺沢は東資料室へ行った」と伝え、存在しない目撃情報の起点を作る。 + location: 東資料室 - id: rumor-repeated at: "21:22" participants: [naruse, shiraishi] facts: [naruse-learned-from-kuga, naruse-repeated-rumor, shiraishi-learned-from-naruse] description: 成瀬が白石へ同じ話を伝え、一つの情報が複数人の証言へ増殖する。 + location: 会館内 - id: east-room-still-unused at: "21:40" participants: [shiraishi] facts: [east-room-unused] description: 東資料室は片付け時のままで、鷺沢が資料を使った痕跡がない。 + location: 東資料室 - id: discovery at: "21:45" participants: [naruse, kuga, shiraishi] facts: [body-found-2145] description: 成瀬が西側事務室で鷺沢の死を発見する。 + location: 西側事務室 characters: - id: kuga name: 久我真帆 @@ -328,12 +343,14 @@ evidences: description: 久我の端末には共同研究の未発表結果を自分単独の成果として投稿する準備が残り、鷺沢からの取り下げ要求も確認できる。 reveal: mode: conversation - condition: 久我か成瀬に事件直前の投稿原稿と鷺沢との対立について尋ねたら開示する。 + condition: 久我か成瀬に事件直前の投稿原稿と鷺沢との対立について尋ねたら開示する。または遺体・現場を調べ、「単独名義の投稿原稿」に関わる資料を確認したら開示する。 sources: - type: character id: kuga - type: character id: naruse + - type: victim + id: victim supports: [kuga-stole-results, sagisawa-found-submission, sagisawa-confronted-kuga] contradicts: ["lie:kuga-no-stolen-results"] - id: embargo-leak diff --git a/db/scenarios/thunder-cablecar-occupied-cabin.yaml b/db/scenarios/thunder-cablecar-occupied-cabin.yaml index 267ffc1..0122c64 100644 --- a/db/scenarios/thunder-cablecar-occupied-cabin.yaml +++ b/db/scenarios/thunder-cablecar-occupied-cabin.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: thunder-cablecar-occupied-cabin meta: - title: 霧岳山頂駅、落雷の夜 + title: 落雷停止、霧岳山頂駅 synopsis: "午後九時三十八分、霧岳ロープウェイ山頂駅の運行事務室で、駅長の高瀬修司が死亡しているのが見つかりました。落雷で本線索道は停止し、山道も土砂流入で通行できません。午後八時五十分以降、山頂駅へ出入りした者はいません。" category: クローズドサークル difficulty: 4 @@ -10,6 +10,14 @@ meta: victim: name: 高瀬修司 introduction: 霧岳ロープウェイ山頂駅駅長 + foundAt: 21:38 + foundIn: 運行事務室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 高瀬修司は運行事務室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「制動点検記録と作業履歴の不一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -100,41 +108,49 @@ timeline: participants: [sudo] facts: [cabin-display-weight-based, tool-case-heavy-enough, sudo-left-tool-case] description: 須藤が保守用工具ケースを非常用搬器の点検席へ置く。 + location: 非常搬器 - id: occupancy-start at: "21:10" participants: [] facts: [occupancy-display-on] description: 工具ケースの荷重で監視盤に『乗員1』が表示される。 + location: 山頂駅 - id: sudo-leaves-cabin at: "21:12" participants: [sudo] facts: [sudo-left-cabin-area] description: 須藤が非常用搬器前を離れる。 + location: 非常搬器 - id: enomoto-sighting at: "21:18" participants: [sudo, enomoto] facts: [enomoto-saw-sudo-2118] description: 榎本が職員通路で須藤を目撃する。 + location: 職員通路 - id: takase-death at: "21:22" participants: [sudo] facts: [sudo-killed-takase] description: 須藤が運行事務室で高瀬を襲い、高瀬は死亡する。 + location: 運行事務室 - id: sudo-return at: "21:28" participants: [sudo] facts: [sudo-returned-cabin] description: 須藤が非常用搬器前へ戻る。 + location: 非常搬器 - id: occupancy-end at: "21:30" participants: [sudo] facts: [occupancy-display-on] description: 須藤が工具ケースを点検席から下ろし、『乗員1』表示が消える。 + location: 非常搬器 - id: discovery at: "21:38" participants: [nagamine, sudo, enomoto, orihara] facts: [body-found-2138] description: 長峰が運行事務室で高瀬の死を発見する。 + location: 運行事務室 characters: - id: sudo @@ -332,10 +348,11 @@ evidences: description: 須藤が実施済みと記した点検の一部に作業履歴がなく、高瀬が翌朝の運転停止と安全管理部への報告を記している。 reveal: mode: conversation - condition: 須藤か長峰に高瀬が事件直前に確認していた制動点検について尋ね、記録と実作業の不一致を追及したら開示する。 + condition: 須藤か長峰に高瀬が事件直前に確認していた制動点検について尋ね、記録と実作業の不一致を追及したら開示する。または遺体・現場を調べ、「制動点検記録と作業履歴の不一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: sudo } - { type: character, id: nagamine } + - { type: victim, id: victim } supports: [sudo-falsified-brake-tests, takase-found-false-tests, takase-planned-suspension] contradicts: [] - id: lost-wallet diff --git a/db/scenarios/tsukimisou.yaml b/db/scenarios/tsukimisou.yaml index 319cf03..c9eae7b 100644 --- a/db/scenarios/tsukimisou.yaml +++ b/db/scenarios/tsukimisou.yaml @@ -15,7 +15,7 @@ schemaVersion: 1 id: tsukimisou-17th-anniversary meta: - title: 月見荘、十七回忌の夜 + title: 十七回忌、月見荘にて synopsis: "十月十四日、午後七時。老舗旅館「月見荘」の離れに、四人の男女が集まりました。女将の高瀬涼子が、十七年前に亡くなった夫の法要を兼ねて開いた、ごく内輪の夕食会です。招かれたのは、涼子と長く関わってきた三人でした。" category: 館もの difficulty: 2 diff --git a/db/scenarios/typhoon-datacenter-maintenance-window.yaml b/db/scenarios/typhoon-datacenter-maintenance-window.yaml index aec86e7..2515040 100644 --- a/db/scenarios/typhoon-datacenter-maintenance-window.yaml +++ b/db/scenarios/typhoon-datacenter-maintenance-window.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: typhoon-datacenter-maintenance-window meta: - title: 湾岸データセンター、台風の夜 + title: 冠水する湾岸データセンター synopsis: "午後十時十五分、湾岸データセンターの予備部品庫で、運用責任者の真田啓介が死亡しているのが見つかりました。台風による冠水で周辺道路は封鎖され、午後九時半以降に建物へ出入りした者はいません。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 真田啓介 introduction: 湾岸データセンター運用責任者 + foundAt: 22:15 + foundIn: 予備部品庫 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 真田啓介は予備部品庫で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「交換部品の請求書と在庫表」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -97,41 +105,49 @@ timeline: participants: [kuze] facts: [kuze-created-scheduled-job] description: 久世が三分間隔で復旧処理を実行する予約ジョブを監視端末に登録する。 + location: 監視室 - id: door-propped at: "21:55" participants: [hatano] facts: [parts-door-propped] description: 搬入のため予備部品庫の扉が半開きに固定される。 + location: 予備部品庫 - id: kuze-leaves at: "21:58" participants: [kuze] facts: [kuze-left-noc] description: 久世が予約ジョブを残して監視室を離れる。 + location: 監視室 - id: scheduled-execution at: "22:00" participants: [] facts: [scheduled-jobs-ran] description: 監視端末で予約された復旧処理が自動実行され始める。 + location: 監視室 - id: corridor-sighting at: "22:01" participants: [kuze, makabe] facts: [makabe-saw-kuze] description: 真壁が東側廊下で久世を目撃する。 + location: 東側廊下 - id: sanada-death at: "22:04" participants: [kuze] facts: [kuze-killed-sanada] description: 久世が予備部品庫で真田を襲い、真田は死亡する。 + location: 予備部品庫 - id: kuze-return at: "22:08" participants: [kuze] facts: [kuze-returned-noc] description: 久世が監視室へ戻る。 + location: 監視室 - id: discovery at: "22:15" participants: [hatano, kuze, koda, makabe] facts: [body-found] description: 波多野が予備部品庫で真田の死を発見する。 + location: 予備部品庫 characters: - id: kuze name: 久世秋穂 @@ -316,10 +332,11 @@ evidences: description: 請求上は交換済みの機器が在庫に残り、久世の処理した保守費だけ数量が一致しない。真田の監査メモもある。 reveal: mode: conversation - condition: 久世か波多野に真田が事件直前に照合していた保守費と在庫について尋ねたら開示する。 + condition: 久世か波多野に真田が事件直前に照合していた保守費と在庫について尋ねたら開示する。または遺体・現場を調べ、「交換部品の請求書と在庫表」に関わる資料を確認したら開示する。 sources: - { type: character, id: kuze } - { type: character, id: hatano } + - { type: victim, id: victim } supports: [kuze-invoice-fraud, sanada-found-fraud, sanada-planned-report] contradicts: [] - id: koda-copy-history diff --git a/db/scenarios/typhoon-offshore-drone-route.yaml b/db/scenarios/typhoon-offshore-drone-route.yaml index e4cd0da..ceb2447 100644 --- a/db/scenarios/typhoon-offshore-drone-route.yaml +++ b/db/scenarios/typhoon-offshore-drone-route.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: typhoon-offshore-drone-route meta: - title: 洋上風力基地、台風圏の夜 + title: 台風圏の洋上風力基地 synopsis: "午前一時二十九分、沖合風力発電基地の会議室で、安全責任者の芳賀俊介が死亡しているのが見つかりました。台風接近で保守船は午後十一時に退避し、ヘリポートも閉鎖。基地には芳賀を含め五人だけが残っています。" category: クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: 芳賀俊介 introduction: 沖合風力発電基地安全責任者 + foundAt: 01:29 + foundIn: 会議室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: 芳賀俊介は会議室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「過去点検写真との一致」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -100,41 +108,49 @@ timeline: participants: [hiiragi] facts: [drone-repeat-route-capable, hiiragi-loaded-auto-route] description: 柊木が十八分間の反復点検ルートをドローンへ読み込ませる。 + location: 屋上 - id: drone-launch at: "01:00" participants: [hiiragi] facts: [drone-flew-auto, route-has-no-manual-input] description: 点検ドローンが自律飛行を開始し、指定地点の自動撮影を続ける。 + location: 屋上 - id: hiiragi-leaves-roof at: "01:02" participants: [hiiragi] facts: [hiiragi-left-roof] description: 柊木がドローンを飛ばしたまま屋上の操縦席を離れる。 + location: 屋上 - id: saegusa-sighting at: "01:08" participants: [hiiragi, saegusa] facts: [saegusa-saw-hiiragi-0108] description: 三枝が会議室へ続く連絡階段で柊木を目撃する。 + location: 連絡階段 - id: haga-death at: "01:12" participants: [hiiragi] facts: [hiiragi-killed-haga] description: 柊木が会議室で芳賀を襲い、芳賀は死亡する。 + location: 会議室 - id: hiiragi-return at: "01:17" participants: [hiiragi] facts: [hiiragi-returned-roof] description: 柊木が屋上の操縦席へ戻る。 + location: 屋上 - id: drone-lands at: "01:18" participants: [hiiragi] facts: [drone-flew-auto] description: 自律飛行を終えた点検ドローンが着陸する。 + location: 屋上 - id: discovery at: "01:29" participants: [kokubu, hiiragi, saegusa, nami] facts: [body-found-0129] description: 国分が会議室で芳賀の死を発見する。 + location: 会議室 characters: - id: hiiragi @@ -331,10 +347,11 @@ evidences: description: 柊木が実施済みとした複数の点検写真が過去の画像と一致し、芳賀が翌朝の安全部報告と資格停止を記している。 reveal: mode: conversation - condition: 柊木か三枝に芳賀が事件直前に確認していた点検写真について尋ね、過去画像との一致を追及したら開示する。 + condition: 柊木か三枝に芳賀が事件直前に確認していた点検写真について尋ね、過去画像との一致を追及したら開示する。または遺体・現場を調べ、「過去点検写真との一致」に関わる資料を確認したら開示する。 sources: - { type: character, id: hiiragi } - { type: character, id: saegusa } + - { type: victim, id: victim } supports: [hiiragi-falsified-inspections, haga-found-false-inspections, haga-planned-report] contradicts: [] - id: protection-reset-log diff --git a/db/scenarios/venetian-lazaretto-sunset-bell.yaml b/db/scenarios/venetian-lazaretto-sunset-bell.yaml index b78bcf5..07b298d 100644 --- a/db/scenarios/venetian-lazaretto-sunset-bell.yaml +++ b/db/scenarios/venetian-lazaretto-sunset-bell.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: venetian-lazaretto-sunset-bell meta: - title: 1796年ヴェネツィア、検疫島の夜 + title: 1796年、検疫島ラッザレット synopsis: "1796年、ヴェネツィア潟。感染症を警戒する検疫島ラッザレットで、監督医ロレンツォ・ヴァーレが記録室で死亡しているのが見つかりました。" category: 歴史クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: ロレンツォ・ヴァーレ introduction: 検疫島ラッザレット監督医 + foundAt: 19:10 + foundIn: 記録室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: ロレンツォ・ヴァーレは記録室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「不足した薬剤の帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -81,31 +89,37 @@ timeline: participants: [marta] facts: [marta-diverted-medicine, lorenzo-found-shortage, lorenzo-confronted-marta] description: ロレンツォが薬剤の不足をマルタへ突きつけ、翌朝の報告を告げる。 + location: 検疫島 - id: corridor-sighting at: "18:41" participants: [marta, nicolo] facts: [nicolo-saw-marta-before-bell] description: ニコロが記録室へ向かう廊下でマルタを見かける。 + location: 記録室 - id: lorenzo-death at: "18:44" participants: [marta] facts: [marta-killed-lorenzo] description: マルタが記録室でロレンツォを襲う。 + location: 記録室 - id: sunset-bell at: "18:47" participants: [pietro] facts: [bell-rang-1847, sunset-bell-not-fixed-hour] description: 当直のピエトロが夕暮れを確認し、日没の鐘を鳴らす。 + location: 鐘楼 - id: marta-returns at: "18:49" participants: [marta] facts: [marta-returned-after-bell] description: マルタが薬剤庫へ戻り、鐘の後はずっとそこにいたように振る舞う。 + location: 薬剤庫 - id: discovery at: "19:10" participants: [marta, nicolo, pietro] facts: [body-found-1910] description: ニコロが記録室でロレンツォの死を発見する。 + location: 記録室 characters: - id: marta name: マルタ・ベッリーニ @@ -257,10 +271,11 @@ evidences: description: 高価な薬剤の実数と帳簿が合わず、ロレンツォが翌朝の報告を示す書き込みを残している。 reveal: mode: conversation - condition: マルタかニコロに薬剤の不足とロレンツォの直前の調査を尋ねたら開示する。 + condition: マルタかニコロに薬剤の不足とロレンツォの直前の調査を尋ねたら開示する。または遺体・現場を調べ、「不足した薬剤の帳簿」に関わる資料を確認したら開示する。 sources: - { type: character, id: marta } - { type: character, id: nicolo } + - { type: victim, id: victim } supports: [marta-diverted-medicine, lorenzo-found-shortage, lorenzo-confronted-marta] contradicts: ["lie:marta-no-shortage"] - id: private-letter diff --git a/db/scenarios/victorian-underground-last-telegram.yaml b/db/scenarios/victorian-underground-last-telegram.yaml index cd0ef20..7f5d1f4 100644 --- a/db/scenarios/victorian-underground-last-telegram.yaml +++ b/db/scenarios/victorian-underground-last-telegram.yaml @@ -1,7 +1,7 @@ schemaVersion: 1 id: victorian-underground-last-telegram meta: - title: 1863年ロンドン、地下工事区画の夜 + title: 1863年、地下鉄工事区画 synopsis: "1863年、ロンドン。世界初の地下鉄が走り始めたその年、延伸工事の夜間区画で主任技師エドワード・ヘイルが死亡しているのが見つかりました。" category: 歴史クローズドサークル difficulty: 5 @@ -10,6 +10,14 @@ meta: victim: name: エドワード・ヘイル introduction: 地下鉄延伸工事主任技師 + foundAt: 22:30 + foundIn: 測量室 + causeOfDeath: 事件性のある外傷による死亡 + findings: + - id: victim-state + statement: エドワード・ヘイルは測量室で倒れており、その場で死亡が確認されている。 + - id: victim-motive-material + statement: 遺体のそばには「水増しされた資材帳簿」に関わる資料が残されている。 briefing: |- ——事件の記録を読み上げます。 @@ -88,31 +96,37 @@ timeline: participants: [bell] facts: [bell-bid-fraud, helale-confronted-bell] description: ヘイルがベルの資材費水増しを指摘し、翌朝に帳簿を提出すると告げる。 + location: 事務室 - id: bell-enters-tunnel at: "21:52" participants: [bell] facts: [bell-left-shaft-one] description: ベルが第一立坑を離れ、第二立坑へ続く連絡坑道へ入る。 + location: 連絡坑道 - id: thomas-sees-bell at: "21:58" participants: [bell, thomas] facts: [thomas-saw-bell-tunnel] description: トーマスが第二立坑寄りの坑道でベルを見かける。 + location: 第2立坑 - id: hale-death at: "22:03" participants: [bell] facts: [bell-killed-hale] description: ベルが第二立坑の測量室でヘイルを襲う。 + location: 測量室 - id: false-telegram at: "22:12" participants: [bell, clara] facts: [bell-sent-telegram, telegram-received-2212] description: ベルが第二立坑からヘイルの符号を添えた電信を送り、クララが第一立坑で受信する。 + location: 第2立坑 - id: discovery at: "22:30" participants: [bell, clara, thomas] facts: [body-found-2230] description: トーマスが測量室でヘイルの死を発見する。 + location: 測量室 characters: - id: bell name: アーサー・ベル @@ -274,10 +288,11 @@ evidences: description: ベルの管理分だけ資材費が不自然に増えており、ヘイルが翌朝会社へ提出する印を付けている。 reveal: mode: conversation - condition: ベルにヘイルと直前に揉めた帳簿について追及するか、クララに二人の口論について尋ねたら開示する。 + condition: ベルにヘイルと直前に揉めた帳簿について追及するか、クララに二人の口論について尋ねたら開示する。または遺体・現場を調べ、「水増しされた資材帳簿」に関わる資料を確認したら開示する。 sources: - { type: character, id: bell } - { type: character, id: clara } + - { type: victim, id: victim } supports: [bell-bid-fraud, helale-confronted-bell] contradicts: [] solution: diff --git a/mocks/_case.js b/mocks/_case.js index 8a6e863..316c32f 100644 --- a/mocks/_case.js +++ b/mocks/_case.js @@ -4,151 +4,155 @@ * file:// で開くので import は使えない。素の + + diff --git a/src/client/components/AlibiChart.tsx b/src/client/components/AlibiChart.tsx index ee7d649..8d1dfaf 100644 --- a/src/client/components/AlibiChart.tsx +++ b/src/client/components/AlibiChart.tsx @@ -284,10 +284,12 @@ export const AlibiChart = ({ )} {s.fix === undefined || truth !== undefined ? null : ( - // 時刻は帯より遅れて出す。先に線が立ち、それから時刻が添う。 + // 時刻は帯より遅れて出す。先に線が立ち、それから時刻が添う(一 目盛りが立つ)。 + // 太らせた一本では、帯が右へ広がったぶん札も 2px 送る。 {s.fix} diff --git a/src/client/components/Motions.stories.tsx b/src/client/components/Motions.stories.tsx index 2a85378..5a73602 100644 --- a/src/client/components/Motions.stories.tsx +++ b/src/client/components/Motions.stories.tsx @@ -129,11 +129,11 @@ const MotionCatalogue = () => ( {/* - * 横中央寄せは transform で書く。Tailwind v4 の -translate-x-1/2 は - * 独立した translate プロパティなので、transform を動かす at-in と二重に効いて - * 動いているあいだだけ倍ずれる。 + * 横中央寄せは -translate-x-1/2 で書く。Tailwind v4 のこれは独立した + * translate プロパティなので、transform を動かす at-in とは合成される。 + * 寄せを transform で書くと、動いているあいだだけ上書きされて横へ飛ぶ。 */} - + 19:08 @@ -148,7 +148,7 @@ const MotionCatalogue = () => ( ? diff --git a/src/client/components/RouteStatus.tsx b/src/client/components/RouteStatus.tsx index db16eb0..d676a9e 100644 --- a/src/client/components/RouteStatus.tsx +++ b/src/client/components/RouteStatus.tsx @@ -8,7 +8,7 @@ import { Link } from '@tanstack/react-router' */ const frame = - 'mx-auto flex min-h-dvh max-w-md flex-col items-center justify-center gap-5 bg-sumi px-5 text-center text-kinari' + 'mx-auto flex min-h-dvh-safe max-w-md flex-col items-center justify-center gap-5 bg-sumi px-5 text-center text-kinari' /** データが届くまでの間。SSR しない画面ではサーバがこれを描く。 */ export const RoutePending = () => ( diff --git a/src/client/components/TypewriterBriefing.tsx b/src/client/components/TypewriterBriefing.tsx index 8d22acd..d0c6c42 100644 --- a/src/client/components/TypewriterBriefing.tsx +++ b/src/client/components/TypewriterBriefing.tsx @@ -284,8 +284,11 @@ export const TypewriterBriefing = ({ paragraphs, soundOn, onFinished }: Props) = /* デスクトップでは下端の中央に読み飛ばしが座るので、その上へ一段よける。 端末では読み飛ばしが右下にいるため、ぶつからず今の高さのままでよい。 + + fixed はビューポート基準で、body に入れたセーフエリアの余白が効かない。 + ホームバーに潜らないよう、ここだけは自分で足す。 */ -
    +
    {waitingForNext && } {reviewing ? ( diff --git a/src/client/hooks/useInterrogation.ts b/src/client/hooks/useInterrogation.ts index 55340a1..3caa202 100644 --- a/src/client/hooks/useInterrogation.ts +++ b/src/client/hooks/useInterrogation.ts @@ -3,6 +3,7 @@ import { askTopic, describeError } from '@/client/lib/api' import { mergeById } from '@/client/lib/merge-by-id' import type { AlibiSegmentData, + Clash, Discovery, Hint, RevelationCard, @@ -44,6 +45,8 @@ export type InterrogationSeed = { hint: Hint /** 時刻表に引ける線。掴んだ手掛かりから引ける分だけがサーバから届く。 */ alibiSegments: AlibiSegmentData[] + /** 供述が噛み合わない区間。揃うまでは無い。 */ + clash: Clash | undefined questionCount: number turn: TurnState | undefined } @@ -71,6 +74,7 @@ export const useInterrogation = (seed: InterrogationSeed) => { * こちらで積むと、サーバが引き直した結果(線の終わりが縮むなど)を取りこぼす。 */ const [alibiSegments, setAlibiSegments] = useState(seed.alibiSegments) + const [clash, setClash] = useState(seed.clash) const [questionCount, setQuestionCount] = useState(seed.questionCount) /** * ターンの進行。正典はサーバ側(DOの質問回数から導かれる)で、 @@ -155,6 +159,7 @@ export const useInterrogation = (seed: InterrogationSeed) => { [params.characterId]: judgement.suggestedQuestions, })) setAlibiSegments(judgement.alibiSegments) + setClash(judgement.clash) setQuestionCount(judgement.questionCount) setTurn(judgement.turn) }, @@ -177,6 +182,7 @@ export const useInterrogation = (seed: InterrogationSeed) => { setHint, alibiSegments, setAlibiSegments, + clash, questionCount, askingCharacterId, error, diff --git a/src/client/hooks/useLitFix.ts b/src/client/hooks/useLitFix.ts new file mode 100644 index 0000000..448d8bc --- /dev/null +++ b/src/client/hooks/useLitFix.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef, useState } from 'react' +import type { AlibiSegment } from '@/client/components/AlibiChart' + +/** 線の同一性。表の側と同じ鍵で数える(who と from が決まれば一本に定まる)。 */ +const keyOf = (segment: AlibiSegment): string => `${segment.who}-${segment.from}` + +/** + * 会話がいま指している目盛り。 + * + * 演出案では「話に出ている時刻と表の上の一本を対にする」とだけ決まっていて、 + * どれを指すかは書かれていない(モックは `19:08 受付` の直書き)。発話の文中から + * 時刻を拾う手もあるが、人物は「十九時八分」とも「夕方」とも言うので、 + * 文字合わせは当たらないときに黙って外れる。 + * + * そこで**直前に増えた線**を指す。時刻が確定するのは会話で裏が取れた瞬間なので、 + * 増えたばかりの一本は、まさにいま話題になっている時刻そのものになる。 + * + * 画面に入った時点で持っている線は指さない。戻ってくるたびに既知の時刻が + * 光るのは、何も起きていないのに何か起きたと言うのと同じなので。 + * + * 一度灯した印は次が来るまで消さない。帯(NewFactBand)は知らせて引くものだが、 + * こちらは会話と表を繋ぐ対応なので、対応が変わるまでは残っているほうが読める。 + */ +export const useLitFix = (segments: AlibiSegment[]): string | undefined => { + const seen = useRef(new Set(segments.map(keyOf))) + const [lit, setLit] = useState(undefined) + + useEffect(() => { + const fresh = segments.filter((segment) => !seen.current.has(keyOf(segment))) + + for (const segment of segments) { + seen.current.add(keyOf(segment)) + } + + /* + 増えた線が複数あるときは、いちばん遅い時刻の一本を採る。会話は時刻を + 遡るより進めることのほうが多く、話の先端に近いのはそちら。 + `HH:mm` は桁が揃っているので、文字の大小がそのまま時刻の前後になる。 + */ + const latest = fresh + .filter((segment) => segment.fix !== undefined) + .toSorted((left, right) => (left.from < right.from ? -1 : 1)) + .at(-1) + + if (latest?.fix !== undefined) { + setLit(latest.fix) + } + }, [segments]) + + return lit +} diff --git a/src/client/index.css b/src/client/index.css index d3bb662..9716e5f 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -68,6 +68,31 @@ body { font-family: var(--font-gothic); /* iOS は user-scalable=no を無視するので、ダブルタップの拡大はここで止める。 */ touch-action: manipulation; + /* + * ホーム画面から開くと、墨は時計とホームバーの下まで届く(viewport-fit=cover)。 + * それは狙った絵なので、地は動かさず、文字だけをこの余白ぶん内側へ寄せる。 + * ノッチの無い端末やブラウザでは env() が 0 になるので、分岐は要らない。 + */ + padding-top: env(safe-area-inset-top); + padding-right: env(safe-area-inset-right); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); +} + +/* + * 画面いっぱいの器。 + * + * 上の padding と対で使う。padding だけだと、中の 100dvh はビューポート全部の + * ままなので余白ぶん背が高くなり、overflow-hidden の画面(聞き込み・導入)では + * 最下部の入力欄が画面の外へ押し出される。器の側も同じだけ縮めて初めて収まる。 + * ——どちらか一方だけ見て「重複している」と消さないこと。 + */ +@utility h-dvh-safe { + height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); +} + +@utility min-h-dvh-safe { + min-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); } /* @@ -169,13 +194,15 @@ body { /* * 目盛りに添う時刻。 * - * translateX(-50%) は目盛りの真上に置くための位置合わせで、動きの一部ではない。 - * キーフレームの側にも書いておかないと、動いているあいだだけ横へずれる。 + * 動きだけを持ち、位置合わせは持たない。目盛りの真上に置くなら、呼ぶ側が + * `translate`(Tailwind の -translate-x-1/2)で寄せる——transform とは別の + * プロパティなので合成され、動いているあいだだけ横へずれることがない。 + * ここに translateX(-50%) を焼くと、真上に置かない縦の表では使えなくなる。 */ @keyframes at-in { from { opacity: 0; - transform: translate(-50%, 5px); + transform: translateY(5px); } } diff --git a/src/client/lib/schemas.ts b/src/client/lib/schemas.ts index 433e802..0f33a45 100644 --- a/src/client/lib/schemas.ts +++ b/src/client/lib/schemas.ts @@ -61,6 +61,8 @@ export const scenarioDetailSchema = scenarioSummarySchema.omit({ characterCount: introduction: z.string().nonempty(), foundAt: z.string().nonempty().nullable(), foundIn: z.string().nonempty().nullable(), + /** 死亡推定時刻。アリバイ表を横断する刻限の線になる。 */ + estimatedDeathAt: z.string().nonempty().nullable(), /** 遺体を調べられる事件か。false なら聞き込みの相手に並べない。 */ investigable: z.boolean(), }) @@ -94,6 +96,13 @@ export const turnStateSchema = z.object({ export const discoverySchema = z.object({ id: z.string().nonempty(), label: z.string().nonempty(), + /** + * 掴んだ証拠の中身。捜査メモが読む。 + * + * 既定を null にしてあるのは、この列より前に焼かれたシナリオ行があるため。 + * ラベルだけでは「何が分かったのか」が残らず、記録が名前の羅列になる。 + */ + description: z.string().nonempty().nullable().default(null), }) export const revelationCardSchema = z.object({ @@ -124,6 +133,15 @@ export const alibiSegmentSchema = z.object({ fix: z.string().nonempty().optional(), }) +/** + * 食い違いの印。掴んだ証拠がある嘘を崩したとき、その嘘が言い張っていた時刻に立つ。 + * 立つ条件が揃わないうちは来ないので、既定は「無し」。 + */ +const clashSchema = z + .object({ at: z.string().nonempty(), label: z.string().nonempty() }) + .nullish() + .transform((value) => (value === null ? undefined : value)) + export const sessionStateSchema = z.object({ sessionId: z.uuid(), scenarioId: z.uuid(), @@ -141,6 +159,8 @@ export const sessionStateSchema = z.object({ revelations: z.array(revelationCardSchema), /** 既定を空にしてあるのは、この機能より前のサーバが返さないため。 */ alibiSegments: z.array(alibiSegmentSchema).default([]), + /** 供述が噛み合わない区間。表の上に一本だけ立つ印なので、揃うまで来ない。 */ + clash: clashSchema, turn: turnStateSchema, }) @@ -152,6 +172,7 @@ export const judgementSchema = z.object({ suggestedQuestions: z.array(z.string().nonempty()), /** 増えた分ではなく、その時点で引ける線すべて。表はこれで置き換える。 */ alibiSegments: z.array(alibiSegmentSchema).default([]), + clash: clashSchema, questionCount: z.number().int(), turn: turnStateSchema, }) @@ -293,6 +314,7 @@ export type CreateSessionResponse = z.infer export type Discovery = z.infer export type RevelationCard = z.infer export type AlibiSegmentData = z.infer +export type Clash = z.infer export type TurnState = z.infer export type SessionState = z.infer export type Judgement = z.infer diff --git a/src/client/screens/AccusationScreen.tsx b/src/client/screens/AccusationScreen.tsx index 9a4960b..818e62d 100644 --- a/src/client/screens/AccusationScreen.tsx +++ b/src/client/screens/AccusationScreen.tsx @@ -163,7 +163,7 @@ export const AccusationScreen = ({ return ( // 地の字送りは端末と机で違う。行の高さを一箇所で決めて、あとは継がせる。 -
    +
    {/* 上部バーは薄く、机の面を最大に取る。ここに出るのは戻り口と残りターンだけ。 */}
    )}
    ) @@ -656,7 +698,19 @@ export const InterrogationScreen = ({ return ( // 記録を読み終えて、その場に入る敷居。強く動かすのは全画面を通してここ一度だけで、 // 他の動きは合図に徹する。他の画面は screen-enter(1.03倍)のまま。 -
    +
    + {/* + 墨の覆いが晴れる。dive と対で八「入り込む」を作る——寄りが戻るのと同時に + 地の墨が引いて、記録を読んでいた場所からこの場へ入る。 + + 器に relative を足さず fixed で浮かせる。足すと、いま自前の relative を + 持たない子の位置の基準がここへ移る。覆いは操作を塞がない。 + */} +
    ) } diff --git a/src/client/screens/ResultScreen.tsx b/src/client/screens/ResultScreen.tsx index 77c9ef2..1a002d7 100644 --- a/src/client/screens/ResultScreen.tsx +++ b/src/client/screens/ResultScreen.tsx @@ -195,7 +195,7 @@ export const ResultScreen = ({ accuseResult, board, onRetry, onRestart }: Props) const deadlineAt: string | undefined = board === undefined ? undefined : board.deadline.at return ( -
    +
    {/* 上部バーは薄く、机の面を最大に取る。端末では判決そのものが見出しになるので畳む。 */}
    diff --git a/src/client/screens/ScenarioSelectScreen.tsx b/src/client/screens/ScenarioSelectScreen.tsx index c95a94b..4649a52 100644 --- a/src/client/screens/ScenarioSelectScreen.tsx +++ b/src/client/screens/ScenarioSelectScreen.tsx @@ -82,7 +82,7 @@ export const ScenarioSelectScreen = ({ } return ( -
    +
    {/* 件数と設定は同じ行に置く。行をもう一本増やさない。 件数は総数を出す——ページ内の件数に置き換えると、全部でいくつあるのかを知る diff --git a/src/client/screens/SettingsScreen.tsx b/src/client/screens/SettingsScreen.tsx index aea60e4..ffc6a9e 100644 --- a/src/client/screens/SettingsScreen.tsx +++ b/src/client/screens/SettingsScreen.tsx @@ -124,7 +124,7 @@ export const SettingsScreen = ({ const soundPickable = briefing === 'typewriter' return ( -
    +
    {/* 戻り口だけは机で等幅をやめる。ここは節の見出しではなく道しるべで、 diff --git a/src/client/stories/fixtures.ts b/src/client/stories/fixtures.ts index 957f061..b8808fd 100644 --- a/src/client/stories/fixtures.ts +++ b/src/client/stories/fixtures.ts @@ -39,6 +39,7 @@ export const SCENARIO: ScenarioDetail = { introduction: '青雨堂店主。初版本の商談を抱えていた', foundAt: '19:15', foundIn: '店の奥', + estimatedDeathAt: '18:50', investigable: true, }, characters: [ @@ -183,13 +184,18 @@ export const INTERROGATION_SEED: InterrogationSeed = { ], }, discoveries: [ - { id: 'ev-receipt', label: '郵便窓口のレシート(19:08 受付)' }, - { id: 'ev-umbrella', label: '裏の路地に残された傘' }, + { + id: 'ev-receipt', + label: '郵便窓口のレシート(19:08 受付)', + description: '小包の控えと同じ番号が打たれている。窓口の機械が刻んだ時刻。', + }, + { id: 'ev-umbrella', label: '裏の路地に残された傘', description: null }, ], revelations: [], hint: { mode: 'normal', places: 3, people: 4 }, // 線は story ごとに違うものを見たいので、種には持たせず画面へ直に渡す。 alibiSegments: [], + clash: undefined, questionCount: 3, turn: { turn: 4, diff --git a/src/routes/sessions/$sessionId/index.tsx b/src/routes/sessions/$sessionId/index.tsx index 9a91334..e85ec25 100644 --- a/src/routes/sessions/$sessionId/index.tsx +++ b/src/routes/sessions/$sessionId/index.tsx @@ -47,8 +47,18 @@ function Interrogation() { interrogation={interrogation} // 支度で選んだ相手。会話が始まっていればそちらが優先される。 firstTarget={initialTarget} - // 刻限と食い違いはまだサーバから出ていないので、線だけを渡す。 - alibi={{ segments: interrogation.alibiSegments }} + /* + 刻限は死亡推定時刻から引く。事件の記録が既に語っている時刻なので、 + 聞き込みが一問も進んでいなくても最初から出してよい。 + */ + alibi={{ + segments: interrogation.alibiSegments, + deadline: + scenario.victim?.estimatedDeathAt == null + ? undefined + : { at: scenario.victim.estimatedDeathAt, label: '死亡推定' }, + clash: interrogation.clash, + }} onAccuse={() => navigate({ to: '/sessions/$sessionId/accuse', params: { sessionId: state.sessionId } }) } diff --git a/src/routes/sessions/$sessionId/route.tsx b/src/routes/sessions/$sessionId/route.tsx index c23744c..926a963 100644 --- a/src/routes/sessions/$sessionId/route.tsx +++ b/src/routes/sessions/$sessionId/route.tsx @@ -53,6 +53,7 @@ function PlaySession() { revelations: state.revelations, hint: state.hint, alibiSegments: state.alibiSegments, + clash: state.clash, questionCount: state.questionCount, turn: state.turn, }) diff --git a/src/server/cache/scenario.ts b/src/server/cache/scenario.ts index 64045ff..c89835c 100644 --- a/src/server/cache/scenario.ts +++ b/src/server/cache/scenario.ts @@ -7,6 +7,7 @@ import { type RevelationEligibilityContext, type RevelationRule, } from '@/server/game/revelations' +import { VICTIM_ID } from '~/db/scenario-definition' import { characters, evidences, revelations, scenarios } from '~/db/schema' /** @@ -29,7 +30,8 @@ const characterKey = (characterId: string) => `character:v2:${characterId}` */ const judgeRubricKey = (scenarioId: string) => `judge-rubric:v2:${scenarioId}` const judgeRevelationsKey = (scenarioId: string) => `judge-revelations:${scenarioId}` -const hintSubjectsKey = (scenarioId: string) => `hint-subjects:${scenarioId}` +// 版を付けてある。数える相手の並びが変わっても、1時間の TTL を待たずに切り替わるように。 +const hintSubjectsKey = (scenarioId: string) => `hint-subjects:v2:${scenarioId}` const SCENARIO_LIST_KEY = 'scenarios:published' /** @@ -281,7 +283,7 @@ export const loadHintSubjects = async ( .from(evidences) .where(eq(evidences.scenarioId, scenarioId)), db - .select({ floorPlan: scenarios.floorPlan }) + .select({ floorPlan: scenarios.floorPlan, investigable: scenarios.victimInvestigable }) .from(scenarios) .where(eq(scenarios.id, scenarioId)) .limit(1), @@ -312,7 +314,17 @@ export const loadHintSubjects = async ( plan === undefined || plan.floorPlan === null ? [] : plan.floorPlan.rooms.map((room) => room.id), - characterIds: characterRows.map((row) => row.id), + /* + * 遺体も数える相手として並べる。上で被害者を人物へ畳んでいるので、 + * ここに載せないと easy の内訳だけ遺体由来の件数が落ちる(総数には入るのに)。 + * + * 調べられない事件では並べない。聞き込みの相手に出てこない相手へ + * 「あと0件」と添えるのは、無いものを数えて見せることになる。 + */ + characterIds: [ + ...characterRows.map((row) => row.id), + ...(plan?.investigable === true ? [VICTIM_ID] : []), + ], } await kv.put(key, JSON.stringify(subjects), { expirationTtl: JUDGE_RUBRIC_TTL_SECONDS }) diff --git a/src/server/game/alibi.ts b/src/server/game/alibi.ts index aefa98a..6e86867 100644 --- a/src/server/game/alibi.ts +++ b/src/server/game/alibi.ts @@ -23,6 +23,18 @@ export type RevealedClues = { evidenceSupports: string[] } +/** 嘘の紐。`about` は authoring のローカル fact ID で、その嘘が言い張っている事実を指す。 */ +export type LieRef = { + id: string + about: string +} + +/** 供述が噛み合わない一点。表を横断して一本だけ立つ。 */ +export type Clash = { + at: string + label: string +} + const MINUTES = /^(\d{2}):(\d{2})$/ const minutesOf = (at: string): number => { @@ -63,6 +75,15 @@ export const knownEventIds = (events: TimelineEvent[], clues: RevealedClues): Se ) } +/** + * 目盛りに添う札。 + * + * 全角空白で区切るのは、時刻と語がどちらも等幅で並ぶ場所だから。 + * 半角では時刻の一部に見えてしまう。 + */ +const fixOf = (event: TimelineEvent): string => + event.record === '' ? event.at : `${event.at} ${event.record}` + /** * 掴んだ出来事から、人ごとの在所の線を引く。 * @@ -110,10 +131,56 @@ export const alibiSegmentsOf = (params: { 裏付けのある線だけ、始まりの時刻を留めた印を持つ。 申告だけの線に時刻を焼くと、本人が言っただけの時刻が 記録で確定した時刻と同じ顔で並ぶ。 + + 記録の名前があれば添える。「19:08」だけでは何がその時刻を留めたのかが + 分からず、プレイヤーは会話へ戻って探すことになる。 */ - ...(event.kind === 'solid' ? { fix: event.at } : {}), + ...(event.kind === 'solid' ? { fix: fixOf(event) } : {}), }, ] }) }) } + +/** 証拠の `contradicts` が嘘を指すときの接頭辞。`"lie:"` の形で書かれる。 */ +const LIE_PREFIX = 'lie:' + +/** + * 供述の食い違いが立つ時刻。 + * + * 「掴んだ証拠がある嘘を突き崩している。その嘘が言い張っていた時刻」に印を置く。 + * 矛盾に手が届いた瞬間、盤面のどこが怪しいのかが一本の線になる、という狙い。 + * + * 印は表に一本だけなので、複数立つときは**いちばん早い時刻**を選ぶ。表は上から下へ + * 読むものなので、崩れはじめる一点を指すのが読み順に合う。それに、後から別の矛盾を + * 掴むたびに印が上下へ飛ぶと、どこを見ていたのか分からなくなる——早い側に寄せておけば、 + * 動くとしても「思っていたより前から食い違っていた」と上へ遡る一方向で済む。 + * + * 掴んでいない出来事の時刻でも印は立つ。そこを伏せると、まさに時刻が争点の事件で + * 印が最後まで現れない。証拠そのものが「その時刻はおかしい」と言っているのだから、 + * 指してよい一点だと見る。 + */ +export const clashOf = (params: { + events: TimelineEvent[] + /** 事件に登場する嘘。人物をまたいで平らにして渡す。 */ + lies: LieRef[] + /** 発見済み証拠の `contradicts` を平らにしたもの。`"lie:xxx"` の形。 */ + contradicts: string[] +}): Clash | undefined => { + const brokenLieIds = new Set( + params.contradicts + .filter((entry) => entry.startsWith(LIE_PREFIX)) + .map((entry) => entry.slice(LIE_PREFIX.length)), + ) + + const disputedFacts = new Set( + params.lies.filter((lie) => brokenLieIds.has(lie.id)).map((lie) => lie.about), + ) + + const at = params.events + .filter((event) => event.facts.some((fact) => disputedFacts.has(fact))) + .map((event) => event.at) + .toSorted((left, right) => minutesOf(left) - minutesOf(right))[0] + + return at === undefined ? undefined : { at, label: '食い違い' } +} diff --git a/src/server/read/scenarios.ts b/src/server/read/scenarios.ts index 0efdc60..b78010f 100644 --- a/src/server/read/scenarios.ts +++ b/src/server/read/scenarios.ts @@ -35,6 +35,8 @@ export type ScenarioDetail = { introduction: string foundAt: string | null foundIn: string | null + /** 死亡推定時刻。アリバイ表を横断する刻限の線になる。 */ + estimatedDeathAt: string | null investigable: boolean } | null /** 既定値を埋めたあとの形。列そのものの型(入力側)ではない。 */ @@ -108,6 +110,7 @@ export const findScenarioDetail = async ( victimFoundAt: scenarios.victimFoundAt, victimFoundIn: scenarios.victimFoundIn, victimInvestigable: scenarios.victimInvestigable, + victimEstimatedDeathAt: scenarios.victimEstimatedDeathAt, difficulty: scenarios.difficulty, estimatedMinutes: scenarios.estimatedMinutes, }) @@ -166,6 +169,7 @@ export const findScenarioDetail = async ( introduction: scenario.victimIntroduction, foundAt: scenario.victimFoundAt, foundIn: scenario.victimFoundIn, + estimatedDeathAt: scenario.victimEstimatedDeathAt, investigable: scenario.victimInvestigable, }, characters: characterRows, diff --git a/src/server/routes/sessions.ts b/src/server/routes/sessions.ts index 60865f4..df9f6f7 100644 --- a/src/server/routes/sessions.ts +++ b/src/server/routes/sessions.ts @@ -13,7 +13,7 @@ import { import type { Db } from '@/server/db/client' import { createDb } from '@/server/db/client' import type { Bindings, Env } from '@/server/env' -import { type AlibiSegment, alibiSegmentsOf } from '@/server/game/alibi' +import { type AlibiSegment, alibiSegmentsOf, type Clash, clashOf } from '@/server/game/alibi' import { buildVictimSheet, type DiscoveredIds } from '@/server/game/examination' import { remainingHints } from '@/server/game/hints' import { acceptRevealedRevelationIds } from '@/server/game/revelations' @@ -250,7 +250,7 @@ const loadAlibiSegments = async ( scenarioId: string, discoveredEvidenceIds: string[], discoveredRevelationIds: string[], -): Promise => { +): Promise<{ segments: AlibiSegment[]; clash: Clash | undefined }> => { const [truthRows, scenarioRows] = await Promise.all([ db .select({ timelineEvents: scenarioTruths.timelineEvents }) @@ -268,14 +268,14 @@ const loadAlibiSegments = async ( const end = scenarioRows[0]?.timeEnd if (events === undefined || events.length === 0 || end === null || end === undefined) { - return [] + return { segments: [], clash: undefined } } const [evidenceRows, revelationRows] = await Promise.all([ discoveredEvidenceIds.length === 0 ? Promise.resolve([]) : db - .select({ supports: evidences.supports }) + .select({ supports: evidences.supports, contradicts: evidences.contradicts }) .from(evidences) .where( and(eq(evidences.scenarioId, scenarioId), inArray(evidences.id, discoveredEvidenceIds)), @@ -297,14 +297,30 @@ const loadAlibiSegments = async ( ), ]) - return alibiSegmentsOf({ - events, - end, - clues: { - revelations: revelationRows, - evidenceSupports: evidenceRows.flatMap((row) => row.supports), - }, - }) + /* + 嘘の紐は人物をまたいで平らにする。どの人物の嘘かは印を立てるのに要らない + ——欲しいのは「その嘘が言い張っていた時刻」だけなので。 + */ + const lieRows = await db + .select({ lieRefs: characters.lieRefs }) + .from(characters) + .where(eq(characters.scenarioId, scenarioId)) + + return { + segments: alibiSegmentsOf({ + events, + end, + clues: { + revelations: revelationRows, + evidenceSupports: evidenceRows.flatMap((row) => row.supports), + }, + }), + clash: clashOf({ + events, + lies: lieRows.flatMap((row) => row.lieRefs), + contradicts: evidenceRows.flatMap((row) => row.contradicts), + }), + } } /** @@ -482,7 +498,7 @@ sessionRoutes.get('/api/sessions/:id', validateSessionId, withEnv, async (c) => snapshot.discoveredEvidenceIds.length === 0 ? Promise.resolve([]) : db - .select({ id: evidences.id, label: evidences.label }) + .select({ id: evidences.id, label: evidences.label, description: evidences.description }) .from(evidences) .where( and( @@ -517,7 +533,7 @@ sessionRoutes.get('/api/sessions/:id', validateSessionId, withEnv, async (c) => 発見済みは必ず DO の snapshot から取る。discoveries / revelation_discoveries の 行から数えると、DO への記録は成功して DB の insert が落ちた回でずれる。 */ - const alibiSegments = await loadAlibiSegments( + const board = await loadAlibiSegments( db, scenarioId, snapshot.discoveredEvidenceIds, @@ -554,7 +570,8 @@ sessionRoutes.get('/api/sessions/:id', validateSessionId, withEnv, async (c) => category: row.category, subject: { type: row.subjectType, id: row.subjectId }, })), - alibiSegments, + alibiSegments: board.segments, + clash: board.clash, turn: turnStateOf(snapshot.questionCount, limits.maxTurns, limits.questionsPerTurn), }) }) @@ -1034,7 +1051,11 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { judgement.revealedEvidenceIds.length === 0 ? Promise.resolve([]) : db - .select({ id: evidences.id, label: evidences.label }) + .select({ + id: evidences.id, + label: evidences.label, + description: evidences.description, + }) .from(evidences) .where(inArray(evidences.id, judgement.revealedEvidenceIds)), revealedRevelationIds.length === 0 @@ -1060,7 +1081,7 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { リロードで組み直した表と食い違う。表の側は key で見分けるので、 既に立っている線が送り直されても動き直さない。 */ - const alibiSegments = await loadAlibiSegments( + const board = await loadAlibiSegments( db, scenarioId, snapshot.discoveredEvidenceIds, @@ -1080,7 +1101,8 @@ sessionRoutes.post('/api/sessions/:id/ask', validateAsk, withEnv, async (c) => { })), contradictionPointedOut: judgement.contradictionPointedOut, suggestedQuestions: judgement.suggestedQuestions, - alibiSegments, + alibiSegments: board.segments, + clash: board.clash, questionCount, turn: turnStateOf(questionCount, limits.maxTurns, limits.questionsPerTurn), }), From c944417c09506123c4c0e966a2dd0ac3efbb08a3 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:28:26 +0000 Subject: [PATCH 23/51] feat(mocks): drive clash line endpoints from column position instead of hardcoded px Store clash participants/time in C.clash and resolve column indices at render time so the line and end pins track cast/column changes instead of drifting when layout shifts. Add pin-lift-x for the alibi table's vertical time axis, mirroring pin-lift's horizontal counterpart, and sync the direction doc and interrogation mock accordingly. Co-Authored-By: Claude --- mocks/_case.js | 11 +++++++ mocks/_mock.js | 40 ++++++++++++++++++++++---- mocks/_motion.css | 21 ++++++++++++++ mocks/desktop/_desk.css | 49 +++++++++++++++++++++++++------- mocks/desktop/interrogation.html | 22 +++++++++----- mocks/direction.html | 8 +++++- mocks/effects/_effects.css | 10 +++++++ 7 files changed, 138 insertions(+), 23 deletions(-) diff --git a/mocks/_case.js b/mocks/_case.js index e476f5e..d81aee4 100644 --- a/mocks/_case.js +++ b/mocks/_case.js @@ -163,6 +163,17 @@ deadline: { at: '18:50', label: '死亡推定' }, turns: 15, + /* + * 食い違い。牧野の申告と瀬名の証言が噛み合わない一点。 + * + * 牧野は十八時三十六分に店を出たと言い、瀬名はその直後から十分のあいだ + * 青雨堂の戸を見ていて、一度も開かなかったと言う。噛み合わないのはこの二人。 + * + * 誰と誰か、何時かはここで決める。表の側に px を書くと、人を入れ替えたときに + * 線だけが元の場所に残る。台本の clash: true が立ったときにこれが表へ出る。 + */ + clash: { at: '18:36', between: ['makino', 'sena'], label: '食い違い' }, + brief: [ '——事件の記録を読み上げます。', '午後七時十五分、商店街の古書店「青雨堂」で、店主の水野英治が店の奥で死亡しているのが見つかりました。外は夕方から激しい雨。閉店時刻は午後六時半でしたが、店内には高価な初版本の商談があり、何人かが遅くまで出入りしていました。', diff --git a/mocks/_mock.js b/mocks/_mock.js index 37e353f..d250560 100644 --- a/mocks/_mock.js +++ b/mocks/_mock.js @@ -242,16 +242,46 @@ '' /* - * 食い違い。牧野の申告と瀬名の証言が噛み合わない区間に一本だけ立てる。 + * 食い違い。噛み合わない二人の列のあいだに一本だけ架ける。 * 表の上でひとつだけの印なので、条件が揃うまで出さない。 + * + * 誰と誰かと何時かは C.clash が持つ。ここが出すのは「何列目と何列目か」だけで、 + * px は CSS の側(--gut-w / --col-n / --bar-mid)が列の実寸から出す—— + * 幅を焼き込むと、人数や列幅が変わったときに線だけが元の場所に残る。 + * + * 両端の目盛りは線の子にしない。親は draw で scaleX(0) から伸びるので、 + * 中に入れると引いているあいだ目盛りまで一緒に潰れて走って見える。 */ if (opts.clash) { - const top = (toMin('18:36') - SPAN.from) * PX_PER_MIN + const cols = C.cast.concat([C.victim]) + const ends = C.clash.between.map((key) => ({ + col: cols.findIndex((p) => p.key === key), + hue: CAST_BY_KEY[key].hue, + })) + const ct = (toMin(C.clash.at) - SPAN.from) * PX_PER_MIN html += '' + - '食い違い' + ct + + 'px; --clash-a:' + + ends[0].col + + '; --clash-b:' + + ends[1].col + + '" data-mock-id="ALI_INT_14">' + + esc(C.clash.label) + + '' + + ends + .map( + (e) => + '', + ) + .join('') } return html } diff --git a/mocks/_motion.css b/mocks/_motion.css index 261c050..68909fd 100644 --- a/mocks/_motion.css +++ b/mocks/_motion.css @@ -155,6 +155,26 @@ animation: pin-lift 620ms 180ms ease-out both; } +/* + * 同じ伸びの、横向きの片割れ。 + * + * 目盛りは時刻の軸に垂直に立つので、軸の向きが変わると目盛りの向きも変わる。 + * 横に流れるレール(突き合わせの帯)では縦の短い印なので pin-lift、 + * 縦に流れるアリバイ表では横の短い棒なので、こちら。倍率も尺も同じで、向きだけが直交する。 + * + * pin-rise のように origin では吸収できない。origin はどこから伸びるかであって、 + * どちらへ伸びるかではないため。伸びる先は帯の外なので、芯を中心に左右へ均等に伸ばす。 + */ +@keyframes pin-lift-x { + 55% { + transform: scaleX(1.5); + } +} + +.pin-lift-x { + animation: pin-lift-x 620ms 180ms ease-out both; +} + /* * 新事実を帯で知らせる。 * @@ -292,6 +312,7 @@ .waver, .draw, .pin-lift, + .pin-lift-x, .band, .fold-open, .row-in, diff --git a/mocks/desktop/_desk.css b/mocks/desktop/_desk.css index 9f23acc..273f881 100644 --- a/mocks/desktop/_desk.css +++ b/mocks/desktop/_desk.css @@ -207,10 +207,18 @@ select { letter-spacing: 0.1em; } -/* 見出しの列と表の列は同じ定義を使う。ずれると表が表でなくなる。 */ +/* 見出しの列と表の列は同じ定義を使う。ずれると表が表でなくなる。 + * + * 格子の寸法は変数でも持つ。列に属さない印(食い違いの線と目盛り)が + * 列の位置を calc で出すため。grid-template-columns と一緒に置いておかないと、 + * 片方だけ直したときに線がひとりでにずれる。 + * --bar-mid は列のなかの帯の芯。下の .bar の left:17px と width:3px から出る。 */ .cols { + --gut-w: 46px; + --col-n: 4; + --bar-mid: 18.5px; display: grid; - grid-template-columns: 46px repeat(4, 1fr); + grid-template-columns: var(--gut-w) repeat(4, 1fr); } .chart-head { @@ -391,9 +399,15 @@ select { color: var(--nezumi-dim); } -/* 食い違い。表の上でひとつだけ立つ印。 */ +/* 食い違い。表の上でひとつだけ立つ印。 + * + * 架ける先は列の番号で受け取る(--clash-a / --clash-b)。左端は a 列の帯の芯、 + * 幅は二列ぶんの間隔——実際に噛み合わない二本のあいだに架かる。 + * 線そのものは顔料を持たない。光らせず、繋ぐだけなので。 */ .clash { position: absolute; + left: calc(var(--gut-w) + (100% - var(--gut-w)) / var(--col-n) * var(--clash-a) + var(--bar-mid)); + width: calc((100% - var(--gut-w)) / var(--col-n) * (var(--clash-b) - var(--clash-a))); height: 1px; border-top: 1px dashed var(--nezumi-dim); opacity: 0.8; @@ -408,14 +422,29 @@ select { background: var(--sumi); padding: 0 4px; } -.clash i { +/* + * 線の両端に立つ目盛り。 + * + * 縦組みの表では時刻が縦に流れるので、軸に垂直な目盛りは横の短い棒になる—— + * .bar.solid::before と同じ寸法(11×1px、帯の芯を中心に)で揃える。 + * 顔料はそれぞれの人のもの。誰と誰が噛み合わないのかを、線ではなく端が言う。 + * + * 線の子にはしない。親は draw で scaleX(0) から伸びるので、中に置くと + * 引いているあいだ目盛りまで一緒に潰れる。だから位置は自前で出す。 + */ +.cpin { position: absolute; - top: -3px; - width: 5px; - height: 5px; - border-radius: 50%; - border: 1px solid var(--nezumi); - background: var(--sumi); + left: calc( + var(--gut-w) + + (100% - var(--gut-w)) / + var(--col-n) * + var(--clash-col) + + var(--bar-mid) + ); + margin-left: -5.5px; + width: 11px; + height: 1px; + background: currentColor; } .legend { diff --git a/mocks/desktop/interrogation.html b/mocks/desktop/interrogation.html index 951ecc0..a425a04 100644 --- a/mocks/desktop/interrogation.html +++ b/mocks/desktop/interrogation.html @@ -141,18 +141,26 @@ }) /* - * 三 ひらめき。噛み合わない区間へ線が左から引かれる。 - * 両端の丸と札は引き終わり(180ms 待って 520ms)を待ってから置く。 - * 線が伸びきる前に端の印だけが宙に浮くのを避けるため。 + * 三 ひらめき。噛み合わない二人の列のあいだへ、線が左から引かれる。 + * + * 引き終わって(180ms 待って 520ms)から、両端の目盛りが一度だけ伸びる。 + * 縦組みの表は時刻が縦に流れるので、目盛りは横の短い棒——伸びる向きも横になる。 + * 伸ばすのは目盛りだけで、在所そのものの帯には触らない。 + * 札も同じ 700ms まで待つ。線が伸びきる前に端の字だけが宙に浮くのを避けるため。 */ var clash = document.querySelector('#chart .clash') if (clash) { clash.classList.add('draw', 'origin-left') - Array.prototype.forEach.call(clash.children, function (mark) { - mark.classList.add('line-in') - mark.style.animationDelay = '700ms' - }) + var note = clash.querySelector('span') + if (note) { + note.classList.add('line-in') + note.style.animationDelay = '700ms' + } } + Array.prototype.forEach.call(document.querySelectorAll('#chart .cpin'), function (pin) { + pin.classList.add('pin-lift-x') + pin.style.animationDelay = '700ms' + }) /* * 五 発話が続く。いま届いた返答だけ、一文ずつ置いていく。間は 0.8 秒。 diff --git a/mocks/direction.html b/mocks/direction.html index 80cc824..6a1f9cd 100644 --- a/mocks/direction.html +++ b/mocks/direction.html @@ -387,6 +387,10 @@ .play .spark .link { animation: draw 520ms 180ms cubic-bezier(.2,.9,.3,1) both; } .play .spark .pin { animation: pin-lift 620ms 180ms ease-out both; } @keyframes pin-lift { 55% { transform: scaleY(1.5); } } + /* 横向きの片割れ。目盛りは時刻の軸に垂直に立つので、軸が縦に流れる + アリバイ表では目盛りが横の短い棒になり、伸びる向きもこちらへ直交する。 */ + @keyframes pin-lift-x { 55% { transform: scaleX(1.5); } } + .pin-lift-x { animation: pin-lift-x 620ms 180ms ease-out both; } /* 新事実発見 — 記録は聞き込み中に画面上にないので、帯で被せて知らせる。 箱は作らず、二本の罫線と薄い覆いだけ。操作は塞がず、2.6 秒で引く。 */ @@ -956,7 +960,9 @@

    十の動きと、その役目

    -

    離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。

    +

    離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。 + 目盛りは時刻の軸に垂直に立つので、伸びる向きは軸の向きで決まります。横に流れるこのレールでは縦(pin-lift)、 + 時刻が縦に流れるアリバイ表では横(pin-lift-x)。伸ばすのは短い目盛りのほうで、在所そのものの帯ではありません。

    diff --git a/mocks/effects/_effects.css b/mocks/effects/_effects.css index 72a51f4..d8036b3 100644 --- a/mocks/effects/_effects.css +++ b/mocks/effects/_effects.css @@ -306,6 +306,16 @@ body { transform: scaleY(1.5); } } +/* 横向きの片割れ。目盛りは時刻の軸に垂直に立つので、軸が縦に流れる + アリバイ表では目盛りが横の短い棒になり、伸びる向きもこちらへ直交する。 */ +@keyframes pin-lift-x { + 55% { + transform: scaleX(1.5); + } +} +.pin-lift-x { + animation: pin-lift-x 620ms 180ms ease-out both; +} /* 新事実発見 — 記録は聞き込み中に画面上にないので、帯で被せて知らせる。 箱は作らず、二本の罫線と薄い覆いだけ。操作は塞がず、2.6 秒で引く。 */ From c9ab87a36a247446ff90ad36479140f406fed853 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:43:33 +0000 Subject: [PATCH 24/51] feat(alibi): draw clash line between the two contradicting people Track who told each lie and which character's evidence broke it, so clashOf can return the pair the dashed line connects instead of just a timestamp. AlibiChart now derives the line's column endpoints and width from actual column layout instead of hardcoded pixels. Co-Authored-By: Claude --- __tests__/server/alibi.test.ts | 73 ++++++++++- src/client/components/AlibiChart.stories.tsx | 2 +- src/client/components/AlibiChart.tsx | 117 ++++++++++++++---- src/client/index.css | 21 ++++ src/client/lib/schemas.ts | 9 +- .../screens/InterrogationScreen.stories.tsx | 6 +- src/client/screens/InterrogationScreen.tsx | 4 +- src/server/game/alibi.ts | 88 ++++++++++--- src/server/routes/sessions.ts | 16 ++- 9 files changed, 276 insertions(+), 60 deletions(-) diff --git a/__tests__/server/alibi.test.ts b/__tests__/server/alibi.test.ts index 83b3cdf..b7e98be 100644 --- a/__tests__/server/alibi.test.ts +++ b/__tests__/server/alibi.test.ts @@ -134,25 +134,38 @@ describe('alibiSegmentsOf', () => { 食い違いの印。証拠が嘘を突き崩したとき、その嘘が言い張っていた時刻に立つ。 */ describe('clashOf', () => { + const SENA = 'sena-uuid' + const LIES: LieRef[] = [ - { id: 'makino-left-early', about: 'store-open' }, - { id: 'kuroda-went-home', about: 'kuroda-visited' }, + { id: 'makino-left-early', about: 'store-open', who: MAKINO }, + { id: 'kuroda-went-home', about: 'kuroda-visited', who: KURODA }, + ] + + /** 瀬名から得た証拠。人物の出所があるので、印のもう片方の端になれる。 */ + const fromSena = (contradicts: string[]) => [ + { contradicts, sources: [{ type: 'character', id: SENA }] }, ] - const clashWith = (contradicts: string[]) => clashOf({ events: EVENTS, lies: LIES, contradicts }) + const clashWith = (contradicts: string[]) => + clashOf({ events: EVENTS, lies: LIES, evidences: fromSena(contradicts) }) test('崩された嘘が無ければ、印は立たない', () => { expect(clashWith([])).toBeUndefined() }) test('証拠が嘘を突き崩すと、その嘘が言い張っていた時刻に立つ', () => { - expect(clashWith(['lie:kuroda-went-home'])).toEqual({ at: '18:41', label: '食い違い' }) + expect(clashWith(['lie:kuroda-went-home'])).toEqual({ + at: '18:41', + label: '食い違い', + between: [KURODA, SENA], + }) }) test('複数崩れても、印はいちばん早い時刻の一つだけ', () => { expect(clashWith(['lie:kuroda-went-home', 'lie:makino-left-early'])).toEqual({ at: '18:20', label: '食い違い', + between: [MAKINO, SENA], }) }) @@ -166,8 +179,56 @@ describe('clashOf', () => { }) test('嘘が指す事実がどの出来事にも無ければ、時刻が決まらないので立たない', () => { - const orphan: LieRef[] = [{ id: 'orphan', about: 'fact-without-event' }] + const orphan: LieRef[] = [{ id: 'orphan', about: 'fact-without-event', who: MAKINO }] + + expect( + clashOf({ events: EVENTS, lies: orphan, evidences: fromSena(['lie:orphan']) }), + ).toBeUndefined() + }) + + /* + 崩した側が誰か分からなければ、線を架ける先が無い。場所や遺体から出た証拠は + 表に列を持たないので、ここでは端になれない。 + */ + test('人物の出所を持たない証拠だけでは、印は立たない', () => { + expect( + clashOf({ + events: EVENTS, + lies: LIES, + evidences: [ + { contradicts: ['lie:kuroda-went-home'], sources: [{ type: 'location', id: 'store' }] }, + ], + }), + ).toBeUndefined() + }) + + /* 自分の嘘を自分で崩す証拠は端にならない。線の幅が消えて、印が一本の柱に潰れる。 */ + test('崩した出所が嘘の主と同じなら、二人にならないので立たない', () => { + expect( + clashOf({ + events: EVENTS, + lies: LIES, + evidences: [ + { contradicts: ['lie:kuroda-went-home'], sources: [{ type: 'character', id: KURODA }] }, + ], + }), + ).toBeUndefined() + }) + + /* + 時刻と二人は同じ嘘から取る。別々に選ぶと、牧野の嘘が言い張る時刻に + 黒田と瀬名の線が架かる——誰も言っていないことを盤面が言い出す。 + */ + test('印の時刻と二人は、同じ嘘から出る', () => { + const clash = clashOf({ + events: EVENTS, + lies: LIES, + evidences: [ + { contradicts: ['lie:makino-left-early'], sources: [{ type: 'character', id: SENA }] }, + { contradicts: ['lie:kuroda-went-home'], sources: [{ type: 'character', id: MAKINO }] }, + ], + }) - expect(clashOf({ events: EVENTS, lies: orphan, contradicts: ['lie:orphan'] })).toBeUndefined() + expect(clash).toEqual({ at: '18:20', label: '食い違い', between: [MAKINO, SENA] }) }) }) diff --git a/src/client/components/AlibiChart.stories.tsx b/src/client/components/AlibiChart.stories.tsx index d869b97..3b46714 100644 --- a/src/client/components/AlibiChart.stories.tsx +++ b/src/client/components/AlibiChart.stories.tsx @@ -122,7 +122,7 @@ export const Interrogating: Story = { span, deadline, activeKey: 'sena', - clash: { at: '18:36', label: '食い違い' }, + clash: { at: '18:36', label: '食い違い', between: ['makino', 'sena'] }, litFix: '19:08 受付', onPick: () => {}, }, diff --git a/src/client/components/AlibiChart.tsx b/src/client/components/AlibiChart.tsx index 8d1dfaf..ffe3e6d 100644 --- a/src/client/components/AlibiChart.tsx +++ b/src/client/components/AlibiChart.tsx @@ -32,8 +32,13 @@ type Props = { deadline?: { at: string; label: string } /** いま聞き込んでいる相手。その列だけ地をわずかに起こす。 */ activeKey?: string - /** 供述が噛み合わない区間。表の上でひとつだけ立つ印なので、揃うまで渡さない。 */ - clash?: { at: string; label: string } + /** + * 供述が噛み合わない区間。表の上でひとつだけ立つ印なので、揃うまで渡さない。 + * + * `between` は噛み合わない二人(`people[].key`)。線はこの二列のあいだに架かり、 + * 両端の目盛りが一度だけ伸びる。どちらが先でもよく、表の並び順に直してから描く。 + */ + clash?: { at: string; label: string; between: [string, string] } /** * 会話でいま指している時刻の `fix`。その目盛りだけ太らせて、 * 話に出ている時刻と表の上の一本を対にする。 @@ -74,6 +79,9 @@ const format = (minutes: number): string => const PX_PER_MIN = 10 const GUTTER = 46 +/** 列のなかの帯の芯。帯の left:17px と width:3px から出る。食い違いの線と目盛りはここに合わせる。 */ +const BAR_MID = 18.5 + /** * 噛み合わない区間の線を引き終わる時刻(`draw` の 180ms 遅れ+520ms)。 * 両端の丸と札はこれを待ってから置く。線と同時に出すと、 @@ -108,6 +116,40 @@ export const AlibiChart = ({ const ticks = Array.from({ length: Math.floor(length / 10) + 1 }, (_, i) => i * 10) const columns = `${GUTTER}px repeat(${people.length}, minmax(0, 1fr))` + /* + 列の実寸は CSS しか知らない(1fr の分け前なので)。px を焼かず calc で書き、 + 人数が変わっても線が列に付いていくようにする。 + */ + const columnWidth = `((100% - ${GUTTER}px) / ${people.length})` + const leftOfColumn = (column: number) => + `calc(${GUTTER}px + ${columnWidth} * ${column} + ${BAR_MID}px)` + const clashTop = clash === undefined ? 0 : (toMinutes(clash.at) - from) * PX_PER_MIN + + /* + 食い違いの線が架かる二列。表の並び順に直すので、left が必ず左になる。 + 片方でも表に列を持たない相手なら描かない——片端しかない線は、何も繋いでいない。 + */ + const clashEnds = (() => { + if (clash === undefined) { + return undefined + } + + const [first, second] = clash.between + const one = people.find((person) => person.key === first) + const other = people.find((person) => person.key === second) + + if (one === undefined || other === undefined) { + return undefined + } + + const oneEnd = { column: people.indexOf(one), person: one } + const otherEnd = { column: people.indexOf(other), person: other } + + return oneEnd.column <= otherEnd.column + ? { left: oneEnd, right: otherEnd } + : { left: otherEnd, right: oneEnd } + })() + return (
    {/* 見出しの列と表の列は同じ定義を使う。ずれると表が表でなくなる。 */} @@ -317,33 +359,56 @@ export const AlibiChart = ({ )} - {clash === undefined ? null : ( - // 離れた二つの供述が噛み合わないと分かった瞬間(三 ひらめき)。 - // 線が左から引かれ、引き終わってから両端の丸と札が置かれる。光らせず、繋ぐ。 - - {/* 両端の丸。どこからどこまでが噛み合っていないのかを、線だけでなく端でも示す。 */} - - + {clash === undefined || clashEnds === undefined ? null : ( + <> + {/* + 離れた二つの供述が噛み合わないと分かった瞬間(三 ひらめき)。 + 線が左から引かれ、引き終わってから札が置かれる。光らせず、繋ぐ。 + + 幅は列の実寸から出す。px を焼き込むと、人数や列幅が変わったときに + 線だけが元の場所へ取り残される。 + */} - {clash.label} + + {clash.label} + - + + {/* + 線の両端に立つ目盛り。どちらの言い分とどちらの言い分が噛み合っていないのかを、 + 線の位置だけでなく端でも示す。顔料は各人のまま——画面が先に誰かを疑わない。 + + 線の子にしない。親は draw で scaleX(0) から伸びるので、中に入れると + 引いているあいだ目盛りまで一緒に潰れて走って見える。 + */} + {[clashEnds.left, clashEnds.right].map((end) => ( + + {/* 伸びるのは目盛りであって帯ではない。縦の表では目盛りが横向きなので pin-lift-x。 */} + + + ))} + )}
    diff --git a/src/client/index.css b/src/client/index.css index 9716e5f..69d86f6 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -257,6 +257,26 @@ body { animation: pin-lift 620ms 180ms ease-out both; } +/* + * 同じ伸びの、横向きの片割れ。 + * + * 目盛りは時刻の軸に垂直に立つので、軸の向きが変わると目盛りの向きも変わる。 + * 横に流れるレールでは縦の短い印なので pin-lift、縦に流れるアリバイ表では + * 横の短い棒なので、こちら。倍率も尺も同じで、向きだけが直交する。 + * + * pin-rise のように origin では吸収できない。origin はどこから伸びるかであって、 + * どちらへ伸びるかではないため。伸びる先は帯の外なので、芯を中心に左右へ均等に伸ばす。 + */ +@keyframes pin-lift-x { + 55% { + transform: scaleX(1.5); + } +} + +.pin-lift-x { + animation: pin-lift-x 620ms 180ms ease-out both; +} + /* * 新事実を帯で知らせる。 * @@ -394,6 +414,7 @@ body { .waver, .draw, .pin-lift, + .pin-lift-x, .band, .fold-open, .row-in, diff --git a/src/client/lib/schemas.ts b/src/client/lib/schemas.ts index 0f33a45..c4c5dd9 100644 --- a/src/client/lib/schemas.ts +++ b/src/client/lib/schemas.ts @@ -136,9 +136,16 @@ export const alibiSegmentSchema = z.object({ /** * 食い違いの印。掴んだ証拠がある嘘を崩したとき、その嘘が言い張っていた時刻に立つ。 * 立つ条件が揃わないうちは来ないので、既定は「無し」。 + * + * `between` は噛み合わない二人。線はこの二列のあいだに架かるので、 + * 二人揃わないと印は描けない——だから長さ2で受ける。 */ const clashSchema = z - .object({ at: z.string().nonempty(), label: z.string().nonempty() }) + .object({ + at: z.string().nonempty(), + label: z.string().nonempty(), + between: z.tuple([z.string().nonempty(), z.string().nonempty()]), + }) .nullish() .transform((value) => (value === null ? undefined : value)) diff --git a/src/client/screens/InterrogationScreen.stories.tsx b/src/client/screens/InterrogationScreen.stories.tsx index 564a3a5..ee8d44c 100644 --- a/src/client/screens/InterrogationScreen.stories.tsx +++ b/src/client/screens/InterrogationScreen.stories.tsx @@ -144,7 +144,7 @@ const Harness = ({ seed: InterrogationSeed detectiveName: string | null segments: AlibiSegment[] - clash?: { at: string; label: string } + clash?: { at: string; label: string; between: [string, string] } }) => { const interrogation = useInterrogation(seed) @@ -205,7 +205,7 @@ const Growing = () => { detectiveName="灰かぶりの探偵" segments={SEGMENTS_LAST.slice(0, count)} // 食い違いは線が出そろってから引く。途中で引くと、繋ぐ先がまだ無い。 - clash={done ? { at: '18:36', label: '食い違い' } : undefined} + clash={done ? { at: '18:36', label: '食い違い', between: [MAKINO.id, SENA.id] } : undefined} /> ) @@ -242,7 +242,7 @@ export const LastTurn: Story = { seed={LAST_TURN_SEED} detectiveName="灰かぶりの探偵" segments={SEGMENTS_LAST} - clash={{ at: '18:36', label: '食い違い' }} + clash={{ at: '18:36', label: '食い違い', between: [MAKINO.id, SENA.id] }} /> ), } diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index 8c1be72..0e5c007 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -25,8 +25,8 @@ import { VICTIM_ID } from '~/db/scenario-definition' type Alibi = { segments: AlibiSegment[] deadline?: { at: string; label: string } - /** 供述が噛み合わない区間。表の上でひとつだけ立つ印なので、揃うまで渡さない。 */ - clash?: { at: string; label: string } + /** 供述が噛み合わない区間。`between` は噛み合わない二人で、線はその二列に架かる。 */ + clash?: { at: string; label: string; between: [string, string] } } type Props = { diff --git a/src/server/game/alibi.ts b/src/server/game/alibi.ts index 6e86867..e69f9e4 100644 --- a/src/server/game/alibi.ts +++ b/src/server/game/alibi.ts @@ -27,12 +27,24 @@ export type RevealedClues = { export type LieRef = { id: string about: string + /** 嘘をついている人物。印の片方の端になる列。 */ + who: string +} + +/** 印を立てるのに要るぶんの証拠。 */ +export type BreakingEvidence = { + /** 崩している嘘。`"lie:xxx"` の形。 */ + contradicts: string[] + /** どこから/誰から得たか。人物の出所が、印のもう片方の端になる。 */ + sources: { type: string; id: string }[] } /** 供述が噛み合わない一点。表を横断して一本だけ立つ。 */ export type Clash = { at: string label: string + /** 噛み合わない二人。線はこの二列のあいだに架かり、両端の目盛りが一度だけ伸びる。 */ + between: [string, string] } const MINUTES = /^(\d{2}):(\d{2})$/ @@ -159,28 +171,74 @@ const LIE_PREFIX = 'lie:' * 掴んでいない出来事の時刻でも印は立つ。そこを伏せると、まさに時刻が争点の事件で * 印が最後まで現れない。証拠そのものが「その時刻はおかしい」と言っているのだから、 * 指してよい一点だと見る。 + * + * 噛み合わない**二人**も併せて返す。嘘の主と、その嘘を崩した証拠の出所の別人。 + * 二人が要るのは、線を二本の柱のあいだに架けるため——どちらの言い分とどちらの言い分が + * 噛み合っていないのかは、線の位置でしか言えない。 + * + * 二人目を出来事の参加者から採らないのは、崩す側がそこに居るとは限らないため。 + * 「Aが郵便局に居たと言い張り、向かいの店主Bがそれを見ていない」——Bの証言はAの + * 出来事の参加者欄には出てこない。証拠の出所を辿るのが、崩した側への正しい道。 */ export const clashOf = (params: { events: TimelineEvent[] /** 事件に登場する嘘。人物をまたいで平らにして渡す。 */ lies: LieRef[] - /** 発見済み証拠の `contradicts` を平らにしたもの。`"lie:xxx"` の形。 */ - contradicts: string[] + /** 発見済みの証拠。崩している嘘と、その出所。 */ + evidences: BreakingEvidence[] }): Clash | undefined => { - const brokenLieIds = new Set( - params.contradicts - .filter((entry) => entry.startsWith(LIE_PREFIX)) - .map((entry) => entry.slice(LIE_PREFIX.length)), - ) + /* + 崩れた嘘ごとに、崩した証拠の出所(人物)を集める。場所や遺体から出た証拠は + 表に列を持たないので、ここでは人物の出所だけを見る。 + */ + const breakers = new Map() + + for (const evidence of params.evidences) { + const from = evidence.sources + .filter((source) => source.type === 'character') + .map((source) => source.id) + + for (const reference of evidence.contradicts) { + if (!reference.startsWith(LIE_PREFIX)) { + continue + } - const disputedFacts = new Set( - params.lies.filter((lie) => brokenLieIds.has(lie.id)).map((lie) => lie.about), - ) + const lieId = reference.slice(LIE_PREFIX.length) + const known = breakers.get(lieId) + + breakers.set(lieId, known === undefined ? from : [...known, ...from]) + } + } + + /* + 時刻と二人は同じ嘘から取る。別々に選ぶと、Aの嘘が言い張る時刻にBとCの線が + 架かる——誰も言っていないことを盤面が言い出す。 + */ + const candidates: { at: string; between: [string, string] }[] = params.lies.flatMap((lie) => { + const brokenBy = breakers.get(lie.id) + + if (brokenBy === undefined) { + return [] + } + + // 自分で自分の嘘を崩す証拠は端にならない。線の幅が消えて、印が一本の柱に潰れる。 + const other = brokenBy.find((who) => who !== lie.who) + + if (other === undefined) { + return [] + } + + const at = params.events + .filter((event) => event.facts.includes(lie.about)) + .map((event) => event.at) + .toSorted((left, right) => minutesOf(left) - minutesOf(right))[0] + + return at === undefined ? [] : [{ at, between: [lie.who, other] }] + }) - const at = params.events - .filter((event) => event.facts.some((fact) => disputedFacts.has(fact))) - .map((event) => event.at) - .toSorted((left, right) => minutesOf(left) - minutesOf(right))[0] + const earliest = candidates.toSorted((left, right) => minutesOf(left.at) - minutesOf(right.at))[0] - return at === undefined ? undefined : { at, label: '食い違い' } + return earliest === undefined + ? undefined + : { at: earliest.at, label: '食い違い', between: earliest.between } } diff --git a/src/server/routes/sessions.ts b/src/server/routes/sessions.ts index df9f6f7..ea99a74 100644 --- a/src/server/routes/sessions.ts +++ b/src/server/routes/sessions.ts @@ -275,7 +275,11 @@ const loadAlibiSegments = async ( discoveredEvidenceIds.length === 0 ? Promise.resolve([]) : db - .select({ supports: evidences.supports, contradicts: evidences.contradicts }) + .select({ + supports: evidences.supports, + contradicts: evidences.contradicts, + sources: evidences.sources, + }) .from(evidences) .where( and(eq(evidences.scenarioId, scenarioId), inArray(evidences.id, discoveredEvidenceIds)), @@ -298,11 +302,11 @@ const loadAlibiSegments = async ( ]) /* - 嘘の紐は人物をまたいで平らにする。どの人物の嘘かは印を立てるのに要らない - ——欲しいのは「その嘘が言い張っていた時刻」だけなので。 + 嘘の紐は人物をまたいで平らにするが、誰の嘘かは持たせたまま運ぶ。 + 印は時刻だけでなく「噛み合わない二人」も指すので、嘘の主が片方の端になる。 */ const lieRows = await db - .select({ lieRefs: characters.lieRefs }) + .select({ id: characters.id, lieRefs: characters.lieRefs }) .from(characters) .where(eq(characters.scenarioId, scenarioId)) @@ -317,8 +321,8 @@ const loadAlibiSegments = async ( }), clash: clashOf({ events, - lies: lieRows.flatMap((row) => row.lieRefs), - contradicts: evidenceRows.flatMap((row) => row.contradicts), + lies: lieRows.flatMap((row) => row.lieRefs.map((lie) => ({ ...lie, who: row.id }))), + evidences: evidenceRows, }), } } From a0883be7030e5530ac3345565befae2291c24dbe Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:53:09 +0000 Subject: [PATCH 25/51] chore: add clash line screenshot Co-Authored-By: Claude --- app-clash-lift.png | Bin 0 -> 52503 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app-clash-lift.png diff --git a/app-clash-lift.png b/app-clash-lift.png new file mode 100644 index 0000000000000000000000000000000000000000..149f1765dc9292f1011c198b7dec2373b9b0a976 GIT binary patch literal 52503 zcmeFZRaBc>)HX`nT>)CyEl}EGEiF*IxCCjTK#LX)65I*F9Rd}kcqtN!ZX{Sp&>+E~ zxD^Q!+(~c^5ah7;_kWk?>WnkSxi}fQm`UDutd+UeT=QAaeAXNONnPmy#WM;rGO`CB zKfKo>BfI?%8QHDidpEDYnc%+>LPqvC*~jJ>YkY zMsvq!i9XJ4m7C-7Iwc8G2&1sLC~*(j7-%DK3E1_@v) zb-`QiXAH!=L3=(4YcKv3x_UmjiX|U7Hq1GCEPIxcyfsJq?XGcGtU-IklCJqCYb@ws z+7ucf|MsXB(`ax;nC3B_Rg$^rR(t2U#?O!rTgE+MBP3zL))+d5}6#nD}`9QSP&EHuV8QI%cvG2QkBp* z%f#PWqfQc3lF|~?oxX}%6&+dcG^T5K#m{f@(kV0txoPEEp+;tzkmYMSkuhadeq~EP z#WQZhT^V;uO)H+lrMz)AJ$OXUo%w;9NUREbp?$RdlMsWyaxYf)+0N=@4i{vpKOB%+ z+wJK>&t2}ujB-*#=^IZ;+1OZ_>W^Yrsyl;Qr-LORK}xf+eq#8fs{JsF-)dv>F_E~% zFW>{Pu%kzIMd|^1o=u5okR==c#pr#vRxh8KFLP^cT(CTwe1IG7kDNQc(S9RIj z@i}X^xjn6Q)z~ChF@y+Ir(NfGL&xPocSV=DtQdy^orYj>chnl}07d zW!jYBO*Zm~gSxCQQNt9FDE;$B;CO^%xsry6KI zrw%&*mU*UM?1u4l%OL3v==Ay#FV0A^c5WlC9T9YHKXE2VRFx0&Um-a;hdy{TPa96l(57c{|4XdxD&2KFjfL!) z-te^=D2jSM6nLofvW4Zdfnu{O@Xsrm-E(dHgOZE!?IcdU?|sU{xTyg>Z{sHEZ!;kD z+KM!cM=rf`i)JvddMjup(Jk^*`}G|{62tbOgRHAFT-F{yBk1t7b058>M4iS6b1Jad z2|AvP9;vJ?tpcV23|wpBXk+Dvn+l{_w;Hg^TERUFR>Wf$qO=8?X z5Gl;nkF_;}^xO}on@7qYQ87KWwz`XzK!e#ME?qymh^k3)L}}tdvwk=u6XFgR2%F%Z8Kgx`TeRRFURzB zjuIgzeH20mXxz1vL$b1Jd9tR`Fsuc>&hDx=4c$Fb{g1z z36=|Hf^>rGY}Tw)69aZEr`n2+HzmNIa-|M82LhnpLx_?lKo&S0*>D2#ReqN&CwZ2l z*1)a}1jY_o5kyk@A`Uq+8#IDWcQ;4`r+E~N%b^Wrt4FZKVAAXE)X0R0%82$qCJ4yW zBXvK-lG}4S3_fPm#Mm3X*XK4Nm64)G;u8|3NSj^|yoSlNsqAHM)_2@tD2NH%E2rN3*Sc!u|AW(hb_I4&7HHmnb=jY;iD=2u_p_72?NBP6^IJ8o3d zY8IHm^vnmVmECCYT!GpAaI#|s1|AQkgB4SiI9Vx@{2Lz5Ahrrf1HY4)Vq#)2F@;`W zHQ3l#iXs1~EQ>f1CJwo>X$RCOe)iLQWmD^9_-WLY6Km z1yxvj^ngetjV8_McN1Tx?Cx;bBw?xpHN^tCuhY_{1+4M#2qs0Pwg0EpHZw@y_>2%4 zhJwvYM8jh1&)XvEA*u0TL#byE5sQ>=7Quz<9!GY|S;m!HryL{wO2_qnMMHh`x0`uc z=W7^VS7!O4+a_!>Sf7obq2`1Hx)M{{(J{mTP6HeNc9t2xe8y|IvDJ z@>@7n*Y8nwvly+H$#T!Xk(>3!){)c4eX7~U7UZKOB4}~lNjVxyGP#NrAZlS(;c?0 z_Hg?qqo;GCj!&JA)*7wU@g%QGcJRgUq$%{EYnhcPBH=4F)KVq1K>D!Go&mF=opClq zQ)_bOrbfsX9IYXZGy$aX{o*+AaUh)I174;tdMA6)X?-EkUZrzr=N!}8(E6K?i>eM@ z0gDu!e(tFstoKB9X_eChPm~1}$Pn|zdOPX-QyOS0I61TRq-v$n#P!)~LVI+XC+&_e zdcqxOZop8{b_aaiXcWY@C~O+&JXkwnTP<}EUemzeV1jEOzukmjAay)8Z!xW;ysTw~(syqcS_YyJCagwaxP>xrqr0bGpI75jH<$lfi z2Fr!4t;SvkEQKJu+c%KL^FmUi;c_*^_jTRt@y_=C%Ag2VReq);`<5>o?v)uh|M_wy zm3tw4wi|c%>2yiQblx%G7R?&4N_H|g1w_Z9vQ?)cXlEl|iEmja{CWhfVorB^OQuz` zKnfH4wx%a-VL z8L2(8L0-FfzU1T8pE1upZHSXSH@Y%f)o&-8)v;qZFRtpjhh~f8r%s!8e1sY^c^LcN zC5JaEx=JUZ);}%^fJ{Nq2x;%}UqXdbKY??ymn`X?g;XMhWG{$9BhL8|sw#Sw(O|S| zA_(TWbLOFN@Ws2QB)X54=P1l1NS@ReKMBZ~vHY5W!*OHJvLT-56T zV2b#zAc6ODpE)Jyc~8EN%rg2-Pm3vWFGk-zPP1=ttI>YH>^#+|ud_K9gqd1+L-a9u zH`%{Z(zrf^JwRvrc^ok=ZDLA^?c3WNnUiTIr7Ib|C5@iG>`X}KmFg(NgIytTo3j}u zroikDqm8^u|B9=KIP0|ZTU9j?&m{*JalzGc+3(1$L&Tg6hqt(rsaAZw{S?kc>1dCT zS~s%t#+x=%azwK2*5^Z0>X*CiS1${7-{;w?J~U0GC4uQ<{+Rbyys>Z)H8b;9tAw3;58ijz|5V}VmN zdVzMXrzL!zzBY_0$!nMgmy9S%(Th9_TeEP6bjPwoL#?R=EY)rA7{d|i^V^G zM=L)u1SDp?coD&^ptyup4pLH8xA0p=k8ksAz2Td9Hz6>mvuBifaZ*(J@c@af3p!Jt zT(6W2bj_`r7WOO3{*ya>)n}=NaWyA8&m||Q@s!dC>7HYl28$!Lygqv0JkGR#A|{$C z>3bv{iEdaseq0R*0l~2whlXgw7xWZp+{6sO$3ujdl;cS|Vud|MlFaGRjm~rp=A?|= zG6t&cDWl<)QJ&evpQ!EEGUZJ^%Rki~qR&ph#>p4AnG26%K@t=8H}~mUaax)NMwXaU zhEHRw@g~?vfq_JOL5fyw=cL3W z?;}}dARF||CNM(51)ia4W=0_AR+QHFQMJs=w!DqvRWvkAP+XEjko5%*yGhRRc(7Go z@IB8idYMxVpeUa2Y~agSy2hry)OG^bm9~bDaKxs}er+aGLGHV|iV{(dB~tQgXut7z)8FDuMop$P93EkH{t+B1#%{eN}*$LI446Hs_$rL5lh) z%E#pgC5=_?(~S7T{dO3K@T^5xc^o)JOP)?+%jJKy>rR@u91!wtcN&kymlP?GRnBZ$F4=SL8 zZ?njwsm2Q%0#lfh_~ff!%Gj#eBv-FIZaZBZ_QK6ta#7~q=@cQ<9BDZ9ei}W;ugn0s zhq;9x?z!$gP9$@pv;^;Y2O4-g^;!a6q74;xK48Q1+&w*U^Osq)N)VMJiL4oX%`=w* znoQqCZEDa(`N@XdIZ`ddzrrK!vPVNb_Bxj&2CflrT*mJ3Oza<0=4C>g)Qcw%M>&A6 z9Qt*ykt=EJSEyh09|eVni8WijQ$mVdQ8@oPBk+cLI@rEw)2?B3$a*MOw)&!MP$tC% zzsWBfSpQXqGx=fusay9HOnU7DF>~1cXeJa@M)N4lIK3wCd07Rd8H$)?Ph z3(cB_7}UZB!iBKI7&fRqOiF=0wmD)HGtN>mO%hNr_fE~S8>Hrkp(YzG!7XPZiM%^p zTKjsu|NWX#4Q70<)iVNPLVafRj5#P76ZcCxN62KBneyr?wcHM#+LY-nLy?AOHW zw1ml*;ZyTYY`P@OE*2UI_jQC%=F+@S^2Wz~+cAY|9 zIP#X?V3|lqv4|y)4jcSRJ(EU*%QODXAQ8g@mxPWP=XOPld-EL+U8eCoBK|>VGfS=O zD>duFdW}J z!^XPv_ET^qS6zO6jhly;2YsR=x>LIZpImzxK(8V~(DWfO;^IWkY*Ci&Tndz_7)>lRuLZkjW} z6PdJyyUVMRbVs9EtUhMN^caeIi)g!wRU9)*HxB09QBr(|I}#HV&6qV#Clwz4^H+Mq z@g{$hSMH*slCiEb3~B&qO7l#|)>rS8dM1i``bq(9`5K6|(Hm~)3;Kb}eQu@c2Vx2N z>dsBxokAMtTi5A3NZj|_%s4d;EYz1ED#O7IdZHdJT-Bh(JEH|%yhvWA&P^RWUxv#c zY0rNDg`}yFumFR%Yg*KJj_#VAX1Qp6yVb0A4g3!zc#UV4)?Rzq-%ed@dsOkbGrW8* z!&Nz~Gl{@g=BcSyQ`BxY^4qraj)BpvQ?gWX@rcjqrhbmH=TBB>S<%+{f>>|KxQ$k# z&){+E6U$RXSdn5xAE1tFuu)fHg%DA?^6-~y_GzEdU8n8sGOkygh)?-rVJg);jK+!Z z^zC<}9(x}-zcKIVWJOxNrc+g@vl_R}bA0@Y^ZbOKvZ3EwcngY$v+`r{{?Y^DC7BXFR%#^HYLC!9?LQ^BYnZOpv5l`;A)z)i7*Kjc!&%ojRfBu^PLKDvQ{mLb zDv>&)$Jb`5Dt4>qD*1_H5mMZ@aNW$4X=_tJx|WYkmhCt0Wc#F}Dh4$SRZ6NaxPLGXvEMUUl^Mfz;rCBf^x`++CDZ zmovAv4kEX9+xA zvEW}b*S=5VAJS&Oz3=zT;mECn^M8pHSG&a-35C(T;LU=vB6vh;S{U3zekkDZNFHu+ z^y~)Dd)zr(<=lv`KIjm*wU%2F3I0(y z5Uz7}emG5fyhfj^Xa}z>-y79Xxyd|PiSNbFf|Epb870;}=*K5sy?3~zX`P)ifuc~` zpra6?QU9Sy%PYbO_pL}(sU0`*x&fvDG19NC3KNf!sqkMO;~X*pl^l_hifhbcuOvA| z)Phz%KQ4W(!`n|0t7hbZCRA~8z2{AcDG08lMf&sRw@pLrGACSBB?D)g`ZNRTv-{aE z@-{L9*g34uh&DVIr;+~P54Uej3*U?ZWXQpifj{EWuh}FF{n2Kg-b3J$27_(i_{m9K z1Fa@|rP&TFWag=XgSMt7vRYf+*T=@h{HK%SpkY?DN!4lA2{#eW1flEXQ(d3PRyU~5 zd1Ex=v5xg#vwJI;j~sUt=vBLToQhp=c>+kmO-JLvwdJzwHyak**|Jy_6?`epZo z>D`0ml(sQTyY;Dxv~aA0K1gEbP1!a(1Lr9KSPY7PB8YMg6Lsz1FC?i+JoD#I zOj0*aT*|8}cr6bMJZb~D&%96xg0#oc4`ztt93Hpey>x@>iSDmE;So7uhDceNf7Czr z%`^tAq$IEr72v^#2~BfcpUHK~LWryH^}$3&=R>#Il}+)AwNE`20Or8`#!DcT$-W!R zu1dX6X9(eSIaw12M*q7pX1TH;-|!0bm^m>Zq=jYH5ASxHT;;PgY6&*VByjmn63`nu zxYdq1T@zpA>_HF?xVoj@ynb=(23d9J=cR#sHBJzW#&RnO)k)w2_y01g>P#i1a)@Sm zCl|#eA=UZy4_wMi8K|0e8tU>TrTmX@d+qP!hu!o{qygx0Z+aLyYG-ZdO*NXH#cyY# zFh;_`rGRQgf{#9PHx2$LxHCW4-$>%QI^u5!NPtDk7H-P5M#&KWX2kee&T^InVt+Be z=nLY{t6=V6R@bE}-Ue-B6$y+=EUeF|!XvD({rTq*mAI5fw=xXTOaVdSQjEWB9YeoV zFq0FNK2H-rOl3)Pt_v(Gj+m{zz}pVLRBVFE=iy$mad=g~4M8jI^;PGURqZ+*-29=0iN6bPi&m@f`k)EFULC-HfTut5^ zebF%rQzg?#H z7$LT%L1noJs4&ksk9vlKg3-lZJa!T=j9rbOfkS3vE0R=7g06Z?OntM}amV4dBU7l6 zXRSj9zRMGgRY`wdIpaJpZWwf^B){3sM;{;2n{tcBxtIk@a@iW2C;6QvW|vcB=b7M~ zWK11)f9>yJ(maXw9CS~k^(f6cbjXRk(GRzjs4zQpU@@oZT1{PI*rOw;eats{B)IBg zh4_dFJ$M^e=Yo>7TEcAYS>_bHvMUi8DSZl7`<8Uf<&BSsAPmb8u`ofw<<;3ZL2>BX z{$YWUcz;2pKoCx$5%DOrgY%A6B0_{BtlewH zl6{=56M=gs8Bi*mBFPTm@0j`;c%x6%s5xw7)75|qD!Dq%&3-FN*J{=@ z+Q%6&0UFPC2TF>5i_Jtce|>C=G~BG+Lx-M@4SNS3)+aMvVv4sAK!xyBTa**d+diO; z2D~w(-RUnf;*@vqyGHLI_BewZx$#`AQ)hXh7LYNM^vsMW=;%mj9I)hejYOf73jwQT zX~u&ai^Oec@6^))&=6d9*Tp=zmT)vqeN^JNpJU`DZ4W)dW^b@;(wa+5iM5lrEqM2+ z-VeWNcKP@9G+{rTO}}On%EuT35rB&?xOC#kD%`uj^9F*0B|BOm72^fuw=;9|Kk1f#)mU zf4P8{7r=VCVabRfL)#eBvk90O+>k5qz#?QQ2)}NDSQy@Jsu<;~(xvrG3nXL*E@7ez znK&;0Jo1<$@ThbHocq;y?Cfy|=herm$%XgTCmW^m8kP?y);YGH-Ru>$F^rfk!eaur zw>BmH9Oj1XfkVF?FFLe_jG*g4)LPP?Du+W|Hz7H@%gM9HPSjjh@MYQKR$1(zAH z-Of_ z0mitzBTfqKp&>1nTJt;e>hGFwWPJo3?6ysfR9=YOH4Qv+OntqjD|M>cvMNh@e-I^UGw6#WCPpit^)HX+22Pnv}vgoj!Hq`*v^UNP+y~0bu zy}j-7;bTi-gL8*;1!BI;mZ=GfZ(gQkK-*%g5hbqhv`!qaQ91hq8U&1w^7cICVR)(+ z`yJuLG}LJE`Q{i~Pshca)F<46&Tmtr_Msu2>4l6>R(wI5@rajPL?*O29Xa`iDEn(% zP({8SbINF8HcJh0kKWDVTwy0JX5SqO@nI#?r<+Od@kzrP$7#h)af6NM(@k!E&$%YBQViCLv5-+TjKjvw z7+1tg?c9COwKCJ^2wSb#8z%`Es6Q*RT(hhjlRgrgL@QA<22>CG8$E)oo^4IyT2dN1 z3HEO&%>u8tbTi8fDx$LWId2po5Xs9agn2#OX70L?tpFP%0*>Q~b>;k0nxvux&~Sh= z-~igU6vaG~0UL-EoYDOUylRksh>0`RYp0R5-Z9PdvtDE<)1cGG?vKN|9>hPQkHi$2 z4Qik&NmZmnZc@o(Ti28+Kp!g?t7=Zo=5~^WXo4Pt`+hmi(VFBmR6k9+-QR?IT(-g$ z3nhXASNcC%-tpCgGVt>tp7(8h-S~^qO>MuN&R~HeM3Tpl_v=pzmWdRH21A*bueWIkW_jZx$Kg|w@q`qqMrEAX{z-X)+cl(#HcT{G)17}|O5%ShuN# zNpy;JBPWZE7kezz$01=+SLYo=!?j>1j&Ym3l8ZliYPTi*A?>r7%o4_w!8L!LNtL!pB$i(auM`P!20 zeud~&8BUwYlk3^pzeQ&aS6JhAkh=RFn>*Je!tvX%Ztd-9u1rS6?k1Lcz2Sk6@Ut_T zty?t;*QIyv#6Zp2W8?-L@zNY6y}Lh4WRRj36>Yg7|E1X@eJ0k2kg?dMnWBffr3ZIzZ)Dc8@55a24R!7ayO# zYZbIx>Z8$)4_n;~aP%+F#{ZeEmUg2EtDRZzqKqBm{M`kQMAl`u)C&S$Dws}-;FP<@ z+#KhA9omi;PdCEP;LLDgW!YnuZvA9!wgj7 z*Hwxg+)tEuCasDo;kh}gV)x<;hr;K{VL#Mj7in9gzU`0U8dRiKG^kzsy4v8tdC@P+ zQuG8eV`eXXh8C+d_4k;%^4C$#bPZmXA|em(8?Ih0l@hIMtLWT0scI}# z+nLwP1)|IM(C{=B*9xG4S1?hjJtdV7DY#pH|-D8g&jBPe)PBWDIdMKjApqAyu*D44$fM>)=3DXc0$XX=3sL! z0ZzK_9SSg&*j{3XlReJ+*oSa5pcl%BOpM3ywhQ*4&@tdiPwg+L9^Fh& zmgsw&Wq!0qN+_f^EhQs6;HdxMB?m!8Q{AKRR=lJl5qKjZrCM4e<3Y7 z)QErQ;&}X+f-G4(@Wt7i8~vaK%VgD7V9$R=pcSi(6O@9q6}8844PN|}PlwWf=>9USC>4O|zS$}#{(P}&F^7_@ zdb>ki+C@n-WL@s7f9?jdWdl`ofAF+9)M{&#TO7b&@0p!7$MzWz$%S%(W)&}L9-l}M7s#Qa$RXzKL(UCgHwE+>TsBHmp5rNVY6w*1RLgN?fhkVLU5qm&4W#~Uz zsQXm05%*kT=JYYjl_^qluUw2i%)$OfgJj(9PU79DaJPiqhg-4AYM=fIIxb?0WV9K1 z`!)ddy^~{V(xjf4+p_2uB=RWUvJDGN;{4`u!yzsyok`|6PE*l?H4R$TxK)Utpr|6qsb>`0G0#Wd&TLkqC+m25Exh=v!>K0!`Rqpfs1*oN9L#JwoHWv{|=ii7??KQ}D;O1gTExE7UZNU6GZNPP^w{kC} zm!{G!%h=cS=i1kL|BN1nE3FIc-PzhvX+O!LC{UwU#7^ONZV;20@Aju&3A~!&UoH$E z=U}bTdzMJPJ4LaXg|#|k#rM^FyGK1n)5Pv8e*}4JhRZ;=RtxgvIZ`ev#!(ME!nq-- zIt!X{alksKZtQlcl(=wT&?bNlsEiM55`Cp?(#H_ReZ1c&s%?7?SZr`ZT7OQy`SXzxB5%89uCDrafxbNe|9A|F{Ea{Kdq_7=q zFQZZW#Ru6qTMs3w`+#q)w21t1*>9jh3e}1sBaF~xpU5b`G*qwlWohMHi|D+xLtQ2} zy$qVsZbAbuQspIQeju#XVRcV`)|G|zda@x)Js&rCZ=Gbt80~!lX?kor<`}~@BU^5y zsywZ-&<^}CQz#GXpKX$=4)kCkrj9ZNR*QjbRv$fdkBQ|Y>$`ouWQWcSkIYD-G)uVn zI05gm?8Zpr>XEz##9_af@@*HO`y@m!Ur;}XwqwZorxrtAq7h$e7z*`bq`kx>k-;7% zKABR=2f^P0bk2lQK4p2BvY~!nXeBqE6xUGnRsL(jexY(l^r0$Vl+x_WPkS0zi6OJ( z@Wv}PXZHy7!txLeQjVs;&y4Z;7peLht9m4Iv5&%Ir~jX}ZpVUnruVn5Rel zZCi+86Da5wT-py2!}xk*Y4Kr2*uVeb#!afe&sU*q-&cZSXcgu^fY5-zw)@$*yV1hzxTsxEobqaS z)cR=)aKL0m(9`VGXV86vGv4DNh;dR!BuBqZU2V0|MA}V#Gf-Z{OA?pWnRP0~AxwkO zo{$}K3@I#TKB`r>*AvQ?kN8?qE=Dh!F6Fyk>|0eb z0D{u(Y$K9|jpAzvmyE7^Guk2^!@pa9E78|@y7`XwFvh0EG(F3wB^*O{Qebnkx`v!-9v^{Un-K-9dY->)2IH#Q|+0T*Tqz^*i8kvMt{F?o**`fHr+nW zCuCgHt4f}XG7Xf2y^ApZRuufiVCNL|4hCY${#h=!b5WXrIcaT$3moSyDJeRzc_m{G zDD|8v!Z~@l9E7Kw*F$XGJZ?Q^mccZqjTF(gYF!WIOV4RjCeHg~gE#`CB z%LcsTu%dhwwP{AuS6v_=>Rifa{VU9Waf!>k1RE&l>TL^)C4hu|@)#(fi^Hw7n!c-Y zFR}Y&^Igkd7C0lXGsp{pv$v2IX_nIDvpbvn|Gc%Ftd-H#2Z5A9|1`J`KPhuCc3fSI zs(fv|v*I39w9crbRHDxS=HV`s@XVFw10U^2Q}a7;H|gk(NWNH24#{C~0qhRlc4A3a zXUTgbs`9aY{mqY}f>wW^N7DfZLVX5GunnqTMhDb!9bI+`JrNzIzb3aHWJO_C{+A1| z608NSR@Shyd@zSs6W+G?;?4l*XUzvJJxzoO!?-`Ek+12+9D zM26kBdokfv!@5I(glnn!;Wys++rhzqQNleQtgk!sLF*8lykg#y1pg!>e=yi#!w?|Z zMI;U+3+iwmhjkC+vFFIVgxmn%EoYfz0d0O99uk@VC zyVReG60waLGx^h*54Xv)DjAOS9zg4Zs(n1P*XdUtvpYy5M0&6|vY^vpG{Ze+T z^^y_IgAF>DPp`WXRw}}HzD3=!ni_Liq(E1_8b{4fuG=+ZtaE5+E^5fAo{zrR+?kVX z%ABf|xvqdaJqOw+wmQWPFj?a&zpmKh;ufAxw^QD_Sb3VdqxKB&o61u+?e*4Sl10}| z3%9Ci*aDg_4TKs#>7Bamvi9SyB7e8W_wexh$Mo_UokHVgZSc@aH3R(dSiEiCdLzii z{{#%3Q9IpBDb?Dn3e$^CHRtkOllGonbE~1|=F@KfMH8*;%6o@gwQDf?4KP9P7mCR% z&C6)6W~1In8Z&t}ifJhcs(*`EM;Ry#1vCs5HH9mOIA&#QwN3QNE`K2^+ zt0eOE?ZieLjDHkQ<;&^!nS~Y{A6?mJw6 z$%b1-y$j?X5Y|88i6l#ue8njXcsV@lv0vE*VjecsDFM}c7*rJ8Gj|O?rald+1}0>K(JSt1js`CVknB&}MtE)Y>p9 zfw^&T4JKWtBuISBGEL`$+tH2ZtL;f(Tc@IluXo3b_%q7*uHJq*g?@c7_rBRRKi=Nr zk%iUjFv{qO>pRT0pR;MSM?3m8hwPZwr4@dH>{GR zDx4(;7y~!z&cj$LZ6d~3kyE)3_$7hEk|^O&E&BANxVWblVhQQEsp+**xHW}<@E^BJ zX-lPn{nmb?VIv0>1mXUYB^6whRfb(qkoA?|EB)jYK4!q^yve)Av`^=E7%Op1V=*Z& z@1`!5yB!>i$)wH94TFH1*wGdjF8v>)3-$<#_)%kPj*XP!TP;QuK!~QXxQRpZ=p*^O za8<*k^k1}gD5+DY?hm00Lk>M-notL4;4qlt9e+FgI z-+bLPkyNwSB9bJV=2R{|gYQ_Pws}f@wf!kksK@W>B{jPPgw7CMKO^|lqf7T0vd5fY z`6@9a=JOI&{{3!MN_VeBVbcv9CvZdFg7sP!EfW?=?eUW&TKd#vT6*|1J7kf#2g==}CLRm~d%0T<`6G27a@v1X(Q`J~+7zl3 zWw_6)HT**EvGZgavxUf0!_QUeTen|3AbG3dK_^vJ2%8O0?@Z0hs^u+r-p8j%#u+OIh>Oeoa!TpiC3!?(S( zD30%45x+|!MFpSMC@5be%`LCNrA%er(fW%qXD+{BzOBZ)EGNBeo(`^$pc9!8kic+i-w<>!Hb`n*wY9AIG-SQ)uWgVZL{&MsykS9^ z#t$wNG!okP4=2#Oihf7hzGiIO_-UG^zW(2I>y;i)Q&PFWhV>7YI7Wgwc=`uEJkX`I z4ce;JwoQ4ua>M(+E@^d^e%RRe&oeOZ#lGhWnmXn1?Z{$exM* zSWO@zLY3)J`cGQ+`?_@7F}#9kWl<}4kb!)jsn<>@+siq<{IbiF!g8Ch_j!C$YR>(E z9rhtl1Vs~*^wb#-mEan^CKw*h-=@hua}utBxmBoytS|(`cq#QpPbH_3DBDqn-;U$> ziNJF!3b;!R_-J-`NR2K&zKQ2t5zH`A$48eAS^53AI`rT8zkS!f_?BoahJh=()x2!z z>z$kXOUx44w{t zDgOP_e&1qU)}V&4!kn4qsA6c>ndDjVO|s9v9I%n_1$D*$;je1Aui@>a)qlrN z$(lR=U!k}D?};+b-*WkX-m`Zi>KvX$4|GR9qO68+sYXQn;R?o%IuuBH&AZrFAKWp0 zN0;%-d$1=hVY&2q7`hq(UtGVe=2nXd2P>>P8aCNzTFBL`iwNtb0em;*A`Skd=sw?m z%lr=&E#*%TO?uhphi=-mMISfuIg-I z>0O`kXYy`6VwDmxcjU+YuD3$lam#AEqvs+^P-7^ zC5qJkboK(RJbfj@*S8r=MsA0}*y%8bcfDp{IZlDZqYGOc1h|GX9L$u^oYdShm~C5l zV+d#J>bw-BfaIF+-ihX2iu^2|3)3Xz98#Fq4`s9b*MgZBZy#$8RqB=>VgkpNN`moz z9MFhGJ>AOA{>axA{&0+NfR3lR3jwK{)bqVvAG6Bjb#ku7{oSTz>X93`Sfq(xpfKOI z6EU5Pltb#NICo1VrFI>uZ2Q~TnnWE|YZr_TFL};w@+wCR{!K>K7tJDbpn1crLuRjM z@b>4Y;5RztW{o`OnmTzFD2Y=iwZ%4jmXl?UBv6f`RtMZx0daSOnpu1Y{yMZatENT~ zmc1>bJtht5zOTDqgvC23ZPX4|A<9DJ1cueEJcMeU_L9OfL3A9L6u^<|LyC~=GwIi_ z&r|*0x2P62Q8LCj+9vjQnaM_PBzr-oW9toV-{IRx({W?x*!|sUrfW4ijdA;3b+js< zcF`Dox&pk0^HJES9XFA-FgiFFdgvg)a_5eXp3srv`6=npKFBhZTM%1DlS+`uqW1^nBV>5u%WBT0j^%Jog@n5Q0%-=gf@nE9ehgD{-$ygBm-NL3#di zBAP^PYafsg5Wi7!Sy+2Sgxba=S&2tQ(?O`RVxwz^%8YcwiJQ12 zxCn4Fa0}T>!{97ltmjP~S4A`pwBJE{SXR~-w2jHeKKwrgfC1nwDgnWQa5<(osAC&V zhyUdQ$Ri9l)S1%6#!zz1Da742h8!l(>vJdL&r`hzTA8~eo(fImtR@L{2eYS zotjtAI|Fw?2!FhTmQssuKhk^HXr<=0o+z$>QZY)Te_0$_u2?ailJ`H3mpOHe)6_m1 zTFhG(;hf62m@dEGy+yL}h=>_*yeEa{z>VYO?n1O?LpMjs?6d1CD;riE5R|??+f~Mh z?1Y$YJ_SN3vM?i69K$us?;sFcuh$L;L&VGes5=NruwJ2?8TgS~eCQI=a5*pIEI)jm zRq$^Dub-}vOxXyCh(Moq_40d8yYHOUS4N%N*@xzgf4aN`C=Cev6!JSb@$riOUM(~l zNbvN3|HuLArt`xE_N1TdUTTe6D*P~@*Y4H>CGTzof8wflH~ec0 z=-k2GGrES8<$QSVV?L_sB)^2*j3O%g=Jb!DlD^N_*W33vy9>(%T=NI&cU-2iNFHr% zEi`VP63({R0RdGP7UdNo8?f%HDY_dELPPUb*8ziS^o-VI znQSo9wsatfVH1)* zz~Nq&{QMjC+*&#V5Z7xhAmD0lzF_Gd)iHb4aaNWu3GG^jQBmCYkahTGMRB?e7ey?y z*ImPsH&6fVev{$R;T$xu2u=)IZZlcSJRQ0G)^w6cMrL-$L(=%W%&5D%qiXStd2H18 z6bG7|?$^dik%hPK{6$4Y^_?MW91s7?+|KT?q5OxYq3lr)y!7D_u@LLPIrHWLRcL4^ z+iy0iNeEry3KVK0=_mZ`nV%XvT(0budpNDps8~&}PHkPm{kyU1D!Hay`V3ay;;J39 z3(vhJ?!z{eU()A~ytpusI$q;w=l=HX+v<&qHG~}+_in>1=n5@nws-P>QTNtiRc>p) zFp7X6p&}t60@5iR0!zA4x{>ZKX;1+Hk?xY1q;xmZ-QC^YJ$Bte2M5a%2X_?c)!zp_{h<+?9;THM%gl_%qVTfb+9>NVUTlUL+f#yPKSv&vmaxjyC#5x~HGfHqj9dj&i0+&E z($f2xt^An0SU@<+O3HAFK4+IimpDbeM^}UAGd0SH+f*4=FFf!1dU1#%cYnRk!{9{mSS})USDyb24&rYZV}ej$FQk=+!LDtN^vdE;5(5idBdhI4|H+)dj#%^<>tIP=_ zuG>~3@@|i1<|J$|({D2iuFzo@W_72Y?31DVAIPZk?aMsr_&Md9R#nSI8`7C+cDe&8 zLgcndEz^7>T(Tu0xJl~oRc1#LtV_;80uMEr0s>F{6=nd1Y`HmUNVadeAZAVN`x_QN-kB*HAP27#0!E z-0T2MIgUnpK%iWqp0`jn7?5b2#%=jFIXOy#UNV~DxxqCjTmxP3;^*k~ac~aY3g}5_ zT^n>Zc6ad1S07{J(T8=BmrLR5mrq?QWE-DOPn&!V4D68XqowBEvPZ}j4hs)#n7b!Z@JcY;_CzUH80!`>v@iXkmQ0Bt;YDj*4YO?kvT54$M zGjV;I8~b~3#l&1(<5!{(k@x zGxKf_mlMW+j(WnKu;i0Kj#x{hY>RDmbwG&>HS+x6&~Ou$ld_MrC@3hQFDOuRH?6p0 z)nY@RhUG9QRT}g^&^?CO|Ki$+LO~xCs~45A402gh-C=?7VA-^!3=6#v8Q3A*8DUk( z(;n;Ze`nAdS`ggdx!D<)gPbj!MG^JVe%zN9I{+N}QxMu`ao&-JwDyesD>hDEuseBo zjq7b=&fKNr>s$!_s94a!^XDrR(pB*+6L4@zEI3OUqKi69I1t)c#^Ss>huESgpW)&2 z78cxXJ>-htR?TWg7S}AwI*o~DOvkc@sWxuGd~Hp=nTX= z3>5093hP?B7^>Omh>N&X+q#Acc=IQZ`D)VE&vVL&lWn7~&8M*>XOwf3QISz98^)%z z`3`D5$HvP(R`T{1evmnWKWafxgkgVnEGR5E%07&MfFQvsE%;GTLM>;kN8p$^fVnvB zUFy$89Y>k7^=?gF%YzGMi6~kcT1qn}T)enViZ&W|_uWFJR$+sfmn5|Omoewn$H(eF zDY3v2djXCZa;Y#W&!qXRMCaVJ5tY}kUruVfJ4Qy`69t+?PY$jup&7bHqHu6l5lt(! zIh6P=u?Z18H<#(WL&_B2znlyD4h@XBtz>IXFRP?Q^yEcMXuPbP+7hQ${@B%p*q)xcZeVcmyVNYhLuoU`GcSd# z=)-F%^017ww6n7~F0QG|%S#^ubU^cUlp2?{v1+Ii#Wwr`0!WBRYHAdUZ$>r*bW(L2 zn;Ox2e0*AIo(Rk9g&!v}L`F@T39R||HMg~rkwcmVg-T0GOG?VdcTNj?ut;~b)TQ^> z*>LF);28M;Z>5kYU7DRjG_DsuSalB>zBh_uY*yAQ7-*sb53mO?QnhWQd z8XQhQ?cL3+J$=49&1dD=0_?#Ywnf%m!r%P1|& zcm#B;tYvSvSh%Mz2Mlf5@;r__G`F|5L*3mkp>>DJ3?3V)(BlZLo1?LwR(u*q47i2| z$;8QC_q-Bw`My*$`c{>is~ZdqPp1tS$%V?fNc1{&zhFDXOC?Q3^)aR6)?=Djb|;|A zm2z_vuAbRlTVrP%>O1QA8oh4hJ2Ww}qF*vO|Kr0(VSc?#cXWNHR{oGw@u#`{f@7D` z@^TZ_x0{}SY#5q_9OLYV`5~_`b5(Jvs-kHpFN8b^(GBE~zujfZDI-2hR)@$s?FwGr zkdMu;?`_$>CnSJloPzCZAk8T3-C?sg#mwh5{J+>rq~q+}ksVG(-|4m&(oB1$3%i9Yfx z0_khwtT8|#)_ zU^F6h{8%*MV^+3{9EX9b8ZPdwnp>tiv*9U89uyF0X>NL8Cc#9`ORwE;V$ZRi$Q*_l zwy3ixoN+T^ve9On^-K*F14Ab7fK#tDu&%Zck1XbSjMs}_&Cx(l>|0x)!f`$zh{jwfEab^G*Ofu)S%qGoQ`F== z4~spV9=kQpz7dt+yN-ZTBs|*n*f13Gojp+3dmW>BQw}|E=W|{0>vKDe(z?RA2e<#^ z{V(U&?BOUEt@d%vXrZ6{=6}o&7Fp}_o^aJREtDveBN8-`l6P;fZ;OgaONoouy>r$~ z9iY_+U9#gaW6O#5N=(!oD=>X;GP%B=7r=}o5hNuQ7&ay;E{Z;AMDgv()ZE^G`4)2z#@9B1VipqeQMef`eOy1Dc|stAWpgETlp|x742E?D}Bw z0JC;yCoD=l!@%n}^wCINMF&YD5 z|2Onw8oizEsv%mLvRP?eSC4QcG?;1bE!uG6<84A*^{frb>g#2Re$wPrtVhe|GIPG> z1cU?0Ev?m^zE^lHJMo2;$MgrH@u$PcH4IdAuI2*h5Rf5tJX z#XDuA=y0ldqRMk3)3hJR!(Y|~(tc>KUTth|?<-x3d=ZJJgSS6nWj~gVjg4DI5wes2 zDY?3P^O}t}M{t!QEG4qJIjzC7p*7tH@GWanY&vm#TmnjJPNiTa$`#VY1o0Y_M`(TB z?F4vuvG49RFBFuQmRBJK3>eXCWJfa&=CUyVk(-xm82=2e!Cbh(YeMPVEB7=xdDB)1 z*c%9vS;u{%TGIm)uT{BqRd#LSMhwc7(-(6EqWxB}KZ7ZVy9vxJw?3hcFcET3 zRws{|?QS_7Oq@-`pIlo!Zt=PodxR69L9cWHDFW*0^|hct>e)8|`Y1?xTAQGd!UFb(U8FbI5<;^RHwwF3)C)*?u&|CHdxy{ zA)Z=(_QzjXKxt{|&8qcML0USkzu$LzPK}6>h#f+6xt+3{oGzWR}_w3yy6m5m%4oK&BCx5g=-+oHp1zbfyl-Z;3e?%OmwISTx1y8T?rT3S{wtN2tB3eD7fuFu7T+alr zm>GM|`2cMGPf)8QZ9q2tjk}Un<-Lt9nl;0rze-1bec@Mj)mwgoZZnQ9ve-&#f z1H7pGk!iJze2ff|@QNikQ>v#3Q6G$emjl$YK za4z5sFzD`{zP=$7Ct2&^x)p`F z3SCL^5W2hIF}R>YphCS#AIp$u?fjyH|M&?GLt+f0hn_wlA~?9+mw=cH`n@+vi;5K` zqbJ5J&6tKI>$P=UV`VqA#^hm+4^M7F0CY~f1JmVgbpm?z7zDuyKP?HFS)xnY;|xjb z*^p}OCGW;-I{S@{p*;@cA}@s0+CEBfr2+okh4;KvH>{1?H|;li0wV#$QRH;`zu3E(Du=cXkI~&t75#3+g@qjsz8c~rX&a?E zdmbO#ihpaWQI_sSdB0pX`U?s&F?etNR=P%Stmhc!_)bpOC5tDH_vUV<7;bhvZhD{> z<@wO73?wAl`xR~}pX?ho{F+|DRs`es{oniT?D!OBU`s%?pgXCWW0GQ>*Mm6jj3e%8 zkR)gv^cZt7s{VF&LD$lmesgQa^a;vScR{~>zb0mO~MbFd4p~j{Z z4`^#pi`jVb+1e0o?Yql5*cWyM0aaMV%{ca@0$VBc(cSUzj1z+OA*ZZ-oZaXm6Yo5= zP!Leb&V?7ipN0CcU)lUKjG0I`t9Zz)TXi9)3c$^{Stpsn83pW3h4~p z$~)!byD-cpSB_oZHaSuK`Yaj=iCz<03^;&QD!BNA_C)cS{)Wi(kKR1&jkT&A;6S{i z5b0Nc%L5oY1MU1xRVIuvo`jUfLE5z9IcUL~zj+gQ@;?<{ZTLhQMFWnB;FmW4I6%gS zE%i6<{MRI4^q$p-U|&D{LVnoqdO9?xT)pXTEtistsgr_Pe|!OAqHCqVjpuh8D5-&} z)yAu5%g5zoP-J6dwEU>g!?>~A%d4)SVBOd8O||yTX-C|3Ur@aXi~XZ%@^49CRaftW zyVKIfHq3vjK@$nuJE^jpilk6{X7=6x+fi5t7Z>W9giLMa9!^sez4hn&!(K!c(2bUW zqo<3qS66|#=7ITpPFaOn&W$E@8u;^(-7N0H<68Qy5Tr31YDJ_~0r%OFF#z2*cpI{5jk%TnJQT-WZef42?+gseA=<~#YfO=^1Q#{$AwniMb-c=KMuhu*xUT; zl`Y7Zl)T$PR4oa5CD-%M9&}M%^{0XLCKg#l2^a03`*vH%PJWh!btK#+2WRObRFG8TaDuMHGGj4qNhZzBO5mKz3upMVF|VT zS?{ou_;^okq%)RgQg>gGa8k*2Cz5}zUJ=r&w-!^p^|9yfr0Ertp`phT3*h2XOEU&` z=j^I4(Y2AniT6MT=IK1ekvpwzrMiwmf;wX#Z;?asF)8f$U;)uSe|mLU@0Oll?|5lH z9ks=e8gQVrO^UNmA2qZ^PULcWG!by5HFXnqCR5A9HZ*KGT@D>%jbd=l<1`{Hm_XzS z`*bE#M|UGRz9704Dz>N1#`4@`7WkO2_9jY?lGkqcS7mr`%$3V|=-D}F zdCkG@vI4J`#O3U2=>d)4N=H60mf^cqnwc@TtUKw-Vmrokm!ngStQNjJh#xZDzMNv< zg$Wj`HsX2UtOcOM$CDGN(`uFsvaV@p<%^c66Z$|*&J$eSTa#u({l?zoMvh}B}&_v#=yl8lI-y59z+dNI2 z*`c9i`D|gs+3F4LYPWUnxvMMpV(V-)-(Ea>HK zQarEA$94;~(#6CC4Bj(#j~h$qdA`SMjt@`R^#NBK&7F0wEW1YHyF|*T5oMsb5fpBw z>(e-zhbOui2C2w%&@IcH?B0Nh{e(S_xLT{p4)Y;Z_@axoAsypM2^nS zo+G37`*;hT9NT^GiAldx(sy3~xi*i_X6Z`FOTUuPRDHGsJz0-0pL3ED=Sidea5aii zcj^npVzsIH?Q} zW?F;(`_0gl(6htHTo=8yqx{-~r9v!3#2$FWqw~P&d%n7Qg5xdEoop&y?ec)Q9PoC* zes0e}5HUh_zkCYju?MSF4AZt<1q@nkHV#}jt`eCgdXs|he{YHDY6+CkDmlIgatGG< znHu!ML&|N2c_MAO(GR#Qs0=}-m>|(@jLfyIjLa2==5?E;$I?hul|+I@?o`>Efp=O? zW#{YiY8#&f(nL}_G`=o}5kWJ4y_E{`UtG?IdioIk~Yg6u)) z#?IXL9@K{qkB^RuRgszr9-^b)T(8vu(RsU6&&$VQ1qt*yMqJd9yS>)+u-x3rdcjwc z+vc+kpUs!;Rx%mb3 zVt6dWpF%bi=j`O4o!H*M6S1_WQHBt7u%~d8^Cmg~AII@5ADx+emtjV_ALcx3h=&Ziq1SxSK{W2)@+ge+8eF{)M_6D?6O>mnS^X~ z^|Tj2e~Om6I~UvElRa)m4PUsgr*_$nnpL)Gh^ACm-|UC*0Dpe2n!I=dW{v9Vz2}nw*6#_Ph;n>2Xsp z5kC1|O7B_xR4Uk4aaBO>9Qz+;^v#`0Cv}E>CuA`5d_jbpx(z z(5^49*frmr@0zOZPAJIRL28BH&id?3CElLwxm)Fa`{f_9~feizSqzU0iw=)^oJB+&E9e_#@Rw&;ovoOmGE?Q~FjT{V<5oS`s+CL)# zVekQCei^+6{d5F}IJif{HjG=t9Z^#&Gk`|}d<}W<@IULru+Qd?D475Ot&VsFw16)D zLL5eB4FQk`I2y2{VeOwb{O^8kmcr879{C``M)xNsqZ%1aa)-|RJ4of_knOv&)L7Q|QymLg&&MVgc9s$mZ`?2ir zEoi9u#}E+juKOpd^M5;pyY4`bcc=o`qLo$(WUg$mu6MWvSi7R;XQ1T9|KAnAXyv-w z+PfvI(YXdeS`Sv?{+{^%-m4J8R$ALBZx|RzG!Kkr!5x{vEJzXXteYAr^sx4yVKCwM zpu?l=f7te5!GZtf;oV&RM>q=LKMQ%y+IIxu3j=jNXqs!pQry5UQ-Rr(u=XEeur^>e z-63wz0j}0y_Iv*}|AGtt6O#7+^BsT{^mB%m=XSAK8@6IDM!)_K`utZycU`Rz4IfEkY}8+?mW@GdiRH|W z1@{a2E2iR?{nCpATA#`-yfn^a0$c%@t$^Yw12J*UjxVn2^S^4%JSCmd;^fS#Ym zAnx7#5&kRL%SPKg*L17?DdV_m4qnxlhy4hq2A67C{5k^HA`UjC#6DeKyN6wIEi+QzH6pI(yB|knE-~S$_B%p$`VG z=DEhPRnI|T02B3K+)cBss!1EcJozr+L)5i#F6#+=lu&`?U43y?fa6*T;PSx$W`sFH za*!})Ur6?ok?qXuN(Y~-`!KlTW35f*(W2Ams1)4MJfBzx`)}RqOXgX@2+e7)S{_i3 zBoBxsZzd;Bh6zb2xzs(pXzv@3wVpu88R8f8u+;h>S|CXd6RUt5I2?iS(F#P>t=Ou@ zp(fS^F{%a9Vni;~nOW{{<6M17>u>l*@|L}<0|tj!Q)Pa-@zlCGM3tA4L_Wi&MUFTb zjptO<@0%=PQ>I##5j0AvAha0Ux?lb^9~JJy?!K1G2OkgviomGl%0B@8CYiTt8eYndx~qa zJd9RXUHJKz(>I5NgnA63m?ZCp?}^X|!aM>wX#XK4NXy8Sc%r>nT{Nj3fJ6KyRkHvShj z)kQK)O6Tzd)h4d`p3h^U_ycND^ieM`)M=l1_AS<*!WF<>nD|LnvdLyVQ`9wumfED{ zd0Bk!pt|4GBwxO7Iw6(I13!4%1fPG*XFVz6BlB93h)dRnZYH?ndSzGN!ROsQxOpD{ zEklT4tSZOV0rhc|%R9^%9oIk^AyLndHMi|&|eNRX@z3E5Ca zqS{`z5ikyFb^E>S?Pm!h83a+aV|v*9Q5BT3wkH_LsD3@)OkakcNmzT`0 zCe0t39+Fp4d_S}mT~kz=lblEKN@zmO7?(&4wIqSlCWW1wl=-$(x%e~B355jprsR^t zA`^T3$G({QH#Dpy^gKGtEofMV^uy2nV5Te_obp3#=LYS)HwU9}6zQr)CRAJlvabvp zMpf*V&GSvG%w`mX_LMX%863hEhMzQ1f$g*C`AaLhumB)6+ww&E$Lz#CedI` z>0esK{}T$?o#p^^R;)!`*_Ixrl2H^|GnnQHWO?;{;+BMeX?z1PN%^Pm=x@dFFDVLh zkb(A(-#@#P;;RZ4uAx6tQGgiz)5}IDA^GG^vn{)Gs{Pk%V5$#j{{(DFQem1xg5^(4 z@WGu$4cAT6Dg$=I*6TkhqYnrb{-s)&sOb2=oWHxlzq#hWYb?)TddbnIt8N0g=C6^z z{vAPW2UD6r`^Rs-{RW@0uzrTUnEFi+r*~=v%u{)k_WR)fo2hx;iNk-c8SZaC<0&tg z^xypg3xEW?{I}{mY&IV>fhzbf&lOCd02iUXapIpa9?Wn6?H}&Uf05|lQ~#|K0W}|% z8t~^b=ls*n_CMYN<}zf=zk?0W12BET!ZmCs|5nofKl=Q8U?6b&A8t_C$pY=)s;)4X z<_Aq!nDkpRSidkQ16*{awsqU{l2eHw^tRw=92D<*aCtO5jLN#hyZ6wyCygCZ#e<}y z1iBx2-;sf>?$(=ce_;Wie)~!Pi%s+Qy8QM_-YMgZI50i4AwI+y^RV1ZwU9%S3j2YA zowK3?e}OvaaoYNBFr>HIG5DitLSM*pCu=OM+g0bpCCly-an!>7s#fhZQ;UBcL|6}s)!0fWqe`hp;bS_TuV;ax8;=hQ z^tsC!1_J4{s&*zIN*U(4ZRj;~de$(nh!#x0ZJ<4R^i8YCM#|#rMPv0#DtMXa(* zX}a8YXthN4NIU@xWss$@&Q0us`*vaT3TY(eJgKNqFXQ=S(WL&7P zklo$cY;q)A+|l+)M%QcWU;Pci?kMh5Wa-}A-Y&7q@~nJUt2Nr^3|f1beX?3ScePbeWHWi8Y5MS)<|B1d!#0pZdfRqB0!U1s&FnOkSsc6@XCl07EUAMb_J zXvNS7cNP}6^BD3sUKp<~JUuNN@_6%cV!cvEe|ZvVY@=Cz8=gntus!%?#CoB0p?_)a z`6ps)b43fuF27MH+?R-eYE14}09z%K&P6WZ$f^nTp^Er&S`{h-D(Pl}QHjBneO0Dpm%hdBrWOG{&Kr%N z^V_Dhb@6pPx+=~)$wk&D4BH9zYusDHs)o%%KAJJcrW}W@GWmJQQWMu7k~yl+jCzc= zf(GyiW>HVd9MH*H1WlJ`dlsa6tZbbuJU?rA7i}73|PQ#_@UuvoET0- z7U~HeS8kSdhxRRqa0-&v*{hLPCC5loN%=ACZ8(=1af;f%E5S<Z9OiSYS~ZXC^f%ErLy++ctJC+sVfpbA+WZ0#Cqe&N&?~T>Z`-~aD|B?2}6dn zk8JeMcXK!$6~O{eYyRHYjIL=-)y3VEly5yM<$_ag2kILmqqyQ4X0x9$C^m(TTr8yq zGMYxMD_g=NOI{I=znh^5>YlDK@XM>s|JXSoX`qu?zP%o&zne&|RFzp{dqYqC)Yi0A z+elYOvD^E5>szT~e>CryTeQH#gL`d(lU2hSiOYYY6n7X`wEb9BX30tAHM^y>w5%4d z<`6}YD1*9O^s{lftJ}KJO^Ye{8wnq-9D3YPwx8oQtfM~z14GLr2zV6?>-_9UUphDG za?9QqNk1a`$OfGZMXOGnWaMJSv$DgkE$dT@cxSrhEukTHkyAb!#_|l0XqBnPqoUm^ zp!PlBkAK5+jJ`fdc+xL;)hn!ll#(L1*W!3VyF|PBP}+IGLKmH04!T z2nC$2AgI%Zh7sV0W<5$YGZs^9ZEw;ZY;V=@I;`7{v0PHqAn8mmGnntAK5IEI{sz$D(QIy#zEA;XIS zcp-CvOHe~XX>U5h*l3fDJ7G0Zv)I0!O0^SGLGp^r-44_75qhQU#wHuf5Esz8&Kd9c zc!G!C?dK^bd(t5XTP0g5M_UOAQx}n-6IB7^49LZHbq@~zQ`;zmbTY#{v2JzUpYB&X z#Xc$oAM7jgOVh8F-%ClIi7F1LIi3g6B_D`wbQC_hoIfU$2oGboMk46_wR&{5WynSj z!Z{im1!2-T7k`tMyJl{l!KXW+bz{pE^@J^vhy14fnpHIycRQxVuBm54PHA9?Q+IrH zNZvj>%cPSoPwulWI>A3d;Nm|zpKx(m=)sQ#L-IwYBkpVv&naSFa)oJH0n^-u#((9G z10eKor1($${r@O`i?~2@$FMEDSW{ zhH`7R->R(Zi{Ig3Tz@md@7(J0vV)5wG=_EVT2fLv{QlyEFQuk_ejZ#Bx!qgCkK1fj z433l@UvAiQpPKDwyeG@?HepAk~li8GT&KiTNM37 z+NoINnT~H%9uLPX=?>+fj7g0G+I9L@_=Bd0YtXZ*+39^DrU8Ax zSG>5$j^!cR1i#iiX&BWoV*YQTjO_F*&o54JY?o#reckzyw+AkzKIh# z&T6-Ivt1$=ydRObIFttI_4R!V1&_KLCP(lY#uUpPr&nLwrI6|_qd3_HyoGgE5dg9# z@|xSoX~o5qE@`~EtqVYSUd*MWJ(@lLuo!Y`t{3S21wo$uu!@cw1yNJj;tF|G)CCN#3+J@}A zrfyI&q;SrR?c}*R+ty*cYjx8u88;5U|L+q03Q;Rh^d7Cvlj5ZTYLXOLe z&{Zr;I{cok=%pJPv*3kwq@lmV;MS-WmQYp}l-2P!Z&y@xnAKy_bfl1FB<4AdyZTfz z`SojNVq&84?3#RmqSw%nh}s&7qM_KP5XDb> zG|JA(8pvC_muQZu*xub9>fV|-s-Bl;8UK+_D=*lou0#p=;ieH}<*>;pAb7EH{^GqL z-)O|!RLU*gzmv&0q<)Z()Xn;>rgq0g%8R{ZKdk~md|Gc(?c^s-O-);)X5nR_s>7~(~#xv^?p}doL8#uWE>NTR`2C~N&-;pak zNOa3XoiML$oozl(%S+7jx)&gxWm6SDNU(8Q&CAi;Kgq(PcXFl_m`8gQ)*{%7SHn6d zDsGeUs%cTR-ml81i>FqJUMFg8xx9qU-p>9zZ%>cr>Pe5N(tIs*1*i?biyQy;^Zf|% z6)O(%`jN_F4qGu-vrl6}gL6Y$DV?zrQc{@sUKy_->96Rh>FWCj2H3dTv>1 zV!v7rA9Z0}a87j1@wu59xnkh-`4=e0jc8@DoW$YbZKhG`ZOc5##t8KD+uGcut(K>T zQ0S-*ax1m3@UV|xrxj);A8hWiPUfu3>VpiZxHYaP$$s}6?nmM$y;{!%oFC?mTlJrq zX1Hl?Z3cn#b`JIrj6ZFoU#(1FH?-3QFG~_%<=qS{s&y-@5ENJ0-?`P6pRe7XM5cwyir3`dD^u zrVhzc+S@NwdtrFFt{Qm7< zDL2Z42O$F-3|L?P!UBH0B6%ZA9;W4Jg-un`DcQ%Q){Qxc2KQ*LD5SCP4z_XteCus+ zo(+U%t;3u|R*o5w7CTUQiBm3T1a+bbWD{NMd3kve6DuWlUe{nRg^wQMtF2Y}Xyj#e zis|Vah={Z(DrpUF2t=(cm)n= z?hl(=L`v2Ioyj2GP)-g?S>f;TckO?cutjya;SKDQE53WF*Y)|vZV}5 z(G<^B6vI@^DXm`XjBrvofkb$l%7)>LILqsSw2Ywd-$L^~JUT?-Ig1Z1p(NXw9Qtye z2L6m^{Uld2g1a6?=09d$D&R(!!LuQI`O>Om5h6P*D{nIiQoD3M?e0ob?e%Z*i_<@g z>ABI_MZ>^g=;{gM_is%{>^o4=XSz7lM?e%^q~8_j;>Bq6A6Pl?Q^#zkdRSo&zg5&uT6*DwO_z^@*E#; zYezs}&~RR7;^KQRx^YP7oX1J$iEN&>k#W^K)zFYS4$jvqlX5+vnkB!$u{KqQv1~j{g4zE;@)z zgT*N%sc+WWuk`}3^=x=c$${wOst zYTunm@S(n(QjA2#=B=sB6+xazxYB*{#wshiatJ3T@jj&`287pRD@ zR~8LJ&poT*?;&9ar>~}mFAIU}<{mnq~C+~}#G`N(a zDjVq>o8!11!`?&`42-0G_em~QJsaXEC#bB%mh{InS%P^h4sNcR;d>*%W7XDSZrI#T z#K=K;e%_(Gi2cRaTjI4~M};uu?x!N3U)ESi;Jkb)d25`tp?lBwn1-RNBkcCQcyAJ+ z1pBK&HSupU*t?4ODsDIF1D;Fj2hZ<)c+16?r4KaIhFsElPn-+2-G~rj`MsAnNXeSufU>aA_y)T#B0}7wx@mzkLmQD|(IDXB zcp^+CpjbC$sj-nH%_u!9YrrL|Z{kBK8r~Zd@9)OO2}RsK`u!v~m=7}H5k*ur+x2Xy zo-mfMIJt$1iFLq#{_Jqo|K(z^-ay#dAT@cw0BDrK@gjP=q|7&9dbB zSdc`{@6Sx)`8;~L^zd`ssp+I-x+K5J*nDf26(TcCQ}@x<@k021AO}`M{Uun9%S=)0 zE`EW=DEf{Na#^{-Rs$bz&F7tIA1?fh`~$+u@vxhQGVUALLW@}Ofs}NxD`^vK1A>K) z4sq>S0#&mpF)|g>YdphA+h(l=%NG?f?r7+~wZ}SQE_(8c&JG5XIuQQ%YK|)1*=kfw zlOrQ5cAU(EI6>HZ%CS4Q8OBEYp)+aZ3@D?BGPtA;^1XxlO?eAlr3#DK>-NOX33cb! zoP9CLm+yRM2KcfRxAq_Qai-szszj*M_i5FUynI=3X;V5dE~i`|4m;Nsu%zg`lG5^z zqmJr{VU~{L6zXg)qw|kRSFrBiXT3oiDvd6f^6l*GThn96e?KPpMaa(HQ98`x;Tc>> z@}RDxOh)wcQm!AxF6lHfGO(- zB)e^x&Z0l`Cjq1pdVVDW>HFi|y8_sht#Ck*eAlq9<;fo}H%umqUo{uS*Ze^>)o+B9 z`*{rQK36qYA67zVpBhm>><0_7 z*qc{u05`;>N}yQXpX7ihFF3)Kp!nruuyUUe+Ue8Xd#XfHvfo zQg=ksauTc&@=bJ8rQE2l6}L=_3* zflj3sQg(u>ZeV28j*X2yJ3LKU8ysGLd^&4|{}`^kWfbD{yLK0#jbdLDqE~(2(#t0+uMycbT|ZYNHmC(-q`AV zXpqJKAtt8sj3_ENJeZb;HTK(Jf@@CC>od1x7B(~2EyIa|<9Gn-C!)K~v+)@aqM zI+;pT4}&|FV~hiK%96`M&KzjcshK7XnZ)>ZsrX*ovc;ADT>O-kzvmrOKdzI{)uq%VrLb z3^`rclvY@;MCeyebfpbTUh1hJA_+gGuLtSs>8-GY2e^59kjup2Kg;{dV&Y;@2T+7I z(dO1(oQ~%RepqzyQ+%wc!C6@ix2!!ANQc*336%?Pl$bYfXh~RJu~1TqRzVnA2xJ?G zd;9|Za_ZE{$Y_VQ6;Y%RMtg)j=KJNm zLyw7Xf`zkJk1&C75mBQ`J~AXRru1VLe;%XSB@J~~oLZh9{YhYHPp%YZtfeS5b=URG z=VA3227MMEwXI@ZmeV@NMI$paby3mGPA$mc8Bq++srFG^wU-&}#M81oAlPplcREsF zYx}049Zfz16pKjr=909Ji}Tf2-qnwUMS51m^9hyH>_Mds@^8dUMucW8Eb5NIN>YD( z5SQItw^I{QTNyb;u>G~w|EhcuD07K25L4F;M{6i+)ifPdvpOEta~JDQh{GOdZ+}{{ zp=C(8uoSv@9y+E(6>M>*g&uOuGp7^L=`#his4KW_ROO7Y4vX{YXjjXt@P>^TJjI25 zt%1{{w8vVfi!-4>qB*2n^Oo2N5sei!j%(=6Wg#Od)}Ts}mr< zdjvnV@*CI#X|~9YWawew-TxGH2z>u{8-923KH=RmO@MGa(Dn)chz!B~&qhX6Aaw7IwcK1cP>IK6)*W&SI$4+epylw-iWq#!O4=^j!Iqk{to zNw|CUZ{sWXNj$o02XzOM9l=r+2mTi!4O>n)&%Gc2W0o*1HZijqe9Zv=&mbDlJ`lB9 zL+N}K47(Pt!4drA^&?&IgBrhI_5a)82?kl4Nbw+h2oBf^_T3Q~w}q;$H{taru@D(| zXdGCKd12UXV0a%G5BQ-3@K9`)eTt){e%A1X6Y6}ICdG)K`5%HKK{Tc2J589&A8uX^ zV7?4#o}a(L!_C`*N|t$j?bzibOWvlBLWf!t5p&>MrU7C}?k(a!ftfc#Cm|Zy#qs_D z-1*b*YT#=0LVDef%@WktK~I}V>aG@WK=y@MFMOBcN8X#MogjDZHu~3X4zyYa$Jr-~ z*hUJQ^)CAdCGF7z8K#5G^(9AHa1h|N2lymxEzYB-zv%H<9_wL{0xt|iu&k)&v*9n zl-c8UIQxX*yA0AN`1^bn21|a~{^ecb&E5a2xbKW=YU|p~IVy;#sEB|Fs0bXSiGY*< zu~3vMpn$X}NH3uW2qY>XQUvK8j(~ttBE2Q_UZwZmTj(VTBzK47`@Ns%yx$$;jyvwS ze+Xk|XRo!_nrp5(=kv_DUf17BRcLJR@s?ZKx#x?`El~%E83Ol( zC0UuXG+Sn&h}GH@H84?ypIM_yL0u3P8xXXumkia7;Dvc6?= z-V`64vePQK8p)k1Q{Z8Ou0O;UlXb?sMP7w17hUk1Xf3_q#{@v=FQ1i_I$pD#Cuhe#}al;4ooU zqQ}6iU3WF1(Rb3*vgZOS$v)B2ER2GlMsp>xjJ%q{QfWP3lDd{gy)Sq6$q-W3&jtmb zy``2t{9PCo5w752vkxO?ognY+G+Kg($qNa^sd9v>DzgC?9tBP!^p6iHUR{wY9yV~Y zwAM}0@UbjFV^FfA;*cAO1}L;ht|$HskhswW%*mp6L>!_VLd+xs)OxlKwPh*O)x|YS0l`d5k@7kfO}VVW0!f1zYeD3`-O*$TE6;ys z{Z_4Qw9>Du=A>-4#kSvD_+gc61=BUH}@Y?LiBHcoivAM6Zj zcFG6}=W_Dy&*-s5OOgE|R@iuSj)3AZu9z|-ry#?WCDxMe=XK(DHlWnwy;7;9oqk`^ z-hkk)PMa~y?YS+q)K+!Z5{Jk&v)qsr7^WM+t~ z5YS&h%__I6m|pY$9+>6yv3NiE<^xf@~9(Zw2T=*z zcS%m9cr#!ZeG{zve?)>Fy%QsF10uIz^WYb@b54ka{V6N41~Y!8RT(W9*EGODNHl+D zi58WWR>`k{9?lYtjT3a%9)(%iC_ncycJ=pu=&&AY5_~!9istq(ze#UTrdTBNRq0IQ z!1KMjWBt>mu|t+XrL0jv2}3>h-?R(iZY`*NSy%>x8_F^s3;ABQ`@n1^wu|x1Jz(S> zX8_s3KLkHv5NrLR%lrL1f76>sY}ax$>71So^lG^@JKnqOzVa#)0CB@<0rF%vS+VM+ zRkr1#f9IL5Q&z9X#74hAOjBv^ILmnS-o?!$+;a5hU;Jub3cq@9nL@A@li|Is#s0&* zXw;2{dQJ`~(hzo8O$vVm^M5hTT{mGp@w{ua1W<~dphx~R6&pW{eFYLBTF-ILwKSX% z1pj<6d51bodj#XU#Elyfk~A3TBl$XVHx36se%JT5l%{!N7&sevs6Kb|sK5G(ai_Nd z!vGZNzWcsj$ez9?3OGjlk-rr1>P?5EGYhv5XA>h-=I*`-lyS1|sQQ#$hJ7CU_)IV$ z5eXIgg|}RcQlh{52L?Hm==WT+4l>c)*5_h0%imQ0>+2x3i_thWz+!euX04lW1ho9rd<3vc5(FiB%E zV=->&1Fu7sjT*zm?XUR`(oh+D*;hx}xL=gJe&g0}q4%-53L1R0OB$nh8c9gVzN5!( z{6Zpc^Jyp4KAy}Da)t5n#mMaVlO7haN_$lFF;2PmeO^Y7dg#p+ETJ}fd=(m~I}&Xy zrH^3!qtYoaTZ+YCOysJBLWLT=4byAZmZxDU3RjeQKh_(ScV?ga%EvRpAP6?Mv^p2O z#KSWqfbdZEQ>+jF+DsqTz-L_QIF+}t*Xk?6M(&&-yWANBPTTClsyDJsymr!Is`l7j zL9v0V80K46tA&1yz;k&#+jrX&OzP$4G##$KXL)stF*0iQ6t?kvy@f(-sJ502kB!l% zd+D>2)&2IZ;85BtZGwu@jt4%=^NHQL!79p%e&^vf~St{)#X=LutDbTH3*7*uB;Zx2_R zhnap|kJz6wjBj|=or?X|EC-8c24@*G8OPm5dSe@k>PDwS<5LsUSkZkA7XDq%$!TRb zzWmXz#k#QS{ccFRoyj^k7hQpP=@It5?(>LXy+*)-V7ZHytz8=4N!M13&*ihs4Nz6N z%~+7OJS9WFFT@Os?!5F*D_X6vNXdgD9L~{sJ0H<1sYi&nhZ*PDL6|-RE zg}@icJa?oCLTx{QdM`LhD)YOz6b@W0rm*-ZqO~Ucwdn(OCYw?CSUbHDY&sz-mR?2l zK~Howk^9yf1ZiDz!Bk}~)cg9q3Jw~OeoQip-N_TIyS_n*O_@zfoxHs4NUzkC+`ZR_ z??P~C14Sa(E9=S{xE;BTq0 z!-P+euOcP-{^(c+ww1kS)E8bvZ7a+xx*8r-5e{4Q0GZtriMMt0K=d7Ko+~dh*vSRb zQ+F0Q*(OTJ17f&gU}dog5~_^()9-3_r@dY{4_ms*br~+8UxZw+^4#>df#kh*6Sqly zWSN1QTn&aIk!=MPZ{ON|FT_f_Drtmr&6}7eNOt-&$!`}+>nEpa>h9U$WyK~Rwy^(73C+C5 zoFnDH`mMY9joMj(Q0mQ=DX%fCC$#KUAA1ff-$(p_74H0{OKe}=k%#o2gk_eN?exQ4 zLefXJRdyyO8ZCLnXYY#5%&k<-6h**8YL8b{N8!W*%TdtPx2J-V}p+q7}#MS962BkSicy@T2K zC)raj)s940D+#L8NbBxCba!a^=0;0z?1$RPS1^>?k>3X(S^At9(S@*9G;c@3VM5)i1+ zGhTU_DD=aV;H376AwFN!hL000&AAA(w5{||5(mApXB*7hOt8EwX}1@DFGY|whXjSV zm54wfu3{f8ri8Mt3lqs6IBiXCE^T(mUL}f9yCKXh{5idH9gaL03!$rYX8r`|Y&(_S?;0AwTXG zhw`bJal_OAYT2l6}Z4`{d4~SS&q#-bEGmI7w?uE??0LsAJ|if z$UI*;-jtG3Ixnl;+&e8HSJ*jljv=E?BfvEBob406k!}lsm98HX%5|2M=(x99Z0ISGytvKgZ4U4QfLNL@nWZppu8OE~gaCjw+VveH>{S#}B%? z3B&Way1;jNhXvQo#|ratZ6!Z$aH6dZpH$cVdA%?jI5cif<^83ELhwFotDcU_ZgyDQ z^705(_ja0UyYjWs(7neaMBxeX0#6`V(ZxLVv@UZ*hPOw9opm*g zroHRxG;Pe}8^~k6GLZ?f$O?t}HQ;@KHAAWn=%k98^f`g>f)8;=8tlhd*`5xIV|5m- z#A8uX{SKCG-rr)qT3dN#DprTPyqw#)$xB1&zk^D)WZ{evISWPJ268fQ)Z{kSqu6Hz>Wz?RflXhFfWsjUM z#6Y890A=}ucM@i+rh;H>OhuPu&NL1w{9HJPKY@5B#^I3wA=EyxV4i9vCRU7R11A}~ z+D$Ya0Og&csv~Ayt&|kh_VFEBV!dL;^+R~)nZ&j9;ZBT`ISK(kY_uK4dODeDf=X&Q zXXi2KeXu9g6;|y1rl~X@a<_65O)_e0N;+zg|n z5MB^o4eviRqPiFn%{0dSG)Vnlv4H)9(h>@_7j(gXns@pSwEl%Zo&gb_>yjt0)g4D( zuIq*aS*0LQ2=^rAwHMcWdR}zJdhIMdTE(_e7?ITs`V&c6ffTXtLYG2;b7(jAxAg zbWaFR(zC66wTB4_*;qNsbhavH8)gSp&2a()n`{Qix0RWp^Ul{xz-l5 z=c>HK^LOf#6>XwhS(CDT2P-TE++D^>lZ_@9}c3?XTlKkXn1f2p6exRA-{Gg3|UDpaxl| zidWPUU3c)y(!YW9<$pgv-PFwPflmAKl?NoS*h!k7psF(2?~Cgny<2=&g6C^WIymyv z1$f77VA}{IA7Fx#(2#?4LZs%@V_RJUd4EaJP}s2}t0uXtx=wBk)wA}hY_oDe=CJ$o ziCPYJsQa2HFO0O)Cw7Q#MBcf(7UZK;>tx(oF@_=pB%KZRFdA|O4CFMyEX;1FA-h{# zjiB>X!O~3Q_T7AX<6OB-Rnd4`>G5GHB!&+?DTX^(G=47VVb(SIe zu7~*UT|=|7jp{-7!E~s7bmgJdK3^rkrK>XGn!FzL#Y)S!5y%e#YLiBo*$)Mt__)gg zhoM}^I248~Hr)~PTwL_VjaMTfk7}3XGfGoDTPvj5^<3KHlS7s0#A|d?`6?$DztVFo z$uFj(ZI9YS#ljaD0~6iW+NwW3W*aB0HZ=i;oNfC%xq-I$gpl~|{xJrO%@aR*(nzDs z$dBRfZu^eBfWyExV0)Eu-aThL-^mxfbzmjwwCiV5x~l!REfACRC%-k z7u46eC<(rE$Msr|Eyt^kTGCrm2_3$$56&O2MVfSENg675e0?WSPX6KBi@Dwps)rn} z1c$4(tN_-MjSRkF;ljz0p#H4b;_pU^o4lgb?~3`a1o$*I7Gn?d2Mn)D%Zz%>Q1(-y z?Mizxx)G}ixMjc_=f4^#VpRw(fMY{2eEdny3pFOF!+eD@cZ57ur&GP3>6v^-#)yuC z^7Ow-sF8{9I6!iDAbYhqSuudiJJ1_Z!`UfEQBrR;&J&uy1Vwf>0 zTwlx%iFBTaah+M})h0YzW+* zYFd25krVpFjhZDGg5@R^S=Fj2D{-kH@uRSdAWS9@(X1jqHG_3RY{oZ$#c{aaNm@?x z1S5z9+cL`s?hacM*(l7H6yw9gG~x85ajdbTDvpqVZop24uf{*$zQTEuMyb_IH7!k*acyjj zI_~OwXhX28>3Yze$r8k7A1Z&!td^s=BO6m;Wb3+ciNi|?jw!^`u^|rjari?NW}BWo zN1R||CN~JLoXK;P$=iv=?m7!LjAZNrs+gk3A37A6y9u}V=u zrR&|30gTbn&Q~x`8#^}6(l7-e|6Cs^X*z9gVSW~Ly@jjp61i1D#vmSyM|4W(n~-zv z1ePFyBQoj_*O7<$fDco`M3~F&Np7y$y6<+5p&nBus4D?g<{GW;Sqt@|eINbs%L1DyB zeC0vCG0I`$$OM1N`1hbXoz+aS>{6TRX>K^iy**1JMXk`K2(`w7lF0m?F)wSpo^d;F z{V;%RSJ64Eti7;k%V7-2I!l=~U@Jw1yzvtM)(<-kyi`A*j(()}MxB))WNXW@mct4< zCV3-r10PY0^uQO=S-VNPDyhPq0o}ci1NKeQs5S)SS|4oao`cGWv&aA}+kH0AV}IG) zxT|0(eRza4>!%PE(SG(s&>iv9;}>aQC$BBhhp{8(>eF3dR^Vb9bEbEPL~8P9PyJL4 zb@AQE(ugUssgXFbKpFtjAavZFY9q{RGDhDKHPdjF`e7#pdeizCupBUS?See#f{kPK zD_reROm=O80<`0afL#|9ZygWVdFn2>MW3^~{OuE?JF|;1Nu>mWri~4UUSEX)dVNGA zI^u|!M5uYx2^;=yHL|!3E!`}VkmIZ*rcgIPoYXwmP^ei1F=*fvY=rE-O3f&)_bCSJ zL}-f)D)LeR03`ulK)r3q^p1fCuX9Lqn(VwZRn&D|U0R_P7&@HpChyZHWlDNX(jf^O zi<>)s&DHh8%A=CSd9O9bqxlUcA_uhXZr!?7cgcu6-A`BO;uJ_@-$Hwaygimg0Cwc@ z1BE2Pu_jd&_A&U34PhUS$uQE4ywYp*El~&%LsaeV2&B-fr z*IHZ6BIg;dnP&!A4O=?(<4ph`jUy<>%_tl93k6x z-25&fI4-fRv&B$LW<^+gV&jNWaM#TjVHe0=d7mz*giH1DVfVRAvn!WvAPxt)+C@|# zJ2x8f55`NeeN*a!Aiz%-7Ep;;_S+o5 zx0lO=#6_y#wQyLjn+?L{Ks=|pVV^%QeaW!RBDZo6jNe1OGFz9(d`#L|fZ*4HJzdD0 zV!f^kscOm<#z66k+flP=W*EDd>(DHO^~-0FXP^?PzZ5M{{SJjVNRn~f{n%>OR_53> zS@SVXI2$ryxckGgvkrhfoaT4Zu;AP2DkzVULH|(hqUQ-xj`KNktx=Kf825%%z+P8D zz)3yx>AK(T1t#I|;iUyNMh3Ouv_-fmuHon>2{La1vVT zref=Es%oiwusfgjrO_zYbJrO-n51^7S|d8)HPt=x_-Nbqni=_JUGlY(8zKH`h~UR;3#R8aIL z>1{qgD=P>4y+qrXSn$^zeBC~DP^2uW=|d-ZjXijj+&{lN#gFSTcyUE zOhEa7=sg^+Sa z*#+Om$h0)dh2*L;=cjebOr4LpyVq}0NXwx6v%rP~{I-c6=;yzklum)$Kbr;JcRDTp zA;t3OJ>~o4M}8D|{DRvtxO{ypJmZ3bDiY~xZDC2de*H|=PYbV@a`(-L|Mqo2pbt`% zY_Wg;b~X9oHU4uSkG&z^f9Yc28Iu3mUt+sfm%Chns0Hf6vHz>B<3|3QL-n5<*`w41 zxUQQu`JZl=29{@^=RTFBJ*97~TE7g#e}1%!`i=8M3PgW_|8KV+4-WfjPC4Os#ovlZ zFRLO{03R(AS1$I0QYFY0bx{LT$#YyL;J^EU|G&HabCxJ0?6muBL#Gh=h%!{_*MZ55 zuX#uL|L+t3tMK%^4hOc7D&3Rk9cH` zAp=_`#dH9Hu!=OUz+IpxvJakJKSSB1G%aR@UPzX*z<=J&_KV)pq;#r^`r(tVz{%`y*$^f>ljB#} zczZq76-5tp{vXY8r2tR-`w;!J9ZIhzrSqLOx2BV)L6bJek6n+Bnah{ikAXf>jCJ+f zMjKXa@j?kv4EggXVGWfeU^AiJN4ja?Wc5cZ#8CSrL2_&0PTNLpxy()%YPS#y zb!$IY9hYmp=?^6rPUz>EzW~X8q72gqL4Y}5;N-6A0vP5R^Xcg(-pw^x%nV6Ky`IhV zf;=;e%gbkjeb2L>9uK6%2jB6u^QX(przyuszIv!(FxFMHCp_i*h129!x4LHr;de$J zUVe7r?vaVOgouRbXmL}i!z~!pQ(Io%6r^9h0}MB4I-uT2%n+`m4L4J=QgYWNB)4!* zO%|=8<7FJ@J?MsV9f=zWy#;}|L`_Cdf708El9}e_W@%|@v(Chih@$dxFptIbVt&1* zQq4^NH^v%ccXxTDVKs!jSl!StI8Nd5Q@KX-@oj;t zUBcVj+w;%b;zaYO*-4vd1kv3K-s7E`+8w%NM%}^ot@Np+a0(_l)zw|zU7jy;uxq^E zDeo1rL;GFv+AxFI6%`(42&u(NkWJ?9J#R(j_&bgbtM4#85<_9b;Es6lAsx?`Y70+| zPOZ}ba&Inp26R88vXcB3Me5%;O6IA5RdrFGDmdq;tI7M_VU3_CiG(G>(Vo*?b`Bf! zyZiNS&$_)+J3Z%>ULiL(I3}!Muz>xHYMoo6qH8!Xgw&P~q`CyD3GzZ*7pwfjxN9~u z=i4UoVfz)H)3^W=4N{Kg9X?CYbL9w&ytDo);4Y82*Fx!gk`94ih~;Jf%n-| z)0WH@k-B$J^kh^~=p6s)%E6j<$=w#h{$~QXm80z@Co5=`bD|~S)tGuuPfz|wRyud@ z-Zg74xh=}M<`Z>~8-wk2wAnf4sN@w~w}61%F5E<7`ftQP+gj941E$jOS%I)Nk%W zXz1hq3Q>aW?DI`K&Idn7S6pW6>F%yP^dZu*enYx%Yfkx|Na_A)qJ*}Pah{$ml@_%O zVzY%oG=cK1WmY+ryJ;i$q_DW5g+!WhOah`vn|%~MJ?n=)T1K0gz9y{=kTJ#Z?^Uyj z!4oWJBh{a+p~r#G%**Mw(yd(y&D#bG+et?n^78WyX8q6|=I!mRC_Y2dXn(t)t*3zN zz*!e1Y4CUWL`SUxohna!nR&HOS^RtYnw`&kgCSE+984{;E^A{&B@S|3;i1L!cHYdI zOxn&)MNpBk=;-ik^E|AqHeeuxb3s(3j~!wtW};}{6+TUvDf~^}g<^V5-t6wVn-&pX zSeSs{;a;bOv^`d%gNFHfjlgYKq-}-^^1uTgt}kCs5+NRc5++F@n>jfh!=KND1nuo@ z4EFb5!Ek10^Vh;9!6RW|VZJ>d0R<=Vs8v#6Ojs`wx3adjvchV(X)lwjQ`60lpS*_QrfinNy#`+?(N55t>(LO{0~mB2 z;;{{TzyC+6X?@Fx_C%>|?aW7&j~tG9dHE3JetPWy*4OHMqFd(0hPdaSpsu@HD~1#M zv%7C5cggS3gMa{_rSZL;nRKhHM;TGR?ji4!>{gZ{fjrzEGu8clx_i4iMYGH0xPzvi zI58>Vg1ejr$Qi+Ra41}D;LCo8cn?xi%G7Kwkgcsf@iJRnBjyT`et2ZJ+1H8v?j}BVIzlaJXkwxgvEC6vGiM?!L+Yw_n&~Qpj20b* zUe&{}($M7Y6zl++Ez@RgZAxkL91ALUIdWtmtIT+=Q0%=KE$ta@)p#a(mYA3riJ{UY z{OkgWuujhUQs~wur&?U}zPO8{_MF0qZ)A*q?sD)zf(6*%6U1b*hN0XAwz9I4pl+xx zwEYb@*ooE}1EdG`b-Ul@Mre4}x`E%MrJkpvmy_e1vUzRSu;c$t8w!*RvfF8^8+@58 z^36)ICi#?-S$HRoW!PxGMp|}qY(|X@$ac6vJI~+N*@}*qNXO}{RX^&>)iH8=;np5f zJ2o~(&&}maZ`zUZhk(Ek6uaM?jvU{`tzVK{|Aw!PtFd)=nR$l~U?WVy72g|vg6!^; zjUnJZ@R~_uOZCm-3L+gN_L;kTLw9{^u-LT;D3<#}V18Ut7BqLKY%A}@l#OdfdOBj; zQO(&Ix)?|96^2x9IMcuL?#KzMsHk8pl5r%oEwu$^S=9HSWAk6Wn#*gTZ|vmpXD%GQ z#VN5H%OIxOlHf7B`1w73qc&fuz?*$aX>5=8pEZ<3++jp*H0@In zh}_DLhx1_rP4Q)flqv7YG5ArTm_U|(sF>UL#IjwdmFik*&?SES0g#*cZF3(h0_R*6H0y}LR&0{FE!Qlx>1~HZ?jQjj!jC>M<3cNz9A1> zxkW{tm!{{Yl#bLqr^jKCjorzXr_s;?qoG}cF#|#Cv0gx|n3mmgJ(^o27`AI)o2`=) zs=?oi_UxGKloP{I9g9yro;|%|BJWne)RP@&h)nUI8Gl64bk{3(b^?#6Gkw8M@0qLT z`PDk(C5Yc7w(nUV%E8st?1r%4om*Sc*5@Ee(9(5Y35l?VcoiiTy7!SlQRdV-BbsKO z=2Ky`5b}^9Yqs>38XaY#?cM0YZ~=$HYX~ICpT| z6a2pC<5M$jvpy@q(Fy{`T#p%c=C?*WhY%?tAnG;oRIMM7BQM`Qn+beN)zI6iO>b`< z*VVNZ+DR7BC=)|Gf0*%vDc?1$A z*EXStJ2k;AO-+1>x*G7Ty$-O<*5I(hyAOax+tksbs-yyOBz)w5$#u)8#yq7sy_nby zk7qTu>?#OT5cgEWH z;kmATmA=NtyCA?^`q)JacbIv*7VPY-Am4e`INW$w0Wceb`FLUs?-mxMq!=Fv zd$)F;q3pfWIQ*j7`Mztq#iUK~g$_^kgoFf59dS{doMkZU8jun+MTi5;YnFJZsl2>x zM=B%KowT5qb55Yzo94*^xX@@wbbt?vPw=gfNR#EXc-S2(*NQw?+y|^@R0U~MaC|?r zC%w~LQb^}3YXG*M;X+j7=UU=67vp6{m2%JBF&i@~me+uAzkdC?Fwu6XB4yblSW#6q zM#9*|b>Z94b|zf|RNryqN5yn2ZrATF)##SxL=hLgh&xC>$T^a1ye z7NXtPfeobkWtYN2apm@IU-39{H_xh;4hBFjZjOvWsBTilxB_x(15p9BW@5)2bkB!Q z=K(5a`+aPxi!&TIUhWB~8pg+0fCY1&Z5(>HaHrBlUb~9U1%b{=QDOlX9z>B9sBYRED5Rxia)JaN=(bGeB(rCrOt@5) z&qMLB5v}K90@+M^U34f1^|??XuSS+2>Aie*c31}8xcZU>bHHuJ3HR+-9X>78;ov~! zu2}6%@n{L5VFmVrc#!fOjdkJ~cv0r+1{j9GQ5vl0&E;R4EsQ$VW&;l7&TDC=AZwSpl!_ApsD?TK1htxa!g4RrXcFy$6Eh=f&cJ5`DwNZGg z&7_LVBZs9HJ+$Dp@{H_Ci;-b|QJ9p4hSn0GX8g}=-SsNcBoK$e!*uKtPCSfHR8+t{ z{aU+qDKlz?MMbh1BY)g!h+wg+3w1n`-Vyua9tT7?cV)4GyYbYuX1o#%Q*=}KbXlyG zRe5M0?U{5f{iG=A&86%<*zK7QoL_F$*W6FpR@NKHzy`bPf(g({WxkZz;RvOcZsLfl zXn!x&Qt2tjNQQ_jHX|j4X>;ujfW>*?2H3@hHs?;I>(=IV@pOhJU~};;R`PH)Gi#h1 zojO%wozwKj_+dv|{XnN+OW9N2SsnMTEasR8YPCgBDX63A9h1ibhW528oTnVk=K3-2 zd1586{hFDZ{T>jjRn*o4$=?B*-C?Q85-WAm1f~!maGIyO4z? zZmd0fFW2vd+Yk@yM_Ur%u>+a4ra9U0zveYzJkJeADav${xtcnSP7v2#duoyzslI)X@^l zv?EgFoFM!ea#r?|utN{M)yM=&NnH18feaO}H7n+{v0XcX^<<3e9?+i{eW9iQOolW4 zRmnwphgWV-41XY2pT@hS^0=AIg>`dLSvV@5XS0?rhRGW(*H!CAhfm`pd`g*far0%d z!vG65#4@5THGJ@C;-%NddSMM-tdu}>P8tuoo*_k<)kc}{a(=`hd|m--}Dm}-Q} zwTUx?U5^!aB7V`a+&nZH=x90DA(Eup+U{R}2{86Rlh`X$DsQMIm+4rgL$pJp61BAw z#YB3$bP#uu)sNvlR(i390f+?Ld>DTib(z&SLus9fi0~M(FAkeEd0)4++pG;M$vcYy zoN05Ndb!r)sXC6k{VGKFg>lSXUY?R`wk0IOXR;wX2=HE)nq(DJL%wO zTLrKW0n47j&0rU;wFY)d2J;|VFfDlD$Sj?%@;Q82hY~EHYeYAH?rZD-WEnUPJm1;M zvY(&9ZLVkQ{JOOVR>R~!Fik0OMmwJmmQ@#(_+!Alf}3rmZ`$xAChNg2=bLA_-t9c6 zZJm!?vl-ns-ScD3EZ)^oQIfMY)zZ?)%7uS7v2aytYipNu+!*xqIgaJBD}< zLbP_ZmUO;VqOUauhq;b!Og{Qe_9-c~dvc4^bU*bRxyi|?@MIO|W2I)AL*5g1a{iV9 zZ2;G8HTBQTFQhh>wa3(ws{KWK)D~`GVH5LFqIa0AZgDld7wDM)7kDbRl6KW93G!tZ z1I`t3twGkud4ad6nxh|*zE4FZChGSuGGle`tI6(1zB>J@aqEOp1$11*y=8DnxTJ)H z(BjhKRYgInGPNWe^HugjU5?1mgv3ibaFJqY5GN%LS#?TIbaHXc+{s}!ueZi83yFl@t1#Rh)NiPAs`;rkiq| zw|#<0RjHuN{W^9Lm)t#~kKJ4fYQg8X&<2>E-E&`tft{V!JI4CjJ!iP#2^kVjw~M8| zy1~{;fYV^UTJ}WtY%g^1H}mqeo}+fx4bsAvrIa~8g9jJI^2ZUQ1QNjG$!=kh>dcgL0?FFPCTBPvzv2-FNy;Q#*+g8vEmy#I@Z1cZ`-wvHR2aPt0JjQ*dEtWu8XyI=61Z+8dW{BK0u zIj3phA=f1-(Qpb1SM|SnGz9|te~(U?9oJm`8!QF%s*3vEr|F|#5UMweKPbm&{7)#% zPpmLM;EeqsZ66TJK<@YvYhmsw-nM>uypz;6M7!Ql+;ach4AQT_>yWat*l z01doZq~K3Vnn1?cKk+BUX8>-${Cq0+U(n~VMeK9xxHaFOkjGB}{1rn@Z~nvnS+*Xf z+RtC$>2ZMTpZF61!2G|*+bE1wHaeS$iMWeu8Z;0Hrh>00`Ly{{N|G6oAlSYrEF?r(CJOJVgy)lKGOD zV>t2RuUh3-$=OE%(nTg4ECosfouQ)m5hp<0Cm%~X+<{9 literal 0 HcmV?d00001 From 112819da877c80d009016b8fb06156b6a49894df Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:55:29 +0000 Subject: [PATCH 26/51] fix(db): correct flight record label in typhoon-offshore-drone-route scenario Co-Authored-By: Claude --- db/scenarios/typhoon-offshore-drone-route.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/scenarios/typhoon-offshore-drone-route.yaml b/db/scenarios/typhoon-offshore-drone-route.yaml index 160b1a4..67449e7 100644 --- a/db/scenarios/typhoon-offshore-drone-route.yaml +++ b/db/scenarios/typhoon-offshore-drone-route.yaml @@ -96,7 +96,7 @@ timeline: at: "00:57" participants: [hiiragi] facts: [drone-repeat-route-capable, hiiragi-loaded-auto-route] - record: 飛行履歴 + record: 飛行ログ description: 柊木が十八分間の反復点検ルートをドローンへ読み込ませる。 location: 屋上 - id: drone-launch From a1e3fd04304b49385feead022ed114b3790c5bdf Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:59:49 +0000 Subject: [PATCH 27/51] fix(alibi): require distinct-character source for clash line detection The clashOf logic in src/server/game/alibi.ts draws a clash line between two people's columns, so it also needs the evidence that breaks a lie to come from a character other than the liar. authoringWarnings only checked timeline/facts linkage, so scenarios with clash-eligible lies but same-source evidence silently produced no clash line. Update the warning condition and docs accordingly. Co-Authored-By: Claude --- db/author.ts | 32 +++++++++++++++++++------------- docs/scenario-authoring.md | 20 ++++++++++++++------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/db/author.ts b/db/author.ts index e0d30f7..6e280d4 100644 --- a/db/author.ts +++ b/db/author.ts @@ -86,30 +86,36 @@ export const authoringWarnings = (definition: ScenarioDefinition): string[] => { /* アリバイ表を横断する「食い違い」の印が、一度でも立てるか。 - 印は src/server/game/alibi.ts の clashOf が決めていて、条件が二つ重なる。 - 証拠がその嘘を崩していること(contradicts)と、崩れた嘘の about が - timeline のどれかの出来事の facts に載っていること。後者が繋がっていないと、 - 矛盾を掴んだのに表が何も言わない事件ができあがる。 + 印は src/server/game/alibi.ts の clashOf が決めていて、条件が三つ重なる。 + 証拠がその嘘を崩していること(contradicts)、崩れた嘘の about が timeline の + どれかの出来事の facts に載っていること、そして**崩した証拠に嘘の主とは別の + 人物の出所(sources の type: character)があること**。印は線を二本の柱の + あいだに架けるので、崩した側が誰か分からなければ架ける先が無い。 + どれか一つでも欠けると、矛盾を掴んだのに表が何も言わない事件ができあがる。 嘘ごとには求めない——動機や身元の嘘は時刻表に載らないのが普通で、 そこまで縛ると時刻と関係のない嘘が書けなくなる。事件を通して一本あればよい。 */ const timelineFacts = new Set(definition.timeline.flatMap((event) => event.facts)) - const brokenLieIds = new Set( - definition.evidences - .flatMap((evidence) => evidence.contradicts) - .filter((reference) => reference.startsWith('lie:')) - .map((reference) => reference.slice(4)), + const hasClash = definition.characters.some((character) => + character.lies.some( + (lie) => + timelineFacts.has(lie.about) && + definition.evidences.some( + (evidence) => + evidence.contradicts.includes(`lie:${lie.id}`) && + evidence.sources.some( + (source) => source.type === 'character' && source.id !== character.id, + ), + ), + ), ) - const hasClash = definition.characters - .flatMap((character) => character.lies) - .some((lie) => brokenLieIds.has(lie.id) && timelineFacts.has(lie.about)) return hasClash ? missingRecords : [ ...missingRecords, - '証拠で崩せる嘘のうち、about が timeline の出来事の facts に載っているものが一つもありません。このままではアリバイ表に「食い違い」の印が一度も立ちません。嘘が言い張っている事実を、時刻表の出来事にも含めてください。', + '証拠で崩せる嘘のうち「about が timeline の出来事の facts に載っていて、崩す証拠に嘘の主とは別の人物の出所がある」ものが一つもありません。このままではアリバイ表に「食い違い」の印が一度も立ちません。嘘が言い張っている事実を時刻表の出来事に含め、その嘘を崩す証拠に別の人物の出所(sources の type: character)を持たせてください。', ] } diff --git a/docs/scenario-authoring.md b/docs/scenario-authoring.md index 4880199..21be29a 100644 --- a/docs/scenario-authoring.md +++ b/docs/scenario-authoring.md @@ -293,10 +293,17 @@ personality: 気弱で愛想笑いが多い。横領の話題では動揺し、 | `evasive` | はっきり否定はせず、話をそらしてやり過ごす | **`about` はアリバイ表の鍵でもあります。** 嘘を崩す証拠(`contradicts` にこの嘘を持つもの)を -プレイヤーが掴むと、表を横断する「食い違い」の印が一本立ちます。印が立つ時刻を決めているのが -この `about` で、**`about` に書いた事実が `timeline` のどれかの出来事の `facts` に載っていないと、 -崩す証拠を掴んでも印は最後まで現れません。** 嘘が言い張っている事実は、必ず時刻表の出来事にも -含めてください。印は一本だけで、複数該当するときはいちばん早い時刻に立ちます。 +プレイヤーが掴むと、表を横断する「食い違い」の線が一本、**嘘の主の列と、崩した証拠の出所の列の +あいだに**架かります。立つには二つの繋がりが要ります。 + +1. **`about` に書いた事実が `timeline` のどれかの出来事の `facts` に載っていること。** + 印が立つ時刻はここから決まります。載っていないと、崩す証拠を掴んでも印は最後まで現れません。 +2. **その嘘を崩す証拠が、嘘の主とは別の人物を `sources`(`type: character`)に持つこと。** + 線は二人のあいだに架かるので、崩した側が誰か分からないと架ける先がありません。 + 場所や遺体だけから出る証拠は、嘘を崩しても線になりません。 + +嘘が言い張っている事実は必ず時刻表の出来事にも含め、それを崩す証拠には人の出所を持たせてください。 +印は一本だけで、複数該当するときはいちばん早い時刻に立ちます。 ### `memories` と `relationships` @@ -484,8 +491,9 @@ solution: `timeline` の各出来事に `participants` と `location` が入っているか、証拠と啓示に `supports` / `relatedFacts` が入っているかを見てください。ここが空だと、推理が進んでいるのに画面が何も変わりません。 併せて、裏付けのある出来事に `record` が入っているか、`lies[].about` の事実が `timeline` の - どれかの出来事にも載っているかを確かめてください。前者が空なら目盛りが裸の時刻になり、 - 後者が繋がっていないと「食い違い」の印が一度も立ちません。 + どれかの出来事にも載っているか、嘘を崩す証拠に嘘の主とは別の人物の出所があるかを + 確かめてください。最初が空なら目盛りが裸の時刻になり、残り二つのどちらが欠けても + 「食い違い」の印が一度も立ちません(§7 の `lies` 参照)。 --- From dfa712690944f3946e4617e7462c246765a0eb99 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:34:25 +0000 Subject: [PATCH 28/51] feat(motions): redraw motion three in the vertical chart form The catalogue drew motion three on a horizontal rail with pin-lift, but the alibi chart runs vertically, so its pins are horizontal bars that stretch along their own length via pin-lift-x. The card no longer showed what the app actually does, which defeats the catalogue's purpose as a comparison surface. Redraw it as two columns with the line drawn between them and both end pins lifting once the line settles, matching AlibiChart. Co-Authored-By: Claude Opus 4.7 --- src/client/components/Motions.stories.tsx | 41 +++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/client/components/Motions.stories.tsx b/src/client/components/Motions.stories.tsx index 5a73602..01d9d5c 100644 --- a/src/client/components/Motions.stories.tsx +++ b/src/client/components/Motions.stories.tsx @@ -159,13 +159,42 @@ const MotionCatalogue = () => ( + 離れた二つの証言が噛み合わないと分かった瞬間。二本の柱のあいだに線が引かれ、 + 引き終わってから両端の目盛りが一度だけ伸びます。光らせず、繋ぐ。 + アリバイ表は縦に時が流れるので目盛りは横棒——伸ばすのは pin-lift-x{' '} + です(横のレールで使う pin-lift の、向きだけ直交した片割れ。原典の横の絵は + mocks/direction.html にあります)。 + + } > - - - - - +
    + {/* 左の柱。線の時刻までは裏付けがあり、そこから先は申告のみ。 */} + + + {/* 右の柱は申告だけの線。破線は明色の顔料で引く——暗色は地に溶けて消える。 */} + + {/* 噛み合わない時刻に、二本のあいだへ線が架かる。 */} + + {/* + 両端の目盛り。線を引き終わってから(700ms——draw の 180ms 遅れ+520ms)、 + 芯を中心に一度だけ伸びる。AlibiChart と同じく、外の枠が浮き(line-in)、 + 中の棒が伸びる(pin-lift-x)。一要素に animation は一つしか持てない。 + */} + + + + + + +
    Date: Tue, 1 Sep 2026 22:35:57 +0000 Subject: [PATCH 29/51] chore: move clash line screenshot out of the repository root Screenshots belong in screenshots/, which .gitignore excludes, so this one should never have been tracked at the root. The file stays on disk for local reference; only the tracked copy goes away. Co-Authored-By: Claude Opus 4.7 --- app-clash-lift.png | Bin 52503 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app-clash-lift.png diff --git a/app-clash-lift.png b/app-clash-lift.png deleted file mode 100644 index 149f1765dc9292f1011c198b7dec2373b9b0a976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52503 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 From f8c94a3afcf8cfa4b3c20168d415a477c9aca51b Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:59:19 +0000 Subject: [PATCH 30/51] docs(db): illustrate the clash line in the authoring guide The two conditions for the clash mark are easy to satisfy on paper and still produce nothing on screen, so authors need to see what they are aiming for. Track the screenshot under docs/images/ and reference it from the lies section, where both conditions are explained. Co-Authored-By: Claude Opus 4.7 --- docs/images/clash-line.png | Bin 0 -> 52503 bytes docs/scenario-authoring.md | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 docs/images/clash-line.png diff --git a/docs/images/clash-line.png b/docs/images/clash-line.png new file mode 100644 index 0000000000000000000000000000000000000000..149f1765dc9292f1011c198b7dec2373b9b0a976 GIT binary patch literal 52503 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 literal 0 HcmV?d00001 diff --git a/docs/scenario-authoring.md b/docs/scenario-authoring.md index 21be29a..1314760 100644 --- a/docs/scenario-authoring.md +++ b/docs/scenario-authoring.md @@ -305,6 +305,11 @@ personality: 気弱で愛想笑いが多い。横領の話題では動揺し、 嘘が言い張っている事実は必ず時刻表の出来事にも含め、それを崩す証拠には人の出所を持たせてください。 印は一本だけで、複数該当するときはいちばん早い時刻に立ちます。 +![食い違いの線が二人の列のあいだに架かり、両端の目盛りが伸びる](images/clash-line.png) + +上は牧野と瀬名のあいだに線が架かった瞬間です。左端が嘘の主の列、右端が崩した証拠の出所の列で、 +両端の目盛りはそれぞれの顔料のまま。**これが出ない事件は、矛盾を掴んでも盤面が何も言いません。** + ### `memories` と `relationships` `memories` は感情の手触りを与える短い記憶。`detail` がそのまま人物に渡ります。 From 341bd4d1dafb10c0e01c40a226490708205bd1e6 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:56:18 +0000 Subject: [PATCH 31/51] feat(mocks): redraw motion three in the vertical chart form The catalogue pages still drew motion three on a horizontal rail with pin-lift, while the screen mock, AlibiChart and the Storybook catalogue all show the vertical table where a pin is a horizontal bar stretched by pin-lift-x. Anyone looking up the motion got a different picture depending on where they looked, and the rail form hid why the vocabulary needs both orientations. Redraw both catalogue pages as two columns with the line drawn between them and the end pins lifting once it settles. Co-Authored-By: Claude Opus 4.7 --- mocks/direction.html | 38 +++++++++++++------- mocks/effects/_effects.css | 71 +++++++++++++++++++++++++++++++++----- mocks/effects/spark.html | 14 ++++---- 3 files changed, 97 insertions(+), 26 deletions(-) diff --git a/mocks/direction.html b/mocks/direction.html index 6a1f9cd..3aa6cf0 100644 --- a/mocks/direction.html +++ b/mocks/direction.html @@ -382,10 +382,21 @@ .play .doubt .qm { animation: at-in 360ms 260ms ease-out both; } @keyframes waver { 0% { transform: scaleY(0); } 45% { transform: scaleY(1.18); } 72% { transform: scaleY(0.88); } } - /* ひらめき — 離れた二本が繋がる */ - .d-rail .link { position: absolute; top: 11px; height: 1px; background: var(--kinari); transform-origin: 0 50%; } + /* ひらめき — 噛み合わない二本のあいだに線が架かる。縦組みで描く(目盛りが横棒になる形)。 */ + /* 実物の列は 140px 間隔。幅いっぱいに広げると線だけが長い十字に見え、表の比率から離れる。 */ + .d-cols { position: relative; height: 100%; width: 320px; margin: 0 auto; } + .d-cols .col { position: absolute; top: 8px; bottom: 8px; width: 3px; } + .d-cols .col.upper { bottom: 50%; } + .d-cols .col.lower { top: 50%; width: 0; border-left: 1px dashed currentColor; opacity: .45; } + .d-cols .col.claim { width: 0; border-left: 1px dashed currentColor; opacity: .55; } + .d-cols .link { position: absolute; top: 50%; height: 0; border-top: 1px dashed var(--nezumi-dim); opacity: .8; transform-origin: 0 50%; } + /* 端は外の枠が浮き、中の棒が伸びる。一要素に animation は一つしか持てない。 */ + .d-cols .cpin { position: absolute; top: 50%; margin-left: -5.5px; } + .d-cols .cpin i { display: block; width: 11px; height: 1px; background: currentColor; } .play .spark .link { animation: draw 520ms 180ms cubic-bezier(.2,.9,.3,1) both; } - .play .spark .pin { animation: pin-lift 620ms 180ms ease-out both; } + /* 線を引き終わる 700ms(draw の 180ms 遅れ+520ms)を待ってから、端を置いて伸ばす。 */ + .play .spark .cpin { animation: line-in 360ms 700ms ease-out both; } + .play .spark .cpin i { animation: pin-lift-x 620ms 700ms ease-out both; } @keyframes pin-lift { 55% { transform: scaleY(1.5); } } /* 横向きの片割れ。目盛りは時刻の軸に垂直に立つので、軸が縦に流れる アリバイ表では目盛りが横の短い棒になり、伸びる向きもこちらへ直交する。 */ @@ -446,7 +457,7 @@ @media (prefers-reduced-motion: reduce) { .play .d-rail .pin.fresh, .play .d-rail .at, .play .d-turn .l, .play .d-hint .fold, .play .d-rows .r, .play .doubt .pin.wobble, .play .doubt .qm, - .play .spark .link, .play .spark .pin, .play .d-over .band, + .play .spark .link, .play .spark .cpin, .play .spark .cpin i, .play .d-over .band, .play .d-crawl .inner, .play .d-type span, .play .d-dive .inner, .play .d-dive .veil { animation: none; } } @@ -953,16 +964,19 @@

    十の動きと、その役目

    三 ひらめき
    -
    - - - - +
    + + + + + +
    -

    離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。 - 目盛りは時刻の軸に垂直に立つので、伸びる向きは軸の向きで決まります。横に流れるこのレールでは縦(pin-lift)、 - 時刻が縦に流れるアリバイ表では横(pin-lift-x)。伸ばすのは短い目盛りのほうで、在所そのものの帯ではありません。

    +

    離れた二つの証言が噛み合わないと分かった瞬間。二本の柱のあいだに線が引かれ、引き終わってから両端の目盛りが一度だけ伸びます。光らせず、繋ぐ。 + 目盛りは時刻の軸に垂直に立つので、伸びる向きは軸の向きで決まります。時刻が縦に流れるこの表では横(pin-lift-x)、 + 横に流れるレールでは縦(pin-lift)。伸ばすのは短い目盛りのほうで、在所そのものの帯ではありません + ——帯は在所の長さを持つので、四十分の区間なら四百px を超えます。

    diff --git a/mocks/effects/_effects.css b/mocks/effects/_effects.css index d8036b3..04cc75f 100644 --- a/mocks/effects/_effects.css +++ b/mocks/effects/_effects.css @@ -287,19 +287,73 @@ body { } } -/* 三 ひらめき — 離れた二本が繋がる */ -.d-rail .link { +/* + * 三 ひらめき — 噛み合わない二本のあいだに線が架かる。 + * + * 縦組みで描く。アリバイ表は時が縦に流れるので、目盛りは軸に垂直な横の短い棒になり、 + * 伸びる向きも横(pin-lift-x)。横のレールで描くと pin-lift との使い分けが絵から消える。 + */ +.d-cols { + position: relative; + height: 100%; + /* 実物の列は 140px 間隔。幅いっぱいに広げると線だけが長い十字に見え、表の比率から離れる。 */ + width: 320px; + margin: 0 auto; +} +/* 在所の柱。裏付けのあるところは実線、申告だけのところは破線。 */ +.d-cols .col { position: absolute; - top: 11px; - height: 1px; - background: var(--kinari); + top: 8px; + bottom: 8px; + width: 3px; +} +.d-cols .col.upper { + bottom: 50%; +} +.d-cols .col.lower { + top: 50%; + width: 0; + border-left: 1px dashed currentColor; + opacity: 0.45; +} +.d-cols .col.claim { + width: 0; + border-left: 1px dashed currentColor; + opacity: 0.55; +} +/* 二本のあいだに架かる線。引き終わってから両端が動く。 */ +.d-cols .link { + position: absolute; + top: 50%; + height: 0; + border-top: 1px dashed var(--nezumi-dim); + opacity: 0.8; transform-origin: 0 50%; } +/* + * 両端の目盛り。外の枠が浮き、中の棒が伸びる——一つの要素に animation は一つしか + * 持てないので、浮きと伸びを親子で分ける。 + */ +.d-cols .cpin { + position: absolute; + top: 50%; + margin-left: -5.5px; +} +.d-cols .cpin i { + display: block; + width: 11px; + height: 1px; + background: currentColor; +} .play .spark .link { animation: draw 520ms 180ms cubic-bezier(0.2, 0.9, 0.3, 1) both; } -.play .spark .pin { - animation: pin-lift 620ms 180ms ease-out both; +/* 線を引き終わる 700ms(draw の 180ms 遅れ+520ms)を待ってから、端を置いて伸ばす。 */ +.play .spark .cpin { + animation: line-in 360ms 700ms ease-out both; +} +.play .spark .cpin i { + animation: pin-lift-x 620ms 700ms ease-out both; } @keyframes pin-lift { 55% { @@ -509,7 +563,8 @@ body { .play .doubt .pin.wobble, .play .doubt .qm, .play .spark .link, - .play .spark .pin, + .play .spark .cpin, + .play .spark .cpin i, .play .d-over .band, .play .d-crawl .inner, .play .d-type span, diff --git a/mocks/effects/spark.html b/mocks/effects/spark.html index 078cae9..270ec92 100644 --- a/mocks/effects/spark.html +++ b/mocks/effects/spark.html @@ -14,14 +14,16 @@
    三 ひらめき
    -
    - - - - +
    + + + + + +
    -

    離れた二つの証言が噛み合った瞬間。二本のあいだに線が引かれ、それぞれの目盛りが一度だけ伸びます。光らせず、繋ぐ。

    +

    離れた二つの証言が噛み合わないと分かった瞬間。二本の柱のあいだに線が引かれ、引き終わってから両端の目盛りが一度だけ伸びます。光らせず、繋ぐ。アリバイ表は縦に時が流れるので目盛りは横棒——伸ばすのは pin-lift-x、横のレールで使う pin-lift の向きだけ直交した片割れです。

    From 4008308ce458a1864353cfe8aee543beb0168de9 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:06:44 +0000 Subject: [PATCH 32/51] feat(mocks): add investigable places alongside cast in overview and interrogation Introduces a separate "places" group in case-overview so scenes/bodies can be examined distinctly from cast members, unifies the row-rendering logic for cast/victim/place entries, and updates interrogation.html to reflect examine-mode copy and headline text when a place or the victim is selected. Co-Authored-By: Claude --- mocks/_case.js | 7 ++- mocks/desktop/_desk.css | 12 +++++ mocks/desktop/case-overview.html | 93 +++++++++++++++++++++----------- mocks/desktop/interrogation.html | 18 +++++-- 4 files changed, 94 insertions(+), 36 deletions(-) diff --git a/mocks/_case.js b/mocks/_case.js index d81aee4..5a813b0 100644 --- a/mocks/_case.js +++ b/mocks/_case.js @@ -497,7 +497,12 @@ reveals: [ { who: 'makino', - from: '18:36', + /* + * 締めを書いていた短い区間。申告の破線と同じ十八時三十六分から引くと、 + * 表の上で二つの札が同じ高さに来て「帳場局へ、雨のなかを」と繋がって読める。 + * 裏付けが立つのは最後の記帳の側なので、そちらへ寄せて重なりを解く。 + */ + from: '18:40', to: '18:44', kind: 'solid', place: '帳場', diff --git a/mocks/desktop/_desk.css b/mocks/desktop/_desk.css index 273f881..088c72c 100644 --- a/mocks/desktop/_desk.css +++ b/mocks/desktop/_desk.css @@ -19,6 +19,8 @@ --kinari: #e9e4d9; --nezumi: #8e8896; --nezumi-dim: #625d6c; + /* 場所は人物の色を持たない。他の色と同じ口から引けるよう、明るい側だけ揃えておく。 */ + --nezumi-t: #aaa4b2; --asagi: #5e8b8c; --asagi-t: #8ab5b5; @@ -609,6 +611,16 @@ select { .cast .face.on { box-shadow: 0 0 0 1.5px currentColor; } +/* + 場所の印。人の丸に対して角を立て、塗らずに罫線で囲う。 + 名簿では人と場所が同じ組みで並ぶので、色を抜いただけでは「暗い人物」に見える。 + 塗った四角にすると今度は箱になるので、線だけで区画を示す。 +*/ +.cast .face.pl { + border-radius: 2px; + border: 1px solid var(--keisen); + background: none; +} .cast .col2 { display: flex; flex-direction: column; diff --git a/mocks/desktop/case-overview.html b/mocks/desktop/case-overview.html index 5178ff6..a0e2b96 100644 --- a/mocks/desktop/case-overview.html +++ b/mocks/desktop/case-overview.html @@ -64,6 +64,12 @@ .work > .grp .cast { display: grid; grid-template-columns: 1fr 1fr; column-gap: 30px; } /* 上端の罫は一行目の二枡ぶん。既定の :first-child だけだと右が欠ける。 */ .work > .grp .cast .r:nth-child(-n + 2) { border-top: 1px solid var(--keisen); } + /* + 場所の束は名簿のすぐ下に続ける。霞は「記録が断ち切れている」ことを示す印なので、 + 二つ目の束には掛けない——掛けると名簿の下端に霧が下りて、行が消えかけて見える。 + */ + .work > .grp.more { padding-top: 14px; } + .work > .grp.more::before { content: none; } /* 名簿を選んだ直後に押すものなので、間を空けずすぐ下に置く。 */ .work > .foot { margin-top: 0; padding-top: 18px; } @@ -117,6 +123,15 @@

    + + +
    @@ -149,45 +164,61 @@

    /* * 名簿には亡くなった人も並べる。別枠にすると、その夜そこに居たのは誰かという - * 一覧が二つに割れる。喋らないが遺体と現場は調べられるので、調べられる事件では - * 押せるようにして、右端の札を「調べる」に替える——押せる相手なのか名簿の上で分かるように。 + * 一覧が二つに割れる。喋らない相手には右端の札を「調べる」に替える—— + * 押せる相手なのか名簿の上で分かるように。 */ + var row = function (p, kind) { + // 所見も死因も無い事件では、遺体の行は押せないまま。押せるのに何も出ない行は作らない。 + var canPick = kind !== 'victim' || C.victim.investigable + var tag = kind === 'cast' ? '' : canPick ? '調べる' : '被害者' + return ( + '' + ) + } + document.getElementById('cast').innerHTML = C.cast - .concat([C.victim]) .map(function (p) { - var isVictim = p.key === C.victim.key - var examinable = isVictim && C.victim.investigable - return ( - '' - ) + return row(p, 'cast') }) + .concat([row(C.victim, 'victim')]) .join('') + // 場所の無い事件では見出しごと出さない。空の見出しは「何か足りない」に見える。 + if (Mock.places.length > 0) { + document.getElementById('placeGrp').hidden = false + document.getElementById('places').innerHTML = Mock.places + .map(function (p) { + return row(p, 'place') + }) + .join('') + } + var who = Mock.cast[pick] var go = document.getElementById('go') - // 遺体は訊く相手ではないので、開始の文言も「調べる」に替わる。 - go.textContent = who.name + (pick === C.victim.key ? 'を調べる' : 'に聞き込みをする') + // 遺体も場所も訊く相手ではないので、開始の文言が「調べる」に替わる。 + go.textContent = who.name + (Mock.examines(pick) ? 'を調べる' : 'に聞き込みをする') go.href = './interrogation.html#turn=1&who=' + pick Array.prototype.forEach.call(document.querySelectorAll('[data-who]'), function (el) { diff --git a/mocks/desktop/interrogation.html b/mocks/desktop/interrogation.html index a425a04..9b951d9 100644 --- a/mocks/desktop/interrogation.html +++ b/mocks/desktop/interrogation.html @@ -52,13 +52,13 @@ -->
    新事実 - 牧野は午後六時三十五分に店を出たと述べた +
    -
    訊けそうなこと
    +
    @@ -78,8 +78,12 @@ // 一枚だけ開いて撮ったときに白紙にならないよう、既定を中盤に置く。 var st = Mock.play(Mock.num('turn', 4), Mock.str('who', undefined)) var me = Mock.cast[st.current] - // 遺体を調べているあいだ。喋る相手ではないので、文言も肩書も変わる。 - var examining = st.current === C.victim.key + /* + 遺体か現場を調べているあいだ。喋る相手ではないので、文言も肩書も変わる。 + 場所はアリバイ表に列を持たないので、ここで開いても表の見出しは光らない—— + 机の上で場所へ相手を替える口は、まだ支度の名簿だけ。 + */ + var examining = Mock.examines(st.current) document.getElementById('ttl').textContent = C.title document.getElementById('turns').textContent = st.turn + ' / ' + C.turns + ' ターン' @@ -104,6 +108,12 @@ document.getElementById('askbox').textContent = examining ? '何を調べる?' : '何について訊く?' document.getElementById('askbtn').textContent = examining ? '調べる' : '訊く' + document.getElementById('hintHead').textContent = examining + ? '調べられそうなこと' + : '訊けそうなこと' + // 帯に出るのは直前に増えた一行。場所を調べていれば、証言ではなく所見から来る。 + document.getElementById('newfact').textContent = + me.fact === undefined ? '牧野は午後六時三十五分に店を出たと述べた' : me.fact // 直近の三塊だけ残す。上は溢れるに任せ、切れ口は CSS の霞が引き受ける。 document.getElementById('log').innerHTML = Mock.log(st.log.slice(-6)) From 54248034d72b2d55c8d2030a88f83e8cc634376b Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:11:24 +0000 Subject: [PATCH 33/51] fix(interrogation): auto-scroll log by observed content height, not char count Switch the interrogation log auto-scroll from tracking accumulated text length to a ResizeObserver on the log content element, since text length changes before paced-reveal actually renders new lines, leaving the view short of the bottom. Also fix the hint element's [hidden] override in the phone mock CSS to scope the display:none to .hint instead of all elements. Co-Authored-By: Claude --- mocks/mobile/_phone.css | 8 +++--- src/client/screens/InterrogationScreen.tsx | 31 ++++++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/mocks/mobile/_phone.css b/mocks/mobile/_phone.css index 3c65d6a..7cd2e47 100644 --- a/mocks/mobile/_phone.css +++ b/mocks/mobile/_phone.css @@ -40,10 +40,6 @@ * { box-sizing: border-box; } -/* 出し入れは hidden で切り替える。display を持つ要素にも効かせておく。 */ -[hidden] { - display: none !important; -} html, body { height: 100%; @@ -520,6 +516,10 @@ textarea { flex-direction: column; border-top: 1px solid var(--keisen); } +/* 台本を使い切ったら畳みごと下ろす。display を持つので hidden だけでは消えない。 */ +.hint[hidden] { + display: none; +} .hint .head { display: flex; justify-content: space-between; diff --git a/src/client/screens/InterrogationScreen.tsx b/src/client/screens/InterrogationScreen.tsx index 0e5c007..104d141 100644 --- a/src/client/screens/InterrogationScreen.tsx +++ b/src/client/screens/InterrogationScreen.tsx @@ -407,16 +407,12 @@ export const InterrogationScreen = ({ */ const enteredTurn = useRef(turn === undefined ? undefined : turn.turn) const logRef = useRef(null) + /** 巻き取りの基準になる中身。伸び縮みを見るのは器ではなくこちら。 */ + const logContentRef = useRef(null) /** 会話がいま指している目盛り。直前に増えた一本を、次が来るまで太らせる。 */ const litFix = useLitFix(alibi.segments) - /** これまでに流れ込んだ字数。返答が一文字伸びるたびに増える。 */ - const logLength = Object.values(conversations).reduce( - (sum, turns) => turns.reduce((count, turn) => count + turn.text.length, sum), - 0, - ) - /* * 増えた手掛かりを一つだけ帯に出す。二つ以上増えた回でも重ねない—— * 読み終える前に次が来ると、どちらも読めないまま消える。 @@ -447,15 +443,28 @@ export const InterrogationScreen = ({ * * 溢れていないあいだは mt-auto が下へ寄せてくれるが、溢れた先は * 巻き取らないと上端(いちばん古い発言)で止まったままになる。 - * 返答が流れているあいだも追いかけたいので、字数が動くたびに巻き取る。 + * + * 見るのは受け取った字数ではなく、中身が実際に占めている高さ。 + * 返答は届いた順のまま usePacedReveal が一文ずつ間を置いて通すので、 + * 字数が増えた時点ではまだ画面に出ていない行がある。そこで巻き取っても、 + * 後から出てくる行のぶんだけ下端から浮く。待ちの点も同じように出入りする。 */ useEffect(() => { const box = logRef.current + const content = logContentRef.current - if (box !== null && logLength > 0) { - box.scrollTop = box.scrollHeight + if (box === null || content === null) { + return } - }, [logLength]) + + const observer = new ResizeObserver(() => { + box.scrollTop = box.scrollHeight + }) + + observer.observe(content) + + return () => observer.disconnect() + }, []) // 経過時間・質問数はローカルでも積み上げているが、サーバの値を定期的に取りに行くことで // /api/sessions/:id の実装がちゃんと動いているかもこの画面で確認できる。 @@ -867,7 +876,7 @@ export const InterrogationScreen = ({ ref={logRef} className="flex min-h-0 flex-1 flex-col overflow-y-auto p-3 lg:p-0 lg:pt-5" > -
    +
    {blocks.length === 0 && (

    話題を投げると、{askerName}が代わりに聞き込みます。 From 917a17cc8a07aab901dddacd881ac35b3a8ac6d0 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:21:44 +0000 Subject: [PATCH 34/51] docs(motions): record why pin-lift stays without a caller Redrawing motion three in the vertical form left pin-lift with no user: the alibi chart is vertical, so only pin-lift-x is ever drawn. The pair is worth keeping because it shows that a pin's orientation follows its time axis, but an unused rule sitting silently is the very thing that made the motion vocabulary drift out of step with the app in the first place. Note the reason at each of the four copies. Co-Authored-By: Claude Opus 4.7 --- mocks/_motion.css | 7 +++++++ mocks/direction.html | 2 ++ mocks/effects/_effects.css | 2 ++ src/client/index.css | 8 ++++++++ 4 files changed, 19 insertions(+) diff --git a/mocks/_motion.css b/mocks/_motion.css index 68909fd..877b9ed 100644 --- a/mocks/_motion.css +++ b/mocks/_motion.css @@ -145,6 +145,13 @@ animation: draw 520ms 180ms cubic-bezier(0.2, 0.9, 0.3, 1) both; } +/* + * 目盛りが一度だけ伸びる。縦向き——時が横に流れる軸で使う。 + * + * いま出番はない。アリバイ表は縦組みなので、実際に描かれるのは横向きの pin-lift-x で、 + * こちらは対として置いてある。目盛りの向きが軸の向きで決まるという規則を、語彙の側で + * 一対にして示しておくため。横に流れる軸を描くことになったら、そのときはこちらを使う。 + */ @keyframes pin-lift { 55% { transform: scaleY(1.5); diff --git a/mocks/direction.html b/mocks/direction.html index 3aa6cf0..80f2229 100644 --- a/mocks/direction.html +++ b/mocks/direction.html @@ -397,6 +397,8 @@ /* 線を引き終わる 700ms(draw の 180ms 遅れ+520ms)を待ってから、端を置いて伸ばす。 */ .play .spark .cpin { animation: line-in 360ms 700ms ease-out both; } .play .spark .cpin i { animation: pin-lift-x 620ms 700ms ease-out both; } + /* 縦向き——時が横に流れる軸で使う。いま出番はなく、下の横向きと対にして + 「目盛りの向きは軸の向きで決まる」を語彙の側で示すために置いてある。 */ @keyframes pin-lift { 55% { transform: scaleY(1.5); } } /* 横向きの片割れ。目盛りは時刻の軸に垂直に立つので、軸が縦に流れる アリバイ表では目盛りが横の短い棒になり、伸びる向きもこちらへ直交する。 */ diff --git a/mocks/effects/_effects.css b/mocks/effects/_effects.css index 04cc75f..6541896 100644 --- a/mocks/effects/_effects.css +++ b/mocks/effects/_effects.css @@ -355,6 +355,8 @@ body { .play .spark .cpin i { animation: pin-lift-x 620ms 700ms ease-out both; } +/* 縦向き——時が横に流れる軸で使う。いま出番はなく、下の横向きと対にして + 「目盛りの向きは軸の向きで決まる」を語彙の側で示すために置いてある。 */ @keyframes pin-lift { 55% { transform: scaleY(1.5); diff --git a/src/client/index.css b/src/client/index.css index 69d86f6..1710ebf 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -247,6 +247,14 @@ body { animation: draw 520ms 180ms cubic-bezier(0.2, 0.9, 0.3, 1) both; } +/* + * 目盛りが一度だけ伸びる。縦向き——時が横に流れる軸で使う。 + * + * いまアプリに出番はない。アリバイ表は縦組みなので、実際に描かれるのは横向きの + * pin-lift-x のほうで、こちらは対として置いてある。消さないのは、目盛りの向きが + * 軸の向きで決まるという規則を、語彙の側で一対にして示しておくため。 + * 横に流れる軸を描くことになったら、そのときはこちらを使う。 + */ @keyframes pin-lift { 55% { transform: scaleY(1.5); From aa2ab265825ab618d93e7f5d7b9effb770f26052 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Wed, 2 Sep 2026 05:45:02 +0000 Subject: [PATCH 35/51] fix(ui): match every screen to its mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Went through all eight screens against mocks/ and closed the gaps the comparison turned up. Two were structural rather than cosmetic: the case overview had no "where to investigate" section at all, and the result screen was missing the whole accused-time comparison on mobile. The rest is line work — rules, spacing, leading, and the pigment assignments. Shared parts moved too: the block button takes the mincho face and wider tracking the mocks use for it, the accusation outline goes to full shu, the select arrow becomes a filled triangle, the textarea border moves to keisen, and avatar discs go neutral so only the letter carries the character's colour. Interrogation also gained the examine state the mocks already described: places sit alongside people, and the wording turns from asking to examining. Co-Authored-By: Claude --- src/client/components/CharacterAvatar.tsx | 8 +- src/client/components/ui/button.tsx | 10 +- src/client/components/ui/select.tsx | 5 +- src/client/components/ui/textarea.tsx | 2 +- src/client/index.css | 5 + src/client/lib/schemas.ts | 16 ++ .../screens/AccusationScreen.stories.tsx | 23 +- src/client/screens/AccusationScreen.tsx | 15 +- src/client/screens/BriefingScreen.tsx | 13 +- .../screens/CaseOverviewScreen.stories.tsx | 19 ++ src/client/screens/CaseOverviewScreen.tsx | 206 +++++++++++++----- src/client/screens/DetectiveSetupScreen.tsx | 66 ++++-- .../screens/InterrogationScreen.stories.tsx | 103 +++++++++ src/client/screens/InterrogationScreen.tsx | 176 ++++++++++----- src/client/screens/ResultScreen.stories.tsx | 8 +- src/client/screens/ResultScreen.tsx | 163 +++++++++++--- src/client/screens/SettingsScreen.tsx | 17 +- 17 files changed, 675 insertions(+), 180 deletions(-) diff --git a/src/client/components/CharacterAvatar.tsx b/src/client/components/CharacterAvatar.tsx index e47b6e0..50baa77 100644 --- a/src/client/components/CharacterAvatar.tsx +++ b/src/client/components/CharacterAvatar.tsx @@ -16,10 +16,10 @@ type Props = { * 消される。使う色は必ず文字列そのままで並べておく。 */ const PALETTE = [ - 'bg-asagi/15 text-asagi-fg', - 'bg-fuji/15 text-fuji-fg', - 'bg-suou/15 text-suou-fg', - 'bg-karashi/15 text-karashi-fg', + 'bg-sumi-2 text-asagi-fg', + 'bg-sumi-2 text-fuji-fg', + 'bg-sumi-2 text-suou-fg', + 'bg-sumi-2 text-karashi-fg', ] /** 字だけに顔料を乗せたいとき(名前・目盛り・縦罫)。丸の下地は要らない。 */ diff --git a/src/client/components/ui/button.tsx b/src/client/components/ui/button.tsx index adcdc7c..c3d5041 100644 --- a/src/client/components/ui/button.tsx +++ b/src/client/components/ui/button.tsx @@ -20,7 +20,7 @@ const buttonVariants = cva( default: 'border border-nezumi-dim font-semibold tracking-widest text-kinari hover:border-nezumi', /** 取り返しのつかない操作。朱が出るのはこの variant だけ。 */ - destructive: 'border border-shu/70 tracking-widest text-shu hover:border-shu', + destructive: 'border border-shu tracking-widest text-shu hover:border-shu', /** 沈めた枠。選ばれていない選択肢や、副次的な入口。 */ outline: 'border border-sumi-3 text-nezumi-dim hover:border-nezumi-dim hover:text-nezumi', /** 丸いアイコンボタン(記・図・推)。 */ @@ -38,8 +38,12 @@ const buttonVariants = cva( default: 'h-8 px-3', sm: 'h-7 px-2 text-xs', lg: 'h-9 px-4', - /** 縦に伸びる主ボタン。高さではなく余白で厚みを出す。 */ - block: 'w-full py-3', + /** + * 縦に伸びる主ボタン。高さではなく余白で厚みを出す。 + * 語りと同じ明朝で、字送りを広く取る——押す前に一拍置かせるため。 + */ + block: + 'w-full py-[11px] font-mincho font-normal tracking-[0.2em] lg:py-[13px] lg:text-[15px]', icon: 'size-9', 'icon-sm': 'size-7', }, diff --git a/src/client/components/ui/select.tsx b/src/client/components/ui/select.tsx index 8aab77a..2afdb84 100644 --- a/src/client/components/ui/select.tsx +++ b/src/client/components/ui/select.tsx @@ -36,7 +36,10 @@ function SelectTrigger({ > {children} - + {/* 山形の線ではなく塗りの三角。線の細さだと墨の上で消える。 */} + ) diff --git a/src/client/components/ui/textarea.tsx b/src/client/components/ui/textarea.tsx index b1d6224..5f74a98 100644 --- a/src/client/components/ui/textarea.tsx +++ b/src/client/components/ui/textarea.tsx @@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {