ai-town / patches /PlayerDetails.tsx
Jofthomas's picture
Jofthomas HF staff
Multiplayer with HF
409830c verified
raw
history blame
No virus
9.06 kB
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,
setSelectedElement,
scrollViewRef,
}: {
worldId: Id<'worlds'>;
engineId: Id<'engines'>;
game: ServerGame;
playerId?: GameId<'players'>;
setSelectedElement: SelectElement;
scrollViewRef: React.RefObject<HTMLDivElement>;
}) {
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 } : '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!</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}
/>
</>
)}
</>
);
}