1
0
Fork 0

Refactor Character -> Player, add separate character class

This commit is contained in:
Pabloader 2025-06-27 12:31:12 +00:00
parent 07d03befa2
commit b3574a8acf
5 changed files with 202 additions and 176 deletions

View File

@ -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;
}

View File

@ -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();

View File

@ -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();

114
src/games/zombies/player.ts Normal file
View File

@ -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;

View File

@ -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);
}
}