#include #include "pick_system.hpp" #include "entities/world.hpp" namespace Nimbin { namespace PickSystem { static constexpr double MAX_REACH_XZ = 3.8; static constexpr double MAX_ANGLE = 0.25; static double angScore(const GameObject& obj, const Vec3& eye, const Vec3& fwd, double max_reach = MAX_REACH_XZ) { double he; if (obj.type == ObjectType::Text) he = static_cast(static_cast(obj).half_extent); else he = obj.shape.half_extents.x; double cy_ = SDL_cos(obj.yaw); double sy_ = SDL_sin(obj.yaw); Vec3 seg_a { obj.pos.x - cy_ * he, obj.pos.y, obj.pos.z + sy_ * he }; Vec3 seg_b { obj.pos.x + cy_ * he, obj.pos.y, obj.pos.z - sy_ * he }; Vec3 seg_dir { seg_b.x - seg_a.x, seg_b.y - seg_a.y, seg_b.z - seg_a.z }; Vec3 w { seg_a.x - eye.x, seg_a.y - eye.y, seg_a.z - eye.z }; double a = Math::dot(fwd, fwd); double b = Math::dot(fwd, seg_dir); double c = Math::dot(seg_dir, seg_dir); double d = Math::dot(fwd, w); double e = Math::dot(seg_dir, w); double denom = a*c - b*b; double seg_t = (denom > 1e-8) ? SDL_clamp((b*d - a*e) / denom, 0.0, 1.0) : 0.0; Vec3 closest { seg_a.x + seg_dir.x * seg_t, seg_a.y + seg_dir.y * seg_t, seg_a.z + seg_dir.z * seg_t }; Vec3 dv { closest.x - eye.x, closest.y - eye.y, closest.z - eye.z }; double xz = SDL_sqrt(dv.x*dv.x + dv.z*dv.z); if (xz > max_reach) return -1.0; double len = Math::length(dv); if (len < 0.001) return -1.0; double t = SDL_clamp(Math::dot(fwd, dv) / len, -1.0, 1.0); double angle = SDL_acos(t); return (angle < MAX_ANGLE) ? angle : -1.0; } PickResult pick(const Camera& camera, const DynArray& words, const DynArray& scenery, const DynArray& walls) { const Vec3& eye = camera.pos; const double floor_y = World::get_FloorY(); PickResult result; double best_angle = MAX_ANGLE; for (size_t i = 0; i < words.size(); i++) { const Text::TokenPtr& w = words[i]; if (!w || !w->interactable || w->is_Whitespace()) continue; if (w->ai.state != Text::Token::Ai::State::Idle) continue; if (w->pos.y < floor_y) continue; double a = angScore(*w, eye, camera.fwd); if (a >= 0.0 && a < best_angle) { best_angle = a; result.kind = PickKind::Word; result.word_index = i; } } for (const GameObjectPtr& s : scenery) { if (!s || !s->interactable) continue; if (s->pos.y < floor_y) continue; double a = angScore(*s, eye, camera.fwd); if (a >= 0.0 && a < best_angle) { best_angle = a; result.kind = PickKind::Scenery; result.scenery_obj = s.get(); } } for (Text::Handler* h : walls) { if (!h || h->edit_Active()) continue; for (const Text::Line& line : h->get_Lines()) { for (const Text::TokenPtr& tp : line.tokens) { Text::Token::Data* t = tp.get(); if (!t || !t->interactable || t->is_Whitespace()) continue; if (t->pos.y < floor_y) continue; double a = angScore(*t, eye, camera.fwd); if (a >= 0.0 && a < best_angle) { best_angle = a; result.kind = PickKind::WallWord; result.wall_handler = h; result.wall_token = t; } } } } return result; } Text::Token::Data* pick_WallToken(const Camera& camera, Text::Handler& wall) { const Vec3& eye = camera.pos; Text::Token::Data* best = nullptr; double best_angle = MAX_ANGLE; for (const Text::Line& line : wall.get_Lines()) { for (const Text::TokenPtr& tp : line.tokens) { Text::Token::Data* t = tp.get(); if (!t) continue; double a = angScore(*t, eye, camera.fwd, /*max_reach=*/1e18); if (a >= 0.0 && a < best_angle) { best_angle = a; best = t; } } } return best; } } // namespace PickSystem } // namespace Nimbin