diff options
Diffstat (limited to 'source/frontend/StarCinematic.cpp')
-rw-r--r-- | source/frontend/StarCinematic.cpp | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/source/frontend/StarCinematic.cpp b/source/frontend/StarCinematic.cpp new file mode 100644 index 0000000..a7a13ee --- /dev/null +++ b/source/frontend/StarCinematic.cpp @@ -0,0 +1,562 @@ +#include "StarCinematic.hpp" +#include "StarJsonExtra.hpp" +#include "StarRoot.hpp" +#include "StarWorldClient.hpp" +#include "StarAssets.hpp" +#include "StarGuiContext.hpp" +#include "StarPlayer.hpp" + +namespace Star { + +const float vWidth = 960.0f; +const float vHeight = 540.0f; + +Cinematic::Cinematic() { + m_completionTime = 0; + m_completable = false; + m_suppressInput = false; +} + +void Cinematic::load(Json const& definition) { + stop(); + + for (auto timeSkipDefinition : definition.getArray("timeSkips", JsonArray())) { + TimeSkip timeSkip; + timeSkip.availableTime = timeSkipDefinition.getFloat("available"); + timeSkip.skipToTime = timeSkipDefinition.getFloat("skipTo"); + m_timeSkips.append(timeSkip); + } + sort(m_timeSkips, [](TimeSkip const& a, TimeSkip const& b) -> bool { + return a.availableTime < b.availableTime; + }); + + for (auto cameraDefinition : definition.getArray("camera", JsonArray())) { + CameraKeyFrame keyFrame; + keyFrame.timecode = cameraDefinition.getFloat("timecode"); + keyFrame.zoom = cameraDefinition.getFloat("zoom", 1.0f); + if (cameraDefinition.contains("pan")) + keyFrame.pan = jsonToVec2F(cameraDefinition.get("pan")); + m_cameraKeyFrames.append(keyFrame); + } + + for (auto panelDefinition : definition.getArray("panels")) { + PanelPtr panel = make_shared<Panel>(); + panel->useCamera = panelDefinition.getBool("useCamera", true); + panel->drawables = panelDefinition.getArray("drawables", {}); + panel->animationFrames = panelDefinition.getInt("animationFrames", std::numeric_limits<int>::max()); + panel->text = panelDefinition.getString("text", ""); + panel->textPosition = TextPositioning(panelDefinition.get("textPosition", JsonObject())); + panel->fontColor = panelDefinition.opt("fontColor").apply(jsonToVec4B).value(Vec4B(255, 255, 255, 255)); + panel->fontSize = panelDefinition.getUInt("fontSize", 8); + panel->avatar = panelDefinition.getString("avatar", ""); + panel->startTime = panelDefinition.getFloat("startTime", 0); + panel->endTime = panelDefinition.getFloat("endTime", 0); + panel->loopTime = panelDefinition.getFloat("loopTime", 0); + for (auto keyframeDefinition : panelDefinition.getArray("keyframes")) { + KeyFrame keyframe; + keyframe.timecode = keyframeDefinition.getFloat("timecode"); + keyframe.command = keyframeDefinition; + panel->keyFrames.append(keyframe); + float endTimecode = panel->endTime > 0 ? std::min(panel->endTime, panel->startTime + keyframe.timecode) : panel->startTime + keyframe.timecode; + m_completionTime = std::max(m_completionTime, endTimecode); + } + m_panels.append(panel); + } + + for (auto audioDefinition : definition.getArray("audio")) { + AudioCue cue; + cue.timecode = audioDefinition.getFloat("timecode"); + cue.loops = audioDefinition.getInt("loops", 0); + cue.endTimecode = audioDefinition.getFloat("endTimecode", 0); + cue.resource = audioDefinition.getString("resource"); + m_audioCues.append(cue); + m_completionTime = std::max(m_completionTime, cue.timecode); + m_completionTime = std::max(m_completionTime, cue.endTimecode); + } + m_activeAudio = std::vector<AudioInstancePtr>(m_audioCues.size()); + + if (definition.contains("offset")) + m_offset = jsonToVec2F(definition.get("offset")); + else + m_offset = {}; + + if (definition.contains("backgroundColor")) + m_backgroundColor = jsonToVec4B(definition.get("backgroundColor")); + else + m_backgroundColor = {}; + m_backgroundFadeTime = definition.getFloat("backgroundFadeTime", 0); + + m_completionTime += 2 * m_backgroundFadeTime; + + m_scissor = definition.getBool("scissor", true); + m_letterbox = definition.getBool("letterbox", true); + m_skippable = definition.getBool("skippable", true); + m_suppressInput = definition.getBool("suppressInput", true); + m_muteSfx = definition.getBool("muteSfx", false); + m_muteMusic = definition.getBool("muteMusic", false); + + m_timer.reset(); + m_timer.start(); +} + +void Cinematic::setPlayer(PlayerPtr player) { + m_player = player; +} + +void Cinematic::update() { + m_currentTimeSkip = {}; + for (auto timeSkip : m_timeSkips) { + if (currentTimecode() >= timeSkip.availableTime && currentTimecode() < timeSkip.skipToTime) + m_currentTimeSkip = timeSkip; + } +} + +bool Cinematic::completed() const { + for (size_t i = 0; i < m_audioCues.size(); ++i) { + if (m_activeAudio[i] && !m_activeAudio[i]->finished()) + return false; + } + + return m_timer.time() >= m_completionTime; +} + +bool Cinematic::completable() const { + return m_completable; +} + +void Cinematic::render() { + if (completed()) + return; + + auto& guiContext = GuiContext::singleton(); + auto mixer = guiContext.mixer(); + auto renderer = guiContext.renderer(); + auto textPainter = guiContext.textPainter(); + + m_windowSize = Vec2F(renderer->screenSize()); + Vec2F screenWindowSize = Vec2F(vWidth * m_drawableScale, vHeight * m_drawableScale); + m_drawableScale = std::min(m_windowSize[0] / vWidth, m_windowSize[1] / vHeight); + m_drawableTranslation = Vec2F(0.5f * (m_windowSize[0] - vWidth * m_drawableScale), 0.5f * (m_windowSize[1] - vHeight * m_drawableScale)); + m_scissorRect = RectI::withSize(Vec2I::floor((m_windowSize / 2) - (screenWindowSize / 2)), Vec2I::ceil(screenWindowSize)); + + updateCamera(m_timer.time()); + + float fadeFactor = 1.0; + if (m_backgroundFadeTime > 0) { + if (m_timer.time() < m_backgroundFadeTime) + fadeFactor = m_timer.time() / m_backgroundFadeTime; + else if (m_completionTime - m_timer.time() < m_backgroundFadeTime) + fadeFactor = max<float>(0.0f, m_completionTime - m_timer.time()) / m_backgroundFadeTime; + } + + if (m_backgroundColor) { + Vec4B backgroundColor = m_backgroundColor.get(); + backgroundColor[3] *= fadeFactor; + renderer->render(renderFlatRect(RectF::withSize(Vec2F(0, 0), m_windowSize), backgroundColor, 0.0f)); + } + + if (m_letterbox && !m_backgroundColor) { + Vec4B letterboxColor = Vec4B(0, 0, 0, 255 * fadeFactor); + if (m_windowSize[0] / vWidth > m_windowSize[1] / vHeight) { + renderer->render(renderFlatRect(RectF(0, 0, m_scissorRect.xMin(), m_windowSize[1]), letterboxColor, 0.0f)); + renderer->render(renderFlatRect(RectF(m_scissorRect.xMax(), 0, m_windowSize[0], m_windowSize[1]), letterboxColor, 0.0f)); + } else { + renderer->render(renderFlatRect(RectF(0, 0, m_windowSize[0], m_scissorRect.yMin()), letterboxColor, 0.0f)); + renderer->render(renderFlatRect(RectF(0, m_scissorRect.yMax(), m_windowSize[0], m_windowSize[1]), letterboxColor, 0.0f)); + } + } + + if (fadeFactor < 1.0f) + return; + + if (m_scissor) + renderer->setScissorRect(m_scissorRect); + + String playerSpecies = ""; + if (m_player) + playerSpecies = m_player->species(); + + for (auto panel : m_panels) { + float drawableScale = m_drawableScale; + Vec2F drawableTranslation = m_drawableTranslation; + if (panel->useCamera) { + drawableScale *= m_cameraZoom; + drawableTranslation += m_cameraPan * drawableScale; + } + + auto values = determinePanelValues(panel, currentTimecode()); + if (values.completable) + m_completable = true; + if (!values.alpha) + continue; + auto frame = strf("%s", ((int)values.frame) % panel->animationFrames); + auto alphaColor = Color::rgbaf(1.0f, 1.0f, 1.0f, values.alpha); + for (auto const& d : panel->drawables) { + Drawable drawable = Drawable(d.set("image", d.getString("image").replaceTags(StringMap<String>{{"species", playerSpecies}, {"frame", frame}}))); + drawable.translate(m_offset); + drawable.scale(values.zoom); + drawable.translate(values.position); + drawable.color *= alphaColor; + drawDrawable(move(drawable), drawableScale, drawableTranslation); + } + if (!panel->avatar.empty() && m_player) { + for (auto drawable : m_player->portrait(PortraitModeNames.getLeft(panel->avatar))) { + drawable.translate(m_offset); + drawable.scale(values.zoom); + drawable.translate(values.position); + drawable.color *= alphaColor; + drawDrawable(move(drawable), drawableScale, drawableTranslation); + } + } + if (!panel->text.empty()) { + textPainter->setFontSize(floor(panel->fontSize * drawableScale)); + auto fontColor = panel->fontColor; + fontColor[3] *= values.alpha; + textPainter->setFontColor(fontColor); + Vec2F position = (m_offset + values.position + Vec2F(panel->textPosition.pos)) * drawableScale + drawableTranslation; + TextPositioning tp = TextPositioning(position, panel->textPosition.hAnchor, panel->textPosition.vAnchor, {}, {}); + if (panel->textPosition.wrapWidth) + tp.wrapWidth = floor(panel->textPosition.wrapWidth.get() * drawableScale); + if (values.textPercentage < 1.0) + tp.charLimit = floor(panel->text.length() * values.textPercentage); + textPainter->renderText(panel->text, tp); + } + } + + if (m_scissor) + renderer->setScissorRect({}); + + for (size_t i = 0; i < m_audioCues.size(); ++i) { + if (m_audioCues[i].endTimecode > 0 && m_audioCues[i].endTimecode <= currentTimecode()) { + if (!m_activeAudio[i]) + continue; + m_activeAudio[i]->stop(); + } else if (m_audioCues[i].timecode <= currentTimecode()) { + if (m_activeAudio[i]) + continue; + AudioInstancePtr audioInstance = make_shared<AudioInstance>(*Root::singleton().assets()->audio(m_audioCues[i].resource)); + audioInstance->setLoops(m_audioCues[i].loops); + audioInstance->setMixerGroup(MixerGroup::Cinematic); + mixer->play(audioInstance); + m_activeAudio[i] = audioInstance; + } + } +} + +void Cinematic::drawDrawable(Drawable const& drawable, float drawableScale, Vec2F const& drawableTranslation) { + auto& guiContext = GuiContext::singleton(); + auto renderer = guiContext.renderer(); + auto textureGroup = guiContext.assetTextureGroup(); + + if (drawable.isImage()) { + auto const& imagePart = drawable.imagePart(); + auto texture = textureGroup->loadTexture(imagePart.image); + auto textureSize = Vec2F(texture->size()); + + RectF imageRect(Vec2F(), textureSize); + + Vec2F screenTranslation = drawable.position * drawableScale + drawableTranslation; + + Vec2F lowerLeft = + imagePart.transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMin())) * drawableScale + + screenTranslation; + Vec2F lowerRight = + imagePart.transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMin())) * drawableScale + + screenTranslation; + Vec2F upperRight = + imagePart.transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMax())) * drawableScale + + screenTranslation; + Vec2F upperLeft = + imagePart.transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMax())) * drawableScale + + screenTranslation; + + Vec4B drawableColor = drawable.color.toRgba(); + + renderer->render(RenderQuad{move(texture), + RenderVertex{lowerLeft, Vec2F(0, 0), drawableColor, 0.0f}, + RenderVertex{lowerRight, Vec2F(textureSize[0], 0), drawableColor, 0.0f}, + RenderVertex{upperRight, Vec2F(textureSize[0], textureSize[1]), drawableColor, 0.0f}, + RenderVertex{upperLeft, Vec2F(0, textureSize[1]), drawableColor, 0.0f}}); + } else { + starAssert(drawable.part.empty()); + } +} + +void Cinematic::updateCamera(float timecode) { + float startZoom = 1.0f; + float startZoomTimecode = -1; + float endZoom = startZoom; + float endZoomTimecode = startZoomTimecode; + + Vec2F startPan = {0, 0}; + float startPanTimecode = -1; + Vec2F endPan = startPan; + float endPanTimecode = startPanTimecode; + + for (auto keyframe : m_cameraKeyFrames) { + if (keyframe.timecode <= timecode) { + startZoom = keyframe.zoom; + startZoomTimecode = keyframe.timecode; + } + if (endZoomTimecode < timecode) { + endZoom = keyframe.zoom; + endZoomTimecode = keyframe.timecode; + } + + if (keyframe.timecode <= timecode) { + startPan = keyframe.pan; + startPanTimecode = keyframe.timecode; + } + if (endPanTimecode < timecode) { + endPan = keyframe.pan; + endPanTimecode = keyframe.timecode; + } + } + + if (startZoom == endZoom) + m_cameraZoom = startZoom; + else if (timecode <= startZoomTimecode) + m_cameraZoom = startZoom; + else if (timecode >= endZoomTimecode) + m_cameraZoom = endZoom; + else + m_cameraZoom = lerp((timecode - startZoomTimecode) / (endZoomTimecode - startZoomTimecode), startZoom, endZoom); + + if (startPan == endPan) + m_cameraPan = startPan; + else if (timecode <= startPanTimecode) + m_cameraPan = startPan; + else if (timecode >= endPanTimecode) + m_cameraPan = endPan; + else + m_cameraPan = lerp((timecode - startPanTimecode) / (endPanTimecode - startPanTimecode), startPan, endPan); +} + +float Cinematic::currentTimecode() const { + return std::min((float)m_timer.time() - m_backgroundFadeTime, m_completionTime - 2 * m_backgroundFadeTime); +} + +Cinematic::PanelValues Cinematic::determinePanelValues(PanelPtr panel, float timecode) { + if (panel->endTime != 0) { + if (timecode > panel->endTime) { + Cinematic::PanelValues result; + result.alpha = 0; + return result; + } + } + + if (panel->startTime != 0) { + if (timecode < panel->startTime) { + Cinematic::PanelValues result; + result.alpha = 0; + return result; + } else { + timecode -= panel->startTime; + } + } + + if (panel->loopTime != 0) { + timecode = fmod(timecode, panel->loopTime); + } + + float startZoom = 0; + float startZoomTimecode = -1; + float endZoom = startZoom; + float endZoomTimecode = startZoomTimecode; + + float startAlpha = 0; + float startAlphaTimecode = -1; + float endAlpha = startAlpha; + float endAlphaTimecode = startAlphaTimecode; + + Vec2F startPosition = {}; + float startPositionTimecode = -1; + Vec2F endPosition = startPosition; + float endPositionTimecode = startPositionTimecode; + + float startFrame = 0; + float startFrameTimecode = -1; + float endFrame = startFrame; + float endFrameTimecode = startFrameTimecode; + + float startTextPercentage = 1; + float startTextPercentageTimecode = -1; + float endTextPercentage = 1; + float endTextPercentageTimecode = -1; + + bool completable = false; + + for (auto keyframe : panel->keyFrames) { + if (keyframe.command.contains("complete")) { + if (keyframe.command.getBool("complete")) + if (keyframe.timecode <= timecode) + completable = true; + } + + if (keyframe.command.contains("zoom")) { + float zoom = keyframe.command.getFloat("zoom"); + if (keyframe.timecode <= timecode) { + startZoom = zoom; + startZoomTimecode = keyframe.timecode; + } + if (endZoomTimecode < timecode) { + endZoom = zoom; + endZoomTimecode = keyframe.timecode; + } + } + + if (keyframe.command.contains("alpha")) { + float alpha = keyframe.command.getFloat("alpha"); + if (keyframe.timecode <= timecode) { + startAlpha = alpha; + startAlphaTimecode = keyframe.timecode; + } + if (endAlphaTimecode < timecode) { + endAlpha = alpha; + endAlphaTimecode = keyframe.timecode; + } + } + + if (keyframe.command.contains("position")) { + Vec2F position = jsonToVec2F(keyframe.command.get("position")); + if (keyframe.timecode <= timecode) { + startPosition = position; + startPositionTimecode = keyframe.timecode; + } + if (endPositionTimecode < timecode) { + endPosition = position; + endPositionTimecode = keyframe.timecode; + } + } + + if (keyframe.command.contains("frame")) { + float frame = keyframe.command.getFloat("frame"); + if (keyframe.timecode <= timecode) { + startFrame = frame; + startFrameTimecode = keyframe.timecode; + } + if (endFrameTimecode < timecode) { + endFrame = frame; + endFrameTimecode = keyframe.timecode; + } + } + + if (keyframe.command.contains("textPercentage")) { + float textPercentage = keyframe.command.getFloat("textPercentage"); + if (keyframe.timecode <= timecode) { + startTextPercentage = textPercentage; + startTextPercentageTimecode = keyframe.timecode; + } + if (endTextPercentageTimecode < timecode) { + endTextPercentage = textPercentage; + endTextPercentageTimecode = keyframe.timecode; + } + } + } + + Cinematic::PanelValues result; + + result.completable = completable; + + if (startZoom == endZoom) + result.zoom = startZoom; + else if (timecode <= startZoomTimecode) + result.zoom = startZoom; + else if (timecode >= endZoomTimecode) + result.zoom = endZoom; + else + result.zoom = lerp((timecode - startZoomTimecode) / (endZoomTimecode - startZoomTimecode), startZoom, endZoom); + + if (startAlpha == endAlpha) + result.alpha = startAlpha; + else if (timecode <= startAlphaTimecode) + result.alpha = startAlpha; + else if (timecode >= endAlphaTimecode) + result.alpha = endAlpha; + else + result.alpha = lerp((timecode - startAlphaTimecode) / (endAlphaTimecode - startAlphaTimecode), startAlpha, endAlpha); + + if (startPosition == endPosition) + result.position = startPosition; + else if (timecode <= startPositionTimecode) + result.position = startPosition; + else if (timecode >= endPositionTimecode) + result.position = endPosition; + else + result.position = lerp((timecode - startPositionTimecode) / (endPositionTimecode - startPositionTimecode), startPosition, endPosition); + + if (startFrame == endFrame) + result.frame = startFrame; + else if (timecode <= startFrameTimecode) + result.frame = startFrame; + else if (timecode >= endFrameTimecode) + result.frame = endFrame; + else + result.frame = lerp((timecode - startFrameTimecode) / (endFrameTimecode - startFrameTimecode), startFrame, endFrame); + + if (startTextPercentage == endTextPercentage) + result.textPercentage = startTextPercentage; + else if (timecode <= startTextPercentageTimecode) + result.textPercentage = startTextPercentage; + else if (timecode >= endTextPercentageTimecode) + result.textPercentage = endTextPercentage; + else + result.textPercentage = + lerp((timecode - startTextPercentageTimecode) / (endTextPercentageTimecode - startTextPercentageTimecode), + startTextPercentage, + endTextPercentage); + + return result; +} + +void Cinematic::setTime(float timecode) { + m_timer.setTime(timecode + m_backgroundFadeTime); +} + +void Cinematic::stop() { + m_timeSkips.clear(); + m_cameraKeyFrames.clear(); + m_panels.clear(); + m_completionTime = 0; + m_timer.stop(); + m_timer.reset(); + for (size_t i = 0; i < m_audioCues.size(); ++i) { + if (m_activeAudio[i]) + m_activeAudio[i]->stop(); + } + m_audioCues.clear(); + m_activeAudio.clear(); + m_completable = false; + m_suppressInput = false; +} + +bool Cinematic::handleInputEvent(InputEvent const& event) { + if (completed()) + return false; + if (event.is<MouseButtonUpEvent>() || event.is<KeyUpEvent>()) + return false; + if (event.is<KeyDownEvent>()) { + if (m_currentTimeSkip) { + setTime(m_currentTimeSkip.take().skipToTime); + return true; + } else if (m_skippable && GuiContext::singleton().actions(event).contains(InterfaceAction::CinematicSkip)) { + stop(); + return true; + } + } + return m_suppressInput; +} + +bool Cinematic::suppressInput() const { + return m_suppressInput && !completed(); +} + +bool Cinematic::muteSfx() const { + return m_muteSfx && !completed(); +} + +bool Cinematic::muteMusic() const { + return m_muteMusic && !completed(); +} + +} |