Core gameplay
This commit is contained in:
parent
baab7c9486
commit
399e3097a3
|
|
@ -0,0 +1,104 @@
|
||||||
|
import Item, { ItemType, type ItemTypeImage } from "./item";
|
||||||
|
import { SpinnerAction } from "./spinner";
|
||||||
|
import Tile from "./tile";
|
||||||
|
|
||||||
|
export default class Character extends Item {
|
||||||
|
public health: number;
|
||||||
|
public inventory: Item[] = [];
|
||||||
|
public tile: Tile = new Tile([0, 0], 1);
|
||||||
|
|
||||||
|
constructor(type: ItemTypeImage) {
|
||||||
|
super(type);
|
||||||
|
this.health = this.maxHealth;
|
||||||
|
const defaultItem = this.defaultItem;
|
||||||
|
if (defaultItem) {
|
||||||
|
this.inventory.push(this.defaultItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxHealth() {
|
||||||
|
return this.type === ItemType.CHAR_BIG ? 7 : 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultItem() {
|
||||||
|
if (this.type === ItemType.CHAR_NINJA) {
|
||||||
|
return new Item(ItemType.WEAPON_KNIFE);
|
||||||
|
}
|
||||||
|
if (this.type === ItemType.CHAR_POLICE) {
|
||||||
|
return new Item(ItemType.WEAPON_PISTOL);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get healingAmount() {
|
||||||
|
return this.type === ItemType.CHAR_NURSE ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get moveBonus() {
|
||||||
|
return this.type === ItemType.CHAR_RUNNER ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDead() {
|
||||||
|
return this.health <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get meleeWeapon() {
|
||||||
|
return this.inventory.find(i => i.isMeleeWeapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
get gun() {
|
||||||
|
return this.inventory.find(i => i.isShootingWeapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
get grenade() {
|
||||||
|
return this.inventory.find(i => i.type === ItemType.WEAPON_GRENADE);
|
||||||
|
}
|
||||||
|
|
||||||
|
get rocketLauncher() {
|
||||||
|
return this.inventory.find(i => i.type === ItemType.WEAPON_ROCKET_LAUNCHER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public heal(character: Character) {
|
||||||
|
character.health += this.healingAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public useItem(item: Item | null | undefined): boolean {
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const itemIndex = this.inventory.findIndex(i => i === item);
|
||||||
|
|
||||||
|
if (itemIndex >= 0) {
|
||||||
|
this.inventory.splice(itemIndex, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns true, if action was performed */
|
||||||
|
public handleSpin(action: SpinnerAction): boolean {
|
||||||
|
switch (action) {
|
||||||
|
case SpinnerAction.RUN:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SpinnerAction.BITE:
|
||||||
|
this.health -= 1;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case SpinnerAction.MELEE:
|
||||||
|
return this.meleeWeapon != null && !this.tile.enemy?.isBoss;
|
||||||
|
|
||||||
|
case SpinnerAction.SHOOT:
|
||||||
|
return this.useItem(this.gun) && !this.tile.enemy?.isBoss;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Characters = {
|
||||||
|
BIG: new Character(ItemType.CHAR_BIG),
|
||||||
|
NINJA: new Character(ItemType.CHAR_NINJA),
|
||||||
|
NURSE: new Character(ItemType.CHAR_NURSE),
|
||||||
|
POLICE: new Character(ItemType.CHAR_POLICE),
|
||||||
|
RUNNER: new Character(ItemType.CHAR_RUNNER),
|
||||||
|
} as const;
|
||||||
|
|
@ -46,6 +46,6 @@ export default abstract class Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract draw(ctx: CanvasRenderingContext2D): void;
|
protected abstract draw(ctx: CanvasRenderingContext2D): void;
|
||||||
protected onClick() {}
|
protected onClick() { }
|
||||||
public abstract update(dt: number): void;
|
public update(_dt: number) { };
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { getRealPoint } from "@common/dom";
|
||||||
import { nextFrame } from "@common/utils";
|
import { nextFrame } from "@common/utils";
|
||||||
import bgImg from './assets/bg.jpg';
|
import bgImg from './assets/bg.jpg';
|
||||||
import TileMap from "./tilemap";
|
import TileMap from "./tilemap";
|
||||||
|
import Inventory from "./inventory";
|
||||||
|
|
||||||
const MAP_SIZE = 12;
|
const MAP_SIZE = 12;
|
||||||
const MAP_PIXEL_SIZE = 1000;
|
const MAP_PIXEL_SIZE = 1000;
|
||||||
|
|
@ -13,11 +14,21 @@ const TILE_SIZE = (MAP_PIXEL_SIZE - MAP_PADDING * 2) / MAP_SIZE;
|
||||||
const SPINNER_SIZE = 200;
|
const SPINNER_SIZE = 200;
|
||||||
const canvas = createCanvas(MAP_PIXEL_SIZE + SPINNER_SIZE, MAP_PIXEL_SIZE);
|
const canvas = createCanvas(MAP_PIXEL_SIZE + SPINNER_SIZE, MAP_PIXEL_SIZE);
|
||||||
const spinner = new Spinner([MAP_PIXEL_SIZE, 0], [SPINNER_SIZE, SPINNER_SIZE]);
|
const spinner = new Spinner([MAP_PIXEL_SIZE, 0], [SPINNER_SIZE, SPINNER_SIZE]);
|
||||||
const map = new TileMap([MAP_PADDING, MAP_PADDING], MAP_SIZE, TILE_SIZE);
|
const map = new TileMap(
|
||||||
|
[MAP_PADDING, MAP_PADDING],
|
||||||
|
MAP_SIZE,
|
||||||
|
TILE_SIZE,
|
||||||
|
);
|
||||||
|
const inventory = new Inventory(
|
||||||
|
map.characters,
|
||||||
|
[MAP_PIXEL_SIZE, SPINNER_SIZE],
|
||||||
|
[SPINNER_SIZE, MAP_PIXEL_SIZE - SPINNER_SIZE],
|
||||||
|
);
|
||||||
|
|
||||||
const entities: Entity[] = [
|
const entities: Entity[] = [
|
||||||
spinner,
|
spinner,
|
||||||
map,
|
map,
|
||||||
|
inventory,
|
||||||
];
|
];
|
||||||
|
|
||||||
async function update(dt: number) {
|
async function update(dt: number) {
|
||||||
|
|
@ -28,7 +39,7 @@ async function render(ctx: CanvasRenderingContext2D) {
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
ctx.fillStyle = 'green';
|
ctx.fillStyle = 'white';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
ctx.drawImage(bgImg, 0, 0, MAP_PIXEL_SIZE, MAP_PIXEL_SIZE);
|
ctx.drawImage(bgImg, 0, 0, MAP_PIXEL_SIZE, MAP_PIXEL_SIZE);
|
||||||
|
|
@ -53,7 +64,8 @@ export default async function main() {
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
spinner.addListener(console.log);
|
spinner.addListener((a) => map.handleSpin(a));
|
||||||
|
|
||||||
canvas.addEventListener('click', onClick);
|
canvas.addEventListener('click', onClick);
|
||||||
canvas.addEventListener('mousemove', onMouseMove);
|
canvas.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import type Character from "./character";
|
||||||
|
import { Characters } from "./character";
|
||||||
|
import Entity from "./entity";
|
||||||
|
|
||||||
|
export default class Inventory extends Entity {
|
||||||
|
constructor(public readonly characters: Character[], position: [number, number], size: [number, number]) {
|
||||||
|
super(position, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawCharacter(ctx: CanvasRenderingContext2D, character: Character) {
|
||||||
|
ctx.drawImage(character.type, 0.1, 0.1, 0.8, 0.8);
|
||||||
|
ctx.fillText(`♥ ${character.health}`, 0.5, 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override draw(ctx: CanvasRenderingContext2D): void {
|
||||||
|
const step = 1 / Object.keys(Characters).length;
|
||||||
|
const columnWidth = this.width * step;
|
||||||
|
|
||||||
|
ctx.scale(step, columnWidth / this.height);
|
||||||
|
ctx.font = `0.5px Arial`;
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
|
||||||
|
for (const char of this.characters) {
|
||||||
|
this.drawCharacter(ctx, char);
|
||||||
|
ctx.translate(1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ export const ItemType = {
|
||||||
|
|
||||||
type ItemTypeType = typeof ItemType;
|
type ItemTypeType = typeof ItemType;
|
||||||
type ImageKey = keyof ItemTypeType;
|
type ImageKey = keyof ItemTypeType;
|
||||||
type ItemTypeImage = ItemTypeType[ImageKey];
|
export type ItemTypeImage = ItemTypeType[ImageKey];
|
||||||
|
|
||||||
export default class Item {
|
export default class Item {
|
||||||
constructor(public readonly type: ItemTypeImage) {
|
constructor(public readonly type: ItemTypeImage) {
|
||||||
|
|
@ -62,16 +62,6 @@ export default class Item {
|
||||||
return this.isItem || this.isWeapon;
|
return this.isItem || this.isWeapon;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCharacter() {
|
|
||||||
return [
|
|
||||||
ItemType.CHAR_BIG,
|
|
||||||
ItemType.CHAR_NINJA,
|
|
||||||
ItemType.CHAR_NURSE,
|
|
||||||
ItemType.CHAR_POLICE,
|
|
||||||
ItemType.CHAR_RUNNER,
|
|
||||||
].includes(this.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
get isEnemy() {
|
get isEnemy() {
|
||||||
return [
|
return [
|
||||||
ItemType.ENEMY_BOSS,
|
ItemType.ENEMY_BOSS,
|
||||||
|
|
@ -112,6 +102,10 @@ export default class Item {
|
||||||
ItemType.WEAPON_SHOTGUN,
|
ItemType.WEAPON_SHOTGUN,
|
||||||
].includes(this.type);
|
].includes(this.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isBoss() {
|
||||||
|
return this.type === ItemType.ENEMY_BOSS;
|
||||||
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return Object.entries(ItemType).find(t => t[1] === this.type)?.[0];
|
return Object.entries(ItemType).find(t => t[1] === this.type)?.[0];
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ namespace Pathfinding {
|
||||||
|
|
||||||
openSet.delete(current);
|
openSet.delete(current);
|
||||||
|
|
||||||
if (current.items.length > 0) continue;
|
if (current !== start && current.items.length > 0) continue;
|
||||||
|
|
||||||
for (const neighbor of current.connections) {
|
for (const neighbor of current.connections) {
|
||||||
// tentative gScore is current’s gScore plus cost to move to neighbor
|
// tentative gScore is current’s gScore plus cost to move to neighbor
|
||||||
|
|
@ -92,7 +92,7 @@ namespace Pathfinding {
|
||||||
const result = new Set<Tile>();
|
const result = new Set<Tile>();
|
||||||
for (const tile of findPossibleTiles(start, steps)) {
|
for (const tile of findPossibleTiles(start, steps)) {
|
||||||
const path = findPath(start, tile);
|
const path = findPath(start, tile);
|
||||||
if (path.length === steps + 1) {
|
if (path.length > 1 && path.length <= steps + 1) {
|
||||||
result.add(tile);
|
result.add(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ export default class Spinner extends Entity {
|
||||||
private listeners = new Set<SpinnerListener>();
|
private listeners = new Set<SpinnerListener>();
|
||||||
|
|
||||||
protected override draw(ctx: CanvasRenderingContext2D) {
|
protected override draw(ctx: CanvasRenderingContext2D) {
|
||||||
|
ctx.scale(0.9, 0.9);
|
||||||
|
ctx.translate(0.05, 0.05);
|
||||||
|
|
||||||
ctx.fillStyle = 'white';
|
ctx.fillStyle = 'white';
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default class Tile extends Entity {
|
||||||
public isOpen = false;
|
public isOpen = false;
|
||||||
|
|
||||||
constructor(position: [number, number], size: number, public type: TileType = TileType.NORMAL) {
|
constructor(position: [number, number], size: number, public type: TileType = TileType.NORMAL) {
|
||||||
super([position[0] * size, position[1] * size], [size, size]);
|
super(position, [size, size]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected draw(ctx: CanvasRenderingContext2D) {
|
protected draw(ctx: CanvasRenderingContext2D) {
|
||||||
|
|
@ -39,10 +39,10 @@ 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.items[0];
|
const item = this.items[0];
|
||||||
ctx.drawImage(item.type, 0.15, 0.15, 0.7, 0.7);
|
ctx.drawImage(item.type, 0.1, 0.1, 0.8, 0.8);
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = 'white';
|
ctx.fillStyle = 'white';
|
||||||
ctx.fillRect(0.15, 0.15, 0.7, 0.7);
|
ctx.fillRect(0.1, 0.1, 0.8, 0.8);
|
||||||
|
|
||||||
ctx.fillStyle = 'black';
|
ctx.fillStyle = 'black';
|
||||||
ctx.font = '0.2px Arial';
|
ctx.font = '0.2px Arial';
|
||||||
|
|
@ -51,6 +51,10 @@ export default class Tile extends Entity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get enemy() {
|
||||||
|
return this.items.find(i => i.isEnemy);
|
||||||
|
}
|
||||||
|
|
||||||
public open(): Item[] {
|
public open(): Item[] {
|
||||||
if (!this.isOpen) {
|
if (!this.isOpen) {
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
|
|
@ -66,10 +70,21 @@ export default class Tile extends Entity {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override onClick(): void {
|
public removeItem(item: Item | null | undefined): boolean {
|
||||||
console.log(...this.open().map(t => t.toString()));
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const itemIndex = this.items.findIndex(i => i === item);
|
||||||
|
|
||||||
|
if (itemIndex >= 0) {
|
||||||
|
this.items.splice(itemIndex, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(dt: number) {
|
public killEnemy() {
|
||||||
|
this.removeItem(this.enemy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,28 @@ 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 Character, { Characters } from "./character";
|
||||||
|
import { SpinnerAction } from "./spinner";
|
||||||
|
|
||||||
|
enum GameState {
|
||||||
|
NORMAL,
|
||||||
|
FIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
export default class TileMap extends Entity {
|
export default class TileMap extends Entity {
|
||||||
private tiles: Tile[] = [];
|
private tiles: Tile[] = [];
|
||||||
private pathTiles: Tile[] = [];
|
|
||||||
public startTile: Tile;
|
public startTile: Tile;
|
||||||
|
|
||||||
constructor(position: [number, number], private mapSize: number, private tileSize: number) {
|
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]);
|
super(position, [mapSize * tileSize, mapSize * tileSize]);
|
||||||
|
this.characters = shuffle(Object.values(Characters)).slice(0, numPlayers);
|
||||||
this.startTile = this.createMap();
|
this.startTile = this.createMap();
|
||||||
|
this.findAvailableTiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
public createMap() {
|
public createMap() {
|
||||||
|
|
@ -19,11 +32,11 @@ export default class TileMap extends Entity {
|
||||||
.map(x =>
|
.map(x =>
|
||||||
range(this.mapSize)
|
range(this.mapSize)
|
||||||
.map(y =>
|
.map(y =>
|
||||||
new Tile([x, y], this.tileSize)
|
new Tile([x * this.tileSize, y * this.tileSize], this.tileSize)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const startTile = new Tile([0, (this.mapSize / 2) - 1], this.tileSize * 2, TileType.START);
|
const startTile = new Tile([0, (this.mapSize - 2) * this.tileSize], this.tileSize * 2, TileType.START);
|
||||||
|
|
||||||
delete map[0][this.mapSize - 2];
|
delete map[0][this.mapSize - 2];
|
||||||
delete map[1][this.mapSize - 2];
|
delete map[1][this.mapSize - 2];
|
||||||
|
|
@ -85,9 +98,16 @@ export default class TileMap extends Entity {
|
||||||
|
|
||||||
startTile.connections.forEach(t => t.connections.push(startTile));
|
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.tiles = map.flat();
|
||||||
|
this.startTile = startTile;
|
||||||
|
|
||||||
this.fillItems();
|
this.fillItems();
|
||||||
|
this.characters.forEach(c => c.tile = startTile);
|
||||||
|
|
||||||
return startTile;
|
return startTile;
|
||||||
}
|
}
|
||||||
|
|
@ -123,52 +143,144 @@ export default class TileMap extends Entity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fillableTiles = this.tiles.filter(t => t.type === TileType.NORMAL);
|
for (const char of Object.values(Characters)) {
|
||||||
for (const [tile, item] of zip(shuffle(fillableTiles), items)) {
|
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);
|
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 {
|
public override handleMouseMove(x: number, y: number): void {
|
||||||
this.tiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
|
this.availableTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override handleClick(x: number, y: number): void {
|
public override handleClick(x: number, y: number): void {
|
||||||
for (const tile of this.tiles) {
|
if (this.state !== GameState.NORMAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const tile of this.availableTiles) {
|
||||||
if (tile.isPointInBounds(x - this.left, y - this.top)) {
|
if (tile.isPointInBounds(x - this.left, y - this.top)) {
|
||||||
this.pathTiles = Pathfinding.findPath(this.startTile, tile);
|
const path = Pathfinding.findPath(this.character.tile, tile);
|
||||||
if (this.pathTiles.length > 0) {
|
if (path.length > 1) {
|
||||||
this.startTile = this.pathTiles.at(-1);
|
this.character.tile = tile;
|
||||||
tile.handleClick(x - this.left, y - this.top);
|
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;
|
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 {
|
protected draw(ctx: CanvasRenderingContext2D): void {
|
||||||
ctx.scale(1 / this.width, 1 / this.height);
|
ctx.scale(1 / this.width, 1 / this.height);
|
||||||
this.tiles.forEach(t => t.render(ctx));
|
|
||||||
|
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeStyle = 'yellow';
|
ctx.fillStyle = 'rgba(0, 255, 0, 0.5)';
|
||||||
|
|
||||||
if (this.pathTiles.length > 0) {
|
if (this.state === GameState.NORMAL && this.availableTiles.length > 0) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
||||||
const [start, ...path] = this.pathTiles;
|
this.availableTiles.forEach(t =>
|
||||||
ctx.moveTo(start.centerX, start.centerY);
|
ctx.fillRect(t.centerX - t.width / 2, t.centerY - t.height / 2, t.width, t.height)
|
||||||
path.forEach(t => ctx.lineTo(t.centerX, t.centerY));
|
);
|
||||||
|
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.beginPath();
|
const w = this.tileSize * 0.8;
|
||||||
ctx.arc(this.startTile.centerX, this.startTile.centerY, this.startTile.width * 0.4, 0, 7);
|
this.characters.toReversed().forEach(c =>
|
||||||
ctx.stroke();
|
ctx.drawImage(c.type, c.tile.centerX - w / 2, c.tile.centerY - w / 2, w, w)
|
||||||
}
|
);
|
||||||
|
|
||||||
public update(dt: number): void {
|
this.tiles.forEach(t => t.render(ctx));
|
||||||
this.tiles.forEach(t => t.update(dt));
|
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeStyle = 'yellow';
|
||||||
|
|
||||||
|
ctx.strokeRect(this.character.tile.centerX - w / 2, this.character.tile.centerY - w / 2, w, w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue