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:
2026-07-01 18:30:44 -04:00
parent 2cf57a989f
commit e9dc05983c
42 changed files with 6249 additions and 0 deletions
@@ -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 &center,
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