Enemy AI
This commit is contained in:
parent
9c0aad2e75
commit
c66446c2b3
|
|
@ -46,6 +46,13 @@ export function* zip(...args: Iterable<any>[]) {
|
|||
}
|
||||
}
|
||||
|
||||
export function* enumerate<T>(iterable: Iterable<T>): Generator<[number, T]> {
|
||||
let i = 0;
|
||||
for (const item of iterable) {
|
||||
yield [i++, item];
|
||||
}
|
||||
}
|
||||
|
||||
export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k);
|
||||
export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
||||
export const lerp = (start: number, end: number, t: number) => (start + (end - start) * t);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { lerp } from "@common/utils";
|
||||
import Item from "./item";
|
||||
import Item, { ItemType } from "./item";
|
||||
import Tile from "./tile";
|
||||
|
||||
const MOVE_DURATION = .1;
|
||||
|
|
@ -8,6 +8,13 @@ export default class Character extends Item {
|
|||
public tile = new Tile([0, 0], 1);
|
||||
private path: Tile[] = [];
|
||||
private pathProgress = 0;
|
||||
|
||||
get moveBonus() {
|
||||
if (this.type === ItemType.CHAR_RUNNER || this.type === ItemType.ENEMY_DOG) return 1;
|
||||
if (this.type === ItemType.ENEMY_ZOMBIE) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public get displayPosition(): [number, number] {
|
||||
if (this.path.length > 1) {
|
||||
|
|
@ -16,6 +23,11 @@ export default class Character extends Item {
|
|||
const currentTileIdx = Math.floor(progress);
|
||||
|
||||
const currentTile = this.path[currentTileIdx];
|
||||
|
||||
if (!currentTile) {
|
||||
return [this.tile.centerX, this.tile.centerY];
|
||||
}
|
||||
|
||||
const nextTile = this.path[currentTileIdx + 1];
|
||||
|
||||
if (!nextTile) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace Pathfinding {
|
|||
* A* search from start to end Tile.
|
||||
* @returns array of Tiles from start to end (inclusive), or [] if no path.
|
||||
*/
|
||||
export function findPath(start: Tile, end: Tile): Tile[] {
|
||||
export function findPath(start: Tile, end: Tile, forEnemy = false): Tile[] {
|
||||
// The set of discovered nodes to be evaluated
|
||||
const openSet: Set<Tile> = new Set([start]);
|
||||
|
||||
|
|
@ -21,6 +21,9 @@ namespace Pathfinding {
|
|||
// For path reconstruction: map each node to the node it can most efficiently be reached from.
|
||||
const cameFrom = new Map<Tile, Tile>();
|
||||
|
||||
let closestTile: Tile = start;
|
||||
let closestDistance = heuristic(closestTile, end);
|
||||
|
||||
while (openSet.size > 0) {
|
||||
// Get the node in openSet having the lowest fScore
|
||||
let current: Tile | null = null;
|
||||
|
|
@ -34,6 +37,12 @@ namespace Pathfinding {
|
|||
}
|
||||
if (!current) break;
|
||||
|
||||
const dist = heuristic(current, end);
|
||||
if (closestDistance < dist) {
|
||||
closestTile = current;
|
||||
closestDistance = dist;
|
||||
}
|
||||
|
||||
// If we’ve reached the goal, reconstruct and return the path.
|
||||
if (current === end) {
|
||||
return reconstructPath(cameFrom, current);
|
||||
|
|
@ -41,7 +50,7 @@ namespace Pathfinding {
|
|||
|
||||
openSet.delete(current);
|
||||
|
||||
if (current !== start && current.items.length > 0) continue;
|
||||
if (current !== start && !forEnemy && current.items.length > 0) continue;
|
||||
|
||||
for (const neighbor of current.activeConnections) {
|
||||
// tentative gScore is current’s gScore plus cost to move to neighbor
|
||||
|
|
@ -60,6 +69,10 @@ namespace Pathfinding {
|
|||
}
|
||||
|
||||
// Open set is empty but goal was never reached
|
||||
if (forEnemy) {
|
||||
return reconstructPath(cameFrom, closestTile);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,10 +35,6 @@ export default class Player extends Character {
|
|||
return this.type === ItemType.CHAR_NURSE ? 2 : 1;
|
||||
}
|
||||
|
||||
get moveBonus() {
|
||||
return this.type === ItemType.CHAR_RUNNER ? 1 : 0;
|
||||
}
|
||||
|
||||
get isDead() {
|
||||
return this.health <= 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type Character from "./character";
|
||||
import Entity from "./entity";
|
||||
import type Item from "./item";
|
||||
import { ItemType } from "./item";
|
||||
|
|
@ -45,7 +46,17 @@ export default class Tile extends Entity {
|
|||
if (this.items.length > 0) {
|
||||
if (this.isOpen) {
|
||||
const item = this.enemy ?? this.items[0];
|
||||
ctx.drawImage(item.type, 0.1, 0.1, 0.8, 0.8);
|
||||
|
||||
if ('displayPosition' in item) {
|
||||
const character = item as Character;
|
||||
const [x, y] = character.displayPosition;
|
||||
const offsetX = (x - this.centerX) / this.width;
|
||||
const offsetY = (y - this.centerY) / this.height;
|
||||
|
||||
ctx.drawImage(character.type, offsetX + 0.1, offsetY + 0.1, 0.8, 0.8);
|
||||
} else {
|
||||
ctx.drawImage(item.type, 0.1, 0.1, 0.8, 0.8);
|
||||
}
|
||||
|
||||
if (this.items.length > 1) {
|
||||
ctx.fillStyle = 'black';
|
||||
|
|
@ -72,8 +83,8 @@ export default class Tile extends Entity {
|
|||
}
|
||||
}
|
||||
|
||||
get enemy() {
|
||||
return this.items.find(i => i.isEnemy);
|
||||
get enemy(): Character | undefined {
|
||||
return this.items.find(i => i.isEnemy) as Character;
|
||||
}
|
||||
|
||||
get activeConnections() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { range, shuffle, zip } from "@common/utils";
|
||||
import { enumerate, randInt, range, shuffle, zip } from "@common/utils";
|
||||
import Entity from "./entity";
|
||||
import Tile, { TileType } from "./tile";
|
||||
import Pathfinding from "./pathfinding";
|
||||
import Item, { ItemType } from "./item";
|
||||
import Player, { Players } from "./player";
|
||||
import Spinner, { SpinnerAction } from "./spinner";
|
||||
import Character from "./character";
|
||||
|
||||
enum GameState {
|
||||
NORMAL,
|
||||
|
|
@ -171,7 +172,14 @@ export default class TileMap extends Entity {
|
|||
|
||||
for (const [type, amount] of itemsMap.entries()) {
|
||||
for (const _ in range(amount)) {
|
||||
const item = new Item(type);
|
||||
const Constructor = [
|
||||
ItemType.ENEMY_BOSS,
|
||||
ItemType.ENEMY_DOG,
|
||||
ItemType.ENEMY_SPIDER,
|
||||
ItemType.ENEMY_ZOMBIE,
|
||||
].includes(type) ? Character : Item;
|
||||
|
||||
const item = new Constructor(type);
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
|
@ -199,10 +207,16 @@ export default class TileMap extends Entity {
|
|||
|
||||
for (const [tile, item] of zip(fillableTiles, shuffle(items))) {
|
||||
tile.items.push(item);
|
||||
if (item instanceof Player) {
|
||||
if (item instanceof Character) {
|
||||
item.tile = tile;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (const tile of this.tiles) {
|
||||
if (tile.items.length === 0) {
|
||||
tile.isOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get player() {
|
||||
|
|
@ -218,6 +232,15 @@ export default class TileMap extends Entity {
|
|||
));
|
||||
}
|
||||
|
||||
private get enemies() {
|
||||
return this.tiles.filter(
|
||||
(tile) =>
|
||||
tile.isOpen
|
||||
&& tile.enemy
|
||||
&& tile.items.length === 1
|
||||
)
|
||||
}
|
||||
|
||||
public override handleMouseMove(x: number, y: number): void {
|
||||
this.activeTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
|
||||
}
|
||||
|
|
@ -350,8 +373,42 @@ export default class TileMap extends Entity {
|
|||
}
|
||||
}
|
||||
setTimeout(alert, 2000, "🎉🎉🎉 ПОБЕДА! 🚗 🎉🎉🎉");
|
||||
} else if (this.currentPlayerIdx === 0) {
|
||||
// TODO zombies turn
|
||||
} else if (this.currentPlayerIdx === 0 && this.players.length < this.foundPlayers.size) { // if someone is dead
|
||||
const enemies = this.enemies;
|
||||
|
||||
loop:
|
||||
for (const enemyTile of shuffle(enemies)) {
|
||||
const moves = Pathfinding.findPossibleMoves(enemyTile, 5);
|
||||
|
||||
for (const move of shuffle(moves)) {
|
||||
if (move.items.length > 0) continue;
|
||||
|
||||
for (const [playerIndex, player] of enumerate(this.players)) {
|
||||
if (player.tile === move) {
|
||||
const enemy = enemyTile.enemy!;
|
||||
const allowedSteps = randInt(1, 5) + enemy.moveBonus;
|
||||
|
||||
console.log({enemy, allowedSteps});
|
||||
if (allowedSteps <= 0) continue;
|
||||
|
||||
const path = Pathfinding.findPath(enemyTile, player.tile, true);
|
||||
|
||||
const targetTile = path[allowedSteps] ?? player.tile;
|
||||
|
||||
enemyTile.removeItem(enemy);
|
||||
targetTile.items.push(enemy);
|
||||
enemy.moveTo(targetTile, path.slice(0, allowedSteps + 1));
|
||||
|
||||
if (targetTile === player.tile) {
|
||||
this.state = GameState.FIGHT;
|
||||
this.currentPlayerIdx = playerIndex;
|
||||
}
|
||||
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -457,5 +514,6 @@ export default class TileMap extends Entity {
|
|||
|
||||
public update(dt: number) {
|
||||
this.players.forEach((player) => player.update(dt));
|
||||
this.enemies.forEach((tile) => tile.enemy!.update(dt));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue