// // Created by bruno on 7.6.2025. // #include #include #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; } } }