Spaces:
Sleeping
Sleeping
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; | |