UNPKG

shellquest

Version:

Terminal-based procedurally generated dungeon crawler

116 lines (103 loc) 3.92 kB
import {Entity} from './Entity.ts'; import type {CollisionEntity} from '../collision/CollisionSystem.ts'; import {TILE_SIZE} from '../TileMap.ts'; import type {ZombieAttackGame} from '../ZombieAttackGame.ts'; import {RGBA} from '@opentui/core'; import type {PixelCanvas} from '../drawing/pixelCanvas.ts'; export abstract class AnimatedCharacter extends Entity implements CollisionEntity { // Collision properties collisionOffsetX: number = 2; // Smaller collision box than sprite collisionOffsetY: number = 4; // Top offset for character sprites collisionWidth: number = TILE_SIZE - 4; // Slightly smaller than tile collisionHeight: number = TILE_SIZE - 4; // Slightly smaller than tile collisionType?: 'player' | 'enemy' | 'projectile' | 'solid'; solid: boolean = true; // Characters block movement by default private lastMoveTime: number = 0; private isMoving: boolean = false; private lastX: number = 0; private lastY: number = 0; private animationTime: number = 0; private readonly BOB_FREQUENCY: number = 5; // 5 times per second constructor(game: ZombieAttackGame, tileName: string) { super(game); this.x = 50 * TILE_SIZE; this.y = 50 * TILE_SIZE; this.width = TILE_SIZE; this.height = TILE_SIZE; this.tileName = tileName; this.layer = 1; this.facingLeft = false; this.verticalAnimationOffset = 0; this.lastX = this.x; this.lastY = this.y; } updateAnimation(deltaTime: number): void { const hasMovedX = this.x !== this.lastX; const hasMovedY = this.y !== this.lastY; if (hasMovedX || hasMovedY) { this.isMoving = true; this.lastMoveTime = 0; // Reset move timer this.lastX = this.x; this.lastY = this.y; } else { this.lastMoveTime += deltaTime; if (this.lastMoveTime > 100) { this.isMoving = false; this.verticalAnimationOffset = 0; return; } } // Update animation based on delta time if (this.isMoving) { this.animationTime += deltaTime; // Calculate bobbing offset using sine wave at specified frequency const cycleTime = 1000 / this.BOB_FREQUENCY; // Time for one complete cycle in ms const phase = (this.animationTime % cycleTime) / cycleTime; // 0 to 1 // Use sine wave for smooth bobbing motion this.verticalAnimationOffset = Math.round(Math.sin(phase * Math.PI * 2) * 0.5 + 0.5); } } move(dx: number, dy: number): void { // Move in pixels this.x += dx; this.y += dy; if (dx < 0) { this.facingLeft = true; } else if (dx > 0) { this.facingLeft = false; } } /** * Render an animated sprite with bobbing effect */ protected renderAnimatedSprite( pixelBuffer: PixelCanvas, tileName: string, flipHorizontal: boolean = false, animationOffset: number = 0, ): void { const pixels = this.game.tileMap.getTilePixels(tileName); if (!pixels) { console.warn(`Tile "${tileName}" not found in tile map.`); return; } // Render each pixel from the tile to the pixel buffer for (let py = 0; py < TILE_SIZE; py++) { for (let px = 0; px < TILE_SIZE; px++) { // Apply animation offset for bobbing effect (only for top half of sprite) let sourceY = py; if (py < 14 && animationOffset > 0) { sourceY = py - 1; if (sourceY < 0) continue; // Skip pixels that would be above the sprite } const sourceX = flipHorizontal ? TILE_SIZE - 1 - px : px; const pixelIndex = sourceY * TILE_SIZE + sourceX; const color = pixels[pixelIndex] || RGBA.fromValues(0, 0, 0, 0); if (color.a > 0) { // Set pixel at the target position pixelBuffer.setPixel(px, py, color); } } } } }