Files
secretpsxsplash/src/cutscene.cpp
2026-03-29 16:14:15 +02:00

403 lines
15 KiB
C++

#include "cutscene.hh"
#include <psyqo/fixed-point.hh>
#include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh>
#include "streq.hh"
#include "uisystem.hh"
namespace psxsplash {
void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio,
UISystem* uiSystem) {
m_cutscenes = cutscenes;
m_count = count;
m_active = nullptr;
m_frame = 0;
m_nextAudio = 0;
m_loop = false;
m_camera = camera;
m_audio = audio;
m_uiSystem = uiSystem;
m_onCompleteRef = LUA_NOREF;
}
bool CutscenePlayer::play(const char* name, bool loop) {
if (!name || !m_cutscenes) return false;
m_loop = loop;
for (int i = 0; i < m_count; i++) {
if (m_cutscenes[i].name && streq(m_cutscenes[i].name, name)) {
m_active = &m_cutscenes[i];
m_frame = 0;
m_nextAudio = 0;
// Capture initial state for pre-first-keyframe blending
for (uint8_t ti = 0; ti < m_active->trackCount; ti++) {
CutsceneTrack& track = m_active->tracks[ti];
track.initialValues[0] = track.initialValues[1] = track.initialValues[2] = 0;
switch (track.trackType) {
case TrackType::CameraPosition:
if (m_camera) {
auto& pos = m_camera->GetPosition();
track.initialValues[0] = (int16_t)pos.x.value;
track.initialValues[1] = (int16_t)pos.y.value;
track.initialValues[2] = (int16_t)pos.z.value;
}
break;
case TrackType::CameraRotation:
if (m_camera) {
track.initialValues[0] = m_camera->GetAngleX();
track.initialValues[1] = m_camera->GetAngleY();
track.initialValues[2] = m_camera->GetAngleZ();
}
break;
case TrackType::ObjectPosition:
if (track.target) {
track.initialValues[0] = (int16_t)track.target->position.x.value;
track.initialValues[1] = (int16_t)track.target->position.y.value;
track.initialValues[2] = (int16_t)track.target->position.z.value;
}
break;
case TrackType::ObjectRotation:
// Initial rotation angles: 0,0,0 (no way to extract Euler from matrix)
break;
case TrackType::ObjectActive:
if (track.target) {
track.initialValues[0] = track.target->isActive() ? 1 : 0;
}
break;
case TrackType::UICanvasVisible:
if (m_uiSystem) {
track.initialValues[0] = m_uiSystem->isCanvasVisible(track.uiHandle) ? 1 : 0;
}
break;
case TrackType::UIElementVisible:
if (m_uiSystem) {
track.initialValues[0] = m_uiSystem->isElementVisible(track.uiHandle) ? 1 : 0;
}
break;
case TrackType::UIProgress:
if (m_uiSystem) {
track.initialValues[0] = m_uiSystem->getProgress(track.uiHandle);
}
break;
case TrackType::UIPosition:
if (m_uiSystem) {
int16_t px, py;
m_uiSystem->getPosition(track.uiHandle, px, py);
track.initialValues[0] = px;
track.initialValues[1] = py;
}
break;
case TrackType::UIColor:
if (m_uiSystem) {
uint8_t cr, cg, cb;
m_uiSystem->getColor(track.uiHandle, cr, cg, cb);
track.initialValues[0] = cr;
track.initialValues[1] = cg;
track.initialValues[2] = cb;
}
break;
}
}
return true;
}
}
return false;
}
void CutscenePlayer::stop() {
if (!m_active) return;
m_active = nullptr;
fireOnComplete();
}
void CutscenePlayer::tick() {
if (!m_active) return;
for (uint8_t i = 0; i < m_active->trackCount; i++) {
applyTrack(m_active->tracks[i]);
}
while (m_nextAudio < m_active->audioEventCount) {
CutsceneAudioEvent& evt = m_active->audioEvents[m_nextAudio];
if (evt.frame <= m_frame) {
if (m_audio) {
m_audio->play(evt.clipIndex, evt.volume, evt.pan);
}
m_nextAudio++;
} else {
break;
}
}
m_frame++;
if (m_frame > m_active->totalFrames) {
if (m_loop) {
// Restart from the beginning
m_frame = 0;
m_nextAudio = 0;
} else {
m_active = nullptr; // Cutscene finished
fireOnComplete();
}
}
}
void CutscenePlayer::fireOnComplete() {
if (m_onCompleteRef == LUA_NOREF || !m_luaState) return;
psyqo::Lua L(m_luaState);
L.rawGetI(LUA_REGISTRYINDEX, m_onCompleteRef);
if (L.isFunction(-1)) {
if (L.pcall(0, 0) != LUA_OK) {
L.pop();
}
} else {
L.pop();
}
// Unreference the callback (one-shot)
luaL_unref(m_luaState, LUA_REGISTRYINDEX, m_onCompleteRef);
m_onCompleteRef = LUA_NOREF;
}
static int32_t applyCurve(int32_t t, InterpMode mode) {
switch (mode) {
default:
case InterpMode::Linear:
return t;
case InterpMode::Step:
return 0;
case InterpMode::EaseIn:
return (int32_t)((int64_t)t * t >> 12);
case InterpMode::EaseOut:
return (int32_t)(((int64_t)t * (8192 - t)) >> 12);
case InterpMode::EaseInOut: {
int64_t t2 = (int64_t)t * t;
int64_t t3 = t2 * t;
return (int32_t)((3 * t2 - 2 * (t3 >> 12)) >> 12);
}
}
}
static bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]) {
if (count == 0) {
out[0] = out[1] = out[2] = 0;
return false;
}
if (frame <= kf[0].getFrame() || count == 1) {
out[0] = kf[0].values[0];
out[1] = kf[0].values[1];
out[2] = kf[0].values[2];
return false;
}
if (frame >= kf[count - 1].getFrame()) {
out[0] = kf[count - 1].values[0];
out[1] = kf[count - 1].values[1];
out[2] = kf[count - 1].values[2];
return false;
}
b = 1;
while (b < count && kf[b].getFrame() <= frame) b++;
a = b - 1;
uint16_t span = kf[b].getFrame() - kf[a].getFrame();
if (span == 0) {
out[0] = kf[a].values[0];
out[1] = kf[a].values[1];
out[2] = kf[a].values[2];
return false;
}
uint32_t num = (uint32_t)(frame - kf[a].getFrame()) << 12;
int32_t rawT = (int32_t)(num / span);
t = applyCurve(rawT, kf[b].getInterp());
return true;
}
void CutscenePlayer::lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) {
uint8_t a, b;
int32_t t;
if (!findKfPair(kf, count, m_frame, a, b, t, out)) {
if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) {
uint16_t span = kf[0].getFrame();
uint32_t num = (uint32_t)m_frame << 12;
int32_t rawT = (int32_t)(num / span);
int32_t ct = applyCurve(rawT, kf[0].getInterp());
for (int i = 0; i < 3; i++) {
int32_t delta = (int32_t)kf[0].values[i] - (int32_t)initial[i];
out[i] = (int16_t)((int32_t)initial[i] + ((delta * ct) >> 12));
}
}
return;
}
for (int i = 0; i < 3; i++) {
int32_t delta = (int32_t)kf[b].values[i] - (int32_t)kf[a].values[i];
out[i] = (int16_t)((int32_t)kf[a].values[i] + ((delta * t) >> 12));
}
}
static constexpr int32_t ANGLE_FULL_CIRCLE = 2048;
static constexpr int32_t ANGLE_HALF_CIRCLE = 1024;
void CutscenePlayer::lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) {
uint8_t a, b;
int32_t t;
if (!findKfPair(kf, count, m_frame, a, b, t, out)) {
if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) {
uint16_t span = kf[0].getFrame();
uint32_t num = (uint32_t)m_frame << 12;
int32_t rawT = (int32_t)(num / span);
int32_t ct = applyCurve(rawT, kf[0].getInterp());
for (int i = 0; i < 3; i++) {
int32_t from = (int32_t)initial[i];
int32_t to = (int32_t)kf[0].values[i];
int32_t delta = to - from;
delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE;
out[i] = (int16_t)(from + ((delta * ct) >> 12));
}
}
return;
}
for (int i = 0; i < 3; i++) {
int32_t from = (int32_t)kf[a].values[i];
int32_t to = (int32_t)kf[b].values[i];
int32_t delta = to - from;
delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE;
out[i] = (int16_t)(from + ((delta * t) >> 12));
}
}
void CutscenePlayer::applyTrack(CutsceneTrack& track) {
if (track.keyframeCount == 0 || !track.keyframes) return;
int16_t out[3];
switch (track.trackType) {
case TrackType::CameraPosition: {
if (!m_camera) return;
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
psyqo::FixedPoint<12> x, y, z;
x.value = (int32_t)out[0];
y.value = (int32_t)out[1];
z.value = (int32_t)out[2];
m_camera->SetPosition(x, y, z);
break;
}
case TrackType::CameraRotation: {
if (!m_camera) return;
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
psyqo::Angle rx, ry, rz;
rx.value = (int32_t)out[0];
ry.value = (int32_t)out[1];
rz.value = (int32_t)out[2];
m_camera->SetRotation(rx, ry, rz);
break;
}
case TrackType::ObjectPosition: {
if (!track.target) return;
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
track.target->position.x.value = (int32_t)out[0];
track.target->position.y.value = (int32_t)out[1];
track.target->position.z.value = (int32_t)out[2];
break;
}
case TrackType::ObjectRotation: {
if (!track.target) return;
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
psyqo::Angle rx, ry, rz;
rx.value = (int32_t)out[0];
ry.value = (int32_t)out[1];
rz.value = (int32_t)out[2];
auto matY = psyqo::SoftMath::generateRotationMatrix33(ry, psyqo::SoftMath::Axis::Y, m_trig);
auto matX = psyqo::SoftMath::generateRotationMatrix33(rx, psyqo::SoftMath::Axis::X, m_trig);
auto matZ = psyqo::SoftMath::generateRotationMatrix33(rz, psyqo::SoftMath::Axis::Z, m_trig);
auto temp = psyqo::SoftMath::multiplyMatrix33(matY, matX);
track.target->rotation = psyqo::SoftMath::multiplyMatrix33(temp, matZ);
break;
}
case TrackType::ObjectActive: {
if (!track.target) return;
CutsceneKeyframe* kf = track.keyframes;
uint8_t count = track.keyframeCount;
int16_t activeVal = (count > 0 && m_frame < kf[0].getFrame())
? track.initialValues[0]
: kf[0].values[0];
for (uint8_t i = 0; i < count; i++) {
if (kf[i].getFrame() <= m_frame) {
activeVal = kf[i].values[0];
} else {
break;
}
}
track.target->setActive(activeVal != 0);
break;
}
// ── UI track types ──
case TrackType::UICanvasVisible: {
if (!m_uiSystem) return;
CutsceneKeyframe* kf = track.keyframes;
uint8_t count = track.keyframeCount;
int16_t val = (count > 0 && m_frame < kf[0].getFrame())
? track.initialValues[0] : kf[0].values[0];
for (uint8_t i = 0; i < count; i++) {
if (kf[i].getFrame() <= m_frame) val = kf[i].values[0];
else break;
}
m_uiSystem->setCanvasVisible(track.uiHandle, val != 0);
break;
}
case TrackType::UIElementVisible: {
if (!m_uiSystem) return;
CutsceneKeyframe* kf = track.keyframes;
uint8_t count = track.keyframeCount;
int16_t val = (count > 0 && m_frame < kf[0].getFrame())
? track.initialValues[0] : kf[0].values[0];
for (uint8_t i = 0; i < count; i++) {
if (kf[i].getFrame() <= m_frame) val = kf[i].values[0];
else break;
}
m_uiSystem->setElementVisible(track.uiHandle, val != 0);
break;
}
case TrackType::UIProgress: {
if (!m_uiSystem) return;
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
int16_t v = out[0];
if (v < 0) v = 0;
if (v > 100) v = 100;
m_uiSystem->setProgress(track.uiHandle, (uint8_t)v);
break;
}
case TrackType::UIPosition: {
if (!m_uiSystem) return;
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
m_uiSystem->setPosition(track.uiHandle, out[0], out[1]);
break;
}
case TrackType::UIColor: {
if (!m_uiSystem) return;
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
uint8_t cr = (out[0] < 0) ? 0 : ((out[0] > 255) ? 255 : (uint8_t)out[0]);
uint8_t cg = (out[1] < 0) ? 0 : ((out[1] > 255) ? 255 : (uint8_t)out[1]);
uint8_t cb = (out[2] < 0) ? 0 : ((out[2] > 255) ? 255 : (uint8_t)out[2]);
m_uiSystem->setColor(track.uiHandle, cr, cg, cb);
break;
}
}
}
} // namespace psxsplash