You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

141 lines
4.8 KiB
JavaScript

import React, {useState, useEffect, useRef} from 'react'
import ReactMarkdown from 'react-markdown'
import styles from './Screen.module.css'
import Reflection from './Reflected'
import Menu from '../Menu/Menu'
import useGameState from '../../hooks/useGameState'
import useSharedState from '../../hooks/useSharedState'
import {game} from '../../engine'
export const ScreenContext = React.createContext()
export default function Text({promptVisible: promptEnabled, handleCommand, showReflection}) {
const inputRef = useRef()
const outputRef = useRef()
const textRef = useRef()
const menuRef = useRef()
const {messages} = useGameState()
const [currentMenu] = useSharedState('currentMenu')
const [_, updateArbitrary] = useState(0) // eslint-disable-line no-unused-vars
const forceRender = () => updateArbitrary(arbitrary => arbitrary + 1)
const [currentInput, setCurrentInput] = useState('')
const [currentScroll, setCurrentScroll] = useState(0)
const currentPause = messages.findIndex(message => (message.type === 'pause' && message.resolved === false))
const outputPaused = currentPause > -1
const printedMessages = !outputPaused ? messages : messages.slice(0, currentPause)
const promptVisible = promptEnabled && !outputPaused
const currentClear = [...printedMessages].reverse().find(message => message.type === 'clear')
const clearedIndex = printedMessages.indexOf(currentClear)
const finalMessages = (clearedIndex < 0) ? printedMessages : printedMessages.slice(clearedIndex)
async function onSubmit(ev) {
if(ev) ev.preventDefault()
if(!promptVisible)
return;
if(!inputRef.current?.value.trim())
return;
if(inputRef.current){
await handleCommand(inputRef.current.value)
inputRef.current.value = ''
setCurrentInput('')
}
if(outputRef.current) {
setImmediate(() => outputRef.current.scrollTop = outputRef.current.scrollHeight)
setCurrentScroll(outputRef.current.scrollHeight)
}
}
useEffect(() => {
const playArea = textRef.current
function onClick(ev) {
// Don't focus if click was in menu area
if(menuRef.current && menuRef.current.contains(ev.target))
return;
// If we have a menu open, don't focus
if(currentMenu)
return;
inputRef.current.focus()
}
playArea.addEventListener('click', onClick)
return () => playArea.removeEventListener('click', onClick)
}, [currentMenu])
useEffect(() => {
if(!currentMenu)
inputRef.current.focus()
}, [currentMenu])
// Unpause output on space or return
useEffect(() => {
if(!outputPaused) return;
function handleKey(ev) {
if(currentMenu) return;
if(ev.key !== ' ' && ev.key !== 'Enter') return;
ev.preventDefault()
inputRef.current.value = ''
setCurrentInput('')
game.getState().messages[currentPause].resolved = true
game.saveDraft()
forceRender()
}
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [currentPause, outputPaused, messages, currentMenu])
// Scroll after unpaused
useEffect(() => {
setImmediate(() => outputRef.current.scrollTop = outputRef.current.scrollHeight)
setCurrentScroll(outputRef.current.scrollHeight)
}, [currentPause])
return (
<>
<div ref={textRef} className={styles.playArea}>
<ScreenContext.Provider value="primary">
<Menu containerRef={menuRef}/>
<div ref={outputRef} onScroll={() => setCurrentScroll(outputRef.current?.scrollTop)} className={styles.output + (!!currentMenu ? ' ' + styles.noMouse : '')}>
{finalMessages.map((message, i) => {
if(message.type === 'message')
return <ReactMarkdown escapeHtml={false} key={i}>{message.message}</ReactMarkdown>
if(message.type === 'command')
return <p key={i} className={styles.command}>{message.command}</p>
return null
})}
{outputPaused && (
<p className={styles.pausePrompt}>
(Press [RETURN] to continue)
</p>
)}
</div>
<form style={{pointerEvents: currentMenu ? 'none' : 'initial'}} className={styles.input + (!promptVisible ? ' ' + styles.hidden : '')} onSubmit={onSubmit}>
<input autoComplete="off" autoFocus ref={inputRef} readOnly={(promptVisible && !currentMenu) ? undefined : ''} onChange={ev => setCurrentInput(ev.target.value)} id="gameInput"/>
</form>
</ScreenContext.Provider>
</div>
{showReflection && (
<ScreenContext.Provider value="secondary">
<Reflection outputPaused={outputPaused} promptVisible={promptVisible} messages={finalMessages} currentInput={currentInput} currentScroll={currentScroll}/>
</ScreenContext.Provider>
)}
</>
)
}