Jofthomas's picture
Jofthomas HF staff
multiplayer
0f725c2
raw
history blame
5.71 kB
import * as PIXI from 'pixi.js';
import { useApp } from '@pixi/react';
import { Player, SelectElement } from './Player.tsx';
import { useEffect, useRef, useState } from 'react';
import PixiStaticMap from './PixiStaticMap.tsx';
import PixiViewport from './PixiViewport.tsx';
import { Viewport } from 'pixi-viewport';
import { Id } from '../../convex/_generated/dataModel';
import { useQuery } from 'convex/react';
import { api } from '../../convex/_generated/api.js';
import { useSendInput } from '../hooks/sendInput.ts';
import { toastOnError } from '../toasts.ts';
import { DebugPath } from './DebugPath.tsx';
import { PositionIndicator } from './PositionIndicator.tsx';
import { SHOW_DEBUG_UI } from './Game.tsx';
import { ServerGame } from '../hooks/serverGame.ts';
export const PixiGame = (props: {
worldId: Id<'worlds'>;
engineId: Id<'engines'>;
game: ServerGame;
historicalTime: number | undefined;
width: number;
height: number;
setSelectedElement: SelectElement;
}) => {
// PIXI setup.
const pixiApp = useApp();
const viewportRef = useRef<Viewport | undefined>();
const oauth = JSON.parse(localStorage.getItem('oauth'));
const oauthToken = oauth ? oauth.userInfo.fullname : undefined;
const humanTokenIdentifier = useQuery(api.world.userStatus, { worldId: props.worldId, oauthToken }) ?? null;
const humanPlayerId = [...props.game.world.players.values()].find(
(p) => p.human === humanTokenIdentifier,
)?.id;
const moveTo = useSendInput(props.engineId, 'moveTo');
// Interaction for clicking on the world to navigate.
const dragStart = useRef<{ screenX: number; screenY: number } | null>(null);
const onMapPointerDown = (e: any) => {
// https://pixijs.download/dev/docs/PIXI.FederatedPointerEvent.html
dragStart.current = { screenX: e.screenX, screenY: e.screenY };
};
const [lastDestination, setLastDestination] = useState<{
x: number;
y: number;
t: number;
} | null>(null);
const onMapPointerUp = async (e: any) => {
if (dragStart.current) {
const { screenX, screenY } = dragStart.current;
dragStart.current = null;
const [dx, dy] = [screenX - e.screenX, screenY - e.screenY];
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 10) {
console.log(`Skipping navigation on drag event (${dist}px)`);
return;
}
}
if (!humanPlayerId) {
return;
}
const viewport = viewportRef.current;
if (!viewport) {
return;
}
const gameSpacePx = viewport.toWorld(e.screenX, e.screenY);
const tileDim = props.game.worldMap.tileDim;
const gameSpaceTiles = {
x: gameSpacePx.x / tileDim,
y: gameSpacePx.y / tileDim,
};
setLastDestination({ t: Date.now(), ...gameSpaceTiles });
const roundedTiles = {
x: Math.floor(gameSpaceTiles.x),
y: Math.floor(gameSpaceTiles.y),
};
console.log(`Moving to ${JSON.stringify(roundedTiles)}`);
await toastOnError(moveTo({ playerId: humanPlayerId, destination: roundedTiles }));
};
const { width, height, tileDim } = props.game.worldMap;
const players = [...props.game.world.players.values()];
// State to manage the current tileSet
const [currentTileSet, setCurrentTileSet] = useState({
background: props.game.worldMap.bgTiles,
objectMap: props.game.worldMap.objectTiles,
decor: props.game.worldMap.decorTiles,
});
// Effect hook to change the tileSet based on a day/night condition
useEffect(() => {
const { cycleState } = props.game.world.gameCycle;
const tileSet = (cycleState === 'Night' || cycleState === 'PlayerKillVoting')
? {
background: props.game.worldMap.bgTilesN,
objectMap: props.game.worldMap.objectTilesN,
decor: props.game.worldMap.decorTilesN,
}
: {
background: props.game.worldMap.bgTiles,
objectMap: props.game.worldMap.objectTiles,
decor: props.game.worldMap.decorTiles,
};
setCurrentTileSet(tileSet);
}, [props.game.world.gameCycle.cycleState]);
// Zoom on the user’s avatar when it is created
useEffect(() => {
if (!viewportRef.current || humanPlayerId === undefined) return;
const humanPlayer = props.game.world.players.get(humanPlayerId)!;
viewportRef.current.animate({
position: new PIXI.Point(humanPlayer.position.x * tileDim, humanPlayer.position.y * tileDim),
scale: 1.5,
});
}, [humanPlayerId]);
return (
<PixiViewport
app={pixiApp}
screenWidth={props.width}
screenHeight={props.height}
worldWidth={width * tileDim}
worldHeight={height * tileDim}
viewportRef={viewportRef}
>
<PixiStaticMap
map={{
...props.game.worldMap,
bgTiles: currentTileSet.background,
objectTiles: currentTileSet.objectMap,
decorTiles: currentTileSet.decor,
serialize: props.game.worldMap.serialize
}}
onpointerup={onMapPointerUp}
onpointerdown={onMapPointerDown}
/>
{players.map(
(p) =>
// Only show the path for the human player in non-debug mode.
(SHOW_DEBUG_UI || p.id === humanPlayerId) && (
<DebugPath key={`path-${p.id}`} player={p} tileDim={tileDim} />
),
)}
{lastDestination && <PositionIndicator destination={lastDestination} tileDim={tileDim} />}
{players.map((p) => (
<Player
key={`player-${p.id}`}
game={props.game}
player={p}
isViewer={p.id === humanPlayerId}
onClick={props.setSelectedElement}
historicalTime={props.historicalTime}
/>
))}
</PixiViewport>
);
};
export default PixiGame;