# Cutscene System Cutscenes are defined in JSON and loaded by `CutsceneDatabase`. Each cutscene is a self-contained object with an array of animated image layers and optional subtitle lines. The file can contain multiple cutscenes: ```json { "cutscenes": [ { "id": "intro", ... }, { "id": "ending", ... } ] } ``` Cutscenes and dialogues are loaded from **separate files**: ```cpp dialogueSystem.loadDatabase("resources/dialogue/uni_interior.json"); // dialogues dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); // cutscenes ``` --- ## Cutscene object ```json { "id": "intro_cutscene", "skippable": true, "durationMs": 8000, "fadeOutMs": 500, "fadeInMs": 500, "endFadeOutMs": 500, "endFadeInMs": 500, "onFadeInCallback": "", "imageSegments": [ ... ], "lines": [ ... ] } ``` | Property | Type | Default | Description | |---|---|---|---| | `id` | string | — | Unique identifier used to start the cutscene from C++ or dialogue (**required**) | | `skippable` | bool | `true` | Whether the player can skip by holding LMB / touch | | `durationMs` | int | `0` | Minimum content duration in ms. The cutscene will not end before this time even if all subtitle lines have finished. `0` means duration is determined solely by subtitle lines or `imageSegments.endMs` | | `fadeOutMs` | int | `0` | Duration of the **opening fade** — game world fades to black before the cutscene images appear | | `fadeInMs` | int | `0` | Duration of the **opening reveal** — cutscene images fade in from black after `fadeOutMs` | | `endFadeOutMs` | int | `0` | Duration of the **closing fade** — cutscene fades to black at the end of content | | `endFadeInMs` | int | `0` | Duration of the **closing reveal** — game world fades back in from black | | `onFadeInCallback` | string | `""` | Lua function name called once the opening fade-in completes (fired after `fadeOutMs + fadeInMs` ms) | | `imageSegments` | array | `[]` | Image layers with motion — see [Image segments](#image-segments) | | `lines` | array | `[]` | Subtitle lines shown sequentially — see [Subtitle lines](#subtitle-lines) | ### Timing model The total cutscene duration is: ``` contentDuration = max(durationMs, max(segment.endMs for all segments)) totalDuration = contentDuration + endFadeOutMs + endFadeInMs ``` The full timeline looks like this: ``` |-- fadeOutMs --|-- fadeInMs --|--- content plays (images + subtitles) ---|-- endFadeOutMs --|-- endFadeInMs --| world→black black→images images→black black→world ``` --- ## Image segments Each entry in `imageSegments` describes one image layer: when it is visible, how it fades in/out, and how it animates from a start pose to an end pose. ```json { "path": "resources/cutscenes/bg_layer.png", "width": 1280, "height": 720, "startMs": 0, "endMs": 8000, "fadeInMs": 300, "fadeOutMs": 300, "easing": "EaseInOutSine", "from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 }, "to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 } } ``` | Property | Type | Default | Description | |---|---|---|---| | `path` | string | — | Path to the PNG image (**required**) | | `width` | int | `0` | Logical width used for all UV and aspect-ratio math. `0` uses the actual texture pixel width | | `height` | int | `0` | Logical height. `0` uses the actual texture pixel height | | `startMs` | int | `0` | Time (ms from cutscene start) when this layer becomes active | | `endMs` | int | `0` | Time (ms) when this layer stops being active. Must be > `startMs` | | `fadeInMs` | int | `0` | Alpha fades from 0 → 1 over this many ms after `startMs`. `0` = instant | | `fadeOutMs` | int | `0` | Alpha fades from 1 → 0 over this many ms before `endMs`. `0` = instant | | `easing` | string | `"Linear"` | Easing applied to the pose interpolation — see [Easing types](#easing-types) | | `from` | pose object | center/1.0 | Pose at `startMs` — see [Image pose](#image-pose) | | `to` | pose object | same as `from` | Pose at `endMs`. If omitted, the layer stays at `from` the whole time | Multiple segments can be active at the same time. They are rendered **in declaration order** (first = bottom layer, last = top layer), which enables parallax layering. --- ## Image pose A pose defines how an image is framed on screen at a given moment. ```json { "centerX": 0.5, "centerY": 0.5, "scale": 1.0 } ``` | Property | Type | Default | Description | |---|---|---|---| | `centerX` | float | `0.5` | Normalized X position (0 = left edge of image, 1 = right edge) of the point that is placed at the horizontal center of the screen | | `centerY` | float | `0.5` | Normalized Y position (0 = top edge, 1 = bottom edge) placed at the screen center | | `scale` | float | `1.0` | Zoom level. `1.0` = the image fills the screen exactly (aspect-ratio corrected). `2.0` = zoomed in 2×, showing half the image area | The runtime interpolates all three values independently from `from` to `to` using the chosen easing. **Coordinate clamping:** `centerX`/`centerY` are automatically clamped so the viewport never shows area outside the image. For a zoomed-in segment (`scale > 1`) you therefore have more freedom to pan; for `scale = 1.0` the center is locked to `0.5/0.5`. ### Pose intuition | Goal | Config | |---|---| | Centered, no zoom | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }` | | Slightly zoomed in on center | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }` | | Pan left to right | `from: { "centerX": 0.3, "scale": 1.2 }` → `to: { "centerX": 0.7, "scale": 1.2 }` | | Zoom out from close-up | `from: { "scale": 1.8 }` → `to: { "scale": 1.0 }` | | Look at top portion | `{ "centerY": 0.2, "scale": 1.3 }` | --- ## Easing types Controls the interpolation curve applied to pose animation between `from` and `to`. | Value | Description | |---|---| | `"Linear"` | Constant speed (default) | | `"EaseInSine"` | Slow start, fast end | | `"EaseOutSine"` | Fast start, slow end | | `"EaseInOutSine"` | Slow start and end, fast middle | | `"EaseInQuad"` | Quadratic slow start | | `"EaseOutQuad"` | Quadratic slow end | | `"EaseInOutQuad"` | Quadratic slow start and end | | `"EaseInCubic"` | Cubic slow start | | `"EaseOutCubic"` | Cubic slow end | | `"EaseInOutCubic"` | Cubic slow start and end | For cinematic camera motion `"EaseInOutSine"` or `"EaseInOutCubic"` give the most natural feel. --- ## Subtitle lines Lines are displayed sequentially on top of the cutscene images. Each line shows until its duration expires (or until the player advances, if `waitForConfirm` is set). ```json { "speaker": "Аида Дженибековна", "text": "Здравствуйте, студенты.", "durationMs": 3000, "waitForConfirm": false, "luaCallback": "" } ``` | Property | Type | Default | Description | |---|---|---|---| | `speaker` | string | `""` | Speaker name shown above the subtitle text. Empty = no name bar | | `text` | string | `""` | Subtitle text. Supports Cyrillic and any codepoint in `resources/symbols.txt` | | `durationMs` | int | `0` | How long this line is displayed in ms. `0` = auto-computed from text length (~17 chars/sec, minimum 1500 ms) | | `waitForConfirm` | bool | `false` | When `true`, the line waits for player input (tap/click/Enter) before advancing. No timer runs | | `luaCallback` | string | `""` | Lua function name called when this line begins. Useful for triggering SFX, spawning effects, etc. | Subtitle lines run on their own timer that is **independent** of the image segments. The cutscene ends when **both** subtitle lines are exhausted **and** `contentDuration` has elapsed. --- ## C++ API ### Starting a cutscene ```cpp // Standalone cutscene (not part of a dialogue): dialogueSystem.startCutscene("intro_cutscene"); // Skip the currently playing cutscene: dialogueSystem.skipCutscene(); ``` ### Callbacks ```cpp // Called when a cutscene begins: dialogueSystem.setOnCutsceneStarted([]() { /* hide HUD, etc. */ }); // Called when a cutscene ends (receives the cutscene id): dialogueSystem.setOnCutsceneFinished([](const std::string& id) { // id == "intro_cutscene" }); // Called when a subtitle line begins (receives luaCallback value): dialogueSystem.setOnCutsceneLineStarted([](const std::string& fn) { scriptEngine.callActivateFunction(fn); }); // Called when the opening fade-in completes (receives onFadeInCallback value): dialogueSystem.setOnCutsceneFadeInComplete([](const std::string& fn) { scriptEngine.callActivateFunction(fn); }); ``` ### Triggering from dialogue A dialogue node of type `CutsceneStart` embeds a cutscene mid-conversation. Dialogue resumes at `next` when the cutscene ends. ```json { "id": "node_cutscene", "type": "CutsceneStart", "cutsceneId": "intro_cutscene", "next": "node_after_cutscene" } ``` --- ## Full examples ### Minimal — static image, timed ```json { "id": "simple", "durationMs": 4000, "fadeOutMs": 300, "fadeInMs": 300, "endFadeOutMs": 300, "endFadeInMs": 300, "imageSegments": [ { "path": "resources/cutscenes/city.png", "width": 1280, "height": 720, "startMs": 0, "endMs": 4000 } ] } ``` ### Two-layer parallax pan Background moves slowly left-to-right; foreground character moves faster, creating depth. ```json { "id": "classroom_intro", "durationMs": 8000, "fadeOutMs": 500, "fadeInMs": 500, "endFadeOutMs": 500, "endFadeInMs": 500, "imageSegments": [ { "path": "resources/cutscenes/classroom_bg.png", "width": 1920, "height": 1080, "startMs": 0, "endMs": 8000, "fadeInMs": 400, "easing": "EaseInOutSine", "from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 }, "to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 } }, { "path": "resources/cutscenes/classroom_teacher.png", "width": 1920, "height": 1080, "startMs": 0, "endMs": 8000, "easing": "EaseInOutSine", "from": { "centerX": 0.35, "centerY": 0.5, "scale": 1.0 }, "to": { "centerX": 0.65, "centerY": 0.5, "scale": 1.0 } } ], "lines": [ { "speaker": "Аида Дженибековна", "text": "Здравствуйте, студенты.", "durationMs": 3000 }, { "speaker": "Аида Дженибековна", "text": "Рассаживайтесь.", "durationMs": 2500 } ] } ``` ### Zoom-in reveal with a second image appearing mid-way ```json { "id": "letter_reveal", "durationMs": 7000, "fadeOutMs": 400, "fadeInMs": 600, "endFadeOutMs": 600, "endFadeInMs": 400, "imageSegments": [ { "path": "resources/cutscenes/desk_bg.png", "width": 1280, "height": 720, "startMs": 0, "endMs": 7000 }, { "path": "resources/cutscenes/letter_closeup.png", "width": 1280, "height": 720, "startMs": 2000, "endMs": 7000, "fadeInMs": 800, "easing": "EaseOutCubic", "from": { "centerX": 0.5, "centerY": 0.5, "scale": 2.5 }, "to": { "centerX": 0.5, "centerY": 0.5, "scale": 1.2 } } ], "lines": [ { "text": "Среди бумаг на столе лежит конверт.", "durationMs": 2500 }, { "speaker": "Главный герой", "text": "«Явитесь в деканат немедленно».", "durationMs": 3000 } ] } ```