First playable version
This commit is contained in:
parent
399e3097a3
commit
bdca1ce602
|
|
@ -62,7 +62,7 @@ export default class Character extends Item {
|
||||||
character.health += this.healingAmount;
|
character.health += this.healingAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public useItem(item: Item | null | undefined): boolean {
|
public removeItem(item: Item | null | undefined): boolean {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +90,7 @@ export default class Character extends Item {
|
||||||
return this.meleeWeapon != null && !this.tile.enemy?.isBoss;
|
return this.meleeWeapon != null && !this.tile.enemy?.isBoss;
|
||||||
|
|
||||||
case SpinnerAction.SHOOT:
|
case SpinnerAction.SHOOT:
|
||||||
return this.useItem(this.gun) && !this.tile.enemy?.isBoss;
|
return this.removeItem(this.gun) && !this.tile.enemy?.isBoss;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default abstract class Entity {
|
||||||
|
|
||||||
public handleClick(x: number, y: number) {
|
public handleClick(x: number, y: number) {
|
||||||
if (this.isPointInBounds(x, y)) {
|
if (this.isPointInBounds(x, y)) {
|
||||||
this.onClick();
|
this.onClick(x - this.left, y - this.right);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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(_x: number, _y: number) { }
|
||||||
public update(_dt: number) { };
|
public update(_dt: number) { };
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +65,7 @@ export default async function main() {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
spinner.addListener((a) => map.handleSpin(a));
|
spinner.addListener((a) => map.handleSpin(a));
|
||||||
|
inventory.addListener((c, i) => map.handleItemUse(c, i));
|
||||||
|
|
||||||
canvas.addEventListener('click', onClick);
|
canvas.addEventListener('click', onClick);
|
||||||
canvas.addEventListener('mousemove', onMouseMove);
|
canvas.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,92 @@
|
||||||
|
import { range } from "@common/utils";
|
||||||
import type Character from "./character";
|
import type Character from "./character";
|
||||||
import { Characters } from "./character";
|
import { Characters } from "./character";
|
||||||
import Entity from "./entity";
|
import Entity from "./entity";
|
||||||
|
import Tile from "./tile";
|
||||||
|
import type Item from "./item";
|
||||||
|
|
||||||
|
export type UseListener = (character: Character, item: Item) => void;
|
||||||
|
|
||||||
export default class Inventory extends Entity {
|
export default class Inventory extends Entity {
|
||||||
|
private tiles: Tile[][];
|
||||||
|
private listeners = new Set<UseListener>();
|
||||||
|
|
||||||
constructor(public readonly characters: Character[], position: [number, number], size: [number, number]) {
|
constructor(public readonly characters: Character[], position: [number, number], size: [number, number]) {
|
||||||
super(position, size);
|
super(position, size);
|
||||||
|
|
||||||
|
const numCharacters = Object.keys(Characters).length;
|
||||||
|
const tileSize = this.width / numCharacters;
|
||||||
|
const numTiles = Math.floor(this.height / tileSize) - 2;
|
||||||
|
|
||||||
|
this.tiles = range(numCharacters).map((x) =>
|
||||||
|
range(numTiles).map((y) =>
|
||||||
|
new Tile([x * tileSize, (2 + y) * tileSize], tileSize)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawCharacter(ctx: CanvasRenderingContext2D, character: Character) {
|
private drawCharacter(ctx: CanvasRenderingContext2D, idx: number) {
|
||||||
|
const character = this.characters[idx];
|
||||||
ctx.drawImage(character.type, 0.1, 0.1, 0.8, 0.8);
|
ctx.drawImage(character.type, 0.1, 0.1, 0.8, 0.8);
|
||||||
ctx.fillText(`♥ ${character.health}`, 0.5, 1.5);
|
ctx.fillText(`💖 ${character.health}`, 0.5, 1.5);
|
||||||
|
|
||||||
|
let y = 2;
|
||||||
|
for (const item of character.inventory) {
|
||||||
|
ctx.drawImage(item.type, 0.1, 0.1 + y, 0.8, 0.8);
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override handleClick(x: number, y: number): void {
|
||||||
|
for (const { tile, item, character } of this.activeTiles) {
|
||||||
|
if (tile.isPointInBounds(x - this.left, y - this.top)) {
|
||||||
|
this.listeners.forEach(l => l(character, item));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override handleMouseMove(x: number, y: number): void {
|
||||||
|
this.activeTiles.forEach(({ tile }) => tile.handleMouseMove(x - this.left, y - this.top));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get activeTiles() {
|
||||||
|
return this.tiles.slice(0, this.characters.length)
|
||||||
|
.flatMap((column, characterIndex) =>
|
||||||
|
column.slice(0, this.characters[characterIndex].inventory.length)
|
||||||
|
.map((tile, tileIndex) => ({
|
||||||
|
tile,
|
||||||
|
character: this.characters[characterIndex],
|
||||||
|
item: this.characters[characterIndex].inventory[tileIndex],
|
||||||
|
}))
|
||||||
|
.filter(({ item }) => item.isConsumable)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(listener: UseListener) {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(listener: UseListener) {
|
||||||
|
this.listeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override draw(ctx: CanvasRenderingContext2D): void {
|
protected override draw(ctx: CanvasRenderingContext2D): void {
|
||||||
const step = 1 / Object.keys(Characters).length;
|
const step = 1 / Object.keys(Characters).length;
|
||||||
const columnWidth = this.width * step;
|
const columnWidth = this.width * step;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
ctx.scale(step, columnWidth / this.height);
|
ctx.scale(step, columnWidth / this.height);
|
||||||
ctx.font = `0.5px Arial`;
|
ctx.font = `0.3px Arial`;
|
||||||
ctx.fillStyle = 'black';
|
ctx.fillStyle = 'black';
|
||||||
|
|
||||||
for (const char of this.characters) {
|
for (const i of range(this.characters.length)) {
|
||||||
this.drawCharacter(ctx, char);
|
this.drawCharacter(ctx, i);
|
||||||
ctx.translate(1, 0);
|
ctx.translate(1, 0);
|
||||||
}
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
ctx.scale(1 / this.width, 1 / this.height);
|
||||||
|
this.activeTiles.forEach(({ tile }) => tile.render(ctx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +107,14 @@ export default class Item {
|
||||||
return this.type === ItemType.ENEMY_BOSS;
|
return this.type === ItemType.ENEMY_BOSS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isConsumable() {
|
||||||
|
return [
|
||||||
|
ItemType.ITEM_HEAL,
|
||||||
|
ItemType.ITEM_PLANKS,
|
||||||
|
ItemType.WEAPON_GRENADE,
|
||||||
|
].includes(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return Object.entries(ItemType).find(t => t[1] === this.type)?.[0];
|
return Object.entries(ItemType).find(t => t[1] === this.type)?.[0];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ export default class Tile extends Entity {
|
||||||
ctx.fillStyle = `rgba(255, 255, 255, 0.2)`;
|
ctx.fillStyle = `rgba(255, 255, 255, 0.2)`;
|
||||||
ctx.fillRect(0, 0, 1, 1);
|
ctx.fillRect(0, 0, 1, 1);
|
||||||
|
|
||||||
ctx.lineWidth = 1 / this.width;
|
// ctx.lineWidth = 1 / this.width;
|
||||||
ctx.strokeStyle = 'red';
|
// ctx.strokeStyle = 'red';
|
||||||
|
|
||||||
// for (const tile of this.connections) {
|
// for (const tile of this.connections) {
|
||||||
// const center = [
|
// const center = [
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,13 @@ export default class TileMap extends Entity {
|
||||||
items.push(char);
|
items.push(char);
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalTiles = this.tiles.filter(t => t.type === TileType.NORMAL);
|
|
||||||
const endTiles = this.tiles.filter(t => t.type === TileType.END);
|
const endTiles = this.tiles.filter(t => t.type === TileType.END);
|
||||||
const endTilesNeighbors = new Set(endTiles.flatMap(t => t.connections));
|
const endTilesNeighbors = new Set(endTiles.flatMap(t => t.connections));
|
||||||
|
const normalTiles = this.tiles.filter(t =>
|
||||||
|
t.type === TileType.NORMAL
|
||||||
|
&& !endTilesNeighbors.has(t)
|
||||||
|
&& !this.startTile.connections.includes(t)
|
||||||
|
);
|
||||||
|
|
||||||
const fillableTiles = [
|
const fillableTiles = [
|
||||||
...endTilesNeighbors,
|
...endTilesNeighbors,
|
||||||
|
|
@ -171,6 +175,9 @@ export default class TileMap extends Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override handleMouseMove(x: number, y: number): void {
|
public override handleMouseMove(x: number, y: number): void {
|
||||||
|
if (this.state !== GameState.NORMAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.availableTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
|
this.availableTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +198,7 @@ export default class TileMap extends Entity {
|
||||||
tile.removeItem(item);
|
tile.removeItem(item);
|
||||||
this.characters.push(item);
|
this.characters.push(item);
|
||||||
this.nextCharacter();
|
this.nextCharacter();
|
||||||
} else if (item.isBoss && this.character.useItem(this.character.rocketLauncher)) {
|
} else if (item.isBoss && this.character.removeItem(this.character.rocketLauncher)) {
|
||||||
tile.killEnemy();
|
tile.killEnemy();
|
||||||
this.nextCharacter();
|
this.nextCharacter();
|
||||||
} else if (item.isEnemy) {
|
} else if (item.isEnemy) {
|
||||||
|
|
@ -225,36 +232,54 @@ export default class TileMap extends Entity {
|
||||||
this.character.tile.items.push(...this.character.inventory);
|
this.character.tile.items.push(...this.character.inventory);
|
||||||
this.characters.splice(this.currentCharacterIdx, 1);
|
this.characters.splice(this.currentCharacterIdx, 1);
|
||||||
this.currentCharacterIdx = this.currentCharacterIdx % this.characters.length;
|
this.currentCharacterIdx = this.currentCharacterIdx % this.characters.length;
|
||||||
|
this.setNormalState();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SpinnerAction.MELEE:
|
case SpinnerAction.MELEE:
|
||||||
case SpinnerAction.SHOOT:
|
case SpinnerAction.SHOOT:
|
||||||
this.character.tile.killEnemy();
|
this.killEnemy();
|
||||||
this.nextCharacter();
|
|
||||||
break;
|
break;
|
||||||
case SpinnerAction.RUN:
|
case SpinnerAction.RUN:
|
||||||
|
this.setNormalState();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (action !== SpinnerAction.BITE) {
|
public handleItemUse(character: Character, item: Item) {
|
||||||
this.state = GameState.NORMAL;
|
const success = character.removeItem(item);
|
||||||
this.findAvailableTiles();
|
if (success) {
|
||||||
|
if (item.type === ItemType.ITEM_HEAL) {
|
||||||
|
character.heal(character);
|
||||||
|
} else if (item.type === ItemType.WEAPON_GRENADE && this.state === GameState.FIGHT) {
|
||||||
|
this.killEnemy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private findAvailableTiles(moveDistance: number = 1) {
|
private findAvailableTiles(moveDistance: number = 1) {
|
||||||
const characterTiles = new Set(this.characters.map(c => c.tile));
|
const characterTiles = new Set(this.characters.map(c => c.tile));
|
||||||
this.availableTiles = Pathfinding.findPossibleMoves(this.character.tile, moveDistance)
|
this.availableTiles = Pathfinding.findPossibleMoves(this.character.tile, moveDistance + this.character.moveBonus)
|
||||||
.filter(t => !characterTiles.has(t));
|
.filter(t => !characterTiles.has(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setNormalState() {
|
||||||
|
this.state = GameState.NORMAL;
|
||||||
|
this.findAvailableTiles();
|
||||||
|
}
|
||||||
|
|
||||||
private nextCharacter() {
|
private nextCharacter() {
|
||||||
this.currentCharacterIdx = (this.currentCharacterIdx + 1) % this.characters.length;
|
this.currentCharacterIdx = (this.currentCharacterIdx + 1) % this.characters.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private killEnemy() {
|
||||||
|
this.character.tile.killEnemy();
|
||||||
|
this.nextCharacter();
|
||||||
|
this.setNormalState();
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
|
@ -276,7 +301,11 @@ export default class TileMap extends Entity {
|
||||||
ctx.drawImage(c.type, c.tile.centerX - w / 2, c.tile.centerY - w / 2, w, w)
|
ctx.drawImage(c.type, c.tile.centerX - w / 2, c.tile.centerY - w / 2, w, w)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.tiles.forEach(t => t.render(ctx));
|
this.tiles.forEach(t => {
|
||||||
|
if (t.items.length > 0 || (this.state === GameState.NORMAL && this.availableTiles.includes(t))) {
|
||||||
|
t.render(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.strokeStyle = 'yellow';
|
ctx.strokeStyle = 'yellow';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue