shellquest
Version:
Terminal-based procedurally generated dungeon crawler
173 lines (143 loc) • 5.4 kB
text/typescript
import {AnimatedCharacter} from './AnimatedCharacter.ts';
import {TILE_SIZE, TileMap} from '../TileMap.ts';
import type {ZombieAttackGame} from '../ZombieAttackGame.ts';
import type {GameLevel} from '../level/types.ts';
import type {CollisionSystem} from '../collision/CollisionSystem.ts';
import type {PixelCanvas} from '../drawing/pixelCanvas.ts';
import {RGBA} from '@opentui/core';
export class Enemy extends AnimatedCharacter {
hp: number = 30;
maxHp: number = 30;
damage: number = 5;
// AI state
private target: {x: number; y: number} | null = null;
private moveSpeed = 0.5; // Slower than player
private aggroRange = TILE_SIZE * 8; // 8 tiles
private attackRange = TILE_SIZE * 1.5; // 1.5 tiles
private lastAttackTime = 0;
private attackCooldown = 1000; // 1 second between attacks
constructor(game: ZombieAttackGame, tileName: string = 'zombie') {
super(game, tileName);
// Set collision properties for enemy
this.collisionType = 'enemy';
this.collisionOffsetX = 4;
this.collisionOffsetY = 6;
this.collisionWidth = TILE_SIZE - 8;
this.collisionHeight = TILE_SIZE - 8;
this.solid = true; // Enemies block movement
}
update(deltaTime: number, level: GameLevel, collisionSystem?: CollisionSystem): void {
// Find player
const player = level.player;
if (!player) return;
// Calculate distance to player
const dx = player.x - this.x;
const dy = player.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Check if player is in aggro range
if (distance <= this.aggroRange) {
this.target = {x: player.x, y: player.y};
// Check if in attack range
if (distance <= this.attackRange) {
this.attack(player);
} else {
// Move towards player
this.moveTowardsTarget(level, collisionSystem);
}
} else {
this.target = null;
// Could add idle behavior here
}
// Update animation
this.updateAnimation(deltaTime);
}
private moveTowardsTarget(level: GameLevel, collisionSystem?: CollisionSystem): void {
if (!this.target) return;
// Calculate direction to target
const dx = this.target.x - this.x;
const dy = this.target.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and apply speed
const moveX = (dx / distance) * this.moveSpeed;
const moveY = (dy / distance) * this.moveSpeed;
if (collisionSystem) {
// Use collision system for movement
const result = collisionSystem.moveWithCollision(this, moveX, moveY, level);
// Update position
const actualMoveX = result.x - this.x;
const actualMoveY = result.y - this.y;
if (actualMoveX !== 0 || actualMoveY !== 0) {
this.x = result.x;
this.y = result.y;
// Update facing direction
if (actualMoveX < 0) {
this.facingLeft = true;
} else if (actualMoveX > 0) {
this.facingLeft = false;
}
// Update entity position in collision system
collisionSystem.updateEntity(this);
}
} else {
// Simple movement without collision
this.move(moveX, moveY);
}
}
}
private attack(player: any): void {
const currentTime = Date.now();
if (currentTime - this.lastAttackTime >= this.attackCooldown) {
// Deal damage to player
if (player.hp > 0) {
player.hp = Math.max(0, player.hp - this.damage);
this.lastAttackTime = currentTime;
// Could add attack animation or effects here
}
}
}
takeDamage(amount: number): void {
this.hp = Math.max(0, this.hp - amount);
// Could add damage effects here
if (this.hp <= 0) {
// Handle death
this.onDeath();
}
}
onDeath(): void {
this.game.level.removeEntity(this);
this.game.collisionSystem.removeEntity(this);
}
render(ctx: PixelCanvas) {
if (this.verticalAnimationOffset !== undefined && this.verticalAnimationOffset !== 0) {
this.renderAnimatedSprite(
ctx,
this.tileName,
this.facingLeft || false,
this.verticalAnimationOffset,
);
} else {
ctx.drawTile(this.game.tileMap, this.tileName, 0, 0, this.facingLeft || false, false);
}
// Render attachments
this.renderEntityAttachments(ctx);
this.drawHealthBar(ctx);
}
private drawHealthBar(ctx: PixelCanvas): void {
const healthBarWidth = 12; // Width in pixels
const healthBarOffsetY = -3; // Position above the enemy
// Calculate health percentage
const healthPercent = Math.max(0, this.hp / this.maxHp);
const filledWidth = Math.floor(healthBarWidth * healthPercent);
// Position the health bar centered above the entity
const barX = Math.floor((TILE_SIZE - healthBarWidth) / 2);
const barY = healthBarOffsetY;
// Draw the health bar background (dark red)
const bgColor = RGBA.fromHex('#400000');
const fgColor = RGBA.fromHex('#ff0000');
// Draw background
for (let x = 0; x < healthBarWidth; x++) {
ctx.setPixel(barX + x, barY, x < filledWidth ? fgColor : bgColor);
}
}
}