#include "hit_detection.h" #include #include #include namespace tactical_shooter { HitDetection::HitDetection() {} void HitDetection::set_entities(const std::vector &entities) { entities_ = &entities; } HitResult HitDetection::raycast_entity( const godot::Vector3 &origin, const godot::Vector3 &direction, float max_distance, uint16_t exclude_id ) const { if (!entities_) return HitResult{}; HitResult best; best.distance = max_distance; for (Entity *entity : *entities_) { if (!entity || !entity->is_alive()) continue; if (entity->get_entity_id() == exclude_id) continue; // Simple sphere intersection test against entity bounding sphere godot::Vector3 entity_pos = entity->get_position(); // Offset center upward for body collision (not at feet) godot::Vector3 body_center = entity_pos; body_center.y += kEntityHeight * 0.5f; godot::Vector3 oc = origin - body_center; float a = direction.dot(direction); float b = 2.0f * oc.dot(direction); float c = oc.dot(oc) - (kEntityRadius * kEntityRadius); float discriminant = b * b - 4.0f * a * c; if (discriminant < 0.0f) continue; float t1 = (-b - std::sqrt(discriminant)) / (2.0f * a); float t2 = (-b + std::sqrt(discriminant)) / (2.0f * a); // Use the closest positive intersection float t = t1; if (t < 0.0f) t = t2; if (t < 0.0f || t > best.distance) continue; best.hit = true; best.entity_id = entity->get_entity_id(); best.distance = t; best.point = origin + direction * t; best.normal = (best.point - body_center).normalized(); best.damage = 0.0f; // filled by process_shot best.hitbox_id = classify_hitbox(*entity, best.point); } return best; } HitResult HitDetection::process_shot( const godot::Vector3 &origin, const godot::Vector3 &direction, uint16_t shooter_id, const WeaponDamage &weapon ) { HitResult hit = raycast_entity(origin, direction, weapon.max_range, shooter_id); if (hit.hit) { float mult = get_hitbox_multiplier(hit.hitbox_id, weapon); hit.damage = weapon.base_damage * mult; } return hit; } float HitDetection::apply_damage(Entity &entity, float damage, float mult) { float raw = damage * mult; // Armor absorbs a portion float armor = entity.get_armor(); float armor_absorb = std::min(raw * 0.5f, armor); armor -= armor_absorb; entity.set_armor(armor); float health_damage = raw - armor_absorb; float new_health = entity.get_health() - health_damage; entity.set_health(new_health); if (new_health <= 0.0f) { entity.kill(); } return health_damage + armor_absorb; } std::vector HitDetection::sphere_overlap( const godot::Vector3 ¢er, float radius, uint16_t exclude_id ) const { std::vector results; if (!entities_) return results; float radius_sq = radius * radius; for (Entity *entity : *entities_) { if (!entity || !entity->is_alive()) continue; if (entity->get_entity_id() == exclude_id) continue; godot::Vector3 body_center = entity->get_position(); body_center.y += kEntityHeight * 0.5f; godot::Vector3 diff = center - body_center; float dist_sq = diff.dot(diff); if (dist_sq <= radius_sq) { HitResult hit; hit.hit = true; hit.entity_id = entity->get_entity_id(); hit.distance = std::sqrt(dist_sq); hit.point = body_center; hit.normal = diff.normalized(); results.push_back(hit); } } return results; } float HitDetection::get_hitbox_multiplier(uint8_t hitbox_id, const WeaponDamage &weapon) const { switch (hitbox_id) { case 1: return weapon.head_multiplier; case 2: return weapon.arm_multiplier; case 3: return weapon.leg_multiplier; default: return weapon.body_multiplier; } } uint8_t HitDetection::classify_hitbox( const Entity &entity, const godot::Vector3 &hit_point ) const { godot::Vector3 entity_pos = entity.get_position(); float relative_y = hit_point.y - entity_pos.y; float height_ratio = relative_y / kEntityHeight; if (height_ratio > 0.85f) return 1; // head if (height_ratio > 0.65f) return 2; // arms/upper body if (height_ratio > 0.25f) return 0; // body return 3; // legs } } // namespace tactical_shooter