#include "character/enemy.hpp" #include #include #include "map/map.hpp" #include "map/path_finding.hpp" std::vector Enemy::enemies; std::vector Enemy::enemies_to_spawn; SDL_FPoint Enemy::colider_default = {7, 7}; EnemyAnimation Enemy::animation_all; size_t Enemy::tile_size = 64; // Persistent thread pool std::vector Enemy::workers; std::vector Enemy::job_queue; std::mutex Enemy::queue_mutex; std::condition_variable Enemy::queue_cv; bool Enemy::stop_threads = false; int Enemy::jobs_remaining = 0; std::condition_variable Enemy::jobs_done_cv; int Enemy::id_next = 1; Enemy::Enemy(WeaponType weapon_type, WeaponMaterial weapon_material, SDL_FPoint position, EnemyStats status) : weapon(weapon_type, weapon_material) { x = position.x; y = position.y; stats = status; id = id_next++; detection_radius = 150 + rand() % 51; speed = 40.0f; run = 1.4f; direction = WalkDirection::DOWN; moving = false; flip = false; collider.w = colider_default.x; collider.h = colider_default.y; collider.x = (tile_size/2 - collider.w/2); collider.y = tile_size - collider.h - 16; collider_world.x = x + collider.x + collider.w * 0.5f; collider_world.y = y + collider.y + collider.h * 0.5f; arm_reach_radius = 6; resources = {}; point_in_reach = false; dist_min = weapon.dimension.x-8; dist_max = weapon.dimension.x; max_death_time = 10.0; stats = status; death_duration = 0; death_duration_max = 0.8; direction_blocked[WalkDirection::DOWN] = false; direction_blocked[WalkDirection::UP] = false; direction_blocked[WalkDirection::LEFT] = false; direction_blocked[WalkDirection::RIGHT] = false; animation = animation_all; } void Enemy::init_All(SDL_Renderer* renderer) { animation_all.run[WalkDirection::DOWN] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Run/Run_up.png", tile_size, tile_size, "None", false, false); animation_all.run[WalkDirection::SIDE] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Run/Run.png", tile_size, tile_size, "None", false, false); animation_all.run[WalkDirection::UP] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Run/Run_up.png", tile_size, tile_size, "None", false, false); animation_all.idle[WalkDirection::DOWN] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Idle/Idle-Sheet_padded.png", tile_size, tile_size, "None", false, false); animation_all.idle[WalkDirection::SIDE] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Idle/Idle-Sheet_padded.png", tile_size, tile_size, "None", false, false); animation_all.idle[WalkDirection::UP] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Idle/Idle-Sheet_padded.png", tile_size, tile_size, "None", false, false); animation_all.attack[WalkDirection::DOWN] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Attack/Attack_Down.png", tile_size, tile_size, "enemy.attack", false, true); animation_all.attack[WalkDirection::SIDE] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Attack/Attack.png", tile_size, tile_size, "enemy.attack", false, true); animation_all.attack[WalkDirection::UP] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Attack/Attack_Up.png", tile_size, tile_size, "enemy.attack", false, true); animation_all.hit[WalkDirection::DOWN] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Idle/Idle-Sheet_padded.png", tile_size, tile_size, "character.hit", false, false); animation_all.hit[WalkDirection::SIDE] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Idle/Idle-Sheet_padded.png", tile_size, tile_size, "character.hit", false, false); animation_all.hit[WalkDirection::UP] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Idle/Idle-Sheet_padded.png", tile_size, tile_size, "character.hit", false, false); animation_all.death[WalkDirection::DOWN] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Death/Death-Sheet_processed.png", tile_size, tile_size, "None", false, false); animation_all.death[WalkDirection::SIDE] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Death/Death-Sheet_processed.png", tile_size, tile_size, "None", false, false); animation_all.death[WalkDirection::UP] = Animation::load(renderer, "Pixel_Crawler/Entities/Mobs/Skeleton Crew/Skeleton - Base/Death/Death-Sheet_processed.png", tile_size, tile_size, "None", false, false); init_Workers(10); } void Enemy::Init() { handle_Animation(); set_Status(EnemyStatus::IDLE); // REINIT POSITION SDL_FPoint new_position = Map::get_ClosestPointWithoutCollision( {static_cast(collider_world.x), static_cast(collider_world.y), collider.w, collider.h}); if (new_position.x == 0 && new_position.y == 0) { std::cout << "No position found!\n"; return; } x = new_position.x - collider.x - collider.w * 0.5; y = new_position.y - collider.y - collider.h * 0.5; } void Enemy::set_Status(const EnemyStatus& s) { status = s; active_anim->timer = 0; active_anim->current_frame = 0; Animation::animation_frame_time = active_anim->frame_time; } void Enemy::handle_Animation() { AnimationData* active_anim_ref = nullptr; WalkDirection dir = direction; if ( direction == WalkDirection::LEFT || direction == WalkDirection::RIGHT) dir = WalkDirection::SIDE; switch (status) { case EnemyStatus::RUN: active_anim_ref = &animation.run[dir]; break; case EnemyStatus::ATTACK: active_anim_ref = &animation.attack[dir]; break; case EnemyStatus::DYING: active_anim_ref = &animation.death[dir]; break; case EnemyStatus::DEAD: active_anim_ref = &animation.death[dir]; break; case EnemyStatus::HIT: active_anim_ref = &animation.hit[dir]; break; default: active_anim_ref = &animation.idle[dir]; break; } if (status == EnemyStatus::DEAD) { if (active_anim_ref) { AnimationData& ad = *active_anim_ref; // lock animation to last frame if (!ad.frames.empty()) { ad.current_frame = ad.frames.size() - 1; } // also stop the timer so update() never progresses ad.timer = 0.0f; } active_anim = active_anim_ref; return; // absolutely do NOT call update() when dead } if (active_anim_ref) { Animation::update(*active_anim_ref, static_cast(Default::delta_time)); active_anim = active_anim_ref; } } void Enemy::Draw(SDL_Renderer* renderer) { #ifdef DEBUG_BUILD // DEBUG COLLIDER SDL_FRect debug_rect { Scale((x + collider.x) + Map::offset.x), Scale((y + collider.y) + Map::offset.y), Scale(collider.w), Scale(collider.h) }; SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); SDL_RenderRect(renderer, &debug_rect); // DEBUG CHARACTER RECTANGLE SDL_SetRenderDrawColor(renderer, 255, 255,255, 255); SDL_FRect debug_rect_1 { Scale(x+Map::offset.x), Scale(y+Map::offset.y), Scale(tile_size), Scale(tile_size) }; SDL_RenderRect(renderer, &debug_rect_1); // DEBUG Arm Range SDL_SetRenderDrawColor(renderer, 0, 255, 0, 100); for (float angle = 0; angle < 2 * M_PI; angle += 0.1f) { float px = collider_world.x + cosf(angle) * arm_reach_radius + Map::offset.x; float py = collider_world.y + sinf(angle) * arm_reach_radius + Map::offset.y; SDL_RenderPoint(renderer, (int)Scale(px), (int)Scale(py)); } // DEBUG Tiles in reach SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(renderer, 0, 255, 0, 80); for (const auto& t : tiles_in_reach) { SDL_FRect tile_rect = { Scale(t.x * Tile::tile_size) + Map::offset.x, Scale(t.y * Tile::tile_size) + Map::offset.y, Scale(Tile::tile_size), Scale(Tile::tile_size) }; SDL_RenderFillRect(renderer, &tile_rect); } #endif // Render Enemy const SDL_FRect& src = active_anim->frames[active_anim->current_frame]; SDL_FRect dst { Scale(x + Map::offset.x), Scale(y + Map::offset.y), Scale(src.w), Scale(src.h) }; /* if (stats.health > 0) if (flip) weapon.Draw(renderer, x + Map::offset.x, y + Map::offset.y); */ SDL_RenderTextureRotated(renderer, active_anim->texture, &src, &dst, 0.0, nullptr, flip ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE); /* if (stats.health > 0) if (!flip) weapon.Draw(renderer, x + Map::offset.x, y + Map::offset.y); */ // HEALTH BAR if (stats.health <= 0) return; float bar_width = Scale(40.0f); float bar_height = Scale(4.0f); // position above the enemy sprite float bar_x = Scale(x + Map::offset.x + tile_size/2) - bar_width/2; float bar_y = Scale(y + Map::offset.y) + bar_height*2 - Scale(4.0f); // background (empty bar) SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255); SDL_FRect bar_bg { bar_x-1, bar_y-1, bar_width+2, bar_height+2 }; SDL_RenderFillRect(renderer, &bar_bg); // current health percentage float ratio = 0.0f; if (stats.health_max > 0.0f) { ratio = stats.health / stats.health_max; if (ratio < 0.0f) ratio = 0.0f; if (ratio > 1.0f) ratio = 1.0f; } // foreground (current health) SDL_SetRenderDrawColor(renderer, 200, 0, 0, 255); SDL_FRect bar_fg { bar_x, bar_y, bar_width * ratio, bar_height }; SDL_RenderFillRect(renderer, &bar_fg); } Enemy* Enemy::is_InEnemyReach(const SDL_FRect& rect) { for (Enemy* enemy : Enemy::enemies) { if (enemy->stats.health <= 0) continue; SDL_FRect other = { enemy->x + enemy->collider.x, enemy->y + enemy->collider.y, enemy->collider.w, enemy->collider.h }; bool overlap = rect.x < other.x + other.w && rect.x + rect.w > other.x && rect.y < other.y + other.h && rect.y + rect.h > other.y; if (overlap) return enemy; } return nullptr; } /* static inline double ms_since(Uint64 start) { static const double freq = static_cast(SDL_GetPerformanceFrequency()); return (SDL_GetPerformanceCounter() - start) * 1000.0 / freq; } */ int EnemyWorkerThread(void*) { while (true) { std::unique_lock lock(Enemy::queue_mutex); Enemy::queue_cv.wait(lock, [] { return Enemy::stop_threads || !Enemy::job_queue.empty(); }); if (Enemy::stop_threads && Enemy::job_queue.empty()) return 0; WorkerData job = Enemy::job_queue.back(); Enemy::job_queue.pop_back(); lock.unlock(); job.enemy->set_NextMoves(*job.grid); std::lock_guard finish_lock(Enemy::queue_mutex); if (--Enemy::jobs_remaining == 0) Enemy::jobs_done_cv.notify_one(); } } void Enemy::init_Workers(int num_threads) { stop_threads = false; jobs_remaining = 0; for (int i = 0; i < num_threads; ++i) { SDL_Thread* th = SDL_CreateThread(EnemyWorkerThread, "EnemyWorker", nullptr); workers.push_back(th); } } void Enemy::shutdown_Workers() { { std::lock_guard lock(queue_mutex); stop_threads = true; } queue_cv.notify_all(); for (SDL_Thread* th : workers) { SDL_WaitThread(th, nullptr); } workers.clear(); } void Enemy::run_GetMovesThreads() { std::vector grid = Map::tile_collission_map; std::set checks = {0,1,2,3,4,5,6,7,8,12,16,32,64,128,256}; // ---- First pass: mark grid exactly like old version ---- for (Enemy* e : Enemy::enemies) { if (e->status != EnemyStatus::RUN || !checks.count(e->next_moves.size())) continue; int tx = static_cast(e->collider_world.x / Tile::tile_collider_size); int ty = static_cast(e->collider_world.y / Tile::tile_collider_size); if (tx < 0 || ty < 0 || tx >= static_cast(Map::size.x * Tile::tile_collider_size) || ty >= static_cast(Map::size.y * Tile::tile_collider_size)) continue; size_t id = static_cast( ty * (Map::size.x * Tile::tile_collider_size) + tx); grid[id] = true; } auto grid_ptr = std::make_shared>(std::move(grid)); // ---- Wait for previous frame to finish ---- { std::unique_lock lock(queue_mutex); jobs_done_cv.wait(lock, [] { return jobs_remaining == 0; }); } // ---- Push jobs ---- { std::lock_guard lock(queue_mutex); for (Enemy* e : Enemy::enemies) { if (e->status != EnemyStatus::RUN) continue; if (e->point_in_reach) continue; if (e->next_position.x == 0 || e->next_position.y == 0 ) continue; job_queue.push_back({ e, grid_ptr }); ++jobs_remaining; } } queue_cv.notify_all(); // ---- Wait like old SDL_WaitThread loop ---- { std::unique_lock lock(queue_mutex); jobs_done_cv.wait(lock, [] { return jobs_remaining == 0; }); } } void Enemy::Decide(Character& player) { //float d_x = player.collider_world.x - collider_world.x; //float d_y = player.collider_world.y - collider_world.y; //size_t dc_x = static_cast(abs(d_x)); //size_t dc_y = static_cast(abs(d_y)); EnemyMovingType r; if (is_character_in_range) { r = EnemyMovingType::CHARACTER; } else if (move_focus != EnemyMovingType::NONE) { r = move_focus; } else { // pick random type excluding CHARACTER do { r = static_cast(1 + rand() % (static_cast(EnemyMovingType::LAST) - 1)); } while (r == EnemyMovingType::CHARACTER); } set_MoveLogic(r, player); } inline bool same_corner(const SDL_FPoint& a, const SDL_FPoint& b, float epsilon = 1.0f) { return std::fabs(a.x - b.x) <= epsilon && std::fabs(a.y - b.y) <= epsilon; } inline bool Enemy::character_InRange(SDL_FPoint char_pos) const { float dx = char_pos.x - collider_world.x; float dy = char_pos.y - collider_world.y; float distance_squared = dx * dx + dy * dy; return distance_squared <= detection_radius * detection_radius; } bool Enemy::set_CollisinnWithEnemy(const SDL_FRect& rect) { if (avoiding_object) return false; for (Enemy* enemy : Enemy::enemies) { if (enemy == this || enemy->stats.health <= 0) continue; SDL_FRect other = { enemy->x + enemy->collider.x, enemy->y + enemy->collider.y, enemy->collider.w, enemy->collider.h }; bool overlap = rect.x < other.x + other.w && rect.x + rect.w > other.x && rect.y < other.y + other.h && rect.y + rect.h > other.y; if (overlap) { last_object_collision.min_x = rect.x; last_object_collision.max_x = rect.x+rect.w; last_object_collision.min_y = rect.y; last_object_collision.max_y = rect.y+rect.h; last_object_collision.valid = true; collidiing_with_enemy = enemy; return true; } } return false; } bool Enemy::is_CollidingWithEnemy(const SDL_FRect& rect) { for (Enemy* enemy : Enemy::enemies) { if (enemy->stats.health <= 0) continue; SDL_FRect other = { enemy->x + enemy->collider.x, enemy->y + enemy->collider.y, enemy->collider.w, enemy->collider.h }; bool overlap = rect.x < other.x + other.w && rect.x + rect.w > other.x && rect.y < other.y + other.h && rect.y + rect.h > other.y; if (overlap) { return true; } } return false; } /* bool Enemy::is_CollidingWithObjects(const SDL_FRect& rect) { const size_t scale = Tile::tile_size / Tile::tile_collider_size; // convert rectangle to collider coordinates int start_cx = static_cast(std::floor(rect.x / float(Tile::tile_collider_size))); int end_cx = static_cast(std::ceil((rect.x + rect.w) / float(Tile::tile_collider_size))); int start_cy = static_cast(std::floor(rect.y / float(Tile::tile_collider_size))); int end_cy = static_cast(std::ceil((rect.y + rect.h) / float(Tile::tile_collider_size))); // loop over tiles overlapped by rectangle in tile units size_t tile_start_x = static_cast(rect.x / Tile::tile_size); size_t tile_end_x = static_cast((rect.x + rect.w) / Tile::tile_size); size_t tile_start_y = static_cast(rect.y / Tile::tile_size); size_t tile_end_y = static_cast((rect.y + rect.h) / Tile::tile_size); bool collision_found = false; float min_x = std::numeric_limits::max(); float min_y = std::numeric_limits::max(); float max_x = std::numeric_limits::lowest(); float max_y = std::numeric_limits::lowest(); for (size_t ty = tile_start_y; ty <= tile_end_y && ty < Map::size.y; ++ty) { for (size_t tx = tile_start_x; tx <= tile_end_x && tx < Map::size.x; ++tx) { size_t map_index = ty * Map::size.x + tx; TileMap& tilemap = Map::tile_map[map_index]; for (const TileObject& obj : tilemap.objects) { Object* object = Object::get_ObjectById(obj.object_id); std::vector collision_map = object->get_CollisionMap(); int obj_abs_x = static_cast(tx) - obj.position.x; int obj_abs_y = static_cast(ty) - obj.position.y; int obj_cx = obj_abs_x * static_cast(scale); int obj_cy = obj_abs_y * static_cast(scale); size_t obj_cw = static_cast(object->dimension.x) * scale; size_t obj_ch = static_cast(object->dimension.y) * scale; for (int cy = obj_cy; cy < obj_cy+static_cast(obj_ch); ++cy) { for (int cx = obj_cx; cx < obj_cx+static_cast(obj_cw); ++cx) { int rel_x = cx - obj_cx; int rel_y = cy - obj_cy; if (rel_x < 0 || rel_y < 0 || static_cast(rel_x) >= obj_cw || static_cast(rel_y) >= obj_ch) continue; size_t obj_index = static_cast(rel_y) * obj_cw + static_cast(rel_x); if (collision_map[obj_index]) { float cell_x = static_cast(cx) * Tile::tile_collider_size; float cell_y = static_cast(cy) * Tile::tile_collider_size; min_x = std::min(min_x, cell_x); min_y = std::min(min_y, cell_y); max_x = std::max(max_x, cell_x + static_cast(Tile::tile_collider_size)); max_y = std::max(max_y, cell_y + static_cast(Tile::tile_collider_size)); if (cx >= start_cx && cx < end_cx && cy >= start_cy && cy < end_cy) collision_found = true; } if (cx >= end_cx && cy >= end_cy && !collision_found) return false; } } } } } if (collision_found) { last_object_collision.min_x = std::min(min_x, max_x); last_object_collision.max_x = std::max(min_x, max_x); last_object_collision.min_y = std::min(min_y, max_y); last_object_collision.max_y = std::max(min_y, max_y); last_object_collision.valid = true; } return collision_found; } */ void Enemy::compute_MapCollisionInterval(size_t cx, size_t cy, size_t max_x, size_t max_y) { last_map_collision.valid = false; const size_t start_index = cy * max_x + cx; if (!Map::tile_collission_map[start_index]) return; size_t min_x = cx; while (min_x > 0 && Map::tile_collission_map[cy * max_x + (min_x - 1)]) min_x--; size_t max_xi = cx; while (max_xi < max_x - 1 && Map::tile_collission_map[cy * max_x + (max_xi + 1)]) max_xi++; size_t min_y = cy; while (min_y > 0 && Map::tile_collission_map[(min_y - 1) * max_x + cx]) min_y--; size_t max_yi = cy; while (max_yi < max_y - 1 && Map::tile_collission_map[(max_yi + 1) * max_x + cx]) max_yi++; last_map_collision.min_x = min_x * Tile::tile_collider_size; last_map_collision.max_x = (max_xi + 1) * Tile::tile_collider_size; last_map_collision.min_y = min_y * Tile::tile_collider_size; last_map_collision.max_y = (max_yi + 1) * Tile::tile_collider_size; last_map_collision.valid = true; } bool Enemy::is_CollidingWithWorld(const SDL_FRect& rect) { const size_t max_cells_x = static_cast(Map::size.x * (Tile::tile_size / Tile::tile_collider_size)); const size_t max_cells_y = static_cast(Map::size.y * (Tile::tile_size / Tile::tile_collider_size)); const float corners[4][2] = { { rect.x, rect.y }, { rect.x + rect.w, rect.y }, { rect.x, rect.y + rect.h }, { rect.x + rect.w, rect.y + rect.h } }; for (int i = 0; i < 4; ++i) { const size_t cell_x = corners[i][0] / Tile::tile_collider_size; const size_t cell_y = corners[i][1] / Tile::tile_collider_size; if (cell_x >= max_cells_x || cell_y >= max_cells_y) return true; const size_t index = static_cast(cell_y * max_cells_x + cell_x); if (Map::tile_collission_map[index]) { const size_t index = cell_y * max_cells_x + cell_x; if (Map::tile_collission_map[index]) { compute_MapCollisionInterval(cell_x, cell_y, max_cells_x, max_cells_y); //std::cout << "COL: " << last_map_collision.min_x << " " << last_map_collision.max_x << " " << last_map_collision.min_y << " " << last_map_collision.max_y << std::endl; return true; } } } return false; } bool Enemy::is_Colliding(const SDL_FRect& rect) { /* const size_t max_cells_x = Map::size.x * (Tile::tile_size / Tile::tile_collider_size); const size_t max_cells_y = Map::size.y * (Tile::tile_size / Tile::tile_collider_size); const float corners[4][2] = { { rect.x, rect.y }, { rect.x + rect.w, rect.y }, { rect.x, rect.y + rect.h }, { rect.x + rect.w, rect.y + rect.h } }; for (int i = 0; i < 4; ++i) { const size_t cell_x = corners[i][0] / Tile::tile_collider_size; const size_t cell_y = corners[i][1] / Tile::tile_collider_size; if (cell_x < 0 || cell_y < 0 || cell_x >= max_cells_x || cell_y >= max_cells_y) return true; const size_t index = static_cast(cell_y) * static_cast(max_cells_x) + static_cast(cell_x); if (Map::tile_collission_map[index]) { const size_t index = static_cast(cell_y) * static_cast(max_cells_x) + static_cast(cell_x); if (Map::tile_collission_map[index]) { compute_MapCollisionInterval(cell_x, cell_y, max_cells_x, max_cells_y); //std::cout << "COL: " << last_map_collision.min_x << " " << last_map_collision.max_x << " " << last_map_collision.min_y << " " << last_map_collision.max_y << std::endl; return true; } } } */ // if (is_CollidingWithObjects(rect)) return true; return set_CollisinnWithEnemy(rect); } void Enemy::move_Path() { if (next_moves.empty()) return; SDL_FPoint move_to_position = next_moves.back(); float cx = x + collider.x + collider.w * 0.5f; float cy = y + collider.y + collider.h * 0.5f; float dx = move_to_position.x - cx; float dy = move_to_position.y - cy; float dist = std::sqrt(dx*dx + dy*dy); const float max_dist = 2; if (dist <= max_dist) { next_moves.pop_back(); if (next_moves.empty()) { // rotate to target float fx = next_position.x - cx; float fy = next_position.y - cy; flip = (fx < 0); if (std::abs(fx) > std::abs(fy)) { direction = (fx < 0 ? WalkDirection::LEFT : WalkDirection::RIGHT); weapon.flip = flip; } else { direction = (fy < 0 ? WalkDirection::UP : WalkDirection::DOWN); } return; } return; } // normalize step float step_len = speed * Default::delta_time; if (dist < step_len) step_len = dist; dx = dx / dist * step_len; dy = dy / dist * step_len; //std::cout << "MOVE_TO_POS: " << move_to_position.x << " " << move_to_position.y << "DX AND DY: " << dx << " " << dy << std::endl; SDL_FRect test = collider; bool is_actually_colliding = is_Colliding({collider.x + x, collider.y + y, collider.w, collider.h}); test.x = x + collider.x + dx; test.y = y + collider.y + dy; if (!is_Colliding(test) || is_actually_colliding) { x += dx; moving_logic.moving_distance.x += std::abs(dx); y += dy; moving_logic.moving_distance.y += std::abs(dy); moving_logic.path_blocked = 0.0; } else { float dx = test.x - collider_world.x; float dy = test.y - collider_world.y; float len = std::sqrt(dx * dx + dy * dy); dx /= len; dy /= len; bool left = (std::rand() % 2) == 0; float px = left ? -dy : dy; float py = left ? dx : -dx; float distance = 40.0f + static_cast(std::rand()) / (static_cast(RAND_MAX) / 40.0f); SDL_FPoint result = { collider_world.x + px * distance, collider_world.y + py * distance }; std::cout << "GETING OUTTTTT\n"; moving_logic.position = Map::get_ClosestPointWithoutCollision({result.x, result.y, collider.w, collider.h}); next_position = moving_logic.position; moving_logic.moving_distance.x = 0.0f; moving_logic.moving_distance.y = 0.0f; point_in_reach = false; moving_logic.path_blocked = 0.0; set_Status(EnemyStatus::RUN); moving_logic.avoid_blockage = true; moving_logic.move_type = EnemyMovingType::WALK; handle_Animation(); return; } // SET WALK DIRECTION flip = (dx < 0); if (fabs(dx) > 0.0f && fabs(dx) > fabs(dy)) { direction = (dx < 0 ? WalkDirection::LEFT : WalkDirection::RIGHT); } else if (dy > 0.0f) { direction = WalkDirection::DOWN; } else if (dy < .0f) { direction = WalkDirection::UP; } if (status != EnemyStatus::RUN) set_Status(EnemyStatus::RUN); // update world positions collider_world.x = x + collider.x + collider.w * 0.5f; collider_world.y = y + collider.y + collider.h * 0.5f; tile_world.x = collider_world.x / Tile::tile_size; tile_world.y = collider_world.y / Tile::tile_size; //set_TilesInReach(); } void Enemy::remove_Dead() { if (Enemy::enemies.size() == 0) return; for (auto it = Enemy::enemies.begin(); it != Enemy::enemies.end(); ) { Enemy* e = *it; if (e->status == EnemyStatus::DEAD && e->death_duration > e->max_death_time) { delete e; it = Enemy::enemies.erase(it); } else { ++it; } } } void Enemy::init_SpawnEnemy(double delay_to_spawn_in_sec, WeaponType weapon_type = WeaponType::SWORD, WeaponMaterial weapon_material = WeaponMaterial::WOOD, SDL_FPoint position = {100,100}, EnemyStats status = {16,16,2,false,false,0,1.2}) { double spawn_date = delay_to_spawn_in_sec + Default::get_TimeSinceStartInSec(); Enemy::enemies_to_spawn.push_back({spawn_date, weapon_type, weapon_material, position, status}); } bool Enemy::spawn_Enemy(SpawnParams& params) { if (params.spawn_date > Default::get_TimeSinceStartInSec()) return false; Enemy* enemy = new Enemy(params.type, params.material, params.pos, params.stats); enemy->Init(); Enemy::enemies.push_back(enemy); return true; } void Enemy::set_NextMoves(const std::vector& grid) { // std::cout << "PL: " << player_collider_world.x << " " << player_collider_world.y << std::endl; // std::cout << "TG: " << target_pos.x << " " << target_pos.y << std::endl; // std::cout << "DS: " << dist_max-dist_min << std::endl; next_moves = find_Path({collider_world.x, collider_world.y, collider.w, collider.h}, next_position, dist_max-dist_min, 0, grid); // std::cout << "NM " << next_moves.size() << " " << collider_world.x << " " << collider_world.y << " " << next_position.x << " " << next_position.y << std::endl; } void Enemy::set_AlignPosition(const SDL_FPoint& pos) { float dist = dist_min + (dist_max-dist_min)/2; //std::cout << "DS: " << dist.x << " " << dist.y < 3) { moving_logic.move_type = EnemyMovingType::NONE; return; } int dist_x = moving_logic.min_distance + rand() % (moving_logic.max_distance - moving_logic.min_distance + 1); int dist_y = moving_logic.min_distance + rand() % (moving_logic.max_distance - moving_logic.min_distance + 1); if (rand() % 2) dist_x = -dist_x; if (rand() % 2) dist_y = -dist_y; int new_x = static_cast(collider_world.x) + dist_x; int new_y = static_cast(collider_world.y) + dist_y; if (new_x < 0 || new_y < 0 || new_x >= map_max_x || new_y >= map_max_y) continue; test_pos.x = new_x; test_pos.y = new_y; test_pos_c = Map::get_ClosestPointWithoutCollision(test_pos); if (player.is_attackable && test_pos_c.x <= 530 && test_pos_c.y <= 334) continue; break; } while (true); // END CLAMP NEW POSITION TO BOUNDARIES WHICH SUUUUUUCkS!!! moving_logic.moving_distance = {0, 0}; // moving_logic.position = Map::get_ClosestPointWithoutCollision(test_pos); next_position = test_pos_c; //set_AlignPosition(moving_logic.position); // std::cout << " MEEE1 " << player.is_attackable << " " << collider_world.x << " " << collider_world.y << std::endl; // std::cout << " MEEE2 " << player.is_attackable << " " << next_position.x << " " << next_position.y << std::endl; sta = EnemyStatus::RUN; point_in_reach = false; break; } case EnemyMovingType::CHARACTER: { bool is_player_alive = player.health_Get() > 0; if (!is_player_alive) { moving_logic.move_type = EnemyMovingType::NONE; break; } moving_logic.object = static_cast(&player); moving_logic.position = player.collider_world; moving_logic.duration = 0.8 + ((double)rand() / RAND_MAX) * (1.8 - 0.8); set_AlignPosition(moving_logic.position); sta = EnemyStatus::RUN; point_in_reach = false; move_focus = EnemyMovingType::CHARACTER; break; } case EnemyMovingType::LAST: moving_logic.move_type = EnemyMovingType::NONE; break; } if (status != sta) { set_Status(sta); handle_Animation(); } } void Enemy::Update(Character& player) { point_in_reach = false; if (player.is_attackable) { is_character_in_range = character_InRange(player.collider_world); if (!is_character_in_range && moving_logic.move_type == EnemyMovingType::CHARACTER) { next_moves.clear(); move_focus = EnemyMovingType::NONE; moving_logic.move_type = EnemyMovingType::NONE; attack_Stop(); } } // IS DEAD if (stats.health <= 0) { if (status == EnemyStatus::DYING || status == EnemyStatus::DEAD) { death(); } handle_Animation(); return; } // std::cout << "MVTY " << (int)moving_logic.move_type << std::endl; // DECIDE // REDECIDE //std::cout << "WLK " << moving_logic.moving_distance.x << " >= " << moving_logic.max_distance << " || " << moving_logic.moving_distance.y << " >= " << moving_logic.max_distance << std::endl; if ( moving_logic.move_type == EnemyMovingType::NONE || ( moving_logic.move_type == EnemyMovingType::IDLE && moving_logic.duration <= 0)) { Decide(player); handle_Animation(); return; } else if (moving_logic.move_type == EnemyMovingType::IDLE) { moving_logic.duration -= Default::delta_time; handle_Animation(); return; } else if (moving_logic.move_type == EnemyMovingType::WALK) { if ( moving_logic.moving_distance.x >= moving_logic.max_distance || moving_logic.moving_distance.y >= moving_logic.max_distance ) { moving_logic.move_type = EnemyMovingType::NONE; next_moves.clear(); handle_Animation(); return; } } float d_x = moving_logic.position.x - collider_world.x; float d_y = moving_logic.position.y - collider_world.y; /* size_t dc_x = static_cast(abs(d_x)); size_t dc_y = static_cast(abs(d_y)); bool dist_ok_x = (dc_x >= dist_min && dc_x <= weapon.dimension.x) && (dc_y <= weapon.dimension.y/2); bool dist_ok_y = (dc_y >= dist_min && dc_y <= weapon.dimension.x) && (dc_x <= weapon.dimension.y/2); */ float dist_sq = d_x * d_x + d_y * d_y; float min_sq = dist_min * dist_min; float max_sq = dist_max * dist_max; //std::cout << "DSTQ " << (int)moving_logic.move_type << " " << next_moves.size()<< " " << dist_sq << " " << min_sq << " " << max_sq << std::endl; bool point_moved = false; if ( player.collider_world.x != moving_logic.position.x || player.collider_world.y != moving_logic.position.y ) { point_moved = true; } if (dist_sq >= min_sq && dist_sq <= max_sq) { point_in_reach = true; moving_logic.avoid_blockage = false; next_moves.clear(); } if (next_moves.empty()) { if (moving_logic.move_type == EnemyMovingType::WALK || (move_focus == EnemyMovingType::CHARACTER && point_moved && moving_logic.duration >= 0)) { float mld = moving_logic.duration; set_Status(EnemyStatus::IDLE); moving_logic.move_type = EnemyMovingType::IDLE; if (moving_logic.move_type == EnemyMovingType::WALK) moving_logic.duration = (moving_logic.moving_distance.x + moving_logic.moving_distance.y) / 100 + 0.5; else moving_logic.duration = mld; std::cout << "Walk DONE !!!!\n"; return; } } else if (next_moves.size() < 2) { ++stuck_moves; if (stuck_moves > 20) { // std::cout << "IM STUCK!!\n"; set_Status(EnemyStatus::IDLE); moving_logic.move_type = EnemyMovingType::IDLE; next_position = collider_world; next_moves.clear(); return; } } if (move_focus == EnemyMovingType::CHARACTER) { moving_logic.duration -= Default::delta_time; // std::cout << moving_logic.duration << std::endl; //std::cout << "NEXT MOVES: " << next_moves.size() << std::endl; if (!moving_logic.avoid_blockage && ((point_moved && moving_logic.duration <= 0) || moving_logic.move_type != EnemyMovingType::CHARACTER)) { set_MoveLogic(moving_logic.move_type, player); return; } } if (moving_logic.move_type == EnemyMovingType::CHARACTER) { if (stats.is_attacking) { Attack(point_in_reach); handle_Animation(); return; } } if ( !point_in_reach ) { move_Path(); handle_Animation(); return; } next_moves.clear(); // SET WALK DIRECTION flip = (d_x < 0); if (fabs(d_x) > 0.0f && fabs(d_x) > fabs(d_y)) { direction = (d_x < 0 ? WalkDirection::LEFT : WalkDirection::RIGHT); } else if (d_y > 0.0f) { direction = WalkDirection::DOWN; } else if (d_y < .0f) { direction = WalkDirection::UP; } switch (moving_logic.move_type) { case EnemyMovingType::CHARACTER: attack_Start(); break; case EnemyMovingType::WALK: moving_logic.move_type = EnemyMovingType::IDLE; set_MoveLogic(moving_logic.move_type, player); break; default: break; } handle_Animation(); } void Enemy::attack_Start() { set_Status(EnemyStatus::ATTACK); handle_Animation(); Animation::animation_frame_time = stats.attack_duration_max / active_anim->frames.size(); stats.attack_duration = 0; stats.is_attacking = true; stats.has_attacked = false; } void Enemy::Attack( bool in_reach) { // i should fic the frame_time to attack_duration // so the animation finishes with the attack end. stats.attack_duration += Default::delta_time; if (stats.attack_duration < stats.attack_duration_max/2) return; if (in_reach && !stats.has_attacked) { #ifdef DEBUG_BUILD std::cout << "Enemy: Attack character!\n"; #endif stats.has_attacked = true; if (moving_logic.move_type == EnemyMovingType::CHARACTER) static_cast(moving_logic.object)->getting_Hit(stats.demage); //player.getting_Hit(stats.demage); } attack_Stop(); } void Enemy::attack_Stop() { if (stats.attack_duration < stats.attack_duration_max) return; set_Status(EnemyStatus::IDLE); stats.attack_duration = 0; stats.is_attacking = false; stats.has_attacked = false; if (moving_logic.move_type == EnemyMovingType::CHARACTER) { if (static_cast(moving_logic.object)->health_Get() <= 0) moving_logic.move_type = EnemyMovingType::NONE; } } void Enemy::getting_Hit(float demage) { // if (status != EnemyStatus::HIT) set_Status(EnemyStatus::HIT); std::cout << "ENEM " << stats.health << " " << demage << std::endl; stats.health -= demage; #ifdef DEBUG_BUILD std::cout << "Enemy: Getting hit! " << stats.health << std::endl; #endif if (stats.health <= 0) { attack_Stop(); stats.health = 0; death(); } } void Enemy::death() { death_duration += Default::delta_time; if (death_duration > death_duration_max) { if (status != EnemyStatus::DEAD) set_Status(EnemyStatus::DEAD); } else { if (status != EnemyStatus::DYING) set_Status(EnemyStatus::DYING); } } void Enemy::set_TilesInReach() { tiles_in_reach.clear(); float reach = arm_reach_radius; float reach_sq = reach * reach; // bounding box of tiles to check int min_tx = static_cast((collider_world.x - reach) / Tile::tile_size); int max_tx = static_cast((collider_world.x + reach) / Tile::tile_size); int min_ty = static_cast((collider_world.y - reach) / Tile::tile_size); int max_ty = static_cast((collider_world.y + reach) / Tile::tile_size); // clamp to map bounds if (min_tx < 0) min_tx = 0; if (min_ty < 0) min_ty = 0; if (max_tx >= static_cast(Map::size.x)) max_tx = Map::size.x - 1; if (max_ty >= static_cast(Map::size.y)) max_ty = Map::size.y - 1; for (int ty = min_ty; ty <= max_ty; ++ty) { for (int tx = min_tx; tx <= max_tx; ++tx) { // tile rectangle float tile_left = tx * static_cast(Tile::tile_size); float tile_top = ty * static_cast(Tile::tile_size); float tile_right = tile_left + Tile::tile_size; float tile_bottom = tile_top + Tile::tile_size; // closest point on tile to character center float closest_x = std::max(tile_left, std::min(collider_world.x, tile_right)); float closest_y = std::max(tile_top, std::min(collider_world.y, tile_bottom)); float dx = closest_x - collider_world.x; float dy = closest_y - collider_world.y; if (dx*dx + dy*dy <= reach_sq) tiles_in_reach.push_back({tx, ty}); } } std::sort(tiles_in_reach.begin(), tiles_in_reach.end(), [this](const SDL_Point& a, const SDL_Point& b) { float ax = a.x * Tile::tile_size + Tile::tile_size * 0.5f; float ay = a.y * Tile::tile_size + Tile::tile_size * 0.5f; float bx = b.x * Tile::tile_size + Tile::tile_size * 0.5f; float by = b.y * Tile::tile_size + Tile::tile_size * 0.5f; float da = (ax - collider_world.x) * (ax - collider_world.x) + (ay - collider_world.y) * (ay - collider_world.y); float db = (bx - collider_world.x) * (bx - collider_world.x) + (by - collider_world.y) * (by - collider_world.y); return da < db; }); } static void WriteStats(std::ofstream &file, const EnemyStats &s) { file.write(reinterpret_cast(&s.health), sizeof(s.health)); file.write(reinterpret_cast(&s.health_max), sizeof(s.health_max)); file.write(reinterpret_cast(&s.demage), sizeof(s.demage)); file.write(reinterpret_cast(&s.is_attacking), sizeof(s.is_attacking)); file.write(reinterpret_cast(&s.has_attacked), sizeof(s.has_attacked)); file.write(reinterpret_cast(&s.attack_duration), sizeof(s.attack_duration)); file.write(reinterpret_cast(&s.attack_duration_max), sizeof(s.attack_duration_max)); } static void ReadStats(std::ifstream &file, EnemyStats &s) { file.read(reinterpret_cast(&s.health), sizeof(s.health)); file.read(reinterpret_cast(&s.health_max), sizeof(s.health_max)); file.read(reinterpret_cast(&s.demage), sizeof(s.demage)); file.read(reinterpret_cast(&s.is_attacking), sizeof(s.is_attacking)); file.read(reinterpret_cast(&s.has_attacked), sizeof(s.has_attacked)); file.read(reinterpret_cast(&s.attack_duration), sizeof(s.attack_duration)); file.read(reinterpret_cast(&s.attack_duration_max), sizeof(s.attack_duration_max)); } bool Enemy::save_All(const std::string &path) { std::ofstream file(path, std::ios::binary | std::ios::trunc); if (!file) return false; uint32_t count_total = static_cast(enemies.size() + enemies_to_spawn.size()); file.write(reinterpret_cast(&count_total), sizeof(count_total)); for (auto *e : enemies) { SpawnParams sp; sp.spawn_date = 0.0; sp.type = e->weapon.type; sp.material = e->weapon.material; sp.pos = { e->x, e->y }; sp.stats = e->stats; file.write(reinterpret_cast(&sp.spawn_date), sizeof(sp.spawn_date)); file.write(reinterpret_cast(&sp.type), sizeof(sp.type)); file.write(reinterpret_cast(&sp.material), sizeof(sp.material)); file.write(reinterpret_cast(&sp.pos.x), sizeof(sp.pos.x)); file.write(reinterpret_cast(&sp.pos.y), sizeof(sp.pos.y)); WriteStats(file, sp.stats); } // save pending double current_time = Default::get_TimeSinceStartInSec(); for (const auto &sp : enemies_to_spawn) { SpawnParams tmp = sp; tmp.spawn_date -= current_time; file.write(reinterpret_cast(&tmp.spawn_date), sizeof(tmp.spawn_date)); file.write(reinterpret_cast(&tmp.type), sizeof(tmp.type)); file.write(reinterpret_cast(&tmp.material), sizeof(tmp.material)); file.write(reinterpret_cast(&tmp.pos.x), sizeof(tmp.pos.x)); file.write(reinterpret_cast(&tmp.pos.y), sizeof(tmp.pos.y)); WriteStats(file, tmp.stats); } return file.good(); } bool Enemy::load_All(const std::string &path) { std::ifstream file(path, std::ios::binary); if (!file) return false; for (auto *e : enemies) delete e; enemies.clear(); enemies_to_spawn.clear(); uint32_t count_total = 0; file.read(reinterpret_cast(&count_total), sizeof(count_total)); for (uint32_t i = 0; i < count_total; ++i) { SpawnParams sp; file.read(reinterpret_cast(&sp.spawn_date), sizeof(sp.spawn_date)); file.read(reinterpret_cast(&sp.type), sizeof(sp.type)); file.read(reinterpret_cast(&sp.material), sizeof(sp.material)); file.read(reinterpret_cast(&sp.pos.x), sizeof(sp.pos.x)); file.read(reinterpret_cast(&sp.pos.y), sizeof(sp.pos.y)); ReadStats(file, sp.stats); double delay = sp.spawn_date; init_SpawnEnemy(delay, sp.type, sp.material, sp.pos, sp.stats); } return file.good(); }