Save workspace artifacts: character-controller, gdextension scaffold, network scripts, map-pipeline, server config
Includes code from task workspaces that was never pushed: - client/characters/ — fps_character_controller.gd (400 lines), fps_camera.gd, input_handler.gd - gdextension/simulation/ — C++ GDExtension scaffold: entity, movement, hit detection, simulation server, state serializer, bitstream (14 files) - scripts/network/ — ENet-based network_manager.gd, server/client_main.gd, player.gd - scripts/map_packaging/ — PCK pipeline: pack_map.gd, map_downloader.gd, map_registry_server.py, README - scripts/config/ — server_config.gd (480 lines) - scenes/ — client_main, server_main, player, test_range - project.godot, export_presets.cfg
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
#include "hit_detection.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace tactical_shooter {
|
||||
|
||||
HitDetection::HitDetection() {}
|
||||
|
||||
void HitDetection::set_entities(const std::vector<Entity *> &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<HitResult> HitDetection::sphere_overlap(
|
||||
const godot::Vector3 ¢er,
|
||||
float radius,
|
||||
uint16_t exclude_id
|
||||
) const {
|
||||
std::vector<HitResult> 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
|
||||
Reference in New Issue
Block a user