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 === 'Day' || cycleState === 'Night') | |
| ? { | |
| background: props.game.worldMap.bgTiles, | |
| objectMap: props.game.worldMap.objectTiles, | |
| decor: props.game.worldMap.decorTiles, | |
| } | |
| : { | |
| background: props.game.worldMap.bgTilesN, | |
| objectMap: props.game.worldMap.objectTilesN, | |
| decor: props.game.worldMap.decorTilesN, | |
| }; | |
| 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; | |