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 d5eb509..c0944eb 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -361,6 +361,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"; @@ -597,6 +610,7 @@ namespace ZL drawBoxesLabels(); drawShip(); + drawCrosshair(); drawTargetHud(); CheckGlError(); } @@ -726,6 +740,230 @@ namespace ZL glEnable(GL_DEPTH_TEST); } + // хелпер прицела: добавляет повернутую 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; @@ -896,41 +1134,38 @@ namespace ZL Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity; Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity; - // условие "если враг не движется — круг не рисуем" const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах) bool targetMoving = (targetVel.norm() > minTargetSpeed); + + // альфа круга + float leadAlpha = targetMoving ? 1.0f : 0.5f; Vector3f leadWorld = shipWorld; bool haveLead = false; - //if (targetMoving) { - // float tLead = 0.0f; - // if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { - // // ограничим случаи, чтобы круг не улетал далеко - // if (tLead > 0.0f && tLead < 8.0f) { - // // подобрать максимум (сек) - // leadWorld = shipWorld + targetVel * tLead; - // haveLead = true; - // } - // } - //} + // чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру) + float distToTarget = (Environment::shipState.position - shipWorld).norm(); + float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f); - if (targetMoving) { + if (!targetMoving) { + // Цель стоит: рисуем lead прямо на ней, но полупрозрачный + leadWorld = shipWorld; + haveLead = true; + } + else { float tLead = 0.0f; - float distToTarget = (Environment::shipState.position - shipWorld).norm(); - const float leadMaxDist = 2500.0f; // максимум - float allowedDist = min(distToTarget, leadMaxDist); + // 1) Пытаемся “правильное” решение перехвата + bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead); - // + небольшой запас 10–20% чтобы не моргало на границе - const float maxLeadTime = (allowedDist / projectileSpeed) * 1.2f; - - if (SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead)) { - if (tLead > 0.0f && tLead < maxLeadTime) { - leadWorld = shipWorld + targetVel * tLead; - haveLead = true; - } + // 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок") + // Это ключевое изменение: lead всегда будет. + if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) { + tLead = std::clamp(distToTarget / projectileSpeed, 0.05f, maxLeadTime); } + + leadWorld = shipWorld + targetVel * tLead; + haveLead = true; } // 2) проекция @@ -1006,7 +1241,10 @@ namespace ZL renderer.EnableVertexAttribArray("vPosition"); - // рисуем кружок упреждения (только если есть решение) + Eigen::Vector4f hudColor = enemyColor; + renderer.RenderUniform4fv("uColor", hudColor.data()); + + if (haveLead) { float leadNdcX, leadNdcY, leadNdcZ, leadClipW; if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) { @@ -1021,28 +1259,30 @@ namespace ZL float thicknessPx = 2.5f; float innerR = max(1.0f, r - thicknessPx); float outerR = r + thicknessPx; + Eigen::Vector4f leadColor = enemyColor; + leadColor.w() = leadAlpha; + renderer.RenderUniform4fv("uColor", leadColor.data()); VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor); hudTempMesh.AssignFrom(ring); renderer.DrawVertexRenderStruct(hudTempMesh); + + renderer.RenderUniform4fv("uColor", hudColor.data()); } } } - - // верх-лево: горизонт + вертикаль + renderer.EnableVertexAttribArray("vPosition"); + drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen); - // верх-право drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness); drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen); - // низ-лево drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness); drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen); - // низ-право drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness); drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen); @@ -1060,12 +1300,9 @@ namespace ZL return; } - // 6) Если цель offscreen: рисуем стрелку на краю - // dir: куда “смотреть” в NDC float dirX = ndcX; float dirY = ndcY; - // если позади камеры — разворачиваем направление if (behind) { dirX = -dirX; dirY = -dirY; @@ -1076,7 +1313,6 @@ namespace ZL dirX /= len; dirY /= len; - // пересечение луча с прямоугольником [-1..1] с отступом float marginNdc = 0.08f; float maxX = 1.0f - marginNdc; float maxY = 1.0f - marginNdc; @@ -1091,16 +1327,13 @@ namespace ZL float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width; float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; - // лёгкая анимация “зова”: смещение по направлению float bob = std::sin(t * 6.0f) * 6.0f; edgeX += dirX * bob; edgeY += dirY * bob; - // стрелка как треугольник + маленький “хвост” float arrowLen = 26.0f; float arrowWid = 14.0f; - // перпендикуляр float px = -dirY; float py = dirX; @@ -1139,23 +1372,18 @@ namespace ZL renderer.PushMatrix(); renderer.LoadIdentity(); - // треугольник-стрелка drawTri(tip, left, right); - // “хвост” (короткая черта) float tailLen = 14.0f; float tailX = edgeX - dirX * 6.0f; float tailY = edgeY - dirY * 6.0f; - // хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению: - // (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей) + drawBar(tailX, tailY, max(thickness, tailLen), thickness); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); - // дистанция рядом со стрелкой - // (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText) { std::string d = std::to_string((int)dist) + "m"; float tx = edgeX + px * 18.0f; diff --git a/src/Space.h b/src/Space.h index 36aed51..dde0f69 100644 --- a/src/Space.h +++ b/src/Space.h @@ -139,6 +139,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(); };