UNPKG

shellquest

Version:

Terminal-based procedurally generated dungeon crawler

173 lines (143 loc) 5.4 kB
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); } } }