diff --git a/resources/config/crosshair_config.json b/resources/config/crosshair_config.json new file mode 100644 index 0000000..d4b2452 --- /dev/null +++ b/resources/config/crosshair_config.json @@ -0,0 +1,21 @@ +{ + "enabled": true, + + "referenceResolution": [1280, 720], + + "color": [1.0, 1.0, 1.0], + "cl_crosshairalpha": 1.0, + "cl_crosshairthickness": 2.0, + + "centerGapPx": 10.0, + + "top": { + "lengthPx": 14.0, + "angleDeg": 90.0 + }, + + "arms": [ + { "lengthPx": 20.0, "angleDeg": 210.0 }, + { "lengthPx": 20.0, "angleDeg": 330.0 } + ] +} \ No newline at end of file diff --git a/src/Space.cpp b/src/Space.cpp index 1336b35..6f2a77b 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -336,6 +336,19 @@ namespace ZL throw std::runtime_error("Failed to load spark emitter config file!"); } + crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json"); + std::cerr << "[Crosshair] loaded=" << crosshairCfgLoaded + << " enabled=" << crosshairCfg.enabled + << " w=" << Environment::width << " h=" << Environment::height + << " alpha=" << crosshairCfg.alpha + << " thickness=" << crosshairCfg.thicknessPx + << " gap=" << crosshairCfg.gapPx << "\n"; + if (!crosshairCfgLoaded) { + std::cerr << "Failed to load crosshair_config.json, using defaults\n"; + } + + + textRenderer = std::make_unique(); if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) { std::cerr << "Failed to init TextRenderer\n"; @@ -566,6 +579,7 @@ namespace ZL drawBoxesLabels(); drawShip(); + drawCrosshair(); drawTargetHud(); CheckGlError(); } @@ -693,6 +707,230 @@ namespace ZL //#endif } + // хелпер прицела: добавляет повернутую 2D-линию в меш прицела + static void AppendRotatedRect2D( + VertexDataStruct& out, + float cx, float cy, + float length, float thickness, + float angleRad, + float z, + const Eigen::Vector3f& rgb) + { + // прямоугольник вдоль локальной оси +X: [-L/2..+L/2] и [-T/2..+T/2] + float hl = length * 0.5f; + float ht = thickness * 0.5f; + + Eigen::Vector2f p0(-hl, -ht); + Eigen::Vector2f p1(-hl, +ht); + Eigen::Vector2f p2(+hl, +ht); + Eigen::Vector2f p3(+hl, -ht); + + float c = std::cos(angleRad); + float s = std::sin(angleRad); + + auto rot = [&](const Eigen::Vector2f& p) -> Vector3f { + float rx = p.x() * c - p.y() * s; + float ry = p.x() * s + p.y() * c; + return Vector3f(cx + rx, cy + ry, z); + }; + + Vector3f v0 = rot(p0); + Vector3f v1 = rot(p1); + Vector3f v2 = rot(p2); + Vector3f v3 = rot(p3); + + // 2 треугольника + out.PositionData.push_back(v0); + out.PositionData.push_back(v1); + out.PositionData.push_back(v2); + out.PositionData.push_back(v2); + out.PositionData.push_back(v3); + out.PositionData.push_back(v0); + + for (int i = 0; i < 6; ++i) out.ColorData.push_back(rgb); + } + + bool Space::loadCrosshairConfig(const std::string& path) + { + using json = nlohmann::json; + + std::string content; + try { + if (std::string(CONST_ZIP_FILE).empty()) content = readTextFile(path); + else { + auto buf = readFileFromZIP(path, CONST_ZIP_FILE); + if (buf.empty()) return false; + content.assign(buf.begin(), buf.end()); + } + json j = json::parse(content); + + if (j.contains("enabled")) crosshairCfg.enabled = j["enabled"].get(); + + if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) { + crosshairCfg.refW = j["referenceResolution"][0].get(); + crosshairCfg.refH = j["referenceResolution"][1].get(); + } + + if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get(); + crosshairCfg.scaleMul = std::clamp(crosshairCfg.scaleMul, 0.1f, 3.0f); + + if (j.contains("color") && j["color"].is_array() && j["color"].size() == 3) { + crosshairCfg.color = Eigen::Vector3f( + j["color"][0].get(), + j["color"][1].get(), + j["color"][2].get() + ); + } + + if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get(); + if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get(); + if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get(); + + if (j.contains("top") && j["top"].is_object()) { + auto t = j["top"]; + if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get(); + if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get(); + } + + crosshairCfg.arms.clear(); + if (j.contains("arms") && j["arms"].is_array()) { + for (auto& a : j["arms"]) { + CrosshairConfig::Arm arm; + arm.lenPx = a.value("lengthPx", 20.0f); + arm.angleDeg = a.value("angleDeg", 210.0f); + crosshairCfg.arms.push_back(arm); + } + } + else { + // дефолт + crosshairCfg.arms.push_back({ 20.0f, 210.0f }); + crosshairCfg.arms.push_back({ 20.0f, 330.0f }); + } + + // clamp + crosshairCfg.alpha = std::clamp(crosshairCfg.alpha, 0.0f, 1.0f); + crosshairCfg.thicknessPx = max(0.5f, crosshairCfg.thicknessPx); + crosshairCfg.gapPx = max(0.0f, crosshairCfg.gapPx); + + crosshairMeshValid = false; // пересобрать + return true; + } + catch (...) { + return false; + } + } + + // пересобирает mesh прицела при изменениях/ресайзе + void Space::rebuildCrosshairMeshIfNeeded() + { + if (!crosshairCfg.enabled) return; + + // если ничего не изменилось — не трогаем VBO + if (crosshairMeshValid && + crosshairLastW == Environment::width && + crosshairLastH == Environment::height && + std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f && + std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f && + std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f && + std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f) + { + return; + } + + crosshairLastW = Environment::width; + crosshairLastH = Environment::height; + crosshairLastAlpha = crosshairCfg.alpha; + crosshairLastThickness = crosshairCfg.thicknessPx; + crosshairLastGap = crosshairCfg.gapPx; + crosshairLastScaleMul = crosshairCfg.scaleMul; + + float cx = Environment::width * 0.5f; + float cy = Environment::height * 0.5f; + + // масштаб от reference (стандартно: по высоте) + float scale = (crosshairCfg.refH > 0) ? (Environment::height / (float)crosshairCfg.refH) : 1.0f; + scale *= crosshairCfg.scaleMul; + + float thickness = crosshairCfg.thicknessPx * scale; + float gap = crosshairCfg.gapPx * scale; + + VertexDataStruct v; + v.PositionData.reserve(6 * (1 + (int)crosshairCfg.arms.size())); + v.ColorData.reserve(6 * (1 + (int)crosshairCfg.arms.size())); + + const float z = 0.0f; + const Eigen::Vector3f rgb = crosshairCfg.color; + + auto deg2rad = [](float d) { return d * 3.1415926535f / 180.0f; }; + + // TOP (короткая палочка сверху) + { + float len = crosshairCfg.topLenPx * scale; + float ang = deg2rad(crosshairCfg.topAngleDeg); + + // сдвигаем сегмент от центра на gap + len/2 по направлению + float dx = std::cos(ang); + float dy = std::sin(ang); + float mx = cx + dx * (gap + len * 0.5f); + float my = cy + dy * (gap + len * 0.5f); + + AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb); + } + + // ARMS (2 луча вниз-влево и вниз-вправо) + for (auto& a : crosshairCfg.arms) + { + float len = a.lenPx * scale; + float ang = deg2rad(a.angleDeg); + + float dx = std::cos(ang); + float dy = std::sin(ang); + float mx = cx + dx * (gap + len * 0.5f); + float my = cy + dy * (gap + len * 0.5f); + + AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb); + } + + crosshairMesh.AssignFrom(v); + crosshairMesh.RefreshVBO(); + crosshairMeshValid = true; + } + + void Space::drawCrosshair() + { + if (!crosshairCfg.enabled) return; + + rebuildCrosshairMeshIfNeeded(); + if (!crosshairMeshValid) return; + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + renderer.shaderManager.PushShader("defaultColor"); + renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushMatrix(); + renderer.LoadIdentity(); + + renderer.EnableVertexAttribArray("vPosition"); + renderer.EnableVertexAttribArray("vColor"); + + Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha); + renderer.RenderUniform4fv("uColor", uColor.data()); + + renderer.DrawVertexRenderStruct(crosshairMesh); + + renderer.DisableVertexAttribArray("vPosition"); + renderer.DisableVertexAttribArray("vColor"); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + } + int Space::pickTargetId() const { int bestId = -1; @@ -867,7 +1105,7 @@ namespace ZL Vector3f leadWorld = shipWorld; bool haveLead = false; - // чтобы круг не улетал далеко : максимум 4 секунды(подстрой под игру) + // чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру) float distToTarget = (Environment::shipState.position - shipWorld).norm(); float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f); diff --git a/src/Space.h b/src/Space.h index 9e97eeb..d5d8a7c 100644 --- a/src/Space.h +++ b/src/Space.h @@ -136,6 +136,42 @@ namespace ZL { int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) void clearTextRendererCache(); + + // Crosshair HUD + struct CrosshairConfig { + bool enabled = true; + int refW = 1280; + int refH = 720; + + float scaleMul = 1.0f; + + Eigen::Vector3f color = { 1.f, 1.f, 1.f }; + float alpha = 1.0f; // cl_crosshairalpha + float thicknessPx = 2.0f; // cl_crosshairthickness + float gapPx = 10.0f; + + float topLenPx = 14.0f; + float topAngleDeg = 90.0f; + + struct Arm { float lenPx; float angleDeg; }; + std::vector arms; + }; + + CrosshairConfig crosshairCfg; + bool crosshairCfgLoaded = false; + + // кеш геометрии + VertexRenderStruct crosshairMesh; + bool crosshairMeshValid = false; + int crosshairLastW = 0, crosshairLastH = 0; + float crosshairLastAlpha = -1.0f; + float crosshairLastThickness = -1.0f; + float crosshairLastGap = -1.0f; + float crosshairLastScaleMul = -1.0f; + + bool loadCrosshairConfig(const std::string& path); + void rebuildCrosshairMeshIfNeeded(); + void drawCrosshair(); };