import { range } from "@common/utils"; import type Player from "./player"; import { Players } from "./player"; import Entity from "./entity"; import Tile from "./tile"; import type Item from "./item"; import type TileMap from "./tilemap"; import { ItemType } from "./item"; export type UseListener = (player: Player, item: Item) => void; interface ActiveTile { tile: Tile; player: Player; transfer?: boolean; heal?: boolean; item?: Item; } export default class Inventory extends Entity { private playerTiles: Tile[]; private transferButtons: Tile[]; private healButtons: Tile[]; private tiles: Tile[][]; private transferTarget: Player | undefined; constructor(public readonly map: TileMap, position: [number, number], size: [number, number]) { super(position, size); const numPlayers = Object.keys(Players).length; const tileSize = this.width / numPlayers; const numTiles = Math.floor(this.height / tileSize) - 2; this.playerTiles = range(numPlayers).map((x) => new Tile([x * tileSize, 0], tileSize) ); this.transferButtons = range(numPlayers).map((x) => new Tile([x * tileSize, 1.5 * tileSize], tileSize / 2) ); this.healButtons = range(numPlayers).map((x) => new Tile([(x + 0.5) * tileSize, 1.5 * tileSize], tileSize / 2) ); this.tiles = range(numPlayers).map((x) => range(numTiles).map((y) => new Tile([x * tileSize, (2 + y) * tileSize], tileSize) ) ); } private get players() { return this.map.players; } private get player() { return this.map.player; } private get activeTiles(): ActiveTile[] { return [ ...this.playerTiles .slice(0, this.players.length) .map((tile, playerIndex) => ({ tile, player: this.players[playerIndex], })), ...this.transferButtons .slice(0, this.players.length) .map((tile, playerIndex) => ({ tile, player: this.players[playerIndex], transfer: true, })) .filter(({ player }) => this.players.some(p => this.map.canTransferTo(p, player)) ), ...this.healButtons .slice(0, this.players.length) .map((tile, playerIndex) => ({ tile, player: this.players[playerIndex], heal: true, })) .filter(({ player }) => this.players.some(healer => this.map.canHeal(healer, player)) ), ...this.tiles .slice(0, this.players.length) .flatMap((column, playerIndex) => column .slice(0, this.players[playerIndex].numItems) .map((tile, tileIndex) => ({ tile, player: this.players[playerIndex], item: this.players[playerIndex].getItem(tileIndex), })) .filter(({ item, player }) => this.transferTarget ? this.map.canTransferTo(player, this.transferTarget) : item.isConsumable) ) ]; } public override handleClick(x: number, y: number): void { for (const { tile, item, player, transfer, heal } of this.activeTiles) { if (tile.isPointInBounds(x - this.left, y - this.top)) { if (heal) { this.handleHeal(player); } else if (transfer) { this.handleToggleTransfer(player); } else if (this.transferTarget && item) { this.handleTransfer(player, item); } else if (item) { this.map.handleItemUse(player, item); } else if (player !== this.player) { player.toggleActive(); } return; } } } public override handleMouseMove(x: number, y: number): void { this.activeTiles.forEach(({ tile }) => tile.handleMouseMove(x - this.left, y - this.top)); this.playerTiles.forEach((tile) => tile.handleMouseMove(x - this.left, y - this.top)); } private handleToggleTransfer(target: Player) { if (this.transferTarget === target) { this.transferTarget = undefined; } else { this.transferTarget = target; } } private handleTransfer(player: Player, item: Item) { if (this.transferTarget) { const itemExisted = player.removeItem(item); if (itemExisted) { this.transferTarget.addItem(item); } } this.transferTarget = undefined; } private handleHeal(target: Player) { const healers = [ this.players.find(p => p.type === ItemType.CHAR_NURSE), target, ...this.players, ]; for (const healer of healers) { if (!healer || !this.map.canHeal(healer, target)) continue; const itemExisted = healer.removeItem(healer.healingKit); if (itemExisted) { healer.heal(target); return; } } } private drawPlayer(ctx: CanvasRenderingContext2D, idx: number) { const player = this.players[idx]; ctx.fillStyle = 'black'; ctx.drawImage(player.type, 0.1, 0.1, 0.8, 0.8); ctx.fillText(`💖 ${player.health}`, 0.5, 1.2); if (player === this.player) { ctx.strokeStyle = 'black'; ctx.lineWidth = 0.03; ctx.strokeRect(0.1, 0.1, 0.8, 0.8); } if (!player.active) { ctx.fillStyle = `rgba(255, 255, 255, 0.5)`; ctx.fillRect(0.1, 0.1, 0.8, 0.8); } let y = 2; for (const item of player) { ctx.drawImage(item.type, 0.1, 0.1 + y, 0.8, 0.8); y += 1; } } protected override draw(ctx: CanvasRenderingContext2D): void { const step = 1 / Object.keys(Players).length; const columnWidth = this.width * step; ctx.save(); ctx.scale(step, columnWidth / this.height); ctx.font = `0.3px Arial`; ctx.fillStyle = 'black'; for (const i of range(this.players.length)) { this.drawPlayer(ctx, i); ctx.translate(1, 0); } ctx.restore(); ctx.scale(1 / this.width, 1 / this.height); ctx.font = `${0.3 * columnWidth}px Arial`; const activeTiles = new Set(this.activeTiles.map(({ tile }) => tile)); this.transferButtons.slice(0, this.players.length) .map((tile, playerIdx) => ({ tile, player: this.players[playerIdx] })) .filter(({ tile }) => activeTiles.has(tile)) .forEach(({ tile, player }) => { if (player === this.transferTarget) { ctx.fillStyle = 'lime'; ctx.fillRect(tile.left, tile.top, tile.width, tile.height); } ctx.fillStyle = 'black'; ctx.fillText('🎁', tile.centerX, tile.centerY); }); ctx.fillStyle = 'black'; this.healButtons.slice(0, this.players.length) .filter((tile) => activeTiles.has(tile)) .forEach((tile) => { ctx.fillText('🩹', tile.centerX, tile.centerY); }); activeTiles.forEach((tile) => tile.render(ctx)); } }