main.cpp: change move assignment in generic raii wrapper, change the Interpreter from struct to class to better reflect what it actually is. window.cpp: implement a workaround for the window to not freeze during dragging. fuck you ms.
660 lines
23 KiB
C++
660 lines
23 KiB
C++
#include <cstdio>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <chrono>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <functional>
|
|
#include <window.h>
|
|
#include <text.h>
|
|
#include <compositor.h>
|
|
#include <image.h>
|
|
#include <audio.h>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RAII handles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// A generic RAII wrapper.
|
|
template <typename T, void(*DestroyFn)(T*)>
|
|
struct EngineHandle {
|
|
T* ptr;
|
|
|
|
// Constructor
|
|
EngineHandle(T* p = nullptr) : ptr(p) {}
|
|
|
|
// Destructor
|
|
~EngineHandle() { if (ptr) DestroyFn(ptr); }
|
|
|
|
// DELETE Copy Constructor (Prevents accidental cloning)
|
|
EngineHandle(const EngineHandle&) = delete;
|
|
|
|
// DELETE Copy Assignment (Prevents accidental overwrites)
|
|
EngineHandle& operator=(const EngineHandle&) = delete;
|
|
|
|
// ALLOW Move Constructor (Safely transfer ownership)
|
|
EngineHandle(EngineHandle&& other) noexcept : ptr(other.ptr) {
|
|
other.ptr = nullptr; // Nullify the old pointer so it doesn't get destroyed
|
|
}
|
|
|
|
// Move Assignment using Swap
|
|
EngineHandle& operator=(EngineHandle&& other) noexcept {
|
|
T* temp = other.ptr;
|
|
other.ptr = ptr;
|
|
ptr = temp;
|
|
return *this;
|
|
}
|
|
|
|
// Implicit conversion operator
|
|
operator T* () const { return ptr; }
|
|
};
|
|
|
|
using WindowHandle = EngineHandle<window_t, window_destroy>;
|
|
using CompositorHandle = EngineHandle<compositor_t, compositor_destroy>;
|
|
using TextHandle = EngineHandle<text_t, text_destroy>;
|
|
using AudioHandle = EngineHandle<audio_t, audio_destroy>;
|
|
using AudioDataHandle = EngineHandle<uint8_t, audio_free_data>;
|
|
using ImageHandle = EngineHandle<uint32_t, image_free>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Script interpreter
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// A command is an opcode plus a closure that does the actual work.
|
|
// Keeping the closure allows the command set to directly capture backend
|
|
// pointers without a fat context struct threaded through everything.
|
|
//
|
|
// The closure returns true when the interpreter should YIELD (stop executing
|
|
// commands this frame and wait). Returning false means continue immediately
|
|
// to the next command.
|
|
|
|
struct Command {
|
|
std::string opcode; // for debugging
|
|
std::function<bool()> execute;
|
|
};
|
|
|
|
enum class WaitKind {
|
|
NONE,
|
|
CLICK, // wait for mouse click or space
|
|
ANIM_ALL, // wait until compositor_is_animating() == false
|
|
ANIM_ELEM, // wait until a specific element stops animating
|
|
END, // script finished, keep rendering until told to quit
|
|
};
|
|
|
|
class Interpreter {
|
|
public:
|
|
std::vector<Command> script;
|
|
size_t pc = 0;
|
|
|
|
WaitKind waiting = WaitKind::NONE;
|
|
element_handle_t wait_elem = 0; // valid when waiting == ANIM_ELEM
|
|
|
|
// Callbacks into the engine, set before first tick
|
|
compositor_t* compositor = nullptr;
|
|
text_t* dialogue = nullptr;
|
|
text_t* name_box = nullptr;
|
|
|
|
// Advance the interpreter by one frame.
|
|
// clicked: true on the frame a left-click or spacebar press was detected.
|
|
// Returns true when the script has fully ended and the engine may quit.
|
|
bool tick(bool clicked) {
|
|
|
|
// ---- resolve current wait condition --------------------------------
|
|
switch (waiting) {
|
|
|
|
case WaitKind::CLICK:
|
|
if (clicked) {
|
|
// If the typewriter is still running, snap it and stay on
|
|
// this line - let the player see the full text first.
|
|
if (text_is_animating(dialogue)) {
|
|
text_show_all_current(dialogue);
|
|
return false;
|
|
}
|
|
waiting = WaitKind::NONE;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case WaitKind::ANIM_ALL:
|
|
if (compositor_is_animating(compositor))
|
|
return false;
|
|
waiting = WaitKind::NONE;
|
|
break;
|
|
|
|
case WaitKind::ANIM_ELEM:
|
|
if (compositor_is_element_animating(compositor, wait_elem))
|
|
return false;
|
|
waiting = WaitKind::NONE;
|
|
break;
|
|
|
|
case WaitKind::END:
|
|
return true;
|
|
|
|
case WaitKind::NONE:
|
|
break;
|
|
}
|
|
|
|
// ---- burn through commands until a yield ---------------------------
|
|
while (pc < script.size()) {
|
|
const Command& cmd = script[pc++];
|
|
bool yield = cmd.execute();
|
|
if (yield) return false;
|
|
}
|
|
|
|
// Ran off the end of the script without an explicit END.
|
|
waiting = WaitKind::END;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Script builder helpers
|
|
//
|
|
// These push Command objects onto the script vector. They capture the
|
|
// interpreter and backend pointers by reference/value as appropriate.
|
|
// All helpers return void and operate on a passed-in script vector so the
|
|
// build function stays readable.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
using Script = std::vector<Command>;
|
|
|
|
static void cmd_speaker(Script& s, text_t* name_box,
|
|
element_handle_t name_id, compositor_t* comp,
|
|
const char* speaker)
|
|
{
|
|
s.push_back({ "SPEAKER", [=]() -> bool {
|
|
text_clear(name_box);
|
|
if (speaker && speaker[0])
|
|
text_push(name_box, speaker, 0.0f, true);
|
|
compositor_mark_content_dirty(comp, name_id);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_line(Script& s, text_t* dialogue,
|
|
element_handle_t dialogue_id, compositor_t* comp,
|
|
const char* text, float speed = 30.0f)
|
|
{
|
|
s.push_back({ "LINE", [=]() -> bool {
|
|
text_clear(dialogue);
|
|
text_push(dialogue, text, speed, true);
|
|
compositor_mark_content_dirty(comp, dialogue_id);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_clear(Script& s, text_t* dialogue,
|
|
element_handle_t dialogue_id, compositor_t* comp)
|
|
{
|
|
s.push_back({ "CLEAR", [=]() -> bool {
|
|
text_clear(dialogue);
|
|
compositor_mark_content_dirty(comp, dialogue_id);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_wait_click(Script& s, Interpreter& interp)
|
|
{
|
|
s.push_back({ "WAIT_CLICK", [&interp]() -> bool {
|
|
interp.waiting = WaitKind::CLICK;
|
|
return true;
|
|
} });
|
|
}
|
|
|
|
static void cmd_wait_anim_all(Script& s, Interpreter& interp)
|
|
{
|
|
s.push_back({ "WAIT_ANIM_ALL", [&interp]() -> bool {
|
|
interp.waiting = WaitKind::ANIM_ALL;
|
|
return true;
|
|
} });
|
|
}
|
|
|
|
static void cmd_wait_anim_elem(Script& s, Interpreter& interp,
|
|
element_handle_t elem)
|
|
{
|
|
s.push_back({ "WAIT_ANIM_ELEM", [&interp, elem]() -> bool {
|
|
interp.waiting = WaitKind::ANIM_ELEM;
|
|
interp.wait_elem = elem;
|
|
return true;
|
|
} });
|
|
}
|
|
|
|
static void cmd_finish_all(Script& s, compositor_t* comp)
|
|
{
|
|
s.push_back({ "ANIM_FINISH_ALL", [=]() -> bool {
|
|
compositor_animation_finish_all(comp);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_anim_scale(Script& s, compositor_t* comp,
|
|
element_handle_t elem, float to,
|
|
float dur, easing_t ease, bool queue, bool parallel)
|
|
{
|
|
s.push_back({ "ANIM_SCALE", [=]() -> bool {
|
|
compositor_animate_scale(comp, elem, to, dur, ease, queue, parallel);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_anim_move(Script& s, compositor_t* comp,
|
|
element_handle_t elem,
|
|
int32_t x, int32_t y,
|
|
float dur, easing_t ease, bool queue, bool parallel)
|
|
{
|
|
s.push_back({ "ANIM_MOVE", [=]() -> bool {
|
|
compositor_animate_move(comp, elem, x, y, dur, ease, queue, parallel);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_anim_fade(Script& s, compositor_t* comp,
|
|
element_handle_t elem, float to_alpha,
|
|
float dur, easing_t ease, bool queue, bool parallel)
|
|
{
|
|
s.push_back({ "ANIM_FADE", [=]() -> bool {
|
|
compositor_animate_fade(comp, elem, to_alpha, dur, ease, queue, parallel);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_set_visible(Script& s, compositor_t* comp,
|
|
element_handle_t elem, bool visible)
|
|
{
|
|
s.push_back({ "ELEM_SHOW/HIDE", [=]() -> bool {
|
|
compositor_set_visible(comp, elem, visible);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_set_scale(Script& s, compositor_t* comp,
|
|
element_handle_t elem, float scale)
|
|
{
|
|
s.push_back({ "ELEM_SET_SCALE", [=]() -> bool {
|
|
compositor_set_scale(comp, elem, scale);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_set_alpha(Script& s, compositor_t* comp,
|
|
element_handle_t elem, float alpha)
|
|
{
|
|
s.push_back({ "ELEM_SET_ALPHA", [=]() -> bool {
|
|
compositor_set_alpha(comp, elem, alpha);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_audio_fade(Script& s, audio_t* audio,
|
|
audio_stream_id_t id,
|
|
float to_vol, float dur)
|
|
{
|
|
s.push_back({ "MUSIC_FADE", [=]() -> bool {
|
|
audio_animate_volume(audio, id, to_vol, dur);
|
|
return false;
|
|
} });
|
|
}
|
|
|
|
static void cmd_end(Script& s, Interpreter& interp)
|
|
{
|
|
s.push_back({ "END", [&interp]() -> bool {
|
|
interp.waiting = WaitKind::END;
|
|
return true;
|
|
} });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int main() {
|
|
srand((unsigned int)time(NULL));
|
|
|
|
const int WIDTH = 1280;
|
|
const int HEIGHT = 720;
|
|
const int CHAR_WIDTH = 400;
|
|
const int CHAR_HEIGHT = 600;
|
|
|
|
const int CENTER_X = WIDTH / 2;
|
|
const int CHAR_BASELINE_Y = HEIGHT - 140;
|
|
const int LEFT_CHAR_CENTER_X = 60 + (CHAR_WIDTH / 2);
|
|
const int RIGHT_CHAR_CENTER_X = WIDTH - 60 - (CHAR_WIDTH / 2);
|
|
const int OFFSCREEN_LEFT_X = LEFT_CHAR_CENTER_X - 660;
|
|
const int OFFSCREEN_RIGHT_X = RIGHT_CHAR_CENTER_X + 660;
|
|
|
|
// ---- window / compositor / audio ---------------------------------------
|
|
|
|
WindowHandle window(window_create(WIDTH, HEIGHT, u8"The Obwarzanek Narrative Engine"));
|
|
if (!window) { printf("Failed to create window\n"); return 1; }
|
|
|
|
CompositorHandle compositor(compositor_init(WIDTH, HEIGHT));
|
|
if (!compositor) { printf("Failed to initialize compositor\n"); return 1; }
|
|
|
|
AudioHandle audio(audio_init());
|
|
if (!audio) { printf("Failed to initialize audio\n"); return 1; }
|
|
|
|
audio_set_global_volume(audio, 1.0f);
|
|
|
|
typedef struct {
|
|
const char* title;
|
|
const char* composer;
|
|
const char* file;
|
|
} music_track_t;
|
|
|
|
static const music_track_t strauss_tracks[] = {
|
|
{"Freikugeln", "Johann Strauss II", "assets/Freikugeln.wav"},
|
|
{"Perpetuum Mobile", "Johann Strauss II", "assets/Perpetuum_Mobile.wav"},
|
|
{"Ohne Sorgen!", "Josef Strauss", "assets/Ohne_Sorgen!.wav"},
|
|
{"Auf Ferienreisen", "Josef Strauss", "assets/Auf_Ferienreisen.wav"},
|
|
{"Dynamiden", "Josef Strauss", "assets/Dynamiden.wav"}
|
|
};
|
|
|
|
int track_idx = rand() % (sizeof(strauss_tracks) / sizeof(strauss_tracks[0]));
|
|
const music_track_t* track = &strauss_tracks[track_idx];
|
|
|
|
int music_size, music_channels, music_rate, music_total_samples;
|
|
AudioDataHandle music_data(audio_load_madpcm(
|
|
track->file,
|
|
&music_size, &music_channels, &music_rate, &music_total_samples));
|
|
if (!music_data) { printf("Failed to load audio\n"); return 1; }
|
|
|
|
audio_stream_id_t music_id = audio_play(
|
|
audio, music_data, music_size, music_channels, music_rate, true);
|
|
|
|
// ---- assets / compositor elements -------------------------------------
|
|
|
|
ImageHandle bg(image_load_resized("assets/background.png", WIDTH, HEIGHT));
|
|
if (bg) {
|
|
element_handle_t bg_id = compositor_insert(
|
|
compositor, bg, WIDTH, HEIGHT, 0, 0, 0, 1.0f, ANCHOR_TOP_LEFT);
|
|
compositor_set_alpha(compositor, bg_id, 1.0f);
|
|
}
|
|
|
|
ImageHandle textbox(image_load_resized("assets/textbox.png", WIDTH, 180));
|
|
if (textbox)
|
|
compositor_insert(compositor, textbox, WIDTH, 180,
|
|
0, HEIGHT, 3, 1.0f, ANCHOR_BOTTOM_LEFT);
|
|
|
|
ImageHandle maksw_happy(image_load_resized("assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT));
|
|
element_handle_t maksw_happy_id = compositor_insert(
|
|
compositor, maksw_happy, CHAR_WIDTH, CHAR_HEIGHT,
|
|
OFFSCREEN_LEFT_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER);
|
|
|
|
ImageHandle maksw_neutral(image_load_resized("assets/maksw2.png", CHAR_WIDTH, CHAR_HEIGHT));
|
|
element_handle_t maksw_neutral_id = compositor_insert(
|
|
compositor, maksw_neutral, CHAR_WIDTH, CHAR_HEIGHT,
|
|
LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER);
|
|
compositor_set_visible(compositor, maksw_neutral_id, false);
|
|
|
|
ImageHandle wikor_img(image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT));
|
|
element_handle_t wikor_id = compositor_insert(
|
|
compositor, wikor_img, CHAR_WIDTH, CHAR_HEIGHT,
|
|
OFFSCREEN_RIGHT_X, CHAR_BASELINE_Y, 2, 1.0f, ANCHOR_BOTTOM_CENTER);
|
|
|
|
TextHandle name_text(text_init(300, 100));
|
|
text_set_font(name_text, "assets/Aller_Rg.ttf", 40.0f);
|
|
element_handle_t name_id = compositor_insert(
|
|
compositor, text_getfb(name_text),
|
|
300, 100, 60, HEIGHT - 180, 4, 1.0f, ANCHOR_TOP_LEFT);
|
|
|
|
TextHandle dialogue_text(text_init(WIDTH - 140, 160));
|
|
text_set_font(dialogue_text, "assets/Vera.ttf", 30.0f);
|
|
element_handle_t dialogue_id = compositor_insert(
|
|
compositor, text_getfb(dialogue_text),
|
|
WIDTH - 140, 160, 60, HEIGHT - 130, 4, 1.0f, ANCHOR_TOP_LEFT);
|
|
|
|
// mask: a wipe graphic, starts off-screen above, slides down to cover
|
|
ImageHandle mask(image_load_resized("assets/mask.png", 1280, 1080));
|
|
element_handle_t mask_id = compositor_insert(
|
|
compositor, mask, 1280, 1080, 0, -1080, 5, 1.0f, ANCHOR_TOP_LEFT);
|
|
compositor_set_visible(compositor, mask_id, false);
|
|
|
|
// mask2: slides off downward to reveal the new scene
|
|
ImageHandle mask2(image_load_resized("assets/mask2.png", 1280, 1080));
|
|
element_handle_t mask2_id = compositor_insert(
|
|
compositor, mask2, 1280, 1080, 0, 0, 5, 1.0f, ANCHOR_TOP_LEFT);
|
|
compositor_set_visible(compositor, mask2_id, false);
|
|
|
|
// ---- build the script -------------------------------------------------
|
|
//
|
|
// Shorthand references so the builder lines stay readable.
|
|
compositor_t* comp = compositor;
|
|
text_t* dlg = dialogue_text;
|
|
text_t* nmb = name_text;
|
|
element_handle_t dlg_id = dialogue_id;
|
|
|
|
Interpreter interp;
|
|
interp.compositor = comp;
|
|
interp.dialogue = dlg;
|
|
interp.name_box = nmb;
|
|
|
|
Script& S = interp.script;
|
|
|
|
// Convenience aliases so each script line stays short.
|
|
// All lambdas below capture by value (element handles, pointers) which is
|
|
// safe because all resources outlive the interpreter.
|
|
|
|
auto speaker = [&](const char* who) {
|
|
cmd_speaker(S, nmb, name_id, comp, who);
|
|
};
|
|
auto line = [&](const char* text, float speed = 30.0f) {
|
|
cmd_line(S, dlg, dlg_id, comp, text, speed);
|
|
};
|
|
auto clear = [&]() { cmd_clear(S, dlg, dlg_id, comp); };
|
|
auto wait_click = [&]() { cmd_wait_click(S, interp); };
|
|
auto wait_anim_all = [&]() { cmd_wait_anim_all(S, interp); };
|
|
auto wait_anim = [&](element_handle_t e) { cmd_wait_anim_elem(S, interp, e); };
|
|
auto finish_all = [&]() { cmd_finish_all(S, comp); };
|
|
auto anim_scale = [&](element_handle_t e, float to, float dur,
|
|
easing_t ease, bool q = false, bool p = false) {
|
|
cmd_anim_scale(S, comp, e, to, dur, ease, q, p);
|
|
};
|
|
auto anim_move = [&](element_handle_t e, int32_t x, int32_t y,
|
|
float dur, easing_t ease, bool q = false, bool p = false) {
|
|
cmd_anim_move(S, comp, e, x, y, dur, ease, q, p);
|
|
};
|
|
auto anim_fade = [&](element_handle_t e, float a, float dur,
|
|
easing_t ease, bool q = false, bool p = false) {
|
|
cmd_anim_fade(S, comp, e, a, dur, ease, q, p);
|
|
};
|
|
auto show = [&](element_handle_t e) { cmd_set_visible(S, comp, e, true); };
|
|
auto hide = [&](element_handle_t e) { cmd_set_visible(S, comp, e, false); };
|
|
auto scale = [&](element_handle_t e, float v) { cmd_set_scale(S, comp, e, v); };
|
|
auto alpha = [&](element_handle_t e, float v) { cmd_set_alpha(S, comp, e, v); };
|
|
|
|
// ------------------------------------------------------------------
|
|
// Scene 1 - maksw slides in, introduces the engine
|
|
// ------------------------------------------------------------------
|
|
|
|
anim_move(maksw_happy_id, CENTER_X, CHAR_BASELINE_Y,
|
|
1.5f, EASE_OUT_CUBIC, true);
|
|
anim_scale(maksw_happy_id, 1.1f, 0.3f, EASE_IN_OUT_CUBIC, /*queue=*/true, /*parallel=*/false);
|
|
speaker("maksw");
|
|
line("Hello and welcome to the Obwarzanek Narrative Engine demo!");
|
|
wait_click();
|
|
|
|
line("Today I'll present to you-");
|
|
wait_click();
|
|
|
|
anim_move(wikor_id, RIGHT_CHAR_CENTER_X, CHAR_BASELINE_Y, 0.4f, EASE_OUT_CUBIC);
|
|
anim_move(maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y - 20,
|
|
0.3f, EASE_OUT_CUBIC);
|
|
anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
|
|
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
speaker("wikor");
|
|
line("Yeah, hello to you too.");
|
|
wait_click();
|
|
|
|
const char* greetings[] = {
|
|
"Oh Wiktor, what a nice surprise!",
|
|
"Oh hey Wiktor, didn't expect to see you!",
|
|
"Wiktor! What brings you here?"
|
|
};
|
|
|
|
const char* responses[] = {
|
|
"No, I was just walking by.",
|
|
"Nah, just passing through.",
|
|
"Not really, just wandering."
|
|
};
|
|
|
|
int greeting_idx = rand() % 3;
|
|
int response_idx = rand() % 3;
|
|
|
|
anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
anim_scale(maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
speaker("maksw");
|
|
line(greetings[greeting_idx]);
|
|
wait_click();
|
|
|
|
line("...Do you need anything?");
|
|
wait_click();
|
|
|
|
anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
speaker("wikor");
|
|
line(responses[response_idx]);
|
|
wait_click();
|
|
|
|
line("Cool engine though.");
|
|
wait_click();
|
|
|
|
finish_all();
|
|
show(mask_id);
|
|
anim_move(mask_id, 0, 0, 0.5f, EASE_OUT_CUBIC);
|
|
wait_anim(mask_id);
|
|
|
|
hide(maksw_happy_id);
|
|
show(maksw_neutral_id);
|
|
scale(maksw_neutral_id, 1.0f);
|
|
scale(wikor_id, 1.05f);
|
|
|
|
hide(mask_id);
|
|
show(mask2_id);
|
|
anim_move(mask2_id, 0, HEIGHT, 0.5f, EASE_OUT_CUBIC);
|
|
speaker("wikor");
|
|
clear();
|
|
wait_anim(mask2_id);
|
|
line("Anyways, how's your day going?");
|
|
wait_click();
|
|
|
|
// ------------------------------------------------------------------
|
|
// Scene 2 - neutral maksw, wikor is chatty
|
|
// ------------------------------------------------------------------
|
|
|
|
finish_all();
|
|
anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
speaker("maksw");
|
|
line("You hear what I'm hearing?");
|
|
wait_click();
|
|
|
|
finish_all();
|
|
anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
speaker("wikor");
|
|
line("What? That music?");
|
|
wait_click();
|
|
|
|
finish_all();
|
|
anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
speaker("maksw");
|
|
char music_line[256];
|
|
snprintf(music_line, sizeof(music_line),
|
|
"Yep! %s by %s!", track->title, track->composer);
|
|
line(music_line);
|
|
wait_click();
|
|
|
|
finish_all();
|
|
anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
speaker("wikor");
|
|
line("Sure thing...");
|
|
wait_click();
|
|
|
|
finish_all();
|
|
line("Like I know who he is.");
|
|
wait_click();
|
|
|
|
// ------------------------------------------------------------------
|
|
// Ending - fade to black, fade out music, then quit
|
|
// ------------------------------------------------------------------
|
|
|
|
finish_all();
|
|
anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC);
|
|
// Reuse mask as a simple alpha-fade overlay (no positional wipe needed)
|
|
show(mask_id);
|
|
alpha(mask_id, 0.0f);
|
|
anim_fade(mask_id, 1.0f, 0.8f, EASE_IN_OUT_CUBIC);
|
|
cmd_audio_fade(S, audio, music_id, 0.0f, 0.6f);
|
|
wait_anim(mask_id); // wait until black, then exit
|
|
cmd_end(S, interp);
|
|
|
|
// ---- main loop --------------------------------------------------------
|
|
|
|
bool running = true;
|
|
|
|
auto last_frame_time = std::chrono::high_resolution_clock::now();
|
|
auto last_report_time = last_frame_time;
|
|
uint64_t frame_count = 0;
|
|
float highest_frame_time = 0.0f;
|
|
float frame_time_sum = 0.0f;
|
|
const std::chrono::duration<float> REPORT_INTERVAL = std::chrono::seconds(1);
|
|
|
|
bool mouse_was_down = false;
|
|
bool key_was_down = false;
|
|
|
|
while (running) {
|
|
if (window_pollevent(window)) running = false;
|
|
|
|
auto current_time = std::chrono::high_resolution_clock::now();
|
|
std::chrono::duration<float> delta_duration = current_time - last_frame_time;
|
|
float dt = delta_duration.count();
|
|
last_frame_time = current_time;
|
|
|
|
// FPS reporting
|
|
frame_count++;
|
|
frame_time_sum += dt;
|
|
if (dt > highest_frame_time) highest_frame_time = dt;
|
|
std::chrono::duration<float> since_report = current_time - last_report_time;
|
|
if (since_report >= REPORT_INTERVAL) {
|
|
if (frame_count > 0) {
|
|
float fps = 1.0f / (frame_time_sum / (float)frame_count);
|
|
printf("FPS: %.2f (over %llu frames in %.2f seconds), highest frame time: %.2fms\n",
|
|
fps, frame_count, since_report.count(), highest_frame_time * 1000.0f);
|
|
}
|
|
frame_count = 0; frame_time_sum = 0.0f; highest_frame_time = 0.0f;
|
|
last_report_time = current_time;
|
|
}
|
|
|
|
// Edge-detect click and keypress
|
|
bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT);
|
|
bool key_is_down = window_is_key_down(window, KEY_SPACE);
|
|
bool clicked = (mouse_is_down && !mouse_was_down) ||
|
|
(key_is_down && !key_was_down);
|
|
mouse_was_down = mouse_is_down;
|
|
key_was_down = key_is_down;
|
|
|
|
// Tick the interpreter
|
|
bool script_done = interp.tick(clicked);
|
|
if (script_done) running = false;
|
|
|
|
// Tick subsystems
|
|
compositor_tick(compositor, dt);
|
|
|
|
if (text_nextframe(name_text, dt)) compositor_mark_content_dirty(compositor, name_id);
|
|
if (text_nextframe(dialogue_text, dt)) compositor_mark_content_dirty(compositor, dialogue_id);
|
|
|
|
const uint32_t* fb = compositor_compose(compositor);
|
|
window_blit(window, fb);
|
|
audio_tick(audio, dt);
|
|
}
|
|
|
|
audio_stop(audio, music_id);
|
|
return 0;
|
|
}
|