1
0
Fork 0

Compare commits

..

No commits in common. "1e4be0ff2cbe6bb89fb4e4b36ff81f76fcb3f7da" and "07d03befa2f9d84586a45c07664b334e59af0899" have entirely different histories.

6 changed files with 179 additions and 234 deletions

View File

@ -48,7 +48,6 @@ export function* zip(...args: Iterable<any>[]) {
export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k); export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k);
export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
export const lerp = (start: number, end: number, t: number) => (start + (end - start) * t);
export const prevent = (e: Event) => (e.preventDefault(), false); export const prevent = (e: Event) => (e.preventDefault(), false);

View File

@ -1,51 +1,114 @@
import { lerp } from "@common/utils"; import Item, { ItemType, type ItemTypeImage } from "./item";
import Item from "./item"; import { SpinnerAction } from "./spinner";
import Tile from "./tile"; import Tile from "./tile";
const MOVE_DURATION = .1;
export default class Character extends Item { export default class Character extends Item {
public tile = new Tile([0, 0], 1); public health: number;
private path: Tile[] = []; public inventory: Item[] = [];
private pathProgress = 0; public tile: Tile = new Tile([0, 0], 1);
public lastDoor: Tile | undefined;
public get displayPosition(): [number, number] { constructor(type: ItemTypeImage) {
if (this.path.length > 1) { super(type);
const numTransitions = this.path.length - 1; this.health = this.maxHealth;
const progress = this.pathProgress * numTransitions; const defaultItem = this.defaultItem;
const currentTileIdx = Math.floor(progress); if (defaultItem) {
this.inventory.push(this.defaultItem);
const currentTile = this.path[currentTileIdx]; }
const nextTile = this.path[currentTileIdx + 1];
if (!nextTile) {
return [currentTile.centerX, currentTile.centerY];
} }
const transitionFraction = progress - currentTileIdx; get maxHealth() {
return this.type === ItemType.CHAR_BIG ? 7 : 5;
return [
lerp(currentTile.centerX, nextTile.centerX, transitionFraction),
lerp(currentTile.centerY, nextTile.centerY, transitionFraction),
]
}
return [this.tile.centerX, this.tile.centerY];
} }
public moveTo(tile: Tile, path: Tile[]) { get defaultItem() {
this.pathProgress = 0; if (this.type === ItemType.CHAR_NINJA) {
this.tile = tile; return new Item(ItemType.WEAPON_KNIFE);
this.path = path; }
if (this.type === ItemType.CHAR_POLICE) {
return new Item(ItemType.WEAPON_PISTOL);
}
return null;
} }
public update(dt: number) { get healingAmount() {
if (this.path.length > 1) { return this.type === ItemType.CHAR_NURSE ? 2 : 1;
if (this.pathProgress >= 1) { }
this.path = [];
this.pathProgress = 0; get moveBonus() {
} else { return this.type === ItemType.CHAR_RUNNER ? 1 : 0;
this.pathProgress += dt / (MOVE_DURATION * (this.path.length - 1)); }
}
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,7 +1,9 @@
export default abstract class Entity { export default abstract class Entity {
public hovered = false; 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) { public render(ctx: CanvasRenderingContext2D) {
ctx.save(); ctx.save();

View File

@ -1,12 +1,12 @@
import { range } from "@common/utils"; import { range } from "@common/utils";
import type Player from "./player"; import type Character from "./character";
import { Players } from "./player"; import { Characters } from "./character";
import Entity from "./entity"; import Entity from "./entity";
import Tile from "./tile"; import Tile from "./tile";
import type Item from "./item"; import type Item from "./item";
import type TileMap from "./tilemap"; import type TileMap from "./tilemap";
export type UseListener = (player: Player, item: Item) => void; export type UseListener = (character: Character, item: Item) => void;
export default class Inventory extends Entity { export default class Inventory extends Entity {
private tiles: Tile[][]; private tiles: Tile[][];
@ -14,26 +14,26 @@ export default class Inventory extends Entity {
constructor(public readonly map: TileMap, position: [number, number], size: [number, number]) { constructor(public readonly map: TileMap, position: [number, number], size: [number, number]) {
super(position, size); super(position, size);
const numPlayers = Object.keys(Players).length; const numCharacters = Object.keys(Characters).length;
const tileSize = this.width / numPlayers; const tileSize = this.width / numCharacters;
const numTiles = Math.floor(this.height / tileSize) - 2; const numTiles = Math.floor(this.height / tileSize) - 2;
this.tiles = range(numPlayers).map((x) => this.tiles = range(numCharacters).map((x) =>
range(numTiles).map((y) => range(numTiles).map((y) =>
new Tile([x * tileSize, (2 + y) * tileSize], tileSize) new Tile([x * tileSize, (2 + y) * tileSize], tileSize)
) )
); );
} }
private get players() { private get characters() {
return this.map.players; return this.map.characters;
} }
private drawPlayer(ctx: CanvasRenderingContext2D, idx: number) { private drawCharacter(ctx: CanvasRenderingContext2D, idx: number) {
const player = this.players[idx]; const character = this.characters[idx];
ctx.drawImage(player.type, 0.1, 0.1, 0.8, 0.8); ctx.drawImage(character.type, 0.1, 0.1, 0.8, 0.8);
ctx.fillText(`💖 ${player.health}`, 0.5, 1.5); ctx.fillText(`💖 ${character.health}`, 0.5, 1.5);
if (player === this.map.player) { if (character === this.map.character) {
ctx.strokeStyle = 'black'; ctx.strokeStyle = 'black';
ctx.lineWidth = 0.03; ctx.lineWidth = 0.03;
@ -41,16 +41,16 @@ export default class Inventory extends Entity {
} }
let y = 2; let y = 2;
for (const item of player.inventory) { for (const item of character.inventory) {
ctx.drawImage(item.type, 0.1, 0.1 + y, 0.8, 0.8); ctx.drawImage(item.type, 0.1, 0.1 + y, 0.8, 0.8);
y += 1; y += 1;
} }
} }
public override handleClick(x: number, y: number): void { public override handleClick(x: number, y: number): void {
for (const { tile, item, player } of this.activeTiles) { for (const { tile, item, character } of this.activeTiles) {
if (tile.isPointInBounds(x - this.left, y - this.top)) { if (tile.isPointInBounds(x - this.left, y - this.top)) {
this.map.handleItemUse(player, item); this.map.handleItemUse(character, item);
break; break;
} }
} }
@ -61,20 +61,20 @@ export default class Inventory extends Entity {
} }
private get activeTiles() { private get activeTiles() {
return this.tiles.slice(0, this.players.length) return this.tiles.slice(0, this.characters.length)
.flatMap((column, playerIndex) => .flatMap((column, characterIndex) =>
column.slice(0, this.players[playerIndex].inventory.length) column.slice(0, this.characters[characterIndex].inventory.length)
.map((tile, tileIndex) => ({ .map((tile, tileIndex) => ({
tile, tile,
player: this.players[playerIndex], character: this.characters[characterIndex],
item: this.players[playerIndex].inventory[tileIndex], item: this.characters[characterIndex].inventory[tileIndex],
})) }))
.filter(({ item }) => item.isConsumable) .filter(({ item }) => item.isConsumable)
); );
} }
protected override draw(ctx: CanvasRenderingContext2D): void { protected override draw(ctx: CanvasRenderingContext2D): void {
const step = 1 / Object.keys(Players).length; const step = 1 / Object.keys(Characters).length;
const columnWidth = this.width * step; const columnWidth = this.width * step;
ctx.save(); ctx.save();
@ -82,8 +82,8 @@ export default class Inventory extends Entity {
ctx.font = `0.3px Arial`; ctx.font = `0.3px Arial`;
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
for (const i of range(this.players.length)) { for (const i of range(this.characters.length)) {
this.drawPlayer(ctx, i); this.drawCharacter(ctx, i);
ctx.translate(1, 0); ctx.translate(1, 0);
} }
ctx.restore(); ctx.restore();

View File

@ -1,114 +0,0 @@
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 Tile, { TileType } from "./tile";
import Pathfinding from "./pathfinding"; import Pathfinding from "./pathfinding";
import Item, { ItemType } from "./item"; import Item, { ItemType } from "./item";
import Player, { Players } from "./player"; import Character, { Characters } from "./character";
import { SpinnerAction } from "./spinner"; import { SpinnerAction } from "./spinner";
enum GameState { enum GameState {
@ -15,14 +15,14 @@ export default class TileMap extends Entity {
private tiles: Tile[] = []; private tiles: Tile[] = [];
public startTile: Tile; public startTile: Tile;
public readonly players: Player[]; public readonly characters: Character[];
private currentPlayerIdx = 0; private currentCharacterIdx = 0;
private state = GameState.NORMAL; private state = GameState.NORMAL;
private availableTiles: Tile[] = []; private availableTiles: Tile[] = [];
constructor(position: [number, number], private mapSize: number, private tileSize: number, numPlayers: number = 2) { constructor(position: [number, number], private mapSize: number, private tileSize: number, numPlayers: number = 2) {
super(position, [mapSize * tileSize, mapSize * tileSize]); super(position, [mapSize * tileSize, mapSize * tileSize]);
this.players = shuffle(Object.values(Players)).slice(0, numPlayers); this.characters = shuffle(Object.values(Characters)).slice(0, numPlayers);
this.startTile = this.createMap(); this.startTile = this.createMap();
this.findAvailableTiles(); this.findAvailableTiles();
} }
@ -128,7 +128,7 @@ export default class TileMap extends Entity {
this.startTile = startTile; this.startTile = startTile;
this.fillItems(); this.fillItems();
this.players.forEach(c => c.tile = startTile); this.characters.forEach(c => c.tile = startTile);
return startTile; return startTile;
} }
@ -164,8 +164,8 @@ export default class TileMap extends Entity {
} }
} }
for (const char of Object.values(Players)) { for (const char of Object.values(Characters)) {
if (this.players.includes(char)) continue; if (this.characters.includes(char)) continue;
items.push(char); items.push(char);
} }
@ -187,14 +187,14 @@ export default class TileMap extends Entity {
for (const [tile, item] of zip(fillableTiles, shuffle(items))) { for (const [tile, item] of zip(fillableTiles, shuffle(items))) {
tile.items.push(item); tile.items.push(item);
if (item instanceof Player) { if (item instanceof Character) {
item.tile = tile; item.tile = tile;
} }
}; };
} }
get player() { get character() {
return this.players[this.currentPlayerIdx]; return this.characters[this.currentCharacterIdx];
} }
get activeTiles() { get activeTiles() {
@ -216,21 +216,21 @@ export default class TileMap extends Entity {
} }
for (const tile of this.availableTiles) { for (const tile of this.availableTiles) {
if (tile.isPointInBounds(x - this.left, y - this.top)) { if (tile.isPointInBounds(x - this.left, y - this.top)) {
const path = Pathfinding.findPath(this.player.tile, tile); const path = Pathfinding.findPath(this.character.tile, tile);
if (path.length > 1) { if (path.length > 1) {
this.player.lastDoor = path.find(t => t.type === TileType.DOOR); this.character.lastDoor = path.find(t => t.type === TileType.DOOR);
this.player.moveTo(tile, path); this.character.tile = tile;
tile.open(); tile.open();
for (const item of tile.items.slice()) { // iterate remaining items for (const item of tile.items.slice()) { // iterate remaining items
if (item.isPickable) { if (item.isPickable) {
if (!tile.enemy) { if (!tile.enemy) {
tile.removeItem(item); tile.removeItem(item);
this.player.inventory.push(item); this.character.inventory.push(item);
} }
} else if (item instanceof Player) { } else if (item instanceof Character) {
tile.removeItem(item); tile.removeItem(item);
this.players.push(item); this.characters.push(item);
} else if (item.isBoss && this.player.removeItem(this.player.rocketLauncher)) { } else if (item.isBoss && this.character.removeItem(this.character.rocketLauncher)) {
tile.killEnemy(); tile.killEnemy();
} else if (item.isEnemy) { } else if (item.isEnemy) {
this.state = GameState.FIGHT; this.state = GameState.FIGHT;
@ -240,7 +240,7 @@ export default class TileMap extends Entity {
} }
} }
if (this.state === GameState.NORMAL) { if (this.state === GameState.NORMAL) {
this.nextPlayer(); this.nextCharacter();
} }
} }
@ -256,14 +256,14 @@ export default class TileMap extends Entity {
this.findAvailableTiles(action); this.findAvailableTiles(action);
break; break;
case GameState.FIGHT: case GameState.FIGHT:
const success = this.player.handleSpin(action); const success = this.character.handleSpin(action);
if (success) { if (success) {
switch (action) { switch (action) {
case SpinnerAction.BITE: case SpinnerAction.BITE:
if (this.player.isDead) { if (this.character.isDead) {
this.player.tile.items.push(...this.player.inventory); this.character.tile.items.push(...this.character.inventory);
this.players.splice(this.currentPlayerIdx, 1); this.characters.splice(this.currentCharacterIdx, 1);
this.currentPlayerIdx = this.currentPlayerIdx % this.players.length; this.currentCharacterIdx = this.currentCharacterIdx % this.characters.length;
this.setNormalState(); this.setNormalState();
} }
break; break;
@ -280,29 +280,29 @@ export default class TileMap extends Entity {
} }
} }
public handleItemUse(player: Player, item: Item) { public handleItemUse(character: Character, item: Item) {
let success = player.hasItem(item); let success = character.hasItem(item);
if (success) { if (success) {
if (item.type === ItemType.ITEM_HEAL) { if (item.type === ItemType.ITEM_HEAL) {
player.heal(player); character.heal(character);
} else if (item.type === ItemType.WEAPON_GRENADE && this.state === GameState.FIGHT) { } else if (item.type === ItemType.WEAPON_GRENADE && this.state === GameState.FIGHT) {
this.killEnemy(); this.killEnemy();
} else if (item.type === ItemType.ITEM_PLANKS && player.lastDoor && !player.lastDoor.enemy) { } else if (item.type === ItemType.ITEM_PLANKS && character.lastDoor && !character.lastDoor.enemy) {
player.lastDoor.type = TileType.LOCKED_DOOR; character.lastDoor.type = TileType.LOCKED_DOOR;
} else { } else {
success = false; success = false;
console.warn(`Dont know how to use ${item}`); console.warn(`Dont know how to use ${item}`);
} }
} }
if (success) { if (success) {
player.removeItem(item); character.removeItem(item);
} }
} }
private findAvailableTiles(moveDistance: number = 1) { private findAvailableTiles(moveDistance: number = 1) {
const playerTiles = new Set(this.players.map(c => c.tile)); const characterTiles = new Set(this.characters.map(c => c.tile));
this.availableTiles = Pathfinding.findPossibleMoves(this.player.tile, moveDistance + this.player.moveBonus) this.availableTiles = Pathfinding.findPossibleMoves(this.character.tile, moveDistance + this.character.moveBonus)
.filter(t => !playerTiles.has(t)); .filter(t => !characterTiles.has(t));
} }
private setNormalState() { private setNormalState() {
@ -310,19 +310,19 @@ export default class TileMap extends Entity {
this.findAvailableTiles(); this.findAvailableTiles();
} }
private nextPlayer() { private nextCharacter() {
this.currentPlayerIdx = (this.currentPlayerIdx + 1) % this.players.length; this.currentCharacterIdx = (this.currentCharacterIdx + 1) % this.characters.length;
} }
private killEnemy() { private killEnemy() {
this.player.tile.killEnemy(); this.character.tile.killEnemy();
this.player.tile.items.slice().forEach((item) => { this.character.tile.items.forEach((item) => {
if (item.isPickable) { if (item.isPickable) {
this.player.inventory.push(item); this.character.inventory.push(item);
this.player.tile.removeItem(item); this.character.tile.removeItem(item);
} }
}); });
this.nextPlayer(); this.nextCharacter();
this.setNormalState(); this.setNormalState();
} }
@ -339,20 +339,15 @@ export default class TileMap extends Entity {
} }
const w = this.tileSize * 0.8; const w = this.tileSize * 0.8;
[...this.players, this.player].forEach((player) => { [...this.characters, this.character].forEach(c =>
const [x, y] = player.displayPosition; ctx.drawImage(c.type, c.tile.centerX - w / 2, c.tile.centerY - w / 2, w, w)
ctx.drawImage(player.type, x - w / 2, y - w / 2, w, w); );
});
this.activeTiles.forEach(t => t.render(ctx)); this.activeTiles.forEach(t => t.render(ctx));
ctx.lineWidth = 5; ctx.lineWidth = 5;
ctx.strokeStyle = 'yellow'; ctx.strokeStyle = 'yellow';
ctx.strokeRect(this.player.tile.centerX - w / 2, this.player.tile.centerY - w / 2, w, w); ctx.strokeRect(this.character.tile.centerX - w / 2, this.character.tile.centerY - w / 2, w, w);
}
public update(dt: number) {
this.players.forEach((player) => player.update(dt));
} }
} }