7.3. Only move to adjacent spaces
It would feel more game-like if the player could only move one space at a time, rather than being able to teleport to any space on the map. Let's add that.
Map library
Let's start with a new library for the map logic. This will keep the complicated, pure functions outside of our systems to make them a little easier to follow. We'll use the "Manhattan distance" approach for checking the distance between two coordinates.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { Coord } from "components/PositionComponent.sol";
library LibMap {
function distance(Coord memory from, Coord memory to) internal pure returns (int32) {
int32 deltaX = from.x > to.x ? from.x - to.x : to.x - from.x;
int32 deltaY = from.y > to.y ? from.y - to.y : to.y - from.y;
return deltaX + deltaY;
}
}
Check distance in move system
Now we can use our new distance function to check that the player is only moving one space at a time.
import { PositionComponent, ID as PositionComponentID, Coord } from "components/PositionComponent.sol";
import { MovableComponent, ID as MovableComponentID } from "components/MovableComponent.sol";
import { MapConfigComponent, ID as MapConfigComponentID, MapConfig } from "components/MapConfigComponent.sol";
import { LibMap } from "libraries/LibMap.sol";
uint256 constant ID = uint256(keccak256("system.Move"));
contract MoveSystem is System {
…
function executeTyped(Coord memory coord) public returns (bytes memory) {
uint256 entityId = addressToEntity(msg.sender);
MovableComponent movable = MovableComponent(getAddressById(components, MovableComponentID));
require(movable.has(entityId), "cannot move");
PositionComponent position = PositionComponent(getAddressById(components, PositionComponentID));
require(LibMap.distance(position.getValue(entityId), coord) == 1, "can only move to adjacent spaces");
// Constrain position to map size, wrapping around if necessary
MapConfig memory mapConfig = MapConfigComponent(getAddressById(components, MapConfigComponentID)).getValue();
coord.x = (coord.x + int32(mapConfig.width)) % int32(mapConfig.width);
coord.y = (coord.y + int32(mapConfig.height)) % int32(mapConfig.height);
position.set(entityId, coord);
Remove click-to-move
Keyboard movement is already configured to move one tile at a time, but we can still click anywhere on the map to move there. With our new distance check, clicking more than one tile away will cause our transactions to fail/revert—not a great user experience. Let's update our client to only allow click-to-spawn.
import { useComponentValue } from "@latticexyz/react";
import { twMerge } from "tailwind-merge";
import { useMUD } from "./MUDContext";
…
export const GameBoard = () => {
…
const {
components: { Position, Player },
api: { joinGame },
playerEntity,
} = useMUD();
…
return (
<div className="inline-grid p-2 bg-lime-500">
{rows.map((y) =>
columns.map((x) => (
<div
key={`${x},${y}`}
className={twMerge(
"w-8 h-8 flex items-center justify-center",
canJoinGame ? "cursor-pointer hover:ring" : null
)}
style={{
gridColumn: x + 1,
gridRow: y + 1,
}}
onClick={(event) => {
event.preventDefault();
if (canJoinGame) {
joinGame(x, y);
}
}}
>