import React , { Component } from "react" ; import ReactTooltip from "react-tooltip" ; import { find , countBy , uniqBy , last } from "lodash" ; import { getImageOfItem } from "../constants/helper" ; import CLASSES from "../constants/classes" ; import { GameLogic , Event } from "../game_logic/gamelogic" ; import { Log } from "../constants/interfaces" ; interface Instruction { events : Event [ ] ; when : number ; isLastMessage : boolean ; ingameWhen : number ; } interface MyProps { gamelogic : GameLogic ; log : ( log ? : Log ) => void ; } interface MyState { show : boolean ; canStop : boolean ; tooltipText : string | JSX . Element ; startAtSpeach : boolean ; } export default class Read extends Component < MyProps , MyState > { synth : SpeechSynthesis ; voices : SpeechSynthesisVoice [ ] ; lineHandlers : NodeJS . Timeout [ ] ; startTime : number ; listeningToSpeach : boolean ; recognition : SpeechRecognition | undefined ; ingameTime = 0 ; /**
     * A small settings menu to enable and disable things
     * And to be able to adjust certain sizes
     */ constructor ( props ) { super ( props ) ; this . synth = window . speechSynthesis ; this . voices = this . synth . getVoices ( ) ; this . state = { show : false , canStop : false , tooltipText : "" , startAtSpeach : true , } ; this . lineHandlers = [ ] ; this . startTime = 0 ; this . listeningToSpeach = false ; const wordList = [ "go" ] ; const grammar = `#JSGF V1.0; grammar colors; public <color> = ${ wordList . join ( " | " ) } ;` ; const anyWindow = window as any ; let speechRecognitionList ; for ( const prefix of [ "" , "webkit" , "moz" , "ms" , "o" ] ) { const speechRecognition = anyWindow [ `${ prefix }SpeechRecognition` ] ; if ( speechRecognition ) { this . recognition = new speechRecognition ( ) ; } const grammarList = anyWindow [ `${ prefix }SpeechGrammarList` ] ; if ( grammarList ) { speechRecognitionList = new grammarList ( ) ; } if ( speechRecognition && speechRecognitionList ) { speechRecognitionList . addFromString ( grammar , 1 ) ; this . recognition . grammars = speechRecognitionList ; this . recognition . continuous = false ; this . recognition . lang = "en-US" ; this . recognition . interimResults = false ; this . recognition . maxAlternatives = 1 ; break ; } } } static isSupported ( ) { return ! ! window . speechSynthesis ; } showSettings = ( _e ) : void => { this . setState ( { show : true , } ) ; } ; hideSettings = ( _e ) : void => { this . setState ( { show : false , } ) ; } ; onMouseEnter = ( _e , item ) : void => { this . setState ( { tooltipText : item , } ) ; } ; wordsToNumber ( words ) { const small = { zero : 0 , one : 1 , two : 2 , three : 3 , four : 4 , five : 5 , six : 6 , seven : 7 , eight : 8 , nine : 9 , ten : 10 , eleven : 11 , twelve : 12 , thirteen : 13 , fourteen : 14 , fifteen : 15 , sixteen : 16 , seventeen : 17 , eighteen : 18 , nineteen : 19 , twenty : 20 , thirty : 30 , forty : 40 , fifty : 50 , sixty : 60 , seventy : 70 , eighty : 80 , ninety : 90 , } ; const a = words . toString ( ) . toLowerCase ( ) . split ( /[\s-]+/ ) ; const n = 0 ; let g = 0 ; a . forEach ( ( w ) => { const x = /^[0-9]+$/ . test ( w ) ? + w : small [ w ] ; if ( x != null ) { g = g + x ; } else if ( w === "hundred" || w === "100" || /^minutes?$/ . test ( w ) ) { g = g * 100 ; } } ) ; return n + g ; } startReading ( startAt ) { if ( this . listeningToSpeach ) { return ; } this . setState ( { canStop : true , } ) ; if ( ! this . state . startAtSpeach || ! this . recognition ) { this . readBuildOrder ( startAt ) ; return ; } this . recognition . start ( ) ; this . listeningToSpeach = true ; this . recognition . onresult = ( event ) => { const voice = event . results [ 0 ] [ 0 ] . transcript ; let startTime = startAt ; if ( /^stop$/ . test ( voice ) ) { this . stopReading ( ) ; this . speak ( "Stopping" ) ; } else if ( /^pause$/ . test ( voice ) ) { this . pauseSpeach ( ) ; this . speak ( `Pausing at ${ this . timeToWords ( this . ingameTime ) }` ) ; } else if ( /^resume$/ . test ( voice ) ) { this . speak ( `Resuming at ${ this . timeToWords ( this . ingameTime ) }` ) ; this . readBuildOrder ( this . ingameTime ) ; } else if ( /^go\b/ . test ( voice ) ) { this . stop ( ) ; const numberWords = voice . replace ( /^go\s+/ , "" ) ; const time = this . wordsToNumber ( numberWords ) ; if ( time ) { const speach = this . timeToWords ( time ) ; if ( time >= 60 ) { const minutes = Math . floor ( time / 100 ) ; const seconds = time % 100 ; startTime = minutes * 60 + seconds ; } else { startTime = time ; } this . speak ( speach ) ; } this . readBuildOrder ( startTime ) ; } } ; this . recognition . onnomatch = ( ) => { this . props . log ( { error : "I didn't recognise what was said." , } ) ; } ; this . recognition . onerror = ( event ) => { if ( event . error && event . error !== "no-speech" ) { this . stopReading ( ) ; } } ; this . recognition . onend = ( ) => { if ( this . listeningToSpeach ) { this . recognition ?. start ( ) ; } } ; } timeToWords ( time ) { if ( time >= 60 ) { const minutes = Math . floor ( time / 100 ) ; const seconds = time % 100 ; if ( seconds === 0 ) { return `${ minutes } minutes` ; } else { return `${ minutes }:${ seconds }` ; } } else { return `${ time } seconds` ; } } readBuildOrder ( startAt = 0 ) { if ( ! this . state . startAtSpeach ) { this . speak ( "3" , 0 ) ; this . speak ( "2" , 1 ) ; this . speak ( "1" , 2 ) ; this . lineHandlers . push ( setTimeout ( ( ) => this . readBuildOrderWithoutTimer ( startAt ) , 3000 ) ) ; } else { this . readBuildOrderWithoutTimer ( startAt ) ; } } readBuildOrderWithoutTimer ( startAt = 0 ) { const instructionList = [ ] ; const lastEvent = this . props . gamelogic . eventLog [ this . props . gamelogic . eventLog . length - 1 ] ; let prevInstruction ; let boEndTime = 0 ; for ( const event of this . props . gamelogic . eventLog ) { const when = ( event . start * 1000 ) / 22.4 - startAt * 1000 ; if ( when >= 0 ) { const isLastMessage = event === lastEvent ; boEndTime = Math . max ( boEndTime , event ?. end || event . start ) ; if ( prevInstruction && prevInstruction . when === when ) { prevInstruction . events . push ( event ) ; prevInstruction . isLastMessage = isLastMessage ; } else { prevInstruction = { events : [ event ] , when , ingameWhen : Math . floor ( event . start / 22.4 ) , isLastMessage , } ; instructionList . push ( prevInstruction ) ; } } } const shownInstructions = [ ] ; instructionList . forEach ( ( instruction , i ) => { const events = instruction . events ; const counts = countBy ( events , "name" ) ; const uniqItems = uniqBy ( events , "name" ) ; const eventsWithPlurals = uniqItems . map ( ( event ) => { const name = event . name . replace ( /_/g , " " ) ; const count = counts [ event . name ] ; return count === 1 ? name : `${ count } ${ name }s` ; } ) ; let text = eventsWithPlurals . length === 1 ? eventsWithPlurals [ 0 ] : `${ eventsWithPlurals . slice ( 0 , - 1 ) . join ( ", " ) }, and ${ last ( eventsWithPlurals ) }` ; if ( instruction . isLastMessage ) { text += ` Then the build order ends at ${ this . secondsToTimestamp ( Math . floor ( boEndTime / 22.4 ) , "seconds" ) }` ; } this . speak ( text , instruction . when , instruction . isLastMessage ) ; const eventElements = uniqItems . map ( ( event , j ) => { const count = counts [ event . name ] ; const image = getImageOfItem ( event ) ; const element = < img key = { `read_icon_${ i }_${ j }` } src = { image } alt = { event . name } / > ; return count === 1 ? ( element ) : ( < span key = { `read_group_${ i }_${ j }` } className = { CLASSES . readIcon } > { element } ✖ { count } < /span>); } ) ; const elements = ( < div key = { `instruc_${ i }` } className = { CLASSES . readInstruction } > < div className = { CLASSES . readIconGroup } > { eventElements } < /div> < div className = { CLASSES . readTime } > { this . secondsToTimestamp ( instruction . ingameWhen ) } < /div> < /div>); for ( let ingameTime = instructionList [ i - 2 ] ?. ingameWhen || 0 ; ingameTime < instruction . ingameWhen ; ingameTime ++ ) { if ( shownInstructions [ ingameTime ] === undefined ) { shownInstructions [ ingameTime ] = [ ] ; } shownInstructions [ ingameTime ] . push ( elements ) ; } } ) ; this . ingameTime = startAt ; for ( let ingameTime = startAt ; shownInstructions [ ingameTime ] ; ingameTime ++ ) { this . lineHandlers . push ( setTimeout ( ( ) => { this . ingameTime = ingameTime ; this . props . log ( { element : ( < div key = "instructions" className = { CLASSES . readContainer } > < div className = { CLASSES . readCurrentTime } > { this . secondsToTimestamp ( ingameTime ) } < /div> < div className = { CLASSES . readInstructionList } > { shownInstructions [ ingameTime ] } < /div> < /div>), hideCloseButton : true , } ) ; } , ( ingameTime - startAt ) * 1000 ) ) ; } this . lineHandlers . push ( setTimeout ( ( ) => this . props . log ( ) , ( shownInstructions . length - startAt + 7 ) * 1000 ) ) ; } secondsToTimestamp ( time , secondsText = "s" ) { const minutes = Math . floor ( time / 60 ) ; const seconds = time % 60 ; return minutes >= 1 ? `${ minutes }:${ seconds > 9 ? "" : "0" }${ seconds }` : `${ seconds }${ secondsText }` ; } speak ( events , when = 0 , isLastMessage = false , language = "Google UK English Female" ) { if ( this . synth . speaking ) { this . stop ( ) ; } this . lineHandlers . push ( setTimeout ( ( ) => { const utterThis = new SpeechSynthesisUtterance ( events ) ; if ( utterThis ) { if ( ! this . voices . length ) { this . voices = this . synth . getVoices ( ) ; } const voice = find ( this . voices , { name : language } ) ; if ( voice ) { utterThis . voice = voice ; } this . synth . speak ( utterThis ) ; if ( isLastMessage ) { this . lineHandlers = [ ] ; this . setState ( { canStop : false , } ) ; } } } , when ) ) ; } stopReading ( ) { this . listeningToSpeach = false ; this . recognition ?. stop ( ) ; this . stop ( ) ; this . setState ( { canStop : false , } ) ; } stop ( ) { this . pauseSpeach ( ) ; this . props . log ( ) ; this . synth . cancel ( ) ; } pauseSpeach ( ) { this . lineHandlers . forEach ( clearTimeout ) ; this . lineHandlers = [ ] ; this . synth . cancel ( ) ; } onStartAtSpeachChange ( ) { this . setState ( { startAtSpeach : ! this . state . startAtSpeach , } ) ; } render ( ) { const classes = CLASSES . dropDown ; const classesDropdown = this . state . show ? `visible ${ classes }` : `hidden ${ classes }` ; const stopVisibility = `${ CLASSES . buttons } ${ this . state . canStop ? "visible" : "hidden" } cursor-pointer` ; const mouseEnterFunc = ( tooltip ) => ( e ) => { this . onMouseEnter ( e , < div > { tooltip } < /div>); } ; const speachStart = ! this . recognition ? ( "" ) : ( < div className = { CLASSES . dropDownContainer } > < div className = { CLASSES . dropDownSubContainer } > < label htmlFor = "voiceCommand" className = { CLASSES . dropDownLabel } data - tip data - for = "readTooltip" onMouseEnter = { mouseEnterFunc ( "Start reading the BO right when you say 'Go' in your microphone" ) } > Start reading when you say & quot ; Go & quot ; < br /> or & quot ; Go 2 : 15 & quot ; . React to & quot ; Stop & quot ; , < br /> & quot ; Pause & quot ; and & quot ; Resume & quot ; < /label> < input name = "voiceCommand" id = "voiceCommand" className = { CLASSES . dropDownInput } type = "checkbox" checked = { this . state . startAtSpeach } onChange = { ( _e ) => this . onStartAtSpeachChange ( ) } /> < /div> < /div>); const readButton = ( < div className = { CLASSES . buttons } onMouseEnter = { this . showSettings } onMouseLeave = { this . hideSettings } > Read BO & nbsp ; & nbsp ; ▷ < div className = { classesDropdown } > < div key = "readStartTime" className = { CLASSES . dropDownContainer } > < div className = { CLASSES . dropDownSubContainer } > < div className = { CLASSES . dropDownLabel } data - tip data - for = "readTooltip" onMouseEnter = { mouseEnterFunc ( "Time at which the speaker should start describing the bo" ) } > Time at which to start < /div> < input className = { CLASSES . dropDownInput } type = "number" placeholder = "0" defaultValue = "0" step = "10" min = "-20" max = "1000" onChange = { ( e ) => ( this . startTime = parseFloat ( e . target . value ) ) } /> < /div> < /div> { speachStart } < div className = { CLASSES . dropDownContainer } > < div className = { CLASSES . dropDownWideButton } onClick = { ( _e ) => this . startReading ( this . startTime ) } > < span className = { CLASSES . centeredButton } > Read ( Beta ) < /span> < /div> < /div> < /div> < /div>); const stopButton = ( < div className = { stopVisibility } onClick = { ( _e ) => this . stopReading ( ) } > Stop □ < /div>); return ( < div className = "flex" > < div > < ReactTooltip place = "bottom" id = "readTooltip" className = "max-w-xs" > { this . state . tooltipText } < /ReactTooltip> { readButton } < /div> { stopButton } < /div>); } }