Spaces:
Sleeping
Sleeping
File size: 6,452 Bytes
90cbf22 df2ef4f 90cbf22 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
import { movementSpeed } from '../../data/characters';
import { COLLISION_THRESHOLD } from '../constants';
import { compressPath, distance, manhattanDistance, pointsEqual } from '../util/geometry';
import { MinHeap } from '../util/minheap';
import { Point, Vector } from '../util/types';
import { Game } from './game';
import { GameId } from './ids';
import { Player } from './player';
import { WorldMap } from './worldMap';
type PathCandidate = {
position: Point;
facing?: Vector;
t: number;
length: number;
cost: number;
prev?: PathCandidate;
};
export function stopPlayer(player: Player) {
delete player.pathfinding;
player.speed = 0;
}
export function movePlayer(
game: Game,
now: number,
player: Player,
destination: Point,
allowInConversation?: boolean,
) {
if (Math.floor(destination.x) !== destination.x || Math.floor(destination.y) !== destination.y) {
throw new Error(`Non-integral destination: ${JSON.stringify(destination)}`);
}
const { position } = player;
// Close enough to current position or destination => no-op.
if (pointsEqual(position, destination)) {
return;
}
// Disallow movement in the corresponding cycle state
const { cycleState } = game.world.gameCycle;
if (cycleState === 'Night' || cycleState === 'PlayerKillVoting') {
// 'villager' cannot move
if (player.playerType(game) === 'villager') {
return;
}
}
// Don't allow players in a conversation to move.
const inConversation = [...game.world.conversations.values()].some(
(c) => c.participants.get(player.id)?.status.kind === 'participating',
);
if (inConversation && !allowInConversation) {
throw new Error(`Can't move when in a conversation. Leave the conversation first!`);
}
player.pathfinding = {
destination: destination,
started: now,
state: {
kind: 'needsPath',
},
};
return;
}
export function findRoute(game: Game, now: number, player: Player, destination: Point) {
const minDistances: PathCandidate[][] = [];
const explore = (current: PathCandidate): Array<PathCandidate> => {
const { x, y } = current.position;
const neighbors = [];
// If we're not on a grid point, first try to move horizontally
// or vertically to a grid point. Note that this can create very small
// deltas between the current position and the nearest grid point so
// be careful to preserve the `facing` vectors rather than trying to
// derive them anew.
if (x !== Math.floor(x)) {
neighbors.push(
{ position: { x: Math.floor(x), y }, facing: { dx: -1, dy: 0 } },
{ position: { x: Math.floor(x) + 1, y }, facing: { dx: 1, dy: 0 } },
);
}
if (y !== Math.floor(y)) {
neighbors.push(
{ position: { x, y: Math.floor(y) }, facing: { dx: 0, dy: -1 } },
{ position: { x, y: Math.floor(y) + 1 }, facing: { dx: 0, dy: 1 } },
);
}
// Otherwise, just move to adjacent grid points.
if (x == Math.floor(x) && y == Math.floor(y)) {
neighbors.push(
{ position: { x: x + 1, y }, facing: { dx: 1, dy: 0 } },
{ position: { x: x - 1, y }, facing: { dx: -1, dy: 0 } },
{ position: { x, y: y + 1 }, facing: { dx: 0, dy: 1 } },
{ position: { x, y: y - 1 }, facing: { dx: 0, dy: -1 } },
);
}
const next = [];
for (const { position, facing } of neighbors) {
const segmentLength = distance(current.position, position);
const length = current.length + segmentLength;
if (blocked(game, now, position, player.id)) {
continue;
}
const remaining = manhattanDistance(position, destination);
const path = {
position,
facing,
// Movement speed is in tiles per second.
t: current.t + (segmentLength / movementSpeed) * 1000,
length,
cost: length + remaining,
prev: current,
};
const existingMin = minDistances[position.y]?.[position.x];
if (existingMin && existingMin.cost <= path.cost) {
continue;
}
minDistances[position.y] ??= [];
minDistances[position.y][position.x] = path;
next.push(path);
}
return next;
};
const startingLocation = player.position;
const startingPosition = { x: startingLocation.x, y: startingLocation.y };
let current: PathCandidate | undefined = {
position: startingPosition,
facing: player.facing,
t: now,
length: 0,
cost: manhattanDistance(startingPosition, destination),
prev: undefined,
};
let bestCandidate = current;
const minheap = MinHeap<PathCandidate>((p0, p1) => p0.cost > p1.cost);
while (current) {
if (pointsEqual(current.position, destination)) {
break;
}
if (
manhattanDistance(current.position, destination) <
manhattanDistance(bestCandidate.position, destination)
) {
bestCandidate = current;
}
for (const candidate of explore(current)) {
minheap.push(candidate);
}
current = minheap.pop();
}
let newDestination = null;
if (!current) {
if (bestCandidate.length === 0) {
return null;
}
current = bestCandidate;
newDestination = current.position;
}
const densePath = [];
let facing = current.facing!;
while (current) {
densePath.push({ position: current.position, t: current.t, facing });
facing = current.facing!;
current = current.prev;
}
densePath.reverse();
return { path: compressPath(densePath), newDestination };
}
export function blocked(game: Game, now: number, pos: Point, playerId?: GameId<'players'>) {
const otherPositions = [...game.world.players.values()]
.filter((p) => p.id !== playerId)
.map((p) => p.position);
return blockedWithPositions(pos, otherPositions, game.worldMap);
}
export function blockedWithPositions(position: Point, otherPositions: Point[], map: WorldMap) {
if (isNaN(position.x) || isNaN(position.y)) {
throw new Error(`NaN position in ${JSON.stringify(position)}`);
}
if (position.x < 0 || position.y < 0 || position.x >= map.width || position.y >= map.height) {
return 'out of bounds';
}
for (const layer of map.objectTiles) {
if (layer[Math.floor(position.x)][Math.floor(position.y)] !== -1) {
return 'world blocked';
}
}
for (const otherPosition of otherPositions) {
if (distance(otherPosition, position) < COLLISION_THRESHOLD) {
return 'player';
}
}
return null;
}
|