shellquest
Version:
Terminal-based procedurally generated dungeon crawler
116 lines (103 loc) • 3.92 kB
text/typescript
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);
}
}
}
}
}