1
0
Fork 0

First playable version

This commit is contained in:
Pabloader 2025-06-26 18:03:58 +00:00
parent 399e3097a3
commit bdca1ce602
7 changed files with 124 additions and 22 deletions

View File

@ -62,7 +62,7 @@ export default class Character extends Item {
character.health += this.healingAmount;
}
public useItem(item: Item | null | undefined): boolean {
public removeItem(item: Item | null | undefined): boolean {
if (!item) {
return false;
}
@ -90,7 +90,7 @@ export default class Character extends Item {
return this.meleeWeapon != null && !this.tile.enemy?.isBoss;
case SpinnerAction.SHOOT:
return this.useItem(this.gun) && !this.tile.enemy?.isBoss;
return this.removeItem(this.gun) && !this.tile.enemy?.isBoss;
}
}
}

View File

@ -37,7 +37,7 @@ export default abstract class Entity {
public handleClick(x: number, y: number) {
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 onClick() { }
protected onClick(_x: number, _y: number) { }
public update(_dt: number) { };
}

View File

@ -65,6 +65,7 @@ export default async function main() {
const ctx = canvas.getContext('2d');
spinner.addListener((a) => map.handleSpin(a));
inventory.addListener((c, i) => map.handleItemUse(c, i));
canvas.addEventListener('click', onClick);
canvas.addEventListener('mousemove', onMouseMove);

View File

@ -1,28 +1,92 @@
import { range } from "@common/utils";
import type Character from "./character";
import { Characters } from "./character";
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 {
private tiles: Tile[][];
private listeners = new Set<UseListener>();
constructor(public readonly characters: Character[], position: [number, number], size: [number, number]) {
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.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 {
const step = 1 / Object.keys(Characters).length;
const columnWidth = this.width * step;
ctx.save();
ctx.scale(step, columnWidth / this.height);
ctx.font = `0.5px Arial`;
ctx.font = `0.3px Arial`;
ctx.fillStyle = 'black';
for (const char of this.characters) {
this.drawCharacter(ctx, char);
for (const i of range(this.characters.length)) {
this.drawCharacter(ctx, i);
ctx.translate(1, 0);
}
ctx.restore();
ctx.scale(1 / this.width, 1 / this.height);
this.activeTiles.forEach(({ tile }) => tile.render(ctx));
}
}

View File

@ -107,6 +107,14 @@ export default class Item {
return this.type === ItemType.ENEMY_BOSS;
}
get isConsumable() {
return [
ItemType.ITEM_HEAL,
ItemType.ITEM_PLANKS,
ItemType.WEAPON_GRENADE,
].includes(this.type);
}
toString() {
return Object.entries(ItemType).find(t => t[1] === this.type)?.[0];
}

View File

@ -21,8 +21,8 @@ export default class Tile extends Entity {
ctx.fillStyle = `rgba(255, 255, 255, 0.2)`;
ctx.fillRect(0, 0, 1, 1);
ctx.lineWidth = 1 / this.width;
ctx.strokeStyle = 'red';
// ctx.lineWidth = 1 / this.width;
// ctx.strokeStyle = 'red';
// for (const tile of this.connections) {
// const center = [

View File

@ -148,9 +148,13 @@ export default class TileMap extends Entity {
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 normalTiles = this.tiles.filter(t =>
t.type === TileType.NORMAL
&& !endTilesNeighbors.has(t)
&& !this.startTile.connections.includes(t)
);
const fillableTiles = [
...endTilesNeighbors,
@ -171,6 +175,9 @@ export default class TileMap extends Entity {
}
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));
}
@ -191,7 +198,7 @@ export default class TileMap extends Entity {
tile.removeItem(item);
this.characters.push(item);
this.nextCharacter();
} else if (item.isBoss && this.character.useItem(this.character.rocketLauncher)) {
} else if (item.isBoss && this.character.removeItem(this.character.rocketLauncher)) {
tile.killEnemy();
this.nextCharacter();
} else if (item.isEnemy) {
@ -225,36 +232,54 @@ export default class TileMap extends Entity {
this.character.tile.items.push(...this.character.inventory);
this.characters.splice(this.currentCharacterIdx, 1);
this.currentCharacterIdx = this.currentCharacterIdx % this.characters.length;
this.setNormalState();
}
break;
case SpinnerAction.MELEE:
case SpinnerAction.SHOOT:
this.character.tile.killEnemy();
this.nextCharacter();
this.killEnemy();
break;
case SpinnerAction.RUN:
this.setNormalState();
break;
}
}
break;
}
}
if (action !== SpinnerAction.BITE) {
this.state = GameState.NORMAL;
this.findAvailableTiles();
public handleItemUse(character: Character, item: Item) {
const success = character.removeItem(item);
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) {
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));
}
private setNormalState() {
this.state = GameState.NORMAL;
this.findAvailableTiles();
}
private nextCharacter() {
this.currentCharacterIdx = (this.currentCharacterIdx + 1) % this.characters.length;
}
private killEnemy() {
this.character.tile.killEnemy();
this.nextCharacter();
this.setNormalState();
}
protected draw(ctx: CanvasRenderingContext2D): void {
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)
);
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.strokeStyle = 'yellow';