Compare commits
3 Commits
1e4be0ff2c
...
c66446c2b3
| Author | SHA1 | Date |
|---|---|---|
|
|
c66446c2b3 | |
|
|
9c0aad2e75 | |
|
|
d2df62cdbe |
|
|
@ -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 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 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);
|
export const lerp = (start: number, end: number, t: number) => (start + (end - start) * t);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { lerp } from "@common/utils";
|
import { lerp } from "@common/utils";
|
||||||
import Item from "./item";
|
import Item, { ItemType } from "./item";
|
||||||
import Tile from "./tile";
|
import Tile from "./tile";
|
||||||
|
|
||||||
const MOVE_DURATION = .1;
|
const MOVE_DURATION = .1;
|
||||||
|
|
@ -8,6 +8,13 @@ export default class Character extends Item {
|
||||||
public tile = new Tile([0, 0], 1);
|
public tile = new Tile([0, 0], 1);
|
||||||
private path: Tile[] = [];
|
private path: Tile[] = [];
|
||||||
private pathProgress = 0;
|
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] {
|
public get displayPosition(): [number, number] {
|
||||||
if (this.path.length > 1) {
|
if (this.path.length > 1) {
|
||||||
|
|
@ -16,6 +23,11 @@ export default class Character extends Item {
|
||||||
const currentTileIdx = Math.floor(progress);
|
const currentTileIdx = Math.floor(progress);
|
||||||
|
|
||||||
const currentTile = this.path[currentTileIdx];
|
const currentTile = this.path[currentTileIdx];
|
||||||
|
|
||||||
|
if (!currentTile) {
|
||||||
|
return [this.tile.centerX, this.tile.centerY];
|
||||||
|
}
|
||||||
|
|
||||||
const nextTile = this.path[currentTileIdx + 1];
|
const nextTile = this.path[currentTileIdx + 1];
|
||||||
|
|
||||||
if (!nextTile) {
|
if (!nextTile) {
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,14 @@ const TILE_SIZE = (MAP_PIXEL_SIZE - MAP_PADDING * 2) / MAP_SIZE;
|
||||||
const SIDEBAR_SIZE = clamp((window.innerWidth - window.innerHeight) / 2, 300, 600);
|
const SIDEBAR_SIZE = clamp((window.innerWidth - window.innerHeight) / 2, 300, 600);
|
||||||
|
|
||||||
const canvas = createCanvas(MAP_PIXEL_SIZE + SIDEBAR_SIZE * 2, MAP_PIXEL_SIZE);
|
const canvas = createCanvas(MAP_PIXEL_SIZE + SIDEBAR_SIZE * 2, MAP_PIXEL_SIZE);
|
||||||
|
|
||||||
|
const spinner = new Spinner([MAP_PIXEL_SIZE + SIDEBAR_SIZE, 0], [SIDEBAR_SIZE, SIDEBAR_SIZE]);
|
||||||
const map = new TileMap(
|
const map = new TileMap(
|
||||||
[MAP_PADDING + SIDEBAR_SIZE, MAP_PADDING],
|
[MAP_PADDING + SIDEBAR_SIZE, MAP_PADDING],
|
||||||
MAP_SIZE,
|
MAP_SIZE,
|
||||||
TILE_SIZE,
|
TILE_SIZE,
|
||||||
|
spinner,
|
||||||
);
|
);
|
||||||
const spinner = new Spinner([MAP_PIXEL_SIZE + SIDEBAR_SIZE, 0], [SIDEBAR_SIZE, SIDEBAR_SIZE]);
|
|
||||||
const inventory = new Inventory(
|
const inventory = new Inventory(
|
||||||
map,
|
map,
|
||||||
[0, 0],
|
[0, 0],
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ namespace Pathfinding {
|
||||||
* A* search from start to end Tile.
|
* A* search from start to end Tile.
|
||||||
* @returns array of Tiles from start to end (inclusive), or [] if no path.
|
* @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
|
// The set of discovered nodes to be evaluated
|
||||||
const openSet: Set<Tile> = new Set([start]);
|
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.
|
// For path reconstruction: map each node to the node it can most efficiently be reached from.
|
||||||
const cameFrom = new Map<Tile, Tile>();
|
const cameFrom = new Map<Tile, Tile>();
|
||||||
|
|
||||||
|
let closestTile: Tile = start;
|
||||||
|
let closestDistance = heuristic(closestTile, end);
|
||||||
|
|
||||||
while (openSet.size > 0) {
|
while (openSet.size > 0) {
|
||||||
// Get the node in openSet having the lowest fScore
|
// Get the node in openSet having the lowest fScore
|
||||||
let current: Tile | null = null;
|
let current: Tile | null = null;
|
||||||
|
|
@ -34,6 +37,12 @@ namespace Pathfinding {
|
||||||
}
|
}
|
||||||
if (!current) break;
|
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 we’ve reached the goal, reconstruct and return the path.
|
||||||
if (current === end) {
|
if (current === end) {
|
||||||
return reconstructPath(cameFrom, current);
|
return reconstructPath(cameFrom, current);
|
||||||
|
|
@ -41,7 +50,7 @@ namespace Pathfinding {
|
||||||
|
|
||||||
openSet.delete(current);
|
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) {
|
for (const neighbor of current.activeConnections) {
|
||||||
// tentative gScore is current’s gScore plus cost to move to neighbor
|
// 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
|
// Open set is empty but goal was never reached
|
||||||
|
if (forEnemy) {
|
||||||
|
return reconstructPath(cameFrom, closestTile);
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,6 @@ export default class Player extends Character {
|
||||||
return this.type === ItemType.CHAR_NURSE ? 2 : 1;
|
return this.type === ItemType.CHAR_NURSE ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get moveBonus() {
|
|
||||||
return this.type === ItemType.CHAR_RUNNER ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isDead() {
|
get isDead() {
|
||||||
return this.health <= 0;
|
return this.health <= 0;
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +55,14 @@ export default class Player extends Character {
|
||||||
return this.inventory.find(i => i.type === ItemType.WEAPON_ROCKET_LAUNCHER);
|
return this.inventory.find(i => i.type === ItemType.WEAPON_ROCKET_LAUNCHER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get keys() {
|
||||||
|
return this.inventory.find(i => i.type === ItemType.ITEM_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
get fuel() {
|
||||||
|
return this.inventory.find(i => i.type === ItemType.ITEM_FUEL);
|
||||||
|
}
|
||||||
|
|
||||||
public heal(player: Player) {
|
public heal(player: Player) {
|
||||||
player.health += this.healingAmount;
|
player.health += this.healingAmount;
|
||||||
}
|
}
|
||||||
|
|
@ -77,11 +81,11 @@ export default class Player extends Character {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasItem(item: Item | null | undefined): boolean {
|
public hasItem(item: Item | ItemTypeImage | null | undefined): boolean {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const itemIndex = this.inventory.findIndex(i => i === item);
|
const itemIndex = this.inventory.findIndex(i => i === item || i.type === item);
|
||||||
|
|
||||||
return itemIndex >= 0;
|
return itemIndex >= 0;
|
||||||
}
|
}
|
||||||
|
|
@ -97,10 +101,10 @@ export default class Player extends Character {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case SpinnerAction.MELEE:
|
case SpinnerAction.MELEE:
|
||||||
return this.meleeWeapon != null && !this.tile.enemy?.isBoss;
|
return !this.tile.enemy?.isBoss && this.meleeWeapon != null;
|
||||||
|
|
||||||
case SpinnerAction.SHOOT:
|
case SpinnerAction.SHOOT:
|
||||||
return this.removeItem(this.gun) && !this.tile.enemy?.isBoss;
|
return !this.tile.enemy?.isBoss && this.removeItem(this.gun);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,20 @@ export default class Spinner extends Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override onClick() {
|
public override onClick() {
|
||||||
|
this.spin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public spin() {
|
||||||
if (!this.fired) return;
|
if (!this.fired) return;
|
||||||
|
|
||||||
this.fired = false;
|
this.fired = false;
|
||||||
this.nextAngle = this.angle + (randInt(5, 10) + Math.random()) * 2 * Math.PI;
|
this.nextAngle = this.angle + (randInt(5, 10) + Math.random()) * 2 * Math.PI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.fired = true;
|
||||||
|
}
|
||||||
|
|
||||||
public addListener(listener: SpinnerListener) {
|
public addListener(listener: SpinnerListener) {
|
||||||
this.listeners.add(listener);
|
this.listeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import type Character from "./character";
|
||||||
import Entity from "./entity";
|
import Entity from "./entity";
|
||||||
import type Item from "./item";
|
import type Item from "./item";
|
||||||
|
import { ItemType } from "./item";
|
||||||
import planks from './assets/items/planks.jpg';
|
|
||||||
|
|
||||||
export enum TileType {
|
export enum TileType {
|
||||||
NORMAL,
|
NORMAL,
|
||||||
|
|
@ -46,7 +46,17 @@ export default class Tile extends Entity {
|
||||||
if (this.items.length > 0) {
|
if (this.items.length > 0) {
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
const item = this.enemy ?? this.items[0];
|
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) {
|
if (this.items.length > 1) {
|
||||||
ctx.fillStyle = 'black';
|
ctx.fillStyle = 'black';
|
||||||
|
|
@ -69,12 +79,12 @@ export default class Tile extends Entity {
|
||||||
y += (Number(this.top < doorway.top) - Number(doorway.top < this.top)) * 0.5;
|
y += (Number(this.top < doorway.top) - Number(doorway.top < this.top)) * 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.drawImage(planks, x - 0.2, y - 0.2, 0.4, 0.4);
|
ctx.drawImage(ItemType.ITEM_PLANKS, x - 0.2, y - 0.2, 0.4, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get enemy() {
|
get enemy(): Character | undefined {
|
||||||
return this.items.find(i => i.isEnemy);
|
return this.items.find(i => i.isEnemy) as Character;
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeConnections() {
|
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 Entity from "./entity";
|
||||||
import Tile, { TileType } from "./tile";
|
import Tile, { TileType } from "./tile";
|
||||||
import Pathfinding from "./pathfinding";
|
import Pathfinding from "./pathfinding";
|
||||||
import Item, { ItemType } from "./item";
|
import Item, { ItemType } from "./item";
|
||||||
import Player, { Players } from "./player";
|
import Player, { Players } from "./player";
|
||||||
import { SpinnerAction } from "./spinner";
|
import Spinner, { SpinnerAction } from "./spinner";
|
||||||
|
import Character from "./character";
|
||||||
|
|
||||||
enum GameState {
|
enum GameState {
|
||||||
NORMAL,
|
NORMAL,
|
||||||
|
|
@ -20,9 +21,21 @@ export default class TileMap extends Entity {
|
||||||
private state = GameState.NORMAL;
|
private state = GameState.NORMAL;
|
||||||
private availableTiles: Tile[] = [];
|
private availableTiles: Tile[] = [];
|
||||||
|
|
||||||
constructor(position: [number, number], private mapSize: number, private tileSize: number, numPlayers: number = 2) {
|
public keysFound = false;
|
||||||
|
public fuelFound = false;
|
||||||
|
public bossKilled = false;
|
||||||
|
public readonly foundPlayers: Set<Player>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
position: [number, number],
|
||||||
|
private mapSize: number,
|
||||||
|
private tileSize: number,
|
||||||
|
private spinner: Spinner,
|
||||||
|
numPlayers: number = 2,
|
||||||
|
) {
|
||||||
super(position, [mapSize * tileSize, mapSize * tileSize]);
|
super(position, [mapSize * tileSize, mapSize * tileSize]);
|
||||||
this.players = shuffle(Object.values(Players)).slice(0, numPlayers);
|
this.players = shuffle(Object.values(Players)).slice(0, numPlayers);
|
||||||
|
this.foundPlayers = new Set(this.players);
|
||||||
this.startTile = this.createMap();
|
this.startTile = this.createMap();
|
||||||
this.findAvailableTiles();
|
this.findAvailableTiles();
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +172,14 @@ export default class TileMap extends Entity {
|
||||||
|
|
||||||
for (const [type, amount] of itemsMap.entries()) {
|
for (const [type, amount] of itemsMap.entries()) {
|
||||||
for (const _ in range(amount)) {
|
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);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,10 +207,16 @@ export default class TileMap extends Entity {
|
||||||
|
|
||||||
for (const [tile, item] of zip(fillableTiles, shuffle(items))) {
|
for (const [tile, item] of zip(fillableTiles, shuffle(items))) {
|
||||||
tile.items.push(item);
|
tile.items.push(item);
|
||||||
if (item instanceof Player) {
|
if (item instanceof Character) {
|
||||||
item.tile = tile;
|
item.tile = tile;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
for (const tile of this.tiles) {
|
||||||
|
if (tile.items.length === 0) {
|
||||||
|
tile.isOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get player() {
|
get player() {
|
||||||
|
|
@ -206,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 {
|
public override handleMouseMove(x: number, y: number): void {
|
||||||
this.activeTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
|
this.activeTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +253,7 @@ export default class TileMap extends Entity {
|
||||||
if (tile.isPointInBounds(x - this.left, y - this.top)) {
|
if (tile.isPointInBounds(x - this.left, y - this.top)) {
|
||||||
const path = Pathfinding.findPath(this.player.tile, tile);
|
const path = Pathfinding.findPath(this.player.tile, tile);
|
||||||
if (path.length > 1) {
|
if (path.length > 1) {
|
||||||
|
this.spinner.stop();
|
||||||
this.player.lastDoor = path.find(t => t.type === TileType.DOOR);
|
this.player.lastDoor = path.find(t => t.type === TileType.DOOR);
|
||||||
this.player.moveTo(tile, path);
|
this.player.moveTo(tile, path);
|
||||||
tile.open();
|
tile.open();
|
||||||
|
|
@ -230,8 +266,10 @@ export default class TileMap extends Entity {
|
||||||
} else if (item instanceof Player) {
|
} else if (item instanceof Player) {
|
||||||
tile.removeItem(item);
|
tile.removeItem(item);
|
||||||
this.players.push(item);
|
this.players.push(item);
|
||||||
|
this.foundPlayers.add(item);
|
||||||
} else if (item.isBoss && this.player.removeItem(this.player.rocketLauncher)) {
|
} else if (item.isBoss && this.player.removeItem(this.player.rocketLauncher)) {
|
||||||
tile.killEnemy();
|
tile.killEnemy();
|
||||||
|
this.bossKilled = true;
|
||||||
} else if (item.isEnemy) {
|
} else if (item.isEnemy) {
|
||||||
this.state = GameState.FIGHT;
|
this.state = GameState.FIGHT;
|
||||||
break;
|
break;
|
||||||
|
|
@ -239,6 +277,14 @@ export default class TileMap extends Entity {
|
||||||
alert(`Unknown item found: ${item}`);
|
alert(`Unknown item found: ${item}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (tile.type === TileType.END) {
|
||||||
|
if (this.player.removeItem(this.player.fuel)) {
|
||||||
|
this.fuelFound = true;
|
||||||
|
}
|
||||||
|
if (this.player.removeItem(this.player.keys)) {
|
||||||
|
this.keysFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (this.state === GameState.NORMAL) {
|
if (this.state === GameState.NORMAL) {
|
||||||
this.nextPlayer();
|
this.nextPlayer();
|
||||||
}
|
}
|
||||||
|
|
@ -265,6 +311,8 @@ export default class TileMap extends Entity {
|
||||||
this.players.splice(this.currentPlayerIdx, 1);
|
this.players.splice(this.currentPlayerIdx, 1);
|
||||||
this.currentPlayerIdx = this.currentPlayerIdx % this.players.length;
|
this.currentPlayerIdx = this.currentPlayerIdx % this.players.length;
|
||||||
this.setNormalState();
|
this.setNormalState();
|
||||||
|
} else if (!this.player.grenade) {
|
||||||
|
this.spinner.spin();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SpinnerAction.MELEE:
|
case SpinnerAction.MELEE:
|
||||||
|
|
@ -273,8 +321,11 @@ export default class TileMap extends Entity {
|
||||||
break;
|
break;
|
||||||
case SpinnerAction.RUN:
|
case SpinnerAction.RUN:
|
||||||
this.setNormalState();
|
this.setNormalState();
|
||||||
|
this.spinner.spin();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else if (!this.player.grenade) {
|
||||||
|
this.spinner.spin();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -311,7 +362,54 @@ export default class TileMap extends Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private nextPlayer() {
|
private nextPlayer() {
|
||||||
|
this.spinner.stop();
|
||||||
this.currentPlayerIdx = (this.currentPlayerIdx + 1) % this.players.length;
|
this.currentPlayerIdx = (this.currentPlayerIdx + 1) % this.players.length;
|
||||||
|
if (this.win) {
|
||||||
|
const endTile = this.tiles.findLast(t => t.type === TileType.END);
|
||||||
|
if (endTile) {
|
||||||
|
for (const player of this.players) {
|
||||||
|
const path = Pathfinding.findPath(player.tile, endTile);
|
||||||
|
player.moveTo(endTile, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(alert, 2000, "🎉🎉🎉 ПОБЕДА! 🚗 🎉🎉🎉");
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private killEnemy() {
|
private killEnemy() {
|
||||||
|
|
@ -326,6 +424,28 @@ export default class TileMap extends Entity {
|
||||||
this.setNormalState();
|
this.setNormalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get win() {
|
||||||
|
if (!this.keysFound || !this.fuelFound || !this.bossKilled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const endTiles = this.tiles.filter(t => t.type === TileType.END);
|
||||||
|
for (const tile of endTiles) {
|
||||||
|
if (tile.items.length !== 0) return false;
|
||||||
|
}
|
||||||
|
for (const player of Object.values(Players)) {
|
||||||
|
if (!this.foundPlayers.has(player)) return false;
|
||||||
|
if (player.isDead) continue;
|
||||||
|
|
||||||
|
if (endTiles.every((tile) => {
|
||||||
|
const path = Pathfinding.findPath(player.tile, tile);
|
||||||
|
return path.length === 0;
|
||||||
|
})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected draw(ctx: CanvasRenderingContext2D): void {
|
protected draw(ctx: CanvasRenderingContext2D): void {
|
||||||
ctx.scale(1 / this.width, 1 / this.height);
|
ctx.scale(1 / this.width, 1 / this.height);
|
||||||
|
|
||||||
|
|
@ -350,9 +470,50 @@ export default class TileMap extends Entity {
|
||||||
ctx.strokeStyle = 'yellow';
|
ctx.strokeStyle = 'yellow';
|
||||||
|
|
||||||
ctx.strokeRect(this.player.tile.centerX - w / 2, this.player.tile.centerY - w / 2, w, w);
|
ctx.strokeRect(this.player.tile.centerX - w / 2, this.player.tile.centerY - w / 2, w, w);
|
||||||
|
|
||||||
|
let x = this.width + this.tileSize / 2 + 10;
|
||||||
|
let y = this.height - this.tileSize / 2;
|
||||||
|
for (const player of Object.values(Players)) {
|
||||||
|
this.drawItem(ctx, player.type, x, y, w, this.foundPlayers.has(player));
|
||||||
|
x += this.tileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = this.width + this.tileSize / 2 + 10;
|
||||||
|
y = this.height - 3 * this.tileSize / 2;
|
||||||
|
for (const [item, found] of zip(
|
||||||
|
[ItemType.ITEM_KEYS, ItemType.ITEM_FUEL, ItemType.ENEMY_BOSS],
|
||||||
|
[this.keysFound, this.fuelFound, this.bossKilled],
|
||||||
|
)) {
|
||||||
|
this.drawItem(ctx, item, x, y, w, found);
|
||||||
|
x += this.tileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = this.width + this.tileSize / 2 + 10;
|
||||||
|
y = this.height - 5 * this.tileSize / 2;
|
||||||
|
for (const tile of this.tiles) {
|
||||||
|
if (tile.type !== TileType.END) continue;
|
||||||
|
|
||||||
|
this.drawItem(ctx, ItemType.ENEMY_ZOMBIE, x, y, w, tile.items.length === 0);
|
||||||
|
x += this.tileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawItem(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
img: HTMLImageElement,
|
||||||
|
x: number, y: number, w: number,
|
||||||
|
found: boolean,
|
||||||
|
) {
|
||||||
|
ctx.drawImage(img, x - w / 2, y - w / 2, w, w);
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
ctx.fillStyle = `rgba(255, 255, 255, 0.6)`;
|
||||||
|
ctx.fillRect(x - w / 2, y - w / 2, w, w);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(dt: number) {
|
public update(dt: number) {
|
||||||
this.players.forEach((player) => player.update(dt));
|
this.players.forEach((player) => player.update(dt));
|
||||||
|
this.enemies.forEach((tile) => tile.enemy!.update(dt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue