diff --git a/resources/dialogue/cutscenes.json b/resources/dialogue/cutscenes.json new file mode 100644 index 0000000..1539188 --- /dev/null +++ b/resources/dialogue/cutscenes.json @@ -0,0 +1,73 @@ +{ + "cutscenes": [ + { + "id": "test_cutscene_01", + "background": "resources/black.png", + "durationMs": 5000, + "fadeOutMs": 500, + "fadeInMs": 500, + "endFadeOutMs": 500, + "endFadeInMs": 500, + "imageSegments": [ + { + "path": "resources/w/cutscenes/cutscene1/cutscene1_wall_x.png", + "startMs": 0, + "endMs": 8000, + "fadeInMs": 300, + "width": 1280, + "height": 720, + "from": { + "centerX": 0.4, + "centerY": 0.5, + "scale": 1.1 + }, + "to": { + "centerX": 0.6, + "centerY": 0.5, + "scale": 1.0 + }, + "easing": "EaseInOutSine" + }, + { + "path": "resources/w/cutscenes/cutscene1/cutscene1_aida1_x.png", + "startMs": 0, + "endMs": 8000, + "width": 1280, + "height": 720, + "from": { + + "centerX": 0.3, + "centerY": 0.5, + "scale": 1.0 + }, + "to": { + "centerX": 0.7, + "centerY": 0.5, + "scale": 1.0 + } + } + ], + "lines": [ + { + "speaker": "Аида Дженибековна", + "portrait": "resources/dialogue/portrait_teacher.png", + "text": "Здравствуйте, студенты. Кого я вижу, где вы были весь семестр?", + "durationMs": 3000 + }, + { + "speaker": "Аида Дженибековна", + "portrait": "resources/dialogue/portrait_teacher.png", + "text": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.", + "durationMs": 3000 + }, + { + "speaker": "Аида Дженибековна", + "portrait": "resources/dialogue/portrait_teacher.png", + "text": "На сегодня лекция завершена. Домашнее задание - к практическому занятию вы должны подготовить презентации, каждый по своей теме.", + "durationMs": 2000, + "background": "resources/test_cutscene001.png" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Location.cpp b/src/Location.cpp index c886f7c..1b5f2fa 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -130,7 +130,7 @@ namespace ZL dialogueSystem.init(renderer, CONST_ZIP_FILE); dialogueSystem.loadDatabase(params.dialoguesJsonPath); - dialogueSystem.loadCutsceneDatabase(params.dialoguesJsonPath); + dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); dialogueSystem.setQuestJournal(journal); npcNameText = std::make_unique(); diff --git a/src/cutscene/CutsceneDatabase.cpp b/src/cutscene/CutsceneDatabase.cpp index 8f01028..8677a5e 100644 --- a/src/cutscene/CutsceneDatabase.cpp +++ b/src/cutscene/CutsceneDatabase.cpp @@ -11,27 +11,18 @@ namespace ZL namespace ZL::Cutscene { EasingType CutsceneDatabase::parseEasingType(const std::string& value) { - if (value == "EaseInSine") return EasingType::EaseInSine; - if (value == "EaseOutSine") return EasingType::EaseOutSine; - if (value == "EaseInOutSine") return EasingType::EaseInOutSine; - if (value == "EaseInQuad") return EasingType::EaseInQuad; - if (value == "EaseOutQuad") return EasingType::EaseOutQuad; - if (value == "EaseInOutQuad") return EasingType::EaseInOutQuad; - if (value == "EaseInCubic") return EasingType::EaseInCubic; - if (value == "EaseOutCubic") return EasingType::EaseOutCubic; + if (value == "EaseInSine") return EasingType::EaseInSine; + if (value == "EaseOutSine") return EasingType::EaseOutSine; + if (value == "EaseInOutSine") return EasingType::EaseInOutSine; + if (value == "EaseInQuad") return EasingType::EaseInQuad; + if (value == "EaseOutQuad") return EasingType::EaseOutQuad; + if (value == "EaseInOutQuad") return EasingType::EaseInOutQuad; + if (value == "EaseInCubic") return EasingType::EaseInCubic; + if (value == "EaseOutCubic") return EasingType::EaseOutCubic; if (value == "EaseInOutCubic") return EasingType::EaseInOutCubic; return EasingType::Linear; } -CutsceneAnchor CutsceneDatabase::parseCutsceneAnchor(const std::string& value) { - if (value == "TopLeft") return CutsceneAnchor::TopLeft; - if (value == "TopRight") return CutsceneAnchor::TopRight; - if (value == "BottomRight") return CutsceneAnchor::BottomRight; - if (value == "BottomLeft") return CutsceneAnchor::BottomLeft; - if (value == "Custom") return CutsceneAnchor::Custom; - return CutsceneAnchor::Center; -} - CutsceneLine CutsceneDatabase::parseCutsceneLine(const json& j) { CutsceneLine line; line.speaker = j.value("speaker", ""); @@ -42,64 +33,50 @@ CutsceneLine CutsceneDatabase::parseCutsceneLine(const json& j) { return line; } -CutsceneCameraPose CutsceneDatabase::parseCutsceneCameraPose(const json& j) { - CutsceneCameraPose pose; - pose.anchor = parseCutsceneAnchor(j.value("anchor", "Center")); - pose.centerX = j.value("centerX", 0.5f); - pose.centerY = j.value("centerY", 0.5f); - pose.zoom = j.value("zoom", 1.0f); - pose.rotationDeg = j.value("rotationDeg", 0.0f); +CutsceneImagePose CutsceneDatabase::parseCutsceneImagePose(const json& j) { + CutsceneImagePose pose; + pose.centerX = j.value("centerX", 0.5f); + pose.centerY = j.value("centerY", 0.5f); + pose.scale = j.value("scale", 1.0f); return pose; } -CutsceneCameraSegment CutsceneDatabase::parseCutsceneCameraSegment(const json& j) { - CutsceneCameraSegment segment; - segment.durationMs = j.value("durationMs", 0); - segment.easing = parseEasingType(j.value("easing", "EaseInOutSine")); +CutsceneImageSegment CutsceneDatabase::parseCutsceneImageSegment(const json& j) { + CutsceneImageSegment seg; + seg.path = j.value("path", ""); + seg.startMs = j.value("startMs", 0); + seg.endMs = j.value("endMs", 0); + seg.fadeInMs = j.value("fadeInMs", 0); + seg.fadeOutMs = j.value("fadeOutMs", 0); + seg.width = j.value("width", 0); + seg.height = j.value("height", 0); + seg.easing = parseEasingType(j.value("easing", "Linear")); if (j.contains("from") && j["from"].is_object()) { - segment.from = parseCutsceneCameraPose(j["from"]); + seg.from = parseCutsceneImagePose(j["from"]); } if (j.contains("to") && j["to"].is_object()) { - segment.to = parseCutsceneCameraPose(j["to"]); + seg.to = parseCutsceneImagePose(j["to"]); } else { - segment.to = segment.from; + seg.to = seg.from; } - return segment; -} - -CutsceneImageCue CutsceneDatabase::parseCutsceneImageCue(const json& j) { - CutsceneImageCue cue; - cue.path = j.value("path", ""); - cue.startMs = j.value("startMs", 0); - cue.endMs = j.value("endMs", 0); - cue.fadeInMs = j.value("fadeInMs", 0); - cue.fadeOutMs = j.value("fadeOutMs", 0); - return cue; + return seg; } StaticCutsceneDefinition CutsceneDatabase::parseCutscene(const json& j) { StaticCutsceneDefinition cutscene; - cutscene.id = j.value("id", ""); - cutscene.background = j.value("background", ""); - cutscene.backgroundWidth = j.value("backgroundWidth", 1280); - cutscene.backgroundHeight= j.value("backgroundHeight", 720); - cutscene.skippable = j.value("skippable", true); - cutscene.durationMs = j.value("durationMs", 0); - cutscene.fadeOutMs = j.value("fadeOutMs", 0); - cutscene.fadeInMs = j.value("fadeInMs", 0); - cutscene.endFadeOutMs = j.value("endFadeOutMs", 0); - cutscene.endFadeInMs = j.value("endFadeInMs", 0); - cutscene.onFadeInCallback= j.value("onFadeInCallback", ""); + cutscene.id = j.value("id", ""); + cutscene.onFadeInCallback = j.value("onFadeInCallback", ""); + cutscene.skippable = j.value("skippable", true); + cutscene.durationMs = j.value("durationMs", 0); + cutscene.fadeOutMs = j.value("fadeOutMs", 0); + cutscene.fadeInMs = j.value("fadeInMs", 0); + cutscene.endFadeOutMs = j.value("endFadeOutMs", 0); + cutscene.endFadeInMs = j.value("endFadeInMs", 0); - if (j.contains("cameraTrack") && j["cameraTrack"].is_array()) { - for (const auto& item : j["cameraTrack"]) { - cutscene.cameraTrack.push_back(parseCutsceneCameraSegment(item)); - } - } - if (j.contains("images") && j["images"].is_array()) { - for (const auto& item : j["images"]) { - cutscene.images.push_back(parseCutsceneImageCue(item)); + if (j.contains("imageSegments") && j["imageSegments"].is_array()) { + for (const auto& item : j["imageSegments"]) { + cutscene.imageSegments.push_back(parseCutsceneImageSegment(item)); } } if (j.contains("lines") && j["lines"].is_array()) { diff --git a/src/cutscene/CutsceneDatabase.h b/src/cutscene/CutsceneDatabase.h index 1e83582..a40a12e 100644 --- a/src/cutscene/CutsceneDatabase.h +++ b/src/cutscene/CutsceneDatabase.h @@ -19,12 +19,10 @@ private: std::unordered_map cutscenes; static EasingType parseEasingType(const std::string& value); - static CutsceneAnchor parseCutsceneAnchor(const std::string& value); static CutsceneLine parseCutsceneLine(const json& j); - static CutsceneCameraPose parseCutsceneCameraPose(const json& j); - static CutsceneCameraSegment parseCutsceneCameraSegment(const json& j); - static CutsceneImageCue parseCutsceneImageCue(const json& j); + static CutsceneImagePose parseCutsceneImagePose(const json& j); + static CutsceneImageSegment parseCutsceneImageSegment(const json& j); static StaticCutsceneDefinition parseCutscene(const json& j); }; diff --git a/src/cutscene/CutsceneOverlay.cpp b/src/cutscene/CutsceneOverlay.cpp index bdb0312..7611160 100644 --- a/src/cutscene/CutsceneOverlay.cpp +++ b/src/cutscene/CutsceneOverlay.cpp @@ -12,13 +12,13 @@ bool CutsceneOverlay::init(Renderer& renderer, const std::string& zipFile) { rendererRef = &renderer; zipFilename = zipFile; - choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile); - choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile); + choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile); + choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile); cutsceneSubtitleTexture = renderer.textureManager.LoadFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile); - nameRenderer = std::make_unique(); - cutsceneRenderer= std::make_unique(); - choiceRenderer = std::make_unique(); + nameRenderer = std::make_unique(); + cutsceneRenderer = std::make_unique(); + choiceRenderer = std::make_unique(); return nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) && @@ -28,12 +28,12 @@ bool CutsceneOverlay::init(Renderer& renderer, const std::string& zipFile) { void CutsceneOverlay::update(const ZL::Dialogue::PresentationModel& model, int deltaMs) { if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) { - cutsceneSkipHintVisible = false; - cutsceneSkipArmed = false; - cutsceneSkipHolding = false; - cutsceneSkipTriggered = false; + cutsceneSkipHintVisible = false; + cutsceneSkipArmed = false; + cutsceneSkipHolding = false; + cutsceneSkipTriggered = false; cutsceneSkipHintRemainingMs = 0; - cutsceneSkipHoldElapsedMs = 0; + cutsceneSkipHoldElapsedMs = 0; return; } @@ -42,24 +42,70 @@ void CutsceneOverlay::update(const ZL::Dialogue::PresentationModel& model, int d if (cutsceneSkipHintVisible) { cutsceneSkipHintRemainingMs -= safeDeltaMs; if (cutsceneSkipHintRemainingMs <= 0) { - cutsceneSkipHintVisible = false; - cutsceneSkipArmed = false; - cutsceneSkipHolding = false; + cutsceneSkipHintVisible = false; + cutsceneSkipArmed = false; + cutsceneSkipHolding = false; cutsceneSkipHintRemainingMs = 0; - cutsceneSkipHoldElapsedMs = 0; + cutsceneSkipHoldElapsedMs = 0; } } if (cutsceneSkipHolding && cutsceneSkipArmed) { cutsceneSkipHoldElapsedMs += safeDeltaMs; if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) { - cutsceneSkipTriggered = true; - cutsceneSkipHolding = false; + cutsceneSkipTriggered = true; + cutsceneSkipHolding = false; cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs; } } } +void CutsceneOverlay::buildImageUV( + const CutsceneImagePose& pose, + float imgW, float imgH, + float screenW, float screenH, + Eigen::Vector2f& outBL, Eigen::Vector2f& outTL, + Eigen::Vector2f& outTR, Eigen::Vector2f& outBR) +{ + const float safeImgW = max(imgW, 1.0f); + const float safeImgH = max(imgH, 1.0f); + const float safeScrnW = max(screenW, 1.0f); + const float safeScrnH = max(screenH, 1.0f); + + const float screenAspect = safeScrnW / safeScrnH; + const float imageAspect = safeImgW / safeImgH; + + // Aspect-ratio corrected base viewport at scale = 1: the portion of the image + // that fills the screen without stretching. + float baseW, baseH; + if (imageAspect >= screenAspect) { + baseH = safeImgH; + baseW = safeImgH * screenAspect; + } + else { + baseW = safeImgW; + baseH = safeImgW / screenAspect; + } + + const float scale = max(pose.scale, 0.01f); + const float viewportW = baseW / scale; + const float viewportH = baseH / scale; + const float halfW = viewportW * 0.5f; + const float halfH = viewportH * 0.5f; + + // Map normalized pose center to image pixel coords; clamp so viewport stays inside image. + const float rawCX = std::clamp(pose.centerX, 0.0f, 1.0f) * safeImgW; + const float rawCY = std::clamp(pose.centerY, 0.0f, 1.0f) * safeImgH; + const float cx = std::clamp(rawCX, halfW, safeImgW - halfW); + const float cy = std::clamp(rawCY, halfH, safeImgH - halfH); + + // Viewport corners in image pixel space, then normalized to UV [0..1]. + outBL = { (cx - halfW) / safeImgW, (cy - halfH) / safeImgH }; + outTL = { (cx - halfW) / safeImgW, (cy + halfH) / safeImgH }; + outTR = { (cx + halfW) / safeImgW, (cy + halfH) / safeImgH }; + outBR = { (cx + halfW) / safeImgW, (cy - halfH) / safeImgH }; +} + void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationModel& model) { if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return; @@ -71,6 +117,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM glEnable(GL_BLEND); + // --- Image layers --- renderer.shaderManager.PushShader("cutsceneFade"); renderer.RenderUniform1i(textureUniformName, 0); renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f); @@ -79,54 +126,17 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM const UiRect screenRect{ 0.0f, 0.0f, W, H }; - std::vector imageLayers = model.cutsceneImages; - if (imageLayers.empty() && !model.backgroundPath.empty()) { - imageLayers.push_back({ model.backgroundPath, 1.0f }); - } - - for (const ZL::Cutscene::PresentedCutsceneImage& layer : imageLayers) { + for (const ZL::Cutscene::PresentedCutsceneImage& layer : model.cutsceneImages) { const auto texture = loadTextureCached(layer.path); if (!texture) continue; - const float imgW = (model.backgroundWidth > 0) ? static_cast(model.backgroundWidth) : static_cast(texture->getWidth()); - const float imgH = (model.backgroundHeight > 0) ? static_cast(model.backgroundHeight) : static_cast(texture->getHeight()); + const float imgW = (layer.width > 0) ? static_cast(layer.width) : static_cast(texture->getWidth()); + const float imgH = (layer.height > 0) ? static_cast(layer.height) : static_cast(texture->getHeight()); - ResolvedViewport layerViewport{}; - if (model.cutsceneCamera.active) { - const ResolvedViewport fromVP = resolveViewportPose(model.cutsceneCamera.from, imgW, imgH, W, H); - const ResolvedViewport toVP = resolveViewportPose(model.cutsceneCamera.to, imgW, imgH, W, H); - layerViewport = blendViewport(fromVP, toVP, std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f)); - } - else { - layerViewport = resolveViewportPose(ZL::Cutscene::CutsceneCameraPose{}, imgW, imgH, W, H); - } + Eigen::Vector2f uvBL, uvTL, uvTR, uvBR; + buildImageUV(layer.pose, imgW, imgH, W, H, uvBL, uvTL, uvTR, uvBR); - const float halfW = layerViewport.widthPx * 0.5f; - const float halfH = layerViewport.heightPx * 0.5f; - const float rotRad = layerViewport.rotationDeg * 3.14159265358979323846f / 180.0f; - const float c = std::cos(rotRad); - const float s = std::sin(rotRad); - - auto rotatePoint = [&](float x, float y) -> Eigen::Vector2f { - return { - layerViewport.centerXPx + x * c - y * s, - layerViewport.centerYPx + x * s + y * c - }; - }; - - const Eigen::Vector2f srcBL = rotatePoint(-halfW, -halfH); - const Eigen::Vector2f srcTL = rotatePoint(-halfW, +halfH); - const Eigen::Vector2f srcTR = rotatePoint(+halfW, +halfH); - const Eigen::Vector2f srcBR = rotatePoint(+halfW, -halfH); - - auto toUV = [&](const Eigen::Vector2f& p) -> Eigen::Vector2f { - return { - std::clamp(p.x() / max(imgW, 1.0f), 0.0f, 1.0f), - std::clamp(p.y() / max(imgH, 1.0f), 0.0f, 1.0f) - }; - }; - - backgroundQuad.rebuildWithUV(screenRect, toUV(srcBL), toUV(srcTL), toUV(srcTR), toUV(srcBR)); + backgroundQuad.rebuildWithUV(screenRect, uvBL, uvTL, uvTR, uvBR); renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha * model.cutsceneGlobalFadeAlpha, 0.0f, 1.0f)); glBindTexture(GL_TEXTURE_2D, texture->getTexID()); renderer.DrawVertexRenderStruct(backgroundQuad.mesh); @@ -136,6 +146,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); + // --- Black fade overlay --- if (model.cutsceneBlackAlpha > 0.001f) { renderer.shaderManager.PushShader("cutsceneBlack"); renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f); @@ -151,6 +162,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM renderer.shaderManager.PopShader(); } + // --- UI overlay: subtitle panel and skip hint --- renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f); @@ -189,6 +201,7 @@ void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationM renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); + // --- Text --- if (model.showCutsceneSubtitle) { if (!model.speaker.empty()) { nameRenderer->drawText( @@ -218,11 +231,11 @@ bool CutsceneOverlay::consumeSkipRequested() { const bool result = cutsceneSkipTriggered; cutsceneSkipTriggered = false; if (result) { - cutsceneSkipHintVisible = false; - cutsceneSkipArmed = false; - cutsceneSkipHolding = false; + cutsceneSkipHintVisible = false; + cutsceneSkipArmed = false; + cutsceneSkipHolding = false; cutsceneSkipHintRemainingMs = 0; - cutsceneSkipHoldElapsedMs = 0; + cutsceneSkipHoldElapsedMs = 0; } return result; } @@ -232,20 +245,20 @@ void CutsceneOverlay::handlePointerDown(float x, float y, const ZL::Dialogue::Pr if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) return; if (!cutsceneSkipArmed) { - cutsceneSkipHintVisible = true; - cutsceneSkipArmed = true; - cutsceneSkipHolding = false; - cutsceneSkipTriggered = false; + cutsceneSkipHintVisible = true; + cutsceneSkipArmed = true; + cutsceneSkipHolding = false; + cutsceneSkipTriggered = false; cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs; - cutsceneSkipHoldElapsedMs = 0; + cutsceneSkipHoldElapsedMs = 0; return; } - cutsceneSkipHintVisible = true; + cutsceneSkipHintVisible = true; cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs; - cutsceneSkipHolding = true; - cutsceneSkipTriggered = false; - cutsceneSkipHoldElapsedMs = 0; + cutsceneSkipHolding = true; + cutsceneSkipTriggered = false; + cutsceneSkipHoldElapsedMs = 0; } void CutsceneOverlay::handlePointerMoved(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& /*model*/) { @@ -254,7 +267,7 @@ void CutsceneOverlay::handlePointerMoved(float /*x*/, float /*y*/, const ZL::Dia bool CutsceneOverlay::handlePointerReleased(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& model) { if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return false; if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) { - cutsceneSkipHolding = false; + cutsceneSkipHolding = false; cutsceneSkipHoldElapsedMs = 0; } return true; @@ -265,103 +278,6 @@ std::shared_ptr CutsceneOverlay::loadTextureCached(const std::string& p return rendererRef->textureManager.LoadFromPng(path, zipFilename); } -float CutsceneOverlay::lerpFloat(float a, float b, float t) { - return a + (b - a) * t; -} - -CutsceneOverlay::ResolvedViewport CutsceneOverlay::resolveViewportPose( - const CutsceneCameraPose& pose, - float texW, float texH, - float screenW, float screenH -) { - ResolvedViewport out{}; - - const float safeTexW = max(texW, 1.0f); - const float safeTexH = max(texH, 1.0f); - const float safeScreenW = max(screenW, 1.0f); - const float safeScreenH = max(screenH, 1.0f); - - const float screenAspect = safeScreenW / safeScreenH; - const float imageAspect = safeTexW / safeTexH; - - float baseViewportW = 0.0f; - float baseViewportH = 0.0f; - - if (imageAspect >= screenAspect) { - baseViewportH = safeTexH; - baseViewportW = safeTexH * screenAspect; - } - else { - baseViewportW = safeTexW; - baseViewportH = safeTexW / screenAspect; - } - - const float zoom = max(pose.zoom, 0.01f); - const float viewportW = baseViewportW / zoom; - const float viewportH = baseViewportH / zoom; - - const float rotRad = pose.rotationDeg * 3.14159265358979323846f / 180.0f; - const float c = std::cos(rotRad); - const float s = std::sin(rotRad); - - const float halfRotatedW = std::abs((viewportW * 0.5f) * c) + std::abs((viewportH * 0.5f) * s); - const float halfRotatedH = std::abs((viewportW * 0.5f) * s) + std::abs((viewportH * 0.5f) * c); - - float centerX = safeTexW * 0.5f; - float centerY = safeTexH * 0.5f; - - switch (pose.anchor) { - case CutsceneAnchor::TopLeft: - centerX = halfRotatedW; - centerY = safeTexH - halfRotatedH; - break; - case CutsceneAnchor::TopRight: - centerX = safeTexW - halfRotatedW; - centerY = safeTexH - halfRotatedH; - break; - case CutsceneAnchor::BottomRight: - centerX = safeTexW - halfRotatedW; - centerY = halfRotatedH; - break; - case CutsceneAnchor::BottomLeft: - centerX = halfRotatedW; - centerY = halfRotatedH; - break; - case CutsceneAnchor::Custom: - centerX = std::clamp(pose.centerX, 0.0f, 1.0f) * safeTexW; - centerY = (1.0f - std::clamp(pose.centerY, 0.0f, 1.0f)) * safeTexH; - centerX = std::clamp(centerX, halfRotatedW, safeTexW - halfRotatedW); - centerY = std::clamp(centerY, halfRotatedH, safeTexH - halfRotatedH); - break; - case CutsceneAnchor::Center: - default: - centerX = safeTexW * 0.5f; - centerY = safeTexH * 0.5f; - break; - } - - out.centerXPx = centerX; - out.centerYPx = centerY; - out.widthPx = viewportW; - out.heightPx = viewportH; - out.rotationDeg = pose.rotationDeg; - return out; -} - -CutsceneOverlay::ResolvedViewport CutsceneOverlay::blendViewport( - const ResolvedViewport& from, - const ResolvedViewport& to, - float t -) { - ResolvedViewport out; - out.centerXPx = lerpFloat(from.centerXPx, to.centerXPx, t); - out.centerYPx = lerpFloat(from.centerYPx, to.centerYPx, t); - out.widthPx = lerpFloat(from.widthPx, to.widthPx, t); - out.heightPx = lerpFloat(from.heightPx, to.heightPx, t); - out.rotationDeg = lerpFloat(from.rotationDeg, to.rotationDeg, t); - return out; -} - std::string CutsceneOverlay::wrapTextToWidth( const std::string& input, const TextRenderer& textRenderer, @@ -395,8 +311,7 @@ std::string CutsceneOverlay::wrapTextToWidth( } }; - for (size_t i = 0; i < input.size(); ++i) { - const char ch = input[i]; + for (const char ch : input) { if (ch == '\n') { pushWord(currentWord); currentWord.clear(); flushLine(); continue; } if (ch == ' ' || ch == '\t' || ch == '\r') { pushWord(currentWord); currentWord.clear(); continue; } currentWord.push_back(ch); diff --git a/src/cutscene/CutsceneOverlay.h b/src/cutscene/CutsceneOverlay.h index dbc154c..937a50a 100644 --- a/src/cutscene/CutsceneOverlay.h +++ b/src/cutscene/CutsceneOverlay.h @@ -25,14 +25,6 @@ public: bool consumeSkipRequested(); private: - struct ResolvedViewport { - float centerXPx = 0.0f; - float centerYPx = 0.0f; - float widthPx = 1.0f; - float heightPx = 1.0f; - float rotationDeg = 0.0f; - }; - Renderer* rendererRef = nullptr; std::string zipFilename; @@ -41,7 +33,6 @@ private: std::shared_ptr cutsceneSubtitleTexture; mutable UiRect lastCutsceneAdvanceRect{}; - mutable UiRect lastCutsceneSkipRect{}; // Skip UX state bool cutsceneSkipHintVisible = false; @@ -65,17 +56,15 @@ private: std::shared_ptr loadTextureCached(const std::string& path); - static float lerpFloat(float a, float b, float t); - static ResolvedViewport resolveViewportPose( - const CutsceneCameraPose& pose, - float texW, float texH, - float screenW, float screenH - ); - static ResolvedViewport blendViewport( - const ResolvedViewport& from, - const ResolvedViewport& to, - float t + // Computes UV corners for one image layer given its pose and actual texture size. + static void buildImageUV( + const CutsceneImagePose& pose, + float imgW, float imgH, + float screenW, float screenH, + Eigen::Vector2f& outBL, Eigen::Vector2f& outTL, + Eigen::Vector2f& outTR, Eigen::Vector2f& outBR ); + static std::string wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale); }; diff --git a/src/cutscene/CutsceneRuntime.cpp b/src/cutscene/CutsceneRuntime.cpp index f9fa621..1532e05 100644 --- a/src/cutscene/CutsceneRuntime.cpp +++ b/src/cutscene/CutsceneRuntime.cpp @@ -41,21 +41,11 @@ bool CutsceneRuntime::start(const std::string& cutsceneId) { cutsceneTimerMs = 0; currentCutsceneLine = def->lines.empty() ? -1 : 0; - int imageTrackDurationMs = 0; - for (size_t i = 0; i < def->images.size(); ++i) { - const CutsceneImageCue& cue = def->images[i]; - int cueEnd = cue.endMs; - if (cueEnd <= cue.startMs) { - if (i + 1 < def->images.size()) { - cueEnd = std::max(def->images[i + 1].startMs, cue.startMs); - } - else { - cueEnd = cue.startMs + std::max(cue.fadeInMs, 0) + std::max(cue.fadeOutMs, 0) + 1000; - } - } - imageTrackDurationMs = std::max(imageTrackDurationMs, cueEnd); + int maxSegmentEndMs = 0; + for (const CutsceneImageSegment& seg : def->imageSegments) { + maxSegmentEndMs = std::max(maxSegmentEndMs, seg.endMs); } - cutsceneContentDurationMs = std::max({ def->durationMs, computeCameraTrackDurationMs(*def), imageTrackDurationMs }); + cutsceneContentDurationMs = std::max(def->durationMs, maxSegmentEndMs); if (cutsceneContentDurationMs <= 0 && def->lines.empty()) { cutsceneContentDurationMs = 3000; } @@ -155,25 +145,6 @@ bool CutsceneRuntime::canSkip() const { void CutsceneRuntime::skip() { if (!canSkip()) return; - - if (!activeCutscene->images.empty()) { - int nextImageStartMs = -1; - for (const CutsceneImageCue& cue : activeCutscene->images) { - if (cue.path.empty()) continue; - if (cue.startMs > cutsceneElapsedMs) { - if (nextImageStartMs < 0 || cue.startMs < nextImageStartMs) { - nextImageStartMs = cue.startMs; - } - } - } - if (nextImageStartMs >= 0) { - cutsceneElapsedMs = nextImageStartMs; - syncLineToElapsedTime(); - refreshPresentation(); - return; - } - } - finish(); } @@ -215,15 +186,10 @@ void CutsceneRuntime::syncLineToElapsedTime() { } void CutsceneRuntime::advanceLine() { - if (!activeCutscene) { - stop(); - return; - } + if (!activeCutscene) { stop(); return; } if (activeCutscene->lines.empty()) return; - std::cout << "[CUTSCENE] advance before current=" << currentCutsceneLine << std::endl; ++currentCutsceneLine; - std::cout << "[CUTSCENE] advance after current=" << currentCutsceneLine << std::endl; cutsceneTimerMs = 0; if (currentCutsceneLine >= static_cast(activeCutscene->lines.size())) { @@ -243,99 +209,43 @@ void CutsceneRuntime::advanceLine() { refreshPresentation(); } -CutsceneCameraBlendState CutsceneRuntime::evaluateCameraBlend() const { - CutsceneCameraBlendState result; - result.active = false; - result.from = {}; - result.to = {}; - result.t = 1.0f; - - if (!activeCutscene || activeCutscene->cameraTrack.empty()) { - return result; - } - - int elapsed = cutsceneElapsedMs; - for (const CutsceneCameraSegment& segment : activeCutscene->cameraTrack) { - const int durationMs = std::max(segment.durationMs, 1); - if (elapsed <= durationMs) { - result.active = true; - result.from = segment.from; - result.to = segment.to; - result.t = applyEasing( - segment.easing, - std::clamp(static_cast(elapsed) / static_cast(durationMs), 0.0f, 1.0f) - ); - return result; - } - elapsed -= durationMs; - } - - result.active = true; - result.from = activeCutscene->cameraTrack.back().to; - result.to = activeCutscene->cameraTrack.back().to; - result.t = 1.0f; - return result; -} - std::vector CutsceneRuntime::evaluateImages() const { std::vector result; if (!activeCutscene) return result; - const std::string& fallbackPath = activeCutscene->background; - - if (activeCutscene->images.empty()) { - if (!fallbackPath.empty()) { - result.push_back({ fallbackPath, 1.0f }); - } - return result; - } - - const int effectiveTotalDuration = (cutsceneContentDurationMs > 0) - ? cutsceneContentDurationMs - : std::max(activeCutscene->durationMs, 1); const int now = std::max(cutsceneElapsedMs, 0); - for (size_t i = 0; i < activeCutscene->images.size(); ++i) { - const CutsceneImageCue& cue = activeCutscene->images[i]; - if (cue.path.empty()) continue; - - const int startMs = std::max(cue.startMs, 0); - int endMs = cue.endMs; - if (endMs <= startMs) { - if (i + 1 < activeCutscene->images.size()) { - endMs = std::max(activeCutscene->images[i + 1].startMs, startMs + 1); - } - else { - endMs = effectiveTotalDuration; - } - } - if (endMs <= startMs) { - endMs = startMs + 1; - } - - if (now < startMs || now > endMs) continue; + for (const CutsceneImageSegment& seg : activeCutscene->imageSegments) { + if (seg.path.empty()) continue; + if (now < seg.startMs || now > seg.endMs) continue; + // Fade-in / fade-out alpha float alpha = 1.0f; - if (cue.fadeInMs > 0 && now < startMs + cue.fadeInMs) { + if (seg.fadeInMs > 0 && now < seg.startMs + seg.fadeInMs) { alpha = std::clamp( - static_cast(now - startMs) / static_cast(cue.fadeInMs), + static_cast(now - seg.startMs) / static_cast(seg.fadeInMs), 0.0f, 1.0f ); } - - if (alpha > 0.0f) { - result.push_back({ cue.path, alpha }); + if (seg.fadeOutMs > 0 && now > seg.endMs - seg.fadeOutMs) { + const float fadeOutAlpha = std::clamp( + static_cast(seg.endMs - now) / static_cast(seg.fadeOutMs), + 0.0f, 1.0f + ); + alpha = std::min(alpha, fadeOutAlpha); } - } - if (result.empty() && !fallbackPath.empty()) { - result.push_back({ fallbackPath, 1.0f }); - } + // Interpolated pose + const float segDuration = static_cast(std::max(seg.endMs - seg.startMs, 1)); + const float rawT = static_cast(now - seg.startMs) / segDuration; + const float easedT = applyEasing(seg.easing, std::clamp(rawT, 0.0f, 1.0f)); - if (!result.empty() && result.front().alpha < 0.999f && - !fallbackPath.empty() && result.front().path != fallbackPath) - { - result.insert(result.begin(), { fallbackPath, 1.0f }); + CutsceneImagePose pose; + pose.centerX = seg.from.centerX + (seg.to.centerX - seg.from.centerX) * easedT; + pose.centerY = seg.from.centerY + (seg.to.centerY - seg.from.centerY) * easedT; + pose.scale = seg.from.scale + (seg.to.scale - seg.from.scale) * easedT; + + result.push_back({ seg.path, alpha, pose, seg.width, seg.height }); } return result; @@ -345,12 +255,8 @@ void CutsceneRuntime::refreshPresentation() { if (!activeCutscene) return; presentation.mode = ZL::Dialogue::PresentationMode::Cutscene; - presentation.backgroundPath = activeCutscene->background; - presentation.backgroundWidth = activeCutscene->backgroundWidth; - presentation.backgroundHeight = activeCutscene->backgroundHeight; - presentation.cutsceneCamera = evaluateCameraBlend(); - presentation.cutsceneImages = evaluateImages(); presentation.cutsceneSkippable = activeCutscene->skippable; + presentation.cutsceneImages = evaluateImages(); const int fadeOutMs = activeCutscene->fadeOutMs; const int fadeInMs = activeCutscene->fadeInMs; @@ -400,20 +306,14 @@ void CutsceneRuntime::refreshPresentation() { presentation.speaker.clear(); presentation.fullText.clear(); presentation.visibleText.clear(); - presentation.portraitPath.clear(); return; } const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine]; - presentation.speaker = line.speaker; presentation.fullText = line.text; presentation.visibleText = line.text; presentation.selectedChoice = 0; - - std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size() - << " current=" << currentCutsceneLine - << std::endl; } float CutsceneRuntime::applyEasing(EasingType easing, float t) { @@ -453,12 +353,4 @@ int CutsceneRuntime::computeFallbackDurationMs(const std::string& text) { return std::max(minDuration, calculated + linger); } -int CutsceneRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene) { - int total = 0; - for (const CutsceneCameraSegment& segment : cutscene.cameraTrack) { - total += std::max(segment.durationMs, 0); - } - return total; -} - } // namespace ZL::Cutscene diff --git a/src/cutscene/CutsceneRuntime.h b/src/cutscene/CutsceneRuntime.h index 2b0444d..758bde7 100644 --- a/src/cutscene/CutsceneRuntime.h +++ b/src/cutscene/CutsceneRuntime.h @@ -50,12 +50,10 @@ private: void advanceLine(); void refreshPresentation(); - CutsceneCameraBlendState evaluateCameraBlend() const; std::vector evaluateImages() const; static float applyEasing(EasingType easing, float t); static int computeFallbackDurationMs(const std::string& text); - static int computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene); }; } // namespace ZL::Cutscene diff --git a/src/cutscene/CutsceneTypes.h b/src/cutscene/CutsceneTypes.h index c11a272..7026f4f 100644 --- a/src/cutscene/CutsceneTypes.h +++ b/src/cutscene/CutsceneTypes.h @@ -18,15 +18,6 @@ enum class EasingType { EaseInOutCubic }; -enum class CutsceneAnchor { - Center, - TopLeft, - TopRight, - BottomRight, - BottomLeft, - Custom -}; - struct CutsceneLine { std::string speaker; std::string text; @@ -35,56 +26,53 @@ struct CutsceneLine { bool waitForConfirm = false; }; -struct CutsceneCameraPose { - CutsceneAnchor anchor = CutsceneAnchor::Center; +// Describes where/how an image is framed on screen. +// centerX/Y: 0..1 normalized over the image; 0.5/0.5 = center of image fills center of screen. +// scale: 1 = image fits screen (aspect-corrected), 2 = zoomed in 2x. +struct CutsceneImagePose { float centerX = 0.5f; float centerY = 0.5f; - float zoom = 1.0f; - float rotationDeg = 0.0f; + float scale = 1.0f; }; -struct CutsceneCameraSegment { - int durationMs = 0; - CutsceneCameraPose from; - CutsceneCameraPose to; - EasingType easing = EasingType::EaseInOutSine; -}; - -struct CutsceneImageCue { +// One image layer: defines path, active time window, fades, and animated movement. +struct CutsceneImageSegment { std::string path; - int startMs = 0; - int endMs = 0; - int fadeInMs = 0; + int startMs = 0; + int endMs = 0; + int fadeInMs = 0; int fadeOutMs = 0; + // Logical size used for all UV/aspect calculations. + // 0 = use actual texture pixel dimensions. + int width = 0; + int height = 0; + CutsceneImagePose from; + CutsceneImagePose to; + EasingType easing = EasingType::Linear; }; struct StaticCutsceneDefinition { std::string id; - std::string background; - int backgroundWidth = 1280; - int backgroundHeight = 720; std::string onFadeInCallback; bool skippable = true; - int durationMs = 0; - int fadeOutMs = 0; - int fadeInMs = 0; - int endFadeOutMs = 0; - int endFadeInMs = 0; - std::vector cameraTrack; - std::vector images; + int durationMs = 0; + int fadeOutMs = 0; + int fadeInMs = 0; + int endFadeOutMs = 0; + int endFadeInMs = 0; + std::vector imageSegments; std::vector lines; }; +// A single image layer at an evaluated point in time, ready for rendering. struct PresentedCutsceneImage { std::string path; float alpha = 1.0f; -}; - -struct CutsceneCameraBlendState { - bool active = false; - CutsceneCameraPose from; - CutsceneCameraPose to; - float t = 1.0f; + CutsceneImagePose pose; + // Logical size for UV math — mirrors CutsceneImageSegment::width/height. + // 0 = use actual texture pixel dimensions. + int width = 0; + int height = 0; }; } // namespace ZL::Cutscene diff --git a/src/dialogue/DialogueRuntime.cpp b/src/dialogue/DialogueRuntime.cpp index d51f180..eecf261 100644 --- a/src/dialogue/DialogueRuntime.cpp +++ b/src/dialogue/DialogueRuntime.cpp @@ -299,13 +299,13 @@ void DialogueRuntime::presentLine(const Node& node) { presentation.fullText = node.text; presentation.visibleText.clear(); presentation.portraitPath = node.portrait; - presentation.backgroundPath.clear(); + presentation.choices.clear(); presentation.selectedChoice = -1; presentation.revealCompleted = node.text.empty(); presentation.showCutsceneSubtitle = false; presentation.cutsceneSkippable = false; - presentation.cutsceneCamera = {}; + presentation.cutsceneImages.clear(); presentation.cutsceneGlobalFadeAlpha = 1.0f; presentation.cutsceneBlackAlpha = 0.0f; @@ -353,12 +353,12 @@ void DialogueRuntime::presentChoices(const Node& node) { presentation.fullText = node.text; presentation.visibleText = node.text; presentation.portraitPath = node.portrait; - presentation.backgroundPath.clear(); + presentation.selectedChoice = -1; presentation.revealCompleted = true; presentation.showCutsceneSubtitle = false; presentation.cutsceneSkippable = false; - presentation.cutsceneCamera = {}; + presentation.cutsceneImages.clear(); presentation.cutsceneGlobalFadeAlpha = 1.0f; presentation.cutsceneBlackAlpha = 0.0f; diff --git a/src/dialogue/DialogueTypes.h b/src/dialogue/DialogueTypes.h index ce5da6f..1424825 100644 --- a/src/dialogue/DialogueTypes.h +++ b/src/dialogue/DialogueTypes.h @@ -115,19 +115,15 @@ struct PresentationModel { std::string fullText; std::string visibleText; std::string portraitPath; - std::string backgroundPath; std::vector choices; int selectedChoice = -1; bool revealCompleted = true; bool showCutsceneSubtitle = false; bool cutsceneSkippable = false; - ZL::Cutscene::CutsceneCameraBlendState cutsceneCamera; std::vector cutsceneImages; float cutsceneGlobalFadeAlpha = 1.0f; float cutsceneBlackAlpha = 0.0f; - int backgroundWidth = 1280; - int backgroundHeight = 720; }; } // namespace ZL::Dialogue