1
0
Fork 0
tsgames/src/games/zombies/tilemap.ts

286 lines
9.8 KiB
TypeScript

import { 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 Character, { Characters } from "./character";
import { SpinnerAction } from "./spinner";
enum GameState {
NORMAL,
FIGHT,
}
export default class TileMap extends Entity {
private tiles: Tile[] = [];
public startTile: Tile;
public readonly characters: Character[];
private currentCharacterIdx = 0;
private state = GameState.NORMAL;
private availableTiles: Tile[] = [];
constructor(position: [number, number], private mapSize: number, private tileSize: number, numPlayers: number = 2) {
super(position, [mapSize * tileSize, mapSize * tileSize]);
this.characters = shuffle(Object.values(Characters)).slice(0, numPlayers);
this.startTile = this.createMap();
this.findAvailableTiles();
}
public createMap() {
const map = range(this.mapSize)
.map(x =>
range(this.mapSize)
.map(y =>
new Tile([x * this.tileSize, y * this.tileSize], this.tileSize)
)
);
const startTile = new Tile([0, (this.mapSize - 2) * this.tileSize], this.tileSize * 2, TileType.START);
delete map[0][this.mapSize - 2];
delete map[1][this.mapSize - 2];
delete map[1][this.mapSize - 1];
map[0][this.mapSize - 1] = startTile;
const verticalWalls: [number, number][] = [
[1, 3], [3, 3], [6, 3], [10, 3],
[3, 4],
[1, 5], [6, 5], [10, 5],
[1, 6], [6, 6], [10, 6],
[1, 8], [6, 8], [10, 8],
[1, 9], [6, 9], [10, 9],
];
const horizontalWalls: [number, number][] = [
[2, 2], [2, 5], [2, 9],
[3, 2], [3, 5], [3, 9],
[4, 2], [5, 5],
[6, 2], [6, 5], [6, 9],
[7, 2], [7, 5], [7, 9],
[8, 2], [8, 5],
[9, 2], [9, 9],
[10, 2], [10, 5], [10, 9],
];
for (let x = 0; x < map.length; x++) {
const column = map[x];
if (!column) continue;
for (let y = 0; y < column.length; y++) {
const tile = column[y];
if (!tile) continue;
tile.connections = [
verticalWalls.find(w => w[0] === x - 1 && w[1] === y)
? undefined
: map[x - 1]?.[y],
verticalWalls.find(w => w[0] === x && w[1] === y)
? undefined
: map[x + 1]?.[y],
horizontalWalls.find(w => w[0] === x && w[1] === y - 1)
? undefined
: map[x]?.[y - 1],
horizontalWalls.find(w => w[0] === x && w[1] === y)
? undefined
: map[x]?.[y + 1],
].filter(t => t != null);
}
}
startTile.connections = [
map[0][this.mapSize - 3],
map[1][this.mapSize - 3],
map[2][this.mapSize - 2],
map[2][this.mapSize - 1],
].filter(t => t);
startTile.connections.forEach(t => t.connections.push(startTile));
const endTiles = [[8, 1], [9, 1], [10, 1]];
for (const [x, y] of endTiles) {
map[x][y].type = TileType.END;
}
this.tiles = map.flat();
this.startTile = startTile;
this.fillItems();
this.characters.forEach(c => c.tile = startTile);
return startTile;
}
public fillItems() {
const itemsMap = new Map([
[ItemType.ENEMY_BOSS, 1],
[ItemType.ENEMY_DOG, 5],
[ItemType.ENEMY_SPIDER, 5],
[ItemType.ENEMY_ZOMBIE, 17],
[ItemType.ITEM_FUEL, 1],
[ItemType.ITEM_HEAL, 6],
[ItemType.ITEM_KEYS, 1],
[ItemType.ITEM_PLANKS, 8],
[ItemType.WEAPON_ASSAULT_RIFLE, 1],
[ItemType.WEAPON_AXE, 1],
[ItemType.WEAPON_CROSSBOW, 1],
[ItemType.WEAPON_GRENADE, 4],
[ItemType.WEAPON_KNIFE, 1],
[ItemType.WEAPON_PISTOL, 1],
[ItemType.WEAPON_ROCKET_LAUNCHER, 1],
[ItemType.WEAPON_SHOTGUN, 1],
]);
const items: Item[] = [];
for (const [type, amount] of itemsMap.entries()) {
for (const _ in range(amount)) {
const item = new Item(type);
items.push(item);
}
}
for (const char of Object.values(Characters)) {
if (this.characters.includes(char)) continue;
items.push(char);
}
const normalTiles = this.tiles.filter(t => t.type === TileType.NORMAL);
const endTiles = this.tiles.filter(t => t.type === TileType.END);
const endTilesNeighbors = new Set(endTiles.flatMap(t => t.connections));
const fillableTiles = [
...endTilesNeighbors,
...this.startTile.connections,
...shuffle(normalTiles),
];
for (const [tile, item] of zip(fillableTiles, shuffle(items))) {
tile.items.push(item);
if (item instanceof Character) {
item.tile = tile;
}
};
}
get character() {
return this.characters[this.currentCharacterIdx];
}
public override handleMouseMove(x: number, y: number): void {
this.availableTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
}
public override handleClick(x: number, y: number): void {
if (this.state !== GameState.NORMAL) {
return;
}
for (const tile of this.availableTiles) {
if (tile.isPointInBounds(x - this.left, y - this.top)) {
const path = Pathfinding.findPath(this.character.tile, tile);
if (path.length > 1) {
this.character.tile = tile;
const items = tile.open();
this.character.inventory.push(...items);
if (tile.items.length > 0) {
for (const item of tile.items) { // iterate remaining items
if (item instanceof Character) {
tile.removeItem(item);
this.characters.push(item);
this.nextCharacter();
} else if (item.isBoss && this.character.useItem(this.character.rocketLauncher)) {
tile.killEnemy();
this.nextCharacter();
} else if (item.isEnemy) {
this.state = GameState.FIGHT;
} else {
alert(`Unknown item found: ${item}`);
}
}
} else {
this.nextCharacter();
}
}
this.findAvailableTiles();
break;
}
}
}
public handleSpin(action: SpinnerAction) {
switch (this.state) {
case GameState.NORMAL:
this.findAvailableTiles(action);
break;
case GameState.FIGHT:
const success = this.character.handleSpin(action);
if (success) {
switch (action) {
case SpinnerAction.BITE:
if (this.character.isDead) {
this.character.tile.items.push(...this.character.inventory);
this.characters.splice(this.currentCharacterIdx, 1);
this.currentCharacterIdx = this.currentCharacterIdx % this.characters.length;
}
break;
case SpinnerAction.MELEE:
case SpinnerAction.SHOOT:
this.character.tile.killEnemy();
this.nextCharacter();
break;
case SpinnerAction.RUN:
break;
}
if (action !== SpinnerAction.BITE) {
this.state = GameState.NORMAL;
this.findAvailableTiles();
}
}
break;
}
}
private findAvailableTiles(moveDistance: number = 1) {
const characterTiles = new Set(this.characters.map(c => c.tile));
this.availableTiles = Pathfinding.findPossibleMoves(this.character.tile, moveDistance)
.filter(t => !characterTiles.has(t));
}
private nextCharacter() {
this.currentCharacterIdx = (this.currentCharacterIdx + 1) % this.characters.length;
}
protected draw(ctx: CanvasRenderingContext2D): void {
ctx.scale(1 / this.width, 1 / this.height);
ctx.lineWidth = 2;
ctx.fillStyle = 'rgba(0, 255, 0, 0.5)';
if (this.state === GameState.NORMAL && this.availableTiles.length > 0) {
ctx.beginPath();
this.availableTiles.forEach(t =>
ctx.fillRect(t.centerX - t.width / 2, t.centerY - t.height / 2, t.width, t.height)
);
ctx.stroke();
}
const w = this.tileSize * 0.8;
this.characters.toReversed().forEach(c =>
ctx.drawImage(c.type, c.tile.centerX - w / 2, c.tile.centerY - w / 2, w, w)
);
this.tiles.forEach(t => t.render(ctx));
ctx.lineWidth = 3;
ctx.strokeStyle = 'yellow';
ctx.strokeRect(this.character.tile.centerX - w / 2, this.character.tile.centerY - w / 2, w, w);
}
}