diff --git a/frontend/public/gekanator/mascot-celebrate.png b/frontend/public/gekanator/mascot-celebrate.png new file mode 100644 index 0000000..6af3e9f Binary files /dev/null and b/frontend/public/gekanator/mascot-celebrate.png differ diff --git a/frontend/public/gekanator/mascot-confident.png b/frontend/public/gekanator/mascot-confident.png new file mode 100644 index 0000000..cd34892 Binary files /dev/null and b/frontend/public/gekanator/mascot-confident.png differ diff --git a/frontend/public/gekanator/mascot-failed.png b/frontend/public/gekanator/mascot-failed.png new file mode 100644 index 0000000..8ff846a Binary files /dev/null and b/frontend/public/gekanator/mascot-failed.png differ diff --git a/frontend/public/gekanator/mascot-idle.png b/frontend/public/gekanator/mascot-idle.png new file mode 100644 index 0000000..127028e Binary files /dev/null and b/frontend/public/gekanator/mascot-idle.png differ diff --git a/frontend/public/gekanator/mascot-thinking-far.png b/frontend/public/gekanator/mascot-thinking-far.png new file mode 100644 index 0000000..f7d38f6 Binary files /dev/null and b/frontend/public/gekanator/mascot-thinking-far.png differ diff --git a/frontend/public/gekanator/mascot-thinking-mid.png b/frontend/public/gekanator/mascot-thinking-mid.png new file mode 100644 index 0000000..b71afae Binary files /dev/null and b/frontend/public/gekanator/mascot-thinking-mid.png differ diff --git a/frontend/public/gekanator/mascot-thinking-near.png b/frontend/public/gekanator/mascot-thinking-near.png new file mode 100644 index 0000000..3538a0e Binary files /dev/null and b/frontend/public/gekanator/mascot-thinking-near.png differ diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 1f49488..ed2f596 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -66,8 +66,8 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: { { name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg }, { name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] }, { name: 'おたのしみ', visible: false, subMenu: [ - { name: 'グカネータ', to: '/gekanator' }, - { name: '上映会 (β)', to: '/theatres/1' }] }, + { name: '上映会 (β)', to: '/theatres/1' }, + { name: 'グカネータ (β)', to: '/gekanator' }] }, { name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [ { name: '一覧', to: '/users', visible: false }, { name: 'お前', to: `/users/${ user?.id }`, visible: false }, diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 7edd39f..a033498 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -790,7 +790,7 @@ const indexedQuestionTextForTag = (key: string): string => { case 'material': return `素材「${ label }」に関係している?` case 'nico': - return `ニコニコに「${ label }」といふタグがついている?` + return `ニコニコに「${ label }」というタグがついている?` default: return `「${ label }」が含まれる?` } @@ -1260,14 +1260,16 @@ const candidatePostsForState = ({ }) } -const allConcreteAnswerOptionsExhaustedForQuestion = ({ +const hasDiscriminatingHardSplitForQuestion = ({ candidateIds, question, + posts, materialIndex, matchIndex, }: { candidateIds: number[] question: GekanatorQuestion | null + posts: Post[] materialIndex: GekanatorQuestionMaterialIndex matchIndex: GekanatorMatchIndex }): boolean => { @@ -1275,17 +1277,16 @@ const allConcreteAnswerOptionsExhaustedForQuestion = ({ return false const dynamicMatchIndex = new Map> () - const posts = [...materialIndex.postById.values ()] + const yesCount = matchingPostCountInIds ({ + candidateIds, + posts, + materialIndex, + matchIndex, + question, + dynamicMatchIndex }) + const noCount = candidateIds.length - yesCount - return (['yes', 'no'] as GekanatorAnswerValue[]).every (answer => - postIdsForHardAnswer ({ - candidateIds, - question, - answer, - posts, - materialIndex, - matchIndex, - dynamicMatchIndex }).length === 0) + return yesCount > 0 && noCount > 0 } @@ -2266,6 +2267,9 @@ const chooseFallbackQuestion = ({ materialIndex: GekanatorQuestionMaterialIndex matchIndex: GekanatorMatchIndex }): GekanatorQuestion | null => { + if (posts.length === 0) + return null + const fallbackPosts = posts .map (post => ({ post, score: scores.get (post.id) ?? 0 })) .sort ((a, b) => b.score - a.score) @@ -2291,25 +2295,19 @@ const chooseFallbackQuestion = ({ question, dynamicMatchIndex }) const noCount = candidateIds.length - yesCount - const knownCount = yesCount + noCount - if (knownCount === 0) + if (yesCount === 0 || noCount === 0) return null return { question, - hasSplit: yesCount > 0 && noCount > 0, - knownCount, + knownCount: candidateIds.length, balance: Math.abs (yesCount - noCount) } }) .filter ((item): item is { question: GekanatorQuestion - hasSplit: boolean knownCount: number balance: number } => item !== null) .sort ((a, b) => { - if (a.hasSplit !== b.hasSplit) - return a.hasSplit ? -1 : 1 - if (a.balance !== b.balance) return a.balance - b.balance @@ -2421,7 +2419,6 @@ const nextQuestionPlanFor = ( winningRunStartAnswerCount } } - const nextQuestionsSinceLastGuess = answers.length - lastGuessQuestionCount const nextWinningRunTargetId = eligiblePosts.length === 1 ? eligiblePosts[0]?.id ?? null @@ -2454,14 +2451,6 @@ const nextQuestionPlanFor = ( && winningRunQuestionCount ( answers, nextWinningRunStartAnswerCount) >= winningRunQuestionLimit - if (answers.length >= hardMaxQuestions) - return { - question: null, - guess: bestPost (eligiblePosts, scores), - guessReason: 'hard_max_questions', - questionMode: null, - winningRunTargetId: nextWinningRunTargetId, - winningRunStartAnswerCount: nextWinningRunStartAnswerCount } if (winningRunFinished) return { question: null, @@ -2504,9 +2493,7 @@ const nextQuestionPlanFor = ( } const evaluationPosts = - nextQuestionsSinceLastGuess >= minQuestionsBeforeCertainGuess - ? eligiblePosts - : availablePosts + eligiblePosts const evaluationQuestions = buildQuestionsForPosts (evaluationPosts) const normalQuestion = chooseQuestion ({ @@ -2523,7 +2510,7 @@ const nextQuestionPlanFor = ( matchIndex }) const fallbackQuestion = normalQuestion ?? chooseFallbackQuestion ({ - posts: evaluationPosts.length > 0 ? evaluationPosts : availablePosts, + posts: evaluationPosts, allPosts: posts, questions: evaluationQuestions, answers, @@ -2545,14 +2532,8 @@ const nextQuestionPlanFor = ( return { question: null, - guess: - answers.length >= hardMaxQuestions - ? bestPost (guessablePosts, scores) - : null, - guessReason: - answers.length >= hardMaxQuestions - ? 'hard_max_questions' - : null, + guess: null, + guessReason: null, questionMode: null, winningRunTargetId: nextWinningRunTargetId, winningRunStartAnswerCount: nextWinningRunStartAnswerCount } @@ -2595,12 +2576,17 @@ const mascotStateFor = ( bestConfidencePercent: number, winningRunActive: boolean, ): MascotState => { - if (phase === 'learned' && resultWon === true) - return 'celebrate' + const resultPhase = + phase === 'end' + || phase === 'review' + || phase === 'learned' - if (phase === 'learned' && resultWon === false) + if (resultPhase && !(resultWon)) return 'failed' + if (resultPhase && resultWon) + return 'celebrate' + switch (phase) { case 'question': @@ -2660,21 +2646,20 @@ const backgroundPostsFor = ({ const GekanatorBackdrop: FC<{ posts: Post[] + mascotAsset: string phase: Phase displayedGuess?: Post | null visualSeed: string motionMode: BackgroundMotionMode winningRunTargetPost?: Post | null - winningRunQuestionCount?: number -}> = ({ - posts, - phase, - displayedGuess = null, - visualSeed, - motionMode, - winningRunTargetPost = null, - winningRunQuestionCount = 0, -}) => { + winningRunQuestionCount?: number }> = ({ posts, + mascotAsset, + phase, + displayedGuess = null, + visualSeed, + motionMode, + winningRunTargetPost = null, + winningRunQuestionCount = 0 }) => { const guessFocusOffset = useMemo (() => { const focusTiles = [ { x: 'calc(max(100vw, 100vh) * 0.5)', @@ -2996,7 +2981,7 @@ const GekanatorBackdrop: FC<{ to-pink-50 dark:from-red-950 dark:via-red-975 dark:to-red-900"/>) return ( -
+
- {displayedBackdropMode !== 'normal' || !(isFlippingTiles) ? ( + {(displayedBackdropMode !== 'normal' || !(isFlippingTiles)) + ? ( - ) : ( + style={{ opacity: renderedSettings.opacity }}/>) + : (
-
) @@ -3503,12 +3491,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { }:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${ rejectedPostIds.size }:${ backgroundPosts.slice (0, 8).map (post => post.id).join ('|') }` - const mascot = mascotStateFor ( - phase, - effectiveResultWon, - eligiblePosts.length, - bestConfidencePercent, - winningRunActive) + const mascot = mascotStateFor (phase, effectiveResultWon, eligiblePosts.length, + bestConfidencePercent, winningRunActive) const mascotAsset = mascotAssetByState[mascot] const mascotAlt = mascotAltByState[mascot] const saveMutation = useMutation ({ @@ -3535,7 +3519,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { onSuccess: async () => { await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () }) setExtraQuestionState ('saved') - setPhase ('learned') + setPhase ('end') }}) const resetExtraQuestionState = () => { @@ -3697,11 +3681,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { matchIndex: acceptedQuestionMatchIndex }) return !(fallbackQuestion) - || allConcreteAnswerOptionsExhaustedForQuestion ({ + || !(hasDiscriminatingHardSplitForQuestion ({ candidateIds: recoveredEligiblePosts.map (post => post.id), question: fallbackQuestion, + posts, materialIndex, - matchIndex: acceptedQuestionMatchIndex }) + matchIndex: acceptedQuestionMatchIndex })) } while (recoveredEligiblePosts.length === 0 || needsPreQuestionRecovery ()) @@ -3962,12 +3947,12 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { const saveAndLearn = () => { if (!(canPersistGame)) { - setPhase ('learned') + setPhase ('end') return } resetExtraQuestionState () - saveReviewedResult (() => setPhase ('learned')) + saveReviewedResult (() => setPhase ('end')) } const submitQuestionSuggestion = () => { @@ -4197,10 +4182,19 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { [String (questionId)]: value }) } - const dialogue = - phase === 'learned' && resultWon - ? <>グカカカカwwwww 洗澡鹿シーザオグカは何でもお見通し! - : <>私は洗澡鹿シーザオグカ。質問から投稿を何でも当ててみせるよ。 + const introDialogue = + <>私は洗澡鹿シーザオグカ。質問から投稿を何でも当ててみせるよ。 + + const winDialogue = + <>グカカカカwwwww 洗澡鹿シーザオグカは何でもお見通し! + + const loseDialogue = + <>ぬわーん! 洗澡鹿シーザオグカ外しちゃったグカー!!!!! + + const resultDialogue = effectiveResultWon ? winDialogue : loseDialogue + + const dialogue = phase === 'learned' ? resultDialogue : introDialogue + const introLoading = isLoading || acceptedQuestionsLoading const readyToStart = !(introLoading) @@ -4287,7 +4281,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { acceptedQuestionsLoading]) return ( - + {`グカネータ | ${ SITE_TITLE }`} @@ -4295,6 +4289,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {performanceMode !== 'lite' && ( = ({ user }) => {

)} {(Boolean (error) || Boolean (acceptedQuestionsError)) &&

グカネータの質問データを読み込めませんでした.

} - {!(canPersistGame) && ( -

- 未ログインのまま遊べますが、結果保存・質問追加・追加学習はできません。 - - 設定 - - から引継ぎコードを復元すると記録できます。 -

)} {phase === 'intro' && readyToStart && restorePromptVisible && (
@@ -4509,7 +4496,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
)} {phase === 'guess' && displayedGuess && (
-

これを想像していたね?

+

思い浮かべているのは、これだね?

{isAdmin && (
@@ -4595,8 +4582,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {phase === 'end' && (
-

ゲーム終了

-

グカカカカwwwww 洗澡鹿シーザオグカは何でもお見通し!

+

{resultDialogue}

{reviewGuessedPost && ( @@ -4682,7 +4668,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { || extraQuestionState === 'loading' || extraQuestionAnswersMutation.isPending} onClick={startExtraQuestions}> - 追加で質問に答へる + 追加で質問に答える
)} @@ -4690,8 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {phase === 'review' && (
-

保存前確認

-

今回の結果を確認してね。

+

結果修正

{reviewGuessedPost && ( @@ -4766,7 +4751,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { {reviewGuessedPostId !== null && reviewCorrectPostId !== null && (

- 判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'} + 判定: + {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}

)} {saveMutation.isError && ( @@ -4775,6 +4761,14 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {

)}
+
)} @@ -4945,7 +4931,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { question => !(extraQuestionAnswers[String (question.id)])) } onClick={saveExtraQuestions}> - 学習する + 送信
{!(canPersistGame) && ( @@ -4953,18 +4939,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => { 未ログインのため追加学習は保存されません。

)}
)} - - {phase === 'learned' && ( -
-

{extraQuestionState === 'saved' ? '学習しました。' : '覚えたよ.次はもっと見通す.'}

- -
)}