From b3574a8acf7cbb539162025d4c456b6f9946b0a4 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Fri, 27 Jun 2025 12:31:12 +0000 Subject: [PATCH] Refactor Character -> Player, add separate character class --- src/games/zombies/character.ts | 130 ++++++--------------------------- src/games/zombies/entity.ts | 4 +- src/games/zombies/inventory.ts | 48 ++++++------ src/games/zombies/player.ts | 114 +++++++++++++++++++++++++++++ src/games/zombies/tilemap.ts | 82 ++++++++++----------- 5 files changed, 202 insertions(+), 176 deletions(-) create mode 100644 src/games/zombies/player.ts diff --git a/src/games/zombies/character.ts b/src/games/zombies/character.ts index 8d14eb0..028e545 100644 --- a/src/games/zombies/character.ts +++ b/src/games/zombies/character.ts @@ -1,114 +1,28 @@ -import Item, { ItemType, type ItemTypeImage } from "./item"; -import { SpinnerAction } from "./spinner"; +import Item from "./item"; import Tile from "./tile"; +const MOVE_DURATION = 0.5; + export default class Character extends Item { - public health: number; - public inventory: Item[] = []; - public tile: Tile = new Tile([0, 0], 1); - public lastDoor: Tile | undefined; + public tile = new Tile([0, 0], 1); + public path: Tile[] = []; + private pathProgress = 0; - constructor(type: ItemTypeImage) { - super(type); - this.health = this.maxHealth; - const defaultItem = this.defaultItem; - if (defaultItem) { - this.inventory.push(this.defaultItem); + public get displayPosition() { + if (this.path.length > 0) { + // TODO calc + } + return [this.tile.centerX, this.tile.centerY]; + } + + public update(dt: number) { + if (this.path.length > 0) { + if (this.pathProgress >= 1) { + this.path = []; + this.pathProgress = 0; + } else { + this.pathProgress += MOVE_DURATION * dt; + } } } - - get maxHealth() { - return this.type === ItemType.CHAR_BIG ? 7 : 5; - } - - get defaultItem() { - if (this.type === ItemType.CHAR_NINJA) { - return new Item(ItemType.WEAPON_KNIFE); - } - if (this.type === ItemType.CHAR_POLICE) { - return new Item(ItemType.WEAPON_PISTOL); - } - return null; - } - - get healingAmount() { - return this.type === ItemType.CHAR_NURSE ? 2 : 1; - } - - get moveBonus() { - return this.type === ItemType.CHAR_RUNNER ? 1 : 0; - } - - get isDead() { - return this.health <= 0; - } - - get meleeWeapon() { - return this.inventory.find(i => i.isMeleeWeapon); - } - - get gun() { - return this.inventory.find(i => i.isShootingWeapon); - } - - get grenade() { - return this.inventory.find(i => i.type === ItemType.WEAPON_GRENADE); - } - - get rocketLauncher() { - return this.inventory.find(i => i.type === ItemType.WEAPON_ROCKET_LAUNCHER); - } - - public heal(character: Character) { - character.health += this.healingAmount; - } - - public removeItem(item: Item | null | undefined): boolean { - if (!item) { - return false; - } - const itemIndex = this.inventory.findIndex(i => i === item); - - if (itemIndex >= 0) { - this.inventory.splice(itemIndex, 1); - return true; - } - - return false; - } - - public hasItem(item: Item | null | undefined): boolean { - if (!item) { - return false; - } - const itemIndex = this.inventory.findIndex(i => i === item); - - return itemIndex >= 0; - } - - /** @returns true, if action was performed */ - public handleSpin(action: SpinnerAction): boolean { - switch (action) { - case SpinnerAction.RUN: - return true; - - case SpinnerAction.BITE: - this.health -= 1; - return true; - - case SpinnerAction.MELEE: - return this.meleeWeapon != null && !this.tile.enemy?.isBoss; - - case SpinnerAction.SHOOT: - return this.removeItem(this.gun) && !this.tile.enemy?.isBoss; - } - } -} - -export const Characters = { - BIG: new Character(ItemType.CHAR_BIG), - NINJA: new Character(ItemType.CHAR_NINJA), - NURSE: new Character(ItemType.CHAR_NURSE), - POLICE: new Character(ItemType.CHAR_POLICE), - RUNNER: new Character(ItemType.CHAR_RUNNER), -} as const; +} \ No newline at end of file diff --git a/src/games/zombies/entity.ts b/src/games/zombies/entity.ts index 8a9092c..291444d 100644 --- a/src/games/zombies/entity.ts +++ b/src/games/zombies/entity.ts @@ -1,9 +1,7 @@ export default abstract class Entity { public hovered = false; - constructor(public position: [number, number], public size: [number, number]) { - - } + constructor(public position: [number, number], public size: [number, number]) { } public render(ctx: CanvasRenderingContext2D) { ctx.save(); diff --git a/src/games/zombies/inventory.ts b/src/games/zombies/inventory.ts index bf81daf..87b7e82 100644 --- a/src/games/zombies/inventory.ts +++ b/src/games/zombies/inventory.ts @@ -1,12 +1,12 @@ import { range } from "@common/utils"; -import type Character from "./character"; -import { Characters } from "./character"; +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"; -export type UseListener = (character: Character, item: Item) => void; +export type UseListener = (player: Player, item: Item) => void; export default class Inventory extends Entity { private tiles: Tile[][]; @@ -14,26 +14,26 @@ export default class Inventory extends Entity { constructor(public readonly map: TileMap, position: [number, number], size: [number, number]) { super(position, size); - const numCharacters = Object.keys(Characters).length; - const tileSize = this.width / numCharacters; + const numPlayers = Object.keys(Players).length; + const tileSize = this.width / numPlayers; const numTiles = Math.floor(this.height / tileSize) - 2; - this.tiles = range(numCharacters).map((x) => + this.tiles = range(numPlayers).map((x) => range(numTiles).map((y) => new Tile([x * tileSize, (2 + y) * tileSize], tileSize) ) ); } - private get characters() { - return this.map.characters; + private get players() { + return this.map.players; } - 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); - if (character === this.map.character) { + private drawPlayer(ctx: CanvasRenderingContext2D, idx: number) { + const player = this.players[idx]; + 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.strokeStyle = 'black'; ctx.lineWidth = 0.03; @@ -41,16 +41,16 @@ export default class Inventory extends Entity { } let y = 2; - for (const item of character.inventory) { + for (const item of player.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) { + for (const { tile, item, player } of this.activeTiles) { if (tile.isPointInBounds(x - this.left, y - this.top)) { - this.map.handleItemUse(character, item); + this.map.handleItemUse(player, item); break; } } @@ -61,20 +61,20 @@ export default class Inventory extends Entity { } private get activeTiles() { - return this.tiles.slice(0, this.characters.length) - .flatMap((column, characterIndex) => - column.slice(0, this.characters[characterIndex].inventory.length) + return this.tiles.slice(0, this.players.length) + .flatMap((column, playerIndex) => + column.slice(0, this.players[playerIndex].inventory.length) .map((tile, tileIndex) => ({ tile, - character: this.characters[characterIndex], - item: this.characters[characterIndex].inventory[tileIndex], + 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(Characters).length; + const step = 1 / Object.keys(Players).length; const columnWidth = this.width * step; ctx.save(); @@ -82,8 +82,8 @@ export default class Inventory extends Entity { ctx.font = `0.3px Arial`; ctx.fillStyle = 'black'; - for (const i of range(this.characters.length)) { - this.drawCharacter(ctx, i); + for (const i of range(this.players.length)) { + this.drawPlayer(ctx, i); ctx.translate(1, 0); } ctx.restore(); diff --git a/src/games/zombies/player.ts b/src/games/zombies/player.ts new file mode 100644 index 0000000..ae5736e --- /dev/null +++ b/src/games/zombies/player.ts @@ -0,0 +1,114 @@ +import Item, { ItemType, type ItemTypeImage } from "./item"; +import Character from "./character"; +import { SpinnerAction } from "./spinner"; +import Tile from "./tile"; + +export default class Player extends Character { + public health: number; + public inventory: Item[] = []; + public lastDoor: Tile | undefined; + + constructor(type: ItemTypeImage) { + super(type); + this.health = this.maxHealth; + const defaultItem = this.defaultItem; + if (defaultItem) { + this.inventory.push(this.defaultItem); + } + } + + get maxHealth() { + return this.type === ItemType.CHAR_BIG ? 7 : 5; + } + + get defaultItem() { + if (this.type === ItemType.CHAR_NINJA) { + return new Item(ItemType.WEAPON_KNIFE); + } + if (this.type === ItemType.CHAR_POLICE) { + return new Item(ItemType.WEAPON_PISTOL); + } + return null; + } + + get healingAmount() { + return this.type === ItemType.CHAR_NURSE ? 2 : 1; + } + + get moveBonus() { + return this.type === ItemType.CHAR_RUNNER ? 1 : 0; + } + + get isDead() { + return this.health <= 0; + } + + get meleeWeapon() { + return this.inventory.find(i => i.isMeleeWeapon); + } + + get gun() { + return this.inventory.find(i => i.isShootingWeapon); + } + + get grenade() { + return this.inventory.find(i => i.type === ItemType.WEAPON_GRENADE); + } + + get rocketLauncher() { + return this.inventory.find(i => i.type === ItemType.WEAPON_ROCKET_LAUNCHER); + } + + public heal(player: Player) { + player.health += this.healingAmount; + } + + public removeItem(item: Item | null | undefined): boolean { + if (!item) { + return false; + } + const itemIndex = this.inventory.findIndex(i => i === item); + + if (itemIndex >= 0) { + this.inventory.splice(itemIndex, 1); + return true; + } + + return false; + } + + public hasItem(item: Item | null | undefined): boolean { + if (!item) { + return false; + } + const itemIndex = this.inventory.findIndex(i => i === item); + + return itemIndex >= 0; + } + + /** @returns true, if action was performed */ + public handleSpin(action: SpinnerAction): boolean { + switch (action) { + case SpinnerAction.RUN: + return true; + + case SpinnerAction.BITE: + this.health -= 1; + return true; + + case SpinnerAction.MELEE: + return this.meleeWeapon != null && !this.tile.enemy?.isBoss; + + case SpinnerAction.SHOOT: + return this.removeItem(this.gun) && !this.tile.enemy?.isBoss; + } + } +} + +export const Players = { + BIG: new Player(ItemType.CHAR_BIG), + NINJA: new Player(ItemType.CHAR_NINJA), + NURSE: new Player(ItemType.CHAR_NURSE), + POLICE: new Player(ItemType.CHAR_POLICE), + RUNNER: new Player(ItemType.CHAR_RUNNER), +} as const; diff --git a/src/games/zombies/tilemap.ts b/src/games/zombies/tilemap.ts index 438a250..54ed2d5 100644 --- a/src/games/zombies/tilemap.ts +++ b/src/games/zombies/tilemap.ts @@ -3,7 +3,7 @@ import Entity from "./entity"; import Tile, { TileType } from "./tile"; import Pathfinding from "./pathfinding"; import Item, { ItemType } from "./item"; -import Character, { Characters } from "./character"; +import Player, { Players } from "./player"; import { SpinnerAction } from "./spinner"; enum GameState { @@ -15,14 +15,14 @@ export default class TileMap extends Entity { private tiles: Tile[] = []; public startTile: Tile; - public readonly characters: Character[]; - private currentCharacterIdx = 0; + public readonly players: Player[]; + private currentPlayerIdx = 0; private state = GameState.NORMAL; private availableTiles: Tile[] = []; constructor(position: [number, number], private mapSize: number, private tileSize: number, numPlayers: number = 2) { super(position, [mapSize * tileSize, mapSize * tileSize]); - this.characters = shuffle(Object.values(Characters)).slice(0, numPlayers); + this.players = shuffle(Object.values(Players)).slice(0, numPlayers); this.startTile = this.createMap(); this.findAvailableTiles(); } @@ -128,7 +128,7 @@ export default class TileMap extends Entity { this.startTile = startTile; this.fillItems(); - this.characters.forEach(c => c.tile = startTile); + this.players.forEach(c => c.tile = startTile); return startTile; } @@ -164,8 +164,8 @@ export default class TileMap extends Entity { } } - for (const char of Object.values(Characters)) { - if (this.characters.includes(char)) continue; + for (const char of Object.values(Players)) { + if (this.players.includes(char)) continue; items.push(char); } @@ -187,14 +187,14 @@ export default class TileMap extends Entity { for (const [tile, item] of zip(fillableTiles, shuffle(items))) { tile.items.push(item); - if (item instanceof Character) { + if (item instanceof Player) { item.tile = tile; } }; } - get character() { - return this.characters[this.currentCharacterIdx]; + get player() { + return this.players[this.currentPlayerIdx]; } get activeTiles() { @@ -216,21 +216,21 @@ export default class TileMap extends Entity { } for (const tile of this.availableTiles) { if (tile.isPointInBounds(x - this.left, y - this.top)) { - const path = Pathfinding.findPath(this.character.tile, tile); + const path = Pathfinding.findPath(this.player.tile, tile); if (path.length > 1) { - this.character.lastDoor = path.find(t => t.type === TileType.DOOR); - this.character.tile = tile; + this.player.lastDoor = path.find(t => t.type === TileType.DOOR); + this.player.tile = tile; tile.open(); for (const item of tile.items.slice()) { // iterate remaining items if (item.isPickable) { if (!tile.enemy) { tile.removeItem(item); - this.character.inventory.push(item); + this.player.inventory.push(item); } - } else if (item instanceof Character) { + } else if (item instanceof Player) { tile.removeItem(item); - this.characters.push(item); - } else if (item.isBoss && this.character.removeItem(this.character.rocketLauncher)) { + this.players.push(item); + } else if (item.isBoss && this.player.removeItem(this.player.rocketLauncher)) { tile.killEnemy(); } else if (item.isEnemy) { this.state = GameState.FIGHT; @@ -240,7 +240,7 @@ export default class TileMap extends Entity { } } if (this.state === GameState.NORMAL) { - this.nextCharacter(); + this.nextPlayer(); } } @@ -256,14 +256,14 @@ export default class TileMap extends Entity { this.findAvailableTiles(action); break; case GameState.FIGHT: - const success = this.character.handleSpin(action); + const success = this.player.handleSpin(action); if (success) { switch (action) { case SpinnerAction.BITE: - if (this.character.isDead) { - this.character.tile.items.push(...this.character.inventory); - this.characters.splice(this.currentCharacterIdx, 1); - this.currentCharacterIdx = this.currentCharacterIdx % this.characters.length; + if (this.player.isDead) { + this.player.tile.items.push(...this.player.inventory); + this.players.splice(this.currentPlayerIdx, 1); + this.currentPlayerIdx = this.currentPlayerIdx % this.players.length; this.setNormalState(); } break; @@ -280,29 +280,29 @@ export default class TileMap extends Entity { } } - public handleItemUse(character: Character, item: Item) { - let success = character.hasItem(item); + public handleItemUse(player: Player, item: Item) { + let success = player.hasItem(item); if (success) { if (item.type === ItemType.ITEM_HEAL) { - character.heal(character); + player.heal(player); } else if (item.type === ItemType.WEAPON_GRENADE && this.state === GameState.FIGHT) { this.killEnemy(); - } else if (item.type === ItemType.ITEM_PLANKS && character.lastDoor && !character.lastDoor.enemy) { - character.lastDoor.type = TileType.LOCKED_DOOR; + } else if (item.type === ItemType.ITEM_PLANKS && player.lastDoor && !player.lastDoor.enemy) { + player.lastDoor.type = TileType.LOCKED_DOOR; } else { success = false; console.warn(`Dont know how to use ${item}`); } } if (success) { - character.removeItem(item); + player.removeItem(item); } } private findAvailableTiles(moveDistance: number = 1) { - const characterTiles = new Set(this.characters.map(c => c.tile)); - this.availableTiles = Pathfinding.findPossibleMoves(this.character.tile, moveDistance + this.character.moveBonus) - .filter(t => !characterTiles.has(t)); + const playerTiles = new Set(this.players.map(c => c.tile)); + this.availableTiles = Pathfinding.findPossibleMoves(this.player.tile, moveDistance + this.player.moveBonus) + .filter(t => !playerTiles.has(t)); } private setNormalState() { @@ -310,19 +310,19 @@ export default class TileMap extends Entity { this.findAvailableTiles(); } - private nextCharacter() { - this.currentCharacterIdx = (this.currentCharacterIdx + 1) % this.characters.length; + private nextPlayer() { + this.currentPlayerIdx = (this.currentPlayerIdx + 1) % this.players.length; } private killEnemy() { - this.character.tile.killEnemy(); - this.character.tile.items.forEach((item) => { + this.player.tile.killEnemy(); + this.player.tile.items.forEach((item) => { if (item.isPickable) { - this.character.inventory.push(item); - this.character.tile.removeItem(item); + this.player.inventory.push(item); + this.player.tile.removeItem(item); } }); - this.nextCharacter(); + this.nextPlayer(); this.setNormalState(); } @@ -339,7 +339,7 @@ export default class TileMap extends Entity { } const w = this.tileSize * 0.8; - [...this.characters, this.character].forEach(c => + [...this.players, this.player].forEach(c => ctx.drawImage(c.type, c.tile.centerX - w / 2, c.tile.centerY - w / 2, w, w) ); @@ -348,6 +348,6 @@ export default class TileMap extends Entity { ctx.lineWidth = 5; ctx.strokeStyle = 'yellow'; - ctx.strokeRect(this.character.tile.centerX - w / 2, this.character.tile.centerY - w / 2, w, w); + ctx.strokeRect(this.player.tile.centerX - w / 2, this.player.tile.centerY - w / 2, w, w); } } \ No newline at end of file