Refactor Character -> Player, add separate character class
This commit is contained in:
parent
07d03befa2
commit
b3574a8acf
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue