Spaces:
Sleeping
Sleeping
| import { useQuery } from 'convex/react'; | |
| import { api } from '../../convex/_generated/api'; | |
| import { Id } from '../../convex/_generated/dataModel'; | |
| import closeImg from '../../assets/close.svg'; | |
| import { SelectElement } from './Player'; | |
| import { Messages } from './Messages'; | |
| import { toastOnError } from '../toasts'; | |
| import { useSendInput } from '../hooks/sendInput'; | |
| import { Player } from '../../convex/aiTown/player'; | |
| import { GameId } from '../../convex/aiTown/ids'; | |
| import { ServerGame } from '../hooks/serverGame'; | |
| export default function PlayerDetails({ | |
| worldId, | |
| engineId, | |
| game, | |
| playerId: playerId, | |
| setSelectedElement, | |
| scrollViewRef, | |
| currentPlayerId, | |
| }: { | |
| worldId: Id<'worlds'>; | |
| engineId: Id<'engines'>; | |
| game: ServerGame; | |
| playerId?: GameId<'players'>; | |
| setSelectedElement: SelectElement; | |
| scrollViewRef: React.RefObject<HTMLDivElement>; | |
| currentPlayerId:GameId<'players'>; | |
| }) { | |
| const oauth = JSON.parse(localStorage.getItem('oauth')); | |
| const oauthToken = oauth ? oauth.userInfo.fullname : undefined; | |
| const humanTokenIdentifier = useQuery(api.world.userStatus, { worldId, oauthToken }); | |
| const players = [...game.world.players.values()]; | |
| const humanPlayer = players.find((p) => p.human === humanTokenIdentifier); | |
| const humanConversation = humanPlayer ? game.world.playerConversation(humanPlayer) : undefined; | |
| // Always select the other player if we're in a conversation with them. | |
| if (humanPlayer && humanConversation) { | |
| const otherPlayerIds = [...humanConversation.participants.keys()].filter( | |
| (p) => p !== humanPlayer.id, | |
| ); | |
| playerId = otherPlayerIds[0]; | |
| } | |
| const player = playerId && game.world.players.get(playerId); | |
| const playerConversation = player && game.world.playerConversation(player); | |
| const previousConversation = useQuery( | |
| api.world.previousConversation, | |
| playerId ? { worldId, playerId: playerId } : 'skip', | |
| ); | |
| const playerDescription = playerId && game.playerDescriptions.get(playerId); | |
| const startConversation = useSendInput(engineId, 'startConversation'); | |
| const acceptInvite = useSendInput(engineId, 'acceptInvite'); | |
| const rejectInvite = useSendInput(engineId, 'rejectInvite'); | |
| const leaveConversation = useSendInput(engineId, 'leaveConversation'); | |
| if (!playerId) { | |
| return ( | |
| <div className="h-full text-xl flex text-center items-center p-4"> | |
| Click on an agent on the map to see chat history. | |
| </div> | |
| ); | |
| } | |
| if (!player) { | |
| return null; | |
| } | |
| const isMe = humanPlayer && player.id === humanPlayer.id; | |
| const canInvite = !isMe && !playerConversation && humanPlayer && !humanConversation; | |
| const sameConversation = | |
| !isMe && | |
| humanPlayer && | |
| humanConversation && | |
| playerConversation && | |
| humanConversation.id === playerConversation.id; | |
| const humanStatus = | |
| humanPlayer && humanConversation && humanConversation.participants.get(humanPlayer.id)?.status; | |
| const playerStatus = playerConversation && playerConversation.participants.get(playerId)?.status; | |
| const haveInvite = sameConversation && humanStatus?.kind === 'invited'; | |
| const waitingForAccept = | |
| sameConversation && playerConversation.participants.get(playerId)?.status.kind === 'invited'; | |
| const waitingForNearby = | |
| sameConversation && playerStatus?.kind === 'walkingOver' && humanStatus?.kind === 'walkingOver'; | |
| const inConversationWithMe = | |
| sameConversation && | |
| playerStatus?.kind === 'participating' && | |
| humanStatus?.kind === 'participating'; | |
| const onStartConversation = async () => { | |
| if (!humanPlayer || !playerId) { | |
| return; | |
| } | |
| console.log(`Starting conversation`); | |
| await toastOnError(startConversation({ playerId: humanPlayer.id, invitee: playerId })); | |
| }; | |
| const onAcceptInvite = async () => { | |
| if (!humanPlayer || !humanConversation || !playerId) { | |
| return; | |
| } | |
| await toastOnError( | |
| acceptInvite({ | |
| playerId: humanPlayer.id, | |
| conversationId: humanConversation.id, | |
| }), | |
| ); | |
| }; | |
| const onRejectInvite = async () => { | |
| if (!humanPlayer || !humanConversation) { | |
| return; | |
| } | |
| await toastOnError( | |
| rejectInvite({ | |
| playerId: humanPlayer.id, | |
| conversationId: humanConversation.id, | |
| }), | |
| ); | |
| }; | |
| const onLeaveConversation = async () => { | |
| if (!humanPlayer || !inConversationWithMe || !humanConversation) { | |
| return; | |
| } | |
| await toastOnError( | |
| leaveConversation({ | |
| playerId: humanPlayer.id, | |
| conversationId: humanConversation.id, | |
| }), | |
| ); | |
| }; | |
| // const pendingSuffix = (inputName: string) => | |
| // [...inflightInputs.values()].find((i) => i.name === inputName) ? ' opacity-50' : ''; | |
| const pendingSuffix = (s: string) => ''; | |
| return ( | |
| <> | |
| <div className="flex gap-4"> | |
| <div className="box w-3/4 sm:w-full mr-auto"> | |
| <h2 className="bg-brown-700 p-2 font-display text-2xl sm:text-4xl tracking-wider shadow-solid text-center"> | |
| {playerDescription?.name} | |
| </h2> | |
| </div> | |
| <a | |
| className="button text-white shadow-solid text-2xl cursor-pointer pointer-events-auto" | |
| onClick={() => setSelectedElement(undefined)} | |
| > | |
| <h2 className="h-full bg-clay-700"> | |
| <img className="w-4 h-4 sm:w-5 sm:h-5" src={closeImg} /> | |
| </h2> | |
| </a> | |
| </div> | |
| {canInvite && ( | |
| <a | |
| className={ | |
| 'mt-6 button text-white shadow-solid text-xl cursor-pointer pointer-events-auto' + | |
| pendingSuffix('startConversation') | |
| } | |
| onClick={onStartConversation} | |
| > | |
| <div className="h-full bg-clay-700 text-center"> | |
| <span>Start conversation</span> | |
| </div> | |
| </a> | |
| )} | |
| {waitingForAccept && ( | |
| <a className="mt-6 button text-white shadow-solid text-xl cursor-pointer pointer-events-auto opacity-50"> | |
| <div className="h-full bg-clay-700 text-center"> | |
| <span>Waiting for accept...</span> | |
| </div> | |
| </a> | |
| )} | |
| {waitingForNearby && ( | |
| <a className="mt-6 button text-white shadow-solid text-xl cursor-pointer pointer-events-auto opacity-50"> | |
| <div className="h-full bg-clay-700 text-center"> | |
| <span>Walking over...</span> | |
| </div> | |
| </a> | |
| )} | |
| {inConversationWithMe && ( | |
| <a | |
| className={ | |
| 'mt-6 button text-white shadow-solid text-xl cursor-pointer pointer-events-auto' + | |
| pendingSuffix('leaveConversation') | |
| } | |
| onClick={onLeaveConversation} | |
| > | |
| <div className="h-full bg-clay-700 text-center"> | |
| <span>Leave conversation</span> | |
| </div> | |
| </a> | |
| )} | |
| {haveInvite && ( | |
| <> | |
| <a | |
| className={ | |
| 'mt-6 button text-white shadow-solid text-xl cursor-pointer pointer-events-auto' + | |
| pendingSuffix('acceptInvite') | |
| } | |
| onClick={onAcceptInvite} | |
| > | |
| <div className="h-full bg-clay-700 text-center"> | |
| <span>Accept</span> | |
| </div> | |
| </a> | |
| <a | |
| className={ | |
| 'mt-6 button text-white shadow-solid text-xl cursor-pointer pointer-events-auto' + | |
| pendingSuffix('rejectInvite') | |
| } | |
| onClick={onRejectInvite} | |
| > | |
| <div className="h-full bg-clay-700 text-center"> | |
| <span>Reject</span> | |
| </div> | |
| </a> | |
| </> | |
| )} | |
| {!playerConversation && player.activity && player.activity.until > Date.now() && ( | |
| <div className="box flex-grow mt-6"> | |
| <h2 className="bg-brown-700 text-base sm:text-lg text-center"> | |
| {player.activity.description} | |
| </h2> | |
| </div> | |
| )} | |
| <div className="desc my-6"> | |
| <p className="leading-tight -m-4 bg-brown-700 text-base sm:text-sm"> | |
| {!isMe && playerDescription?.description} | |
| {isMe && <i>This is you! You are a {playerDescription?.type}</i>} | |
| {!isMe && inConversationWithMe && ( | |
| <> | |
| <br /> | |
| <br />(<i>Conversing with you!</i>) | |
| </> | |
| )} | |
| </p> | |
| </div> | |
| {!isMe && playerConversation && playerStatus?.kind === 'participating' && ( | |
| <Messages | |
| worldId={worldId} | |
| engineId={engineId} | |
| inConversationWithMe={inConversationWithMe ?? false} | |
| conversation={{ kind: 'active', doc: playerConversation }} | |
| humanPlayer={humanPlayer} | |
| scrollViewRef={scrollViewRef} | |
| /> | |
| )} | |
| {!playerConversation && previousConversation && ( | |
| <> | |
| <div className="box flex-grow"> | |
| <h2 className="bg-brown-700 text-lg text-center">Previous conversation</h2> | |
| </div> | |
| <Messages | |
| worldId={worldId} | |
| engineId={engineId} | |
| inConversationWithMe={false} | |
| conversation={{ kind: 'archived', doc: previousConversation }} | |
| humanPlayer={humanPlayer} | |
| scrollViewRef={scrollViewRef} | |
| /> | |
| </> | |
| )} | |
| </> | |
| ); | |
| } | |