このコミットが含まれているのは:
バイナリファイルは表示されません.
|
変更後 幅: | 高さ: | サイズ: 559 KiB |
バイナリファイルは表示されません.
|
変更後 幅: | 高さ: | サイズ: 146 KiB |
バイナリファイルは表示されません.
|
変更後 幅: | 高さ: | サイズ: 1.2 MiB |
バイナリファイルは表示されません.
|
変更後 幅: | 高さ: | サイズ: 188 KiB |
バイナリファイルは表示されません.
|
変更後 幅: | 高さ: | サイズ: 201 KiB |
バイナリファイルは表示されません.
|
変更後 幅: | 高さ: | サイズ: 196 KiB |
バイナリファイルは表示されません.
|
変更後 幅: | 高さ: | サイズ: 179 KiB |
@@ -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 },
|
||||
|
||||
@@ -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<string, Set<number>> ()
|
||||
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 (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="fixed [inset:48px_0_0_0] z-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<motion.div
|
||||
className="relative shrink-0"
|
||||
@@ -3060,13 +3045,16 @@ const GekanatorBackdrop: FC<{
|
||||
layout={displayedBackdropMode !== 'normal'}
|
||||
transition={{ duration: tileFlipDuration, ease: 'easeInOut' }}
|
||||
style={{ perspective: 1600 }}>
|
||||
{displayedBackdropMode !== 'normal' || !(isFlippingTiles) ? (
|
||||
{(displayedBackdropMode !== 'normal' || !(isFlippingTiles))
|
||||
? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
src={['intro', 'end'].includes (phase)
|
||||
? mascotAsset
|
||||
: thumbnail}
|
||||
alt=""
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
style={{ opacity: renderedSettings.opacity }}/>
|
||||
) : (
|
||||
style={{ opacity: renderedSettings.opacity }}/>)
|
||||
: (
|
||||
<motion.div
|
||||
className="absolute inset-0"
|
||||
initial={{ rotateY: 0 }}
|
||||
@@ -3098,7 +3086,7 @@ const GekanatorBackdrop: FC<{
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-yellow-50/76 via-white/58
|
||||
<div className="fixed inset-0 z-0 bg-gradient-to-br from-yellow-50/76 via-white/58
|
||||
to-pink-100/62 dark:from-red-950/78 dark:via-red-975/60
|
||||
dark:to-red-900/66"/>
|
||||
</div>)
|
||||
@@ -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 <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>は何でもお見通し!</>
|
||||
: <>私は<ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>。質問から投稿を何でも当ててみせるよ。</>
|
||||
const introDialogue =
|
||||
<>私は<ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>。質問から投稿を何でも当ててみせるよ。</>
|
||||
|
||||
const winDialogue =
|
||||
<>グカカカカwwwww <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>は何でもお見通し!</>
|
||||
|
||||
const loseDialogue =
|
||||
<>ぬわーん! <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>外しちゃったグカー!!!!!</>
|
||||
|
||||
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 (
|
||||
<MainArea className="relative overflow-hidden bg-yellow-50 dark:bg-red-975">
|
||||
<MainArea className="relative isolate overflow-x-hidden bg-yellow-50 dark:bg-red-975">
|
||||
<Helmet>
|
||||
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
@@ -4295,6 +4289,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
{performanceMode !== 'lite' && (
|
||||
<GekanatorBackdrop
|
||||
posts={backgroundPosts}
|
||||
mascotAsset={mascotAsset}
|
||||
phase={phase}
|
||||
displayedGuess={displayedGuess}
|
||||
visualSeed={backgroundVisualSeed}
|
||||
@@ -4367,14 +4362,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</p>)}
|
||||
{(Boolean (error) || Boolean (acceptedQuestionsError))
|
||||
&& <p>グカネータの質問データを読み込めませんでした.</p>}
|
||||
{!(canPersistGame) && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||
未ログインのまま遊べますが、結果保存・質問追加・追加学習はできません。
|
||||
<PrefetchLink to="/users/settings" className="ml-1 underline">
|
||||
設定
|
||||
</PrefetchLink>
|
||||
から引継ぎコードを復元すると記録できます。
|
||||
</p>)}
|
||||
|
||||
{phase === 'intro' && readyToStart && restorePromptVisible && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -4509,7 +4496,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</div>)}
|
||||
{phase === 'guess' && displayedGuess && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xl font-bold">これを想像していたね?</p>
|
||||
<p className="text-xl font-bold">思い浮かべているのは、これだね?</p>
|
||||
{isAdmin && (
|
||||
<div className="rounded border border-yellow-100 px-3 py-2
|
||||
text-sm dark:border-red-900">
|
||||
@@ -4595,8 +4582,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
{phase === 'end' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-neutral-500">ゲーム終了</p>
|
||||
<p className="text-xl font-bold">グカカカカwwwww <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>は何でもお見通し!</p>
|
||||
<p className="text-xl font-bold">{resultDialogue}</p>
|
||||
</div>
|
||||
|
||||
{reviewGuessedPost && (
|
||||
@@ -4682,7 +4668,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
|| extraQuestionState === 'loading'
|
||||
|| extraQuestionAnswersMutation.isPending}
|
||||
onClick={startExtraQuestions}>
|
||||
追加で質問に答へる
|
||||
追加で質問に答える
|
||||
</button>
|
||||
</div>
|
||||
</div>)}
|
||||
@@ -4690,8 +4676,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
{phase === 'review' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-neutral-500">保存前確認</p>
|
||||
<p className="text-xl font-bold">今回の結果を確認してね。</p>
|
||||
<p className="text-xl font-bold">結果修正</p>
|
||||
</div>
|
||||
|
||||
{reviewGuessedPost && (
|
||||
@@ -4766,7 +4751,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
|
||||
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
||||
判定: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'}
|
||||
判定:
|
||||
{reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
|
||||
</p>)}
|
||||
|
||||
{saveMutation.isError && (
|
||||
@@ -4775,6 +4761,14 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
</p>)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-neutral-300 px-4 py-2
|
||||
hover:bg-neutral-100 dark:border-neutral-700
|
||||
dark:hover:bg-red-900"
|
||||
onClick={() => setPhase ('end')}>
|
||||
戻る
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
|
||||
@@ -4788,14 +4782,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
onClick={saveAndLearn}>
|
||||
完了
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-neutral-300 px-4 py-2
|
||||
hover:bg-neutral-100 dark:border-neutral-700
|
||||
dark:hover:bg-red-900"
|
||||
onClick={() => setPhase ('end')}>
|
||||
戻る
|
||||
</button>
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
@@ -4945,7 +4931,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
question => !(extraQuestionAnswers[String (question.id)]))
|
||||
}
|
||||
onClick={saveExtraQuestions}>
|
||||
学習する
|
||||
送信
|
||||
</button>
|
||||
</div>
|
||||
{!(canPersistGame) && (
|
||||
@@ -4953,18 +4939,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
|
||||
未ログインのため追加学習は保存されません。
|
||||
</p>)}
|
||||
</div>)}
|
||||
|
||||
{phase === 'learned' && (
|
||||
<div className="space-y-3">
|
||||
<p>{extraQuestionState === 'saved' ? '学習しました。' : '覚えたよ.次はもっと見通す.'}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-pink-600 px-4 py-2 font-bold text-white
|
||||
hover:bg-pink-500"
|
||||
onClick={reset}>
|
||||
もう一回
|
||||
</button>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする