From bdca1ce602073ef17ab6343d01f4cc246e35838b Mon Sep 17 00:00:00 2001 From: Pabloader Date: Thu, 26 Jun 2025 18:03:58 +0000 Subject: [PATCH] First playable version --- src/games/zombies/character.ts | 4 +- src/games/zombies/entity.ts | 4 +- src/games/zombies/index.ts | 1 + src/games/zombies/inventory.ts | 74 +++++++++++++++++++++++++++++++--- src/games/zombies/item.ts | 8 ++++ src/games/zombies/tile.ts | 4 +- src/games/zombies/tilemap.ts | 51 ++++++++++++++++++----- 7 files changed, 124 insertions(+), 22 deletions(-) diff --git a/src/games/zombies/character.ts b/src/games/zombies/character.ts index 1e1c988..919c43e 100644 --- a/src/games/zombies/character.ts +++ b/src/games/zombies/character.ts @@ -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; } } } diff --git a/src/games/zombies/entity.ts b/src/games/zombies/entity.ts index 129430d..69f60de 100644 --- a/src/games/zombies/entity.ts +++ b/src/games/zombies/entity.ts @@ -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) { }; } \ No newline at end of file diff --git a/src/games/zombies/index.ts b/src/games/zombies/index.ts index 01a870b..ce8ebaa 100644 --- a/src/games/zombies/index.ts +++ b/src/games/zombies/index.ts @@ -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); diff --git a/src/games/zombies/inventory.ts b/src/games/zombies/inventory.ts index b746349..3b821ea 100644 --- a/src/games/zombies/inventory.ts +++ b/src/games/zombies/inventory.ts @@ -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(); + 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)); } } \ No newline at end of file diff --git a/src/games/zombies/item.ts b/src/games/zombies/item.ts index f9318fa..cbdc4ef 100644 --- a/src/games/zombies/item.ts +++ b/src/games/zombies/item.ts @@ -106,6 +106,14 @@ export default class Item { get isBoss() { 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]; diff --git a/src/games/zombies/tile.ts b/src/games/zombies/tile.ts index 835aaa7..450aa9f 100644 --- a/src/games/zombies/tile.ts +++ b/src/games/zombies/tile.ts @@ -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 = [ diff --git a/src/games/zombies/tilemap.ts b/src/games/zombies/tilemap.ts index 2a58131..70c7f25 100644 --- a/src/games/zombies/tilemap.ts +++ b/src/games/zombies/tilemap.ts @@ -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; } - - if (action !== SpinnerAction.BITE) { - this.state = GameState.NORMAL; - this.findAvailableTiles(); - } } break; } } + 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(); + } + } + } + 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';