350 lines
11 KiB
C++
350 lines
11 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "avatar/Avatar.h"
|
|
#include "dialogue/Dialogue.h"
|
|
#include "graphics/AmbientOcclusion.h"
|
|
#include "graphics/AntiAliasing.h"
|
|
#include "graphics/Canvas.h"
|
|
#include "graphics/DrawItem2D.h"
|
|
#include "graphics/Font.h"
|
|
#include "graphics/GBuffer.h"
|
|
#include "graphics/GlobalIllumination.h"
|
|
#include "graphics/Graphics.h"
|
|
#include "graphics/Grass.h"
|
|
#include "graphics/Light.h"
|
|
#include "graphics/Material.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "graphics/Outline.h"
|
|
#include "graphics/Quad.h"
|
|
#include "graphics/RenderControl.h"
|
|
#include "graphics/RenderSystem.h"
|
|
#include "graphics/ScreenSpaceReflection.h"
|
|
#include "graphics/Shader.h"
|
|
#include "graphics/Texture.h"
|
|
#include "graphics/Volumetric.h"
|
|
#include "graphics/Water.h"
|
|
#include "graphics/Waterfall.h"
|
|
#include "ui/UI.h"
|
|
#include "ui/UISystem.h"
|
|
#include "ui/Widget.h"
|
|
#include "window/Window.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace eve::dialogue;
|
|
using eve::avatar::Avatar;
|
|
using eve::avatar::AvatarInstance;
|
|
using namespace eve::graphics;
|
|
using namespace eve::ui;
|
|
|
|
namespace {
|
|
|
|
void hideAllSprites() {
|
|
if (ecs::current()->getManager<Renderable2D>() == nullptr) return;
|
|
auto view = ecs::View<Renderable2D, Renderable2D::Sprite>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [sp] = *it;
|
|
sp->visible = false;
|
|
}
|
|
}
|
|
|
|
AvatarInstance *makePortrait(Avatar *avmod, float r, float g, float b) {
|
|
AvatarInstance *av = avmod->newImageAvatar();
|
|
av->addLayer("body", nullptr, 0);
|
|
av->setLayerSize("body", 140.f, 280.f);
|
|
av->setLayerColor("body", r, g, b, 1.f);
|
|
av->addLayer("face", nullptr, 1);
|
|
av->setLayerSize("face", 90.f, 90.f);
|
|
av->setLayerOffset("face", 25.f, 30.f);
|
|
av->setLayerColor("face", r * 0.7f + 0.25f, g * 0.7f + 0.2f, b * 0.7f + 0.2f, 1.f);
|
|
av->addLayer("mouth", nullptr, 2);
|
|
av->setLayerSize("mouth", 36.f, 14.f);
|
|
av->setLayerOffset("mouth", 52.f, 95.f);
|
|
av->setLayerColor("mouth", 0.85f, 0.25f, 0.3f, 0.15f);
|
|
av->defineExpression("talk", "mouth=1");
|
|
av->defineExpression("idle", "mouth=0.15");
|
|
return av;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("dialogue.charactersAndStage") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
REQUIRE(dlg != nullptr);
|
|
|
|
CHECK(dlg->registerCharacter("alice", "Alice"));
|
|
CHECK(dlg->registerCharacter("bob", "Bob"));
|
|
CHECK(dlg->hasCharacter("alice"));
|
|
CHECK_EQ(dlg->getDisplayName("bob"), std::string("Bob"));
|
|
CHECK_EQ(dlg->getCharacterCount(), 2);
|
|
|
|
AvatarInstance *aliceAv = avmod->newImageAvatar();
|
|
aliceAv->addLayer("body", nullptr, 0);
|
|
aliceAv->setLayerSize("body", 100.f, 200.f);
|
|
aliceAv->setPosition(0.f, 300.f);
|
|
CHECK(dlg->bindAvatar("alice", aliceAv));
|
|
|
|
dlg->setSlotX("left", 0.2f);
|
|
CHECK(dlg->show("alice", "left"));
|
|
CHECK(dlg->isShown("alice"));
|
|
CHECK_EQ(dlg->getSlot("alice"), std::string("left"));
|
|
dlg->syncStage(800.f, 600.f);
|
|
CHECK_EQ(aliceAv->getX(), 160.f); // 0.2 * 800
|
|
CHECK(aliceAv->isVisible());
|
|
|
|
CHECK(dlg->hide("alice"));
|
|
CHECK(!dlg->isShown("alice"));
|
|
|
|
dlg->reset();
|
|
aliceAv->release();
|
|
delete aliceAv;
|
|
}
|
|
|
|
TEST_CASE("dialogue.typewriterAndAdvance") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
dlg->registerCharacter("hero", "Hero");
|
|
dlg->setTypeSpeed(10.f); // 10 codepoints / sec
|
|
|
|
dlg->say("hero", "Hello");
|
|
CHECK(dlg->isTyping());
|
|
CHECK_EQ(dlg->getPhase(), std::string("typing"));
|
|
CHECK_EQ(dlg->getSpeakerName(), std::string("Hero"));
|
|
|
|
dlg->update(0.2f); // 2 chars
|
|
CHECK_EQ(dlg->getVisibleText(), std::string("He"));
|
|
CHECK(dlg->isTyping());
|
|
|
|
dlg->update(1.0f); // finish
|
|
CHECK(dlg->isWaitingAdvance());
|
|
CHECK_EQ(dlg->getVisibleText(), std::string("Hello"));
|
|
|
|
dlg->advance();
|
|
CHECK(dlg->isIdle());
|
|
|
|
dlg->setTypeSpeed(0.f);
|
|
dlg->narrate("Instant");
|
|
CHECK(dlg->isWaitingAdvance());
|
|
CHECK_EQ(dlg->getVisibleText(), std::string("Instant"));
|
|
dlg->advance();
|
|
CHECK(dlg->isIdle());
|
|
}
|
|
|
|
TEST_CASE("dialogue.utf8Typewriter") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
dlg->setTypeSpeed(100.f);
|
|
// UTF-8 bytes for 你好 / 你 (avoid u8 literals → char8_t in C++20).
|
|
const std::string hi = "\xe4\xbd\xa0\xe5\xa5\xbd";
|
|
const std::string first = "\xe4\xbd\xa0";
|
|
dlg->narrate(hi);
|
|
dlg->update(0.01f); // 1 codepoint
|
|
CHECK_EQ(dlg->getVisibleText(), first);
|
|
dlg->skipTyping();
|
|
CHECK_EQ(dlg->getVisibleText(), hi);
|
|
CHECK(dlg->isWaitingAdvance());
|
|
}
|
|
|
|
TEST_CASE("dialogue.choices") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
dlg->registerCharacter("n", "N");
|
|
dlg->say("n", "Pick one");
|
|
dlg->skipTyping();
|
|
|
|
dlg->clearChoices();
|
|
CHECK(dlg->addChoice("a", "Alpha"));
|
|
CHECK(dlg->addChoice("b", "Beta"));
|
|
CHECK_EQ(dlg->getChoiceCount(), 2);
|
|
dlg->presentChoices();
|
|
CHECK(dlg->isWaitingChoice());
|
|
CHECK_EQ(dlg->getChoiceLabel(0), std::string("Alpha"));
|
|
|
|
CHECK(!dlg->selectChoice(99));
|
|
CHECK(dlg->selectChoice(1));
|
|
CHECK_EQ(dlg->getSelectedChoiceId(), std::string("b"));
|
|
CHECK(dlg->isIdle());
|
|
}
|
|
|
|
TEST_CASE("dialogue.expressionForward") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
dlg->registerCharacter("c", "C");
|
|
AvatarInstance *av = avmod->newImageAvatar();
|
|
av->addLayer("face", nullptr, 0);
|
|
av->defineExpression("happy", "face=1");
|
|
dlg->bindAvatar("c", av);
|
|
CHECK(dlg->setExpression("c", "happy"));
|
|
CHECK_EQ(av->getExpression(), std::string("happy"));
|
|
CHECK(dlg->setMotion("c", "wave"));
|
|
CHECK_EQ(av->getMotion(), std::string("wave"));
|
|
av->release();
|
|
delete av;
|
|
}
|
|
|
|
TEST_CASE("dialogue.lipSyncWhileTyping") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
dlg->registerCharacter("hero", "Hero");
|
|
AvatarInstance *av = avmod->newImageAvatar();
|
|
av->addLayer("mouthOpen", nullptr, 1);
|
|
av->setLayerSize("mouthOpen", 40.f, 20.f);
|
|
av->setLayerColor("mouthOpen", 0.8f, 0.2f, 0.2f, 0.f);
|
|
dlg->bindAvatar("hero", av);
|
|
dlg->show("hero", "center");
|
|
|
|
dlg->setLipSyncEnabled(true);
|
|
dlg->setLipSyncParameter("mouthOpen");
|
|
dlg->setLipSyncAmplitude(0.9f);
|
|
dlg->setTypeSpeed(20.f);
|
|
dlg->say("hero", "Hello there");
|
|
|
|
CHECK(dlg->isTyping());
|
|
dlg->update(0.05f);
|
|
CHECK(dlg->getLipSyncValue() > 0.1f);
|
|
CHECK(av->getParameter("mouthOpen") > 0.1f);
|
|
|
|
dlg->skipTyping();
|
|
dlg->update(0.5f); // ease shut
|
|
CHECK(dlg->getLipSyncValue() < 0.2f);
|
|
|
|
av->release();
|
|
delete av;
|
|
}
|
|
|
|
TEST_CASE("dialogue.stage.avatarTypewriterPreview") {
|
|
hideAllSprites();
|
|
|
|
auto *win = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
auto *ui = UI::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
REQUIRE(ui != nullptr);
|
|
eve::window::WindowSettings s;
|
|
s.width = 800;
|
|
s.height = 480;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
REQUIRE(ui->initBackend());
|
|
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
dlg->registerCharacter("alice", "Alice");
|
|
dlg->registerCharacter("bob", "Bob");
|
|
|
|
AvatarInstance *alice = makePortrait(avmod, 0.35f, 0.55f, 0.95f);
|
|
AvatarInstance *bob = makePortrait(avmod, 0.9f, 0.45f, 0.35f);
|
|
dlg->bindAvatar("alice", alice);
|
|
dlg->bindAvatar("bob", bob);
|
|
dlg->setSlotX("left", 0.12f);
|
|
dlg->setSlotX("right", 0.72f);
|
|
dlg->show("alice", "left");
|
|
dlg->show("bob", "right");
|
|
|
|
dlg->setLipSyncEnabled(true);
|
|
dlg->setLipSyncParameter("mouth");
|
|
dlg->setLipSyncAmplitude(0.95f);
|
|
dlg->setTypeSpeed(18.f);
|
|
|
|
auto *cam = Camera2D::createCamera();
|
|
cam->data()->r = 0.08f;
|
|
cam->data()->g = 0.09f;
|
|
cam->data()->b = 0.12f;
|
|
|
|
// Floor / stage backdrop.
|
|
auto *stage = Renderable2D::create();
|
|
stage->transform()->x = 0;
|
|
stage->transform()->y = 360;
|
|
stage->sprite()->width = 800;
|
|
stage->sprite()->height = 120;
|
|
stage->sprite()->r = 0.12f;
|
|
stage->sprite()->g = 0.14f;
|
|
stage->sprite()->b = 0.18f;
|
|
stage->sprite()->visible = true;
|
|
|
|
enum Phase { Line1, Line2, Choices, Done };
|
|
Phase phase = Line1;
|
|
dlg->say("alice", "Welcome to the EVEngine stage preview.");
|
|
dlg->setExpression("alice", "talk");
|
|
dlg->setExpression("bob", "idle");
|
|
|
|
int frames = 0;
|
|
bool sawChoice = false;
|
|
for (int frame = 0; frame < 150; ++frame) {
|
|
++frames;
|
|
dlg->update(0.016f);
|
|
dlg->syncStage(float(gfx->getWidth()), float(gfx->getHeight()));
|
|
avmod->sync();
|
|
|
|
if (phase == Line1 && dlg->isWaitingAdvance()) {
|
|
dlg->advance();
|
|
dlg->say("bob", "Looks like the portraits and typewriter are alive.");
|
|
dlg->setExpression("alice", "idle");
|
|
dlg->setExpression("bob", "talk");
|
|
phase = Line2;
|
|
} else if (phase == Line2 && dlg->isWaitingAdvance()) {
|
|
dlg->advance();
|
|
dlg->clearChoices();
|
|
dlg->addChoice("cont", "Continue");
|
|
dlg->addChoice("exit", "Exit preview");
|
|
dlg->presentChoices();
|
|
dlg->setExpression("bob", "idle");
|
|
phase = Choices;
|
|
} else if (phase == Choices && dlg->isWaitingChoice()) {
|
|
sawChoice = true;
|
|
if (frame > 110) {
|
|
dlg->selectChoice(0);
|
|
phase = Done;
|
|
}
|
|
}
|
|
|
|
const std::string speaker = dlg->getSpeakerName();
|
|
const std::string body = dlg->getVisibleText();
|
|
std::vector<WidgetDesc> kids = {
|
|
text(speaker.empty() ? "Narrator" : speaker, "speaker"),
|
|
separator("sep"),
|
|
text(body.empty() ? "..." : body, "line"),
|
|
};
|
|
if (dlg->isWaitingChoice()) {
|
|
kids.push_back(separator("sep2"));
|
|
for (int i = 0; i < dlg->getChoiceCount(); ++i) {
|
|
kids.push_back(text("• " + dlg->getChoiceLabel(i), "c" + std::to_string(i)));
|
|
}
|
|
} else if (dlg->isWaitingAdvance()) {
|
|
kids.push_back(text("[advance]", "hint"));
|
|
}
|
|
|
|
REQUIRE(UIHost::resolve(ui->remountAs("dlg", window("Dialogue", kids, "root"))).has_value());
|
|
ui->beginFrameAndRender();
|
|
RenderSystem::render(*gfx);
|
|
ui->dispatchEvents();
|
|
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
ui->processEvent(&e);
|
|
if (e.type == SDL_QUIT) break;
|
|
}
|
|
SDL_Delay(16);
|
|
}
|
|
|
|
CHECK_GT(frames, 100);
|
|
CHECK(sawChoice);
|
|
CHECK(dlg->isIdle());
|
|
|
|
stage->sprite()->visible = false;
|
|
alice->setVisible(false);
|
|
bob->setVisible(false);
|
|
alice->sync();
|
|
bob->sync();
|
|
dlg->reset();
|
|
alice->release();
|
|
bob->release();
|
|
delete alice;
|
|
delete bob;
|
|
win->close();
|
|
}
|