From 71ea75503278eb6025c9b95f6b67776bde8c2e2d Mon Sep 17 00:00:00 2001 From: Pabloader Date: Sun, 29 Jun 2025 12:22:00 +0000 Subject: [PATCH] Items transfer & healing --- build/html.ts | 5 + src/games/zombies/inventory.ts | 178 +++++++++++++++++++++++++++------ src/games/zombies/item.ts | 5 +- src/games/zombies/player.ts | 9 ++ src/games/zombies/tilemap.ts | 60 +++++++++-- 5 files changed, 219 insertions(+), 38 deletions(-) diff --git a/build/html.ts b/build/html.ts index a99db1d..8118264 100644 --- a/build/html.ts +++ b/build/html.ts @@ -93,6 +93,11 @@ export async function buildHTML(game: string, { production = false, portable = f if (production) { const minifyResult = UglifyJS.minify(script, { module: true, + toplevel: true, + mangle: { + toplevel: true, + properties: true, + }, }); if (minifyResult.error) { console.warn(`Minify error: ${minifyResult.error}`); diff --git a/src/games/zombies/inventory.ts b/src/games/zombies/inventory.ts index 87b7e82..26c9bb5 100644 --- a/src/games/zombies/inventory.ts +++ b/src/games/zombies/inventory.ts @@ -5,11 +5,24 @@ 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); @@ -18,6 +31,18 @@ export default class Inventory extends Entity { 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) @@ -29,17 +54,118 @@ export default class Inventory extends Entity { 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.map.canTransferTo(this.player, player)), + + ...this.healButtons + .slice(0, this.players.length) + .map((tile, playerIndex) => ({ + tile, + player: this.players[playerIndex], + heal: true, + })) + .filter(({ player }) => + this.player.hasItem(ItemType.ITEM_HEALING_KIT) + && this.map.canHeal(this.player, player) + ), + + ...this.tiles + .slice(0, this.players.length) + .flatMap((column, playerIndex) => + column + .slice(0, this.players[playerIndex].inventory.length) + .map((tile, tileIndex) => ({ + tile, + player: this.players[playerIndex], + item: this.players[playerIndex].inventory[tileIndex], + })) + .filter(({ item, player }) => this.transferTarget + ? this.player === player + : 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.map.handleHeal(player); + } else if (transfer) { + this.handleToggleTransfer(player); + } else if (this.transferTarget && item) { + this.handleTransfer(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(item: Item) { + if (this.transferTarget) { + const itemExisted = this.player.removeItem(item); + if (itemExisted) { + this.transferTarget.inventory.push(item); + } + } + this.transferTarget = undefined; + } + 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.5); - if (player === this.map.player) { + 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.inventory) { ctx.drawImage(item.type, 0.1, 0.1 + y, 0.8, 0.8); @@ -47,32 +173,6 @@ export default class Inventory extends Entity { } } - public override handleClick(x: number, y: number): void { - for (const { tile, item, player } of this.activeTiles) { - if (tile.isPointInBounds(x - this.left, y - this.top)) { - this.map.handleItemUse(player, 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.players.length) - .flatMap((column, playerIndex) => - column.slice(0, this.players[playerIndex].inventory.length) - .map((tile, tileIndex) => ({ - tile, - player: this.players[playerIndex], - item: this.players[playerIndex].inventory[tileIndex], - })) - .filter(({ item }) => item.isConsumable) - ); - } - protected override draw(ctx: CanvasRenderingContext2D): void { const step = 1 / Object.keys(Players).length; const columnWidth = this.width * step; @@ -89,6 +189,28 @@ export default class Inventory extends Entity { ctx.restore(); ctx.scale(1 / this.width, 1 / this.height); + + ctx.font = `${0.3 * columnWidth}px Arial`; + this.transferButtons.slice(0, this.players.length) + .map((tile, playerIdx) => ({ tile, player: this.players[playerIdx] })) + .filter(({ player }) => this.map.canTransferTo(this.player, player)) + .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) + .map((tile, playerIdx) => ({ tile, player: this.players[playerIdx] })) + .filter(({ player }) => this.map.canHeal(this.player, player)) + .forEach(({ tile }) => { + ctx.fillText('🩹', tile.centerX, tile.centerY); + }); + 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 cbdc4ef..6ba8c5c 100644 --- a/src/games/zombies/item.ts +++ b/src/games/zombies/item.ts @@ -36,7 +36,7 @@ export const ItemType = { ENEMY_ZOMBIE: zombie, ITEM_FUEL: fuel, - ITEM_HEAL: heal, + ITEM_HEALING_KIT: heal, ITEM_KEYS: keys, ITEM_PLANKS: planks, @@ -74,7 +74,7 @@ export default class Item { get isItem() { return [ ItemType.ITEM_FUEL, - ItemType.ITEM_HEAL, + ItemType.ITEM_HEALING_KIT, ItemType.ITEM_KEYS, ItemType.ITEM_PLANKS, ].includes(this.type); @@ -109,7 +109,6 @@ export default class Item { get isConsumable() { return [ - ItemType.ITEM_HEAL, ItemType.ITEM_PLANKS, ItemType.WEAPON_GRENADE, ].includes(this.type); diff --git a/src/games/zombies/player.ts b/src/games/zombies/player.ts index 5318da8..3a60c82 100644 --- a/src/games/zombies/player.ts +++ b/src/games/zombies/player.ts @@ -7,6 +7,7 @@ export default class Player extends Character { public health: number; public inventory: Item[] = []; public lastDoor: Tile | undefined; + public active = true; constructor(type: ItemTypeImage) { super(type); @@ -63,6 +64,10 @@ export default class Player extends Character { return this.inventory.find(i => i.type === ItemType.ITEM_FUEL); } + get healingKit() { + return this.inventory.find(i => i.type === ItemType.ITEM_HEALING_KIT); + } + public heal(player: Player) { player.health += this.healingAmount; } @@ -90,6 +95,10 @@ export default class Player extends Character { return itemIndex >= 0; } + public toggleActive() { + this.active = !this.active; + } + /** @returns true, if action was performed */ public handleSpin(action: SpinnerAction): boolean { switch (action) { diff --git a/src/games/zombies/tilemap.ts b/src/games/zombies/tilemap.ts index 500927f..7b2a0d0 100644 --- a/src/games/zombies/tilemap.ts +++ b/src/games/zombies/tilemap.ts @@ -154,7 +154,7 @@ export default class TileMap extends Entity { [ItemType.ENEMY_ZOMBIE, 17], [ItemType.ITEM_FUEL, 1], - [ItemType.ITEM_HEAL, 6], + [ItemType.ITEM_HEALING_KIT, 6], [ItemType.ITEM_KEYS, 1], [ItemType.ITEM_PLANKS, 8], @@ -241,6 +241,38 @@ export default class TileMap extends Entity { ) } + public canTransferTo(player: Player, otherPlayer: Player): boolean { + if (otherPlayer === player) return false; + + if (player.inventory.length === 0) return false; + + if (player.tile === otherPlayer.tile) { + return true; + } + for (const tile of player.tile.connections) { + if (otherPlayer.tile === tile) { + return true; + } + } + + return false; + } + + public canHeal(player: Player, otherPlayer: Player): boolean { + if (!player.healingKit) return false; + + return player === otherPlayer || this.canTransferTo(player, otherPlayer); + } + + public handleHeal(target: Player) { + if (target) { + const itemExisted = this.player.removeItem(this.player.healingKit); + if (itemExisted) { + this.player.heal(target); + } + } + } + public override handleMouseMove(x: number, y: number): void { this.activeTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top)); } @@ -334,9 +366,7 @@ export default class TileMap extends Entity { public handleItemUse(player: Player, item: Item) { let success = player.hasItem(item); if (success) { - if (item.type === ItemType.ITEM_HEAL) { - player.heal(player); - } else if (item.type === ItemType.WEAPON_GRENADE && this.state === GameState.FIGHT) { + if (item.type === ItemType.WEAPON_GRENADE && this.state === GameState.FIGHT) { this.killEnemy(); } else if (item.type === ItemType.ITEM_PLANKS && player.lastDoor && !player.lastDoor.enemy) { player.lastDoor.type = TileType.LOCKED_DOOR; @@ -363,7 +393,23 @@ export default class TileMap extends Entity { private nextPlayer() { this.spinner.stop(); - this.currentPlayerIdx = (this.currentPlayerIdx + 1) % this.players.length; + let overflow = false; + let startIndex = this.currentPlayerIdx; + do { + let nextIdx = this.currentPlayerIdx + 1; + if (nextIdx >= this.players.length) { + if (overflow) { + this.currentPlayerIdx = 0; + this.player.active = true; + break; + } else { + overflow = true; + nextIdx = 0; + } + } + this.currentPlayerIdx = nextIdx; + } while (!this.player.active); + if (this.win) { const endTile = this.tiles.findLast(t => t.type === TileType.END); if (endTile) { @@ -373,7 +419,7 @@ export default class TileMap extends Entity { } } setTimeout(alert, 2000, "🎉🎉🎉 ПОБЕДА! 🚗 🎉🎉🎉"); - } else if (this.currentPlayerIdx === 0 && this.players.length < this.foundPlayers.size) { // if someone is dead + } else if (overflow && this.players.length < this.foundPlayers.size) { // if someone is dead const enemies = this.enemies; loop: @@ -388,7 +434,7 @@ export default class TileMap extends Entity { const enemy = enemyTile.enemy!; const allowedSteps = randInt(1, 5) + enemy.moveBonus; - console.log({enemy, allowedSteps}); + console.log({ enemy, allowedSteps }); if (allowedSteps <= 0) continue; const path = Pathfinding.findPath(enemyTile, player.tile, true);