346 lines
12 KiB
C
346 lines
12 KiB
C
//
|
|
// Created by bruno on 7.6.2025.
|
|
//
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <dirent.h>
|
|
#include "entity.h"
|
|
#include "../player/player.h"
|
|
#include "../util/pathfinding.h"
|
|
#include "../util/font.h"
|
|
#include "../util/audio.h"
|
|
|
|
EntityArray entities;
|
|
|
|
EntityTypeReg EntityRegistry[ENTITY_MAX_COUNT];
|
|
|
|
EnemySpawnState currentSpawnState;
|
|
|
|
void renderEntities(SDL_Renderer *renderer, SDL_Rect playerRect) {
|
|
SDL_Texture *oldTarget = SDL_GetRenderTarget(renderer);
|
|
SDL_SetRenderTarget(renderer, entityTexture);
|
|
SDL_RenderClear(mainRenderer);
|
|
for (int i = 0; i < entities.activeCount; i++) {
|
|
Entity *ent = &entities.entities[i];
|
|
SDL_Rect renderRect = ent->renderRect;
|
|
adjustRect(&renderRect, playerRect);
|
|
if (!checkCollision(renderRect, screenRect)) {
|
|
continue;
|
|
}
|
|
EntityTypeReg entType = EntityRegistry[ent->type];
|
|
char animationFrame = (animationStep / entType.animation.divisor) % entType.animation.frameCount;
|
|
SDL_RenderCopy(renderer, atlasTexture, &entType.animation.atlasRects[animationFrame], &renderRect);
|
|
char healthStr[12];
|
|
snprintf(healthStr, 12, "%d/%d", ent->health, entType.maxHealth);
|
|
renderText(renderer, fonts[3], healthStr, renderRect.x, renderRect.y);
|
|
}
|
|
SDL_SetRenderTarget(renderer, oldTarget);
|
|
}
|
|
|
|
void updateEntities(Player *plr) {
|
|
for (int i = 0; i < entities.activeCount; i++) {
|
|
Entity *ent = &entities.entities[i];
|
|
EntityTypeReg entT = EntityRegistry[ent->type];
|
|
|
|
if (ent->health > entT.maxHealth || ent->health <= 0) {
|
|
remove_entity(&entities, i);
|
|
continue;
|
|
}
|
|
|
|
bool atTargetSnapshot = ent->tileRect.x == ent->targetSnapshot.x &&
|
|
ent->tileRect.y == ent->targetSnapshot.y;
|
|
|
|
bool atTarget = ent->tileRect.x == ent->target.x &&
|
|
ent->tileRect.y == ent->target.y;
|
|
|
|
if (animationStep >= ent->entityNextTick) {
|
|
if (sqrt(pow(abs(plr->tileRect.x - ent->tileRect.x), 2) +
|
|
pow(abs(plr->tileRect.y - ent->tileRect.y), 2)) < ENEMY_RANGE) {
|
|
plr->health -= ENEMY_DAMAGE;
|
|
}
|
|
|
|
for (int y = ent->tileRect.y - ENEMY_RANGE; y <= ent->tileRect.y + ENEMY_RANGE; y++) {
|
|
if (y < 0 || y >= MAP_HEIGHT) continue;
|
|
for (int x = ent->tileRect.x - ENEMY_RANGE; x <= ent->tileRect.x + ENEMY_RANGE; x++) {
|
|
if (x < 0 || x >= MAP_WIDTH) continue;
|
|
|
|
Tile *targTile = &tileMap[y][x];
|
|
if (targTile->type == TYPE_AIR) continue;
|
|
|
|
targTile->health -= ENEMY_DAMAGE;
|
|
if (targTile->health <= 0) {
|
|
if (targTile->audioCh < NUM_SYNTH_VOICES) {
|
|
audioData.synthVoices[targTile->audioCh].volume = 0;
|
|
}
|
|
memset(targTile->items, 0, sizeof(targTile->items));
|
|
targTile->type = TYPE_AIR;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool shouldRetryPathfinding = (
|
|
ent->path.length == 0 &&
|
|
!atTarget &&
|
|
(animationStep % 10 == 0)
|
|
);
|
|
|
|
if (atTargetSnapshot || shouldRetryPathfinding) {
|
|
MiniRect fallbackTarget = ent->target;
|
|
|
|
// If the target is not walkable, search nearby
|
|
if (!isWalkable(ent->target)) {
|
|
int bestDist = 999999;
|
|
bool found = false;
|
|
|
|
for (int dy = -5; dy <= 5; dy++) {
|
|
for (int dx = -5; dx <= 5; dx++) {
|
|
int nx = ent->target.x + dx;
|
|
int ny = ent->target.y + dy;
|
|
|
|
if (nx < 0 || ny < 0 || nx >= MAP_WIDTH || ny >= MAP_HEIGHT) continue;
|
|
|
|
MiniRect check = {nx, ny};
|
|
|
|
if (!isWalkable(check)) continue;
|
|
|
|
int dist = abs(dx) + abs(dy); // Manhattan distance
|
|
if (dist < bestDist) {
|
|
bestDist = dist;
|
|
fallbackTarget = check;
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
// No walkable fallback tile found
|
|
ent->path.length = 0;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Attempt pathfinding to fallbackTarget
|
|
if (find_path(ent->tileRect, fallbackTarget)) {
|
|
ent->path = reconstruct_path(fallbackTarget);
|
|
ent->path.stepIndex = 0;
|
|
ent->targetSnapshot = fallbackTarget;
|
|
ent->fromTile = ent->tileRect;
|
|
ent->toTile = ent->path.steps[0];
|
|
ent->interpolateTick = 0;
|
|
ent->entityNextTick = animationStep + entT.entityTickRate;
|
|
} else if (atTargetSnapshot) {
|
|
ent->path.length = 0;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Movement
|
|
if (ent->path.length > 0 && ent->path.stepIndex < ent->path.length &&
|
|
animationStep >= ent->entityNextTick) {
|
|
ent->fromTile = ent->tileRect;
|
|
ent->toTile = ent->path.steps[ent->path.stepIndex];
|
|
ent->tileRect = ent->toTile;
|
|
ent->entityNextTick = animationStep + entT.entityTickRate;
|
|
ent->interpolateTick = 0;
|
|
ent->path.stepIndex++;
|
|
}
|
|
|
|
// Interpolation
|
|
MiniRect from = {
|
|
.x = ent->fromTile.x * TILE_SIZE,
|
|
.y = ent->fromTile.y * TILE_SIZE
|
|
};
|
|
MiniRect to = {
|
|
.x = ent->toTile.x * TILE_SIZE,
|
|
.y = ent->toTile.y * TILE_SIZE
|
|
};
|
|
|
|
float t = (float) ent->interpolateTick / entT.entityTickRate;
|
|
ent->renderRect.x = (int) (from.x + (to.x - from.x) * t);
|
|
ent->renderRect.y = (int) (from.y + (to.y - from.y) * t);
|
|
|
|
if (ent->interpolateTick < entT.entityTickRate) {
|
|
ent->interpolateTick++;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void registerEntity(char fname[20], SDL_Renderer *renderer) {
|
|
char name[21];
|
|
|
|
// Load animation frames
|
|
int frame = 0;
|
|
int indexEntity = 0;
|
|
char texturePath[80];
|
|
|
|
if (sscanf(fname, "%d_%20[^_]_%d.png", &indexEntity, name, &frame) == 3) {
|
|
// Success: you now have index, fname, and frame
|
|
} else {
|
|
fprintf(stderr, "Invalid format: %s\n", fname);
|
|
}
|
|
strcpy(EntityRegistry[indexEntity].name, name);
|
|
snprintf(texturePath, sizeof(texturePath), "./assets/entities/%s", fname);
|
|
SDL_Texture *texture = IMG_LoadTexture(renderer, texturePath);
|
|
if (!texture) {
|
|
if (frame == 0) {
|
|
fprintf(stderr, "Failed to load entity texture %s: %s\n", texturePath, IMG_GetError());
|
|
}
|
|
}
|
|
|
|
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE);
|
|
|
|
//printf("Ent %s to %d\n", fname, indexEntity);
|
|
|
|
EntityRegistry[indexEntity].animation.atlasRects[frame] = allocate_32x32(texture, renderer);
|
|
|
|
EntityRegistry[indexEntity].type = indexEntity;
|
|
EntityRegistry[indexEntity].animation.frameCount = frame + 1;
|
|
EntityRegistry[indexEntity].animation.divisor = 1;
|
|
EntityRegistry[indexEntity].entityTickRate = 16;
|
|
EntityRegistry[indexEntity].maxHealth = 100;
|
|
|
|
if (indexEntity + 1 > backgroundTileTypeIndex) {
|
|
backgroundTileTypeIndex = indexEntity + 1;
|
|
}
|
|
}
|
|
|
|
void loadEntities(SDL_Renderer *renderer) {
|
|
DIR *dir = opendir("./assets/entities");
|
|
if (!dir) {
|
|
perror("Failed to open entities directory");
|
|
return;
|
|
}
|
|
|
|
char *entityNames[ENTITY_MAX_COUNT];
|
|
int entityCount = 0;
|
|
|
|
struct dirent *entry;
|
|
while ((entry = readdir(dir))) {
|
|
char *dot = strrchr(entry->d_name, '.');
|
|
if (!dot || strcmp(dot, ".png") != 0) continue;
|
|
|
|
// Check if baseName already stored
|
|
int found = 0;
|
|
for (int i = 0; i < entityCount; ++i) {
|
|
if (strcmp(entityNames[i], entry->d_name) == 0) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found && entityCount < ENTITY_MAX_COUNT) {
|
|
entityNames[entityCount++] = strdup(entry->d_name); // Only store base, not full file name
|
|
}
|
|
}
|
|
closedir(dir);
|
|
|
|
qsort(entityNames, entityCount, sizeof(char *), compareStrings);
|
|
|
|
// Call registerEntity on each base name
|
|
for (int i = 0; i < entityCount; ++i) {
|
|
char fileName[64];
|
|
snprintf(fileName, sizeof(fileName), "%s", entityNames[i]);
|
|
registerEntity(fileName, renderer);
|
|
free(entityNames[i]);
|
|
}
|
|
EntityRegistry[GHOST].animation.divisor = 16;
|
|
}
|
|
|
|
int add_entity(EntityArray *arr, Entity t) {
|
|
if (arr->activeCount >= ENTITY_MAX_COUNT) return 0;
|
|
arr->entities[arr->activeCount] = t;
|
|
arr->activeCount++;
|
|
return arr->activeCount - 1;
|
|
}
|
|
|
|
void remove_entity(EntityArray *arr, int index) {
|
|
if (index < 0 || index >= arr->activeCount) return;
|
|
arr->activeCount--;
|
|
arr->entities[index] = arr->entities[arr->activeCount]; // swap with last active
|
|
}
|
|
|
|
|
|
bool isTileAlreadyTargeted(int x, int y) {
|
|
for (int i = 0; i < entities.activeCount; i++) {
|
|
Entity *ent = &entities.entities[i];
|
|
if (ent->target.x == x && ent->target.y == y) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void spawn_enemy_at_random_tile() {
|
|
Entity ent;
|
|
memset(&ent, 0, sizeof(Entity));
|
|
|
|
// Start by placing it at a default location (same as before)
|
|
int offsetX = (rand() % (SPAWN_RADIUS * 2 + 1)) - SPAWN_RADIUS;
|
|
int offsetY = (rand() % (SPAWN_RADIUS * 2 + 1)) - SPAWN_RADIUS;
|
|
|
|
ent.tileRect.x = enemySpawn.x + offsetX;
|
|
ent.tileRect.y = enemySpawn.y + offsetY;
|
|
ent.renderRect.x = ent.tileRect.x * TILE_SIZE;
|
|
ent.renderRect.y = ent.tileRect.y * TILE_SIZE;
|
|
ent.renderRect.w = TILE_SIZE;
|
|
ent.renderRect.h = TILE_SIZE;
|
|
ent.health = 100;
|
|
ent.type = GHOST;
|
|
|
|
// Try to find a unique, walkable target tile near the player
|
|
for (int attempt = 0; attempt < MAX_SPAWN_ATTEMPTS; attempt++) {
|
|
int targetOffsetX = (rand() % (SPAWN_RADIUS * 2 + 1)) - SPAWN_RADIUS;
|
|
int targetOffsetY = (rand() % (SPAWN_RADIUS * 2 + 1)) - SPAWN_RADIUS;
|
|
|
|
int targetX = mainPlayer.tileRect.x + targetOffsetX;
|
|
int targetY = mainPlayer.tileRect.y + targetOffsetY;
|
|
MiniRect target = {targetX, targetY};
|
|
|
|
if (!isWalkable(target)) continue;
|
|
if (isTileAlreadyTargeted(targetX, targetY)) continue;
|
|
|
|
ent.target.x = targetX;
|
|
ent.target.y = targetY;
|
|
|
|
add_entity(&entities, ent);
|
|
return;
|
|
}
|
|
|
|
printf("Failed to spawn enemy: no unique target found after %d attempts.\n", MAX_SPAWN_ATTEMPTS);
|
|
}
|
|
void updateWaveLogic(WaveInfo *info) {
|
|
if (info->waveRunning) {
|
|
Wave *currentWave = &info->waves[info->waveCounter];
|
|
|
|
// Cooldown between enemy spawns
|
|
if (--currentSpawnState.spawnCooldown <= 0 &&
|
|
currentSpawnState.enemiesSpawned < currentWave->enemies[0].count) {
|
|
|
|
spawn_enemy_at_random_tile();
|
|
|
|
currentSpawnState.enemiesSpawned++;
|
|
currentSpawnState.spawnCooldown = SPAWN_COOLDOWN;
|
|
}
|
|
|
|
// All enemies spawned
|
|
if (currentSpawnState.enemiesSpawned >= currentWave->enemies[0].count) {
|
|
// Wait for all enemies to be dead before ending wave
|
|
if (entities.activeCount == 0) {
|
|
info->waveRunning = false;
|
|
info->waveTimer = info->waves[info->waveCounter].timeUntilNext;
|
|
}
|
|
}
|
|
} else {
|
|
// Timer starts only after wave is completely over (including enemies)
|
|
if (--info->waveTimer <= 0 && info->waveCounter + 1 < info->totalWaves) {
|
|
// Start next wave
|
|
info->waveCounter++;
|
|
info->waveRunning = true;
|
|
|
|
currentSpawnState.enemiesSpawned = 0;
|
|
currentSpawnState.spawnCooldown = 0;
|
|
}
|
|
}
|
|
} |