scummvm/engines/tetraedge/game/character.cpp
2024-08-04 12:47:37 +03:00

1150 lines
37 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/hashmap.h"
#include "common/path.h"
#include "common/file.h"
#include "common/debug.h"
#include "common/util.h"
#include "tetraedge/tetraedge.h"
#include "tetraedge/game/character.h"
#include "tetraedge/game/application.h"
#include "tetraedge/game/game.h"
#include "tetraedge/game/character_settings_xml_parser.h"
#include "tetraedge/te/te_model_animation.h"
#include "tetraedge/te/te_core.h"
namespace Tetraedge {
/*static*/ Common::HashMap<Common::String, Character::CharacterSettings> *Character::_globalCharacterSettings = nullptr;
/*static*/ Common::HashMap<Common::String, TeIntrusivePtr<TeModelAnimation>> *Character::_animCacheMap = nullptr;
// /*static*/ Common::Array<Character::AnimCacheElement> *Character::_animCache = nullptr;
// /*static*/ uint Character::_animCacheSize = 0;
void Character::CharacterSettings::clear() {
_name.clear();
_modelFileName.clear();
_defaultScale = TeVector3f32();
_idleAnimFileName.clear();
_walkSettings.clear();
_walkSpeed = 0.0f;
_cutSceneCurveDemiPosition = TeVector3f32();
_defaultEyes.clear();
_defaultMouth.clear();
_defaultBody.clear();
_invertNormals = false;
}
void Character::WalkSettings::clear() {
for (int i = 0; i < 4; i++) {
_walkParts[i] = AnimSettings();
}
}
Character::Character() : _walkCurveStart(0), _lastFrame(-1), _callbacksChanged(false),
_notWalkAnim(false), _returnToIdleAnim(false), _walkModeStr("Walk"),
_needsSomeUpdate(false), _positionFlag(false), _lookingAtTallThing(false),
_stepSound1("sounds/SFX/PAS_H_BOIS1.ogg"), _stepSound2("sounds/SFX/PAS_H_BOIS2.ogg"),
_freeMoveZone(nullptr), _animSoundOffset(0), _lastAnimFrame(0), _charLookingAt(nullptr),
_recallageY(true), _walkToFlag(false), _walkCurveEnd(0.0f), _walkCurveLast(0.0f),
_walkCurveLen(0.0f), _walkCurveIncrement(0.0f), _walkEndAnimG(false), _walkTotalFrames(0),
_walkCurveNextLength(0.0f), _walkedLength(0.0f), _walkLoopAnimLen(0.0f), _walkEndGAnimLen(0.0f),
_walkStartAnimLen(0.0f), _walkStartAnimFrameCount(0), _walkLoopAnimFrameCount(0),
_walkEndGAnimFrameCount(0), _hasAnchor(false), _charLookingAtOffset(0.0f) {
_curModelAnim.setDeleteFn(&TeModelAnimation::deleteLaterStatic);
}
Character::~Character() {
_model->setVisible(false);
_model->bonesUpdatedSignal().remove(this, &Character::onBonesUpdate);
deleteAnim();
Game *game = g_engine->getGame();
Common::Array<TeIntrusivePtr<TeModel>> &models = game->scene().models();
for (uint i = 0; i < models.size(); i++) {
if (models[i] == _model) {
models.remove_at(i);
break;
}
}
removeAnim();
for (uint s = 0; s < 2; s++) {
if (!_shadowModel[s])
continue;
for (uint i = 0; i < models.size(); i++) {
if (models[i] == _shadowModel[s]) {
models.remove_at(i);
break;
}
}
}
}
/*static*/
void Character::cleanup() {
if (_globalCharacterSettings)
delete _globalCharacterSettings;
_globalCharacterSettings = nullptr;
animCacheFreeAll();
}
void Character::addCallback(const Common::String &animKey, const Common::String &fnName, float triggerFrame, float maxCalls) {
Callback *c = new Callback();
c->_luaFn = fnName;
c->_lastCheckFrame = 0;
c->_triggerFrame = (int)triggerFrame;
c->_maxCalls = (int)maxCalls;
// Slight difference here to orig (that sets -NAN) because of
// the way this gets used later, setting large negative is more correct.
c->_callsMade = (maxCalls == -1.0 ? -1e9 : 0.0f);
if (g_engine->gameType() == TetraedgeEngine::kSyberia) {
// Syberia 1 has slightly weird logic to decide what key to use.
//
// WORKAROUND: This callback seems to be set too late.. frame 31, but it
// only gets to 15? Some bug in the way anim blends hand off?
// for scenes/CitSpace2/34230/Logic34230.lua
//
if (fnName == "ChangeClef" && c->_triggerFrame == 31)
c->_triggerFrame = 15;
const Common::Path animPath = _model->anim()->loadedPath();
// Another difference.. the original messes with paths a bit - just
// use the file name, since it's already limited by character.
Common::String animName = animPath.baseName();
if (animName.empty())
animName = animPath.toString();
if (_callbacks.contains(animName)) {
_callbacks[animName].push_back(c);
} else {
Common::Path animKeyPath(animKey);
Common::Array<Callback *> callbacks;
callbacks.push_back(c);
_callbacks.setVal(animKeyPath.baseName(), callbacks);
}
} else if (g_engine->gameType() == TetraedgeEngine::kSyberia2){
// Syberia 2 is simpler, it always uses a lower-case version of the anim
// file in the passed key.
Common::String key = Common::Path(animKey).baseName();
key.toLowercase();
if (_callbacks.contains(key)) {
_callbacks[key].push_back(c);
} else {
Common::Array<Callback *> callbacks;
callbacks.push_back(c);
_callbacks.setVal(key, callbacks);
}
} else {
error("addCallback: Unsupported game type.");
}
}
/*static*/
void Character::animCacheFreeAll() {
/*
if (_animCache) {
for (const auto &entry : (*_animCache))
_animCacheSize -= entry._size;
delete _animCache;
_animCache = nullptr;
} */
if (_animCacheMap) {
delete _animCacheMap;
_animCacheMap = nullptr;
}
}
/*static*/
void Character::animCacheFreeOldest() {
// Unused?
//_animCacheSize -= _animCache[_animCache.size() - 1]._size;
//_animCache.pop_back();
}
/*static*/
TeIntrusivePtr<TeModelAnimation> Character::animCacheLoad(const Common::Path &path) {
const Common::String pathStr = path.toString();
if (!_animCacheMap) {
_animCacheMap = new Common::HashMap<Common::String, TeIntrusivePtr<TeModelAnimation>>();
}
if (_animCacheMap->contains(pathStr)) {
// Copy from the cache (keep the cached instance clean)
return new TeModelAnimation(*_animCacheMap->getVal(pathStr));
}
TeIntrusivePtr<TeModelAnimation> modelAnim = new TeModelAnimation();
if (!modelAnim->load(path)) {
warning("Failed to load anim %s", path.toString().c_str());
}
_animCacheMap->setVal(pathStr, modelAnim);
return modelAnim;
}
float Character::animLength(const TeModelAnimation &modelanim, int bone, int lastframe) {
int last = modelanim.lastFrame();
if (lastframe > last)
lastframe = last;
int first = modelanim.firstFrame();
const TeVector3f32 starttrans = translationVectorFromAnim(modelanim, bone, first);
const TeVector3f32 endtrans = translationVectorFromAnim(modelanim, bone, lastframe);
const TeVector3f32 secondtrans = translationVectorFromAnim(modelanim, bone, first + 1);
return ((endtrans.z() - starttrans.z()) + secondtrans.z()) - starttrans.z();
}
float Character::animLengthFromFile(const Common::String &animname, uint32 *pframeCount, uint lastframe /* = 9999 */) {
if (animname.empty()) {
*pframeCount = 0;
return 0.0f;
}
TeIntrusivePtr<TeModelAnimation> anim = _model->anim();
if (!anim->loadedPath().toString().contains(animname)) {
Common::Path animpath("models/Anims");
animpath.joinInPlace(animname);
anim = animCacheLoad(animpath);
if (!anim)
error("Character::animLengthFromFile couldn't load anim %s", animname.c_str());
}
// The "Pere" or "father" bone is the root.
float animLen = animLength(*anim, anim->findBone(rootBone()), lastframe);
int frameCount = anim->lastFrame() + 1 - anim->firstFrame();
*pframeCount = frameCount;
return animLen * _model->scale().z();
}
bool Character::blendAnimation(const Common::String &animname, float amount, bool repeat, bool returnToIdle) {
Common::Path animpath("models/Anims");
animpath.joinInPlace(animname);
_notWalkAnim = !(animname.contains(_characterSettings._idleAnimFileName)
|| animname.contains(walkAnim(WalkPart_Start))
|| animname.contains(walkAnim(WalkPart_Loop))
|| animname.contains(walkAnim(WalkPart_EndG))
|| animname.contains(walkAnim(WalkPart_EndD)));
if (_curModelAnim) {
_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
_curModelAnim->unbind();
_curModelAnim->reset();
}
_curModelAnim = animCacheLoad(animpath);
assert(_curModelAnim);
_curModelAnim->reset();
_curModelAnim->onFinished().add(this, &Character::onModelAnimationFinished);
_curModelAnim->bind(_model);
_model->blendAnim(_curModelAnim, amount, repeat);
_lastFrame = -1;
_curModelAnim->play();
_curAnimName = animname;
_returnToIdleAnim = !repeat && returnToIdle;
return true;
}
TeVector3f32 Character::correctPosition(const TeVector3f32 &pos) {
bool flag;
TeVector3f32 result = _freeMoveZone->correctCharacterPosition(pos, &flag, true);
if (!flag)
result.y() = _model->position().y();
return result;
}
float Character::curveOffset() {
return _walkCurveStart;
}
void Character::deleteAllCallback() {
_callbacksChanged = true;
for (auto &pair : _callbacks) {
for (Callback *c : pair._value) {
delete c;
}
}
_callbacks.clear();
}
void Character::deleteAnim() {
if (_curModelAnim) {
_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
_curModelAnim->unbind();
_curModelAnim->reset();
}
_model->removeAnim();
_curModelAnim.release();
}
void Character::deleteCallback(const Common::String &key, const Common::String &fnName, float f) {
_callbacksChanged = true;
assert(_model->anim());
Common::String animFile = _model->anim()->loadedPath().baseName();
if (!_callbacks.contains(animFile))
return;
Common::Array<Callback *> &cbs = _callbacks.getVal(animFile);
for (uint i = 0; i < cbs.size(); i++) {
if (fnName.empty()) {
delete cbs[i];
// don't remove from array, clear at the end.
} else if (cbs[i]->_luaFn == fnName) {
if (f == -1 || cbs[i]->_triggerFrame == f) {
delete cbs[i];
cbs.remove_at(i);
i--;
}
}
}
if (fnName.empty())
cbs.clear();
if (cbs.empty())
_callbacks.erase(animFile);
}
void Character::endMove() {
if (g_engine->getGame()->scene()._character == this)
walkMode("Walk");
_onFinishedSignal.call();
stop();
}
const Character::WalkSettings *Character::getCurrentWalkFiles() {
for (const auto & walkSettings : _characterSettings._walkSettings) {
if (walkSettings._key == _walkModeStr)
return &walkSettings._value;
}
return nullptr;
}
bool Character::isFramePassed(int frameno) {
return (frameno > _lastAnimFrame && _model->anim()->curFrame2() >= frameno);
}
bool Character::isWalkEnd() {
const Common::String animFile = _model->anim()->loadedPath().baseName();
for (const auto & walkSettings : _characterSettings._walkSettings) {
if (walkSettings._value._walkParts[WalkPart_EndD]._file.contains(animFile)
|| walkSettings._value._walkParts[WalkPart_EndG]._file.contains(animFile))
return true;
}
return false;
}
int Character::leftStepFrame(enum Character::WalkPart walkpart) {
const Character::WalkSettings *settings = getCurrentWalkFiles();
if (settings) {
return settings->_walkParts[(int)walkpart]._stepLeft;
}
return -1;
}
int Character::rightStepFrame(enum Character::WalkPart walkpart) {
const Character::WalkSettings *settings = getCurrentWalkFiles();
if (settings) {
return settings->_walkParts[(int)walkpart]._stepRight;
}
return -1;
}
bool Character::loadModel(const Common::String &mname, bool unused) {
assert(_globalCharacterSettings);
if (_model) {
_model->bonesUpdatedSignal().remove(this, &Character::onBonesUpdate);
}
_model = new TeModel();
_model->bonesUpdatedSignal().add(this, &Character::onBonesUpdate);
if (!_globalCharacterSettings->contains(mname))
return false;
_characterSettings = _globalCharacterSettings->getVal(mname);
_model->setTexturePath("models/Textures");
_model->setEnableLights(true);
if (!_model->load(Common::Path("models").join(_characterSettings._modelFileName))) {
warning("Failed to load character model %s", _characterSettings._modelFileName.c_str());
return false;
}
_model->setName(mname);
_model->setScale(_characterSettings._defaultScale);
if (_characterSettings._invertNormals)
_model->invertNormals();
for (auto &mesh : _model->meshes())
mesh->setVisible(true);
// Set all mouthes, eyes, etc not visible by default
_model->setVisibleByName("_B_", false);
_model->setVisibleByName("_Y_", false);
_model->setVisibleByName("_M_", false);
_model->setVisibleByName("_E_", false);
// Note: game loops through "faces" here, but it only ever uses the default ones.
_model->setVisibleByName(_characterSettings._defaultEyes, true);
_model->setVisibleByName(_characterSettings._defaultMouth, true);
_model->setVisibleByName(_characterSettings._defaultBody, true);
setAnimation(_characterSettings._idleAnimFileName, true);
_walkStartAnimLen = animLengthFromFile(walkAnim(WalkPart_Start), &_walkStartAnimFrameCount);
_walkEndGAnimLen = animLengthFromFile(walkAnim(WalkPart_EndG), &_walkEndGAnimFrameCount);
_walkLoopAnimLen = animLengthFromFile(walkAnim(WalkPart_Loop), &_walkLoopAnimFrameCount);
if (g_engine->gameType() == TetraedgeEngine::kSyberia) {
// Only Syberia 1 has the simple shadow.
TeIntrusivePtr<Te3DTexture> shadow = Te3DTexture::makeInstance();
TeCore *core = g_engine->getCore();
shadow->load(core->findFile("models/Textures/simple_shadow_alpha.tga"));
for (int i = 0; i < 2; i++) {
TeModel *pmodel = new TeModel();
_shadowModel[i] = pmodel;
pmodel->setName("Shadow");
Common::Array<TeVector3f32> arr;
arr.resize(4);
arr[0] = TeVector3f32(-60.0, 0.0, -60.0);
arr[1] = TeVector3f32(-60.0, 0.0, 60.0);
arr[2] = TeVector3f32(60.0, 0.0, -60.0);
arr[3] = TeVector3f32(60.0, 0.0, 60.0);
pmodel->setQuad(shadow, arr, TeColor(0xff, 0xff, 0xff, 0x50));
}
}
return true;
}
/*static*/
bool Character::loadSettings(const Common::Path &path) {
CharacterSettingsXmlParser parser;
parser.setAllowText();
if (_globalCharacterSettings)
delete _globalCharacterSettings;
_globalCharacterSettings = new Common::HashMap<Common::String, CharacterSettings>();
parser.setCharacterSettings(_globalCharacterSettings);
// WORKAROUND: This file contains invalid comments
// eg, <!--------- and a comment-inside-a-comment.
// patch them before parsing.
Common::File xmlFile;
if (!xmlFile.open(path))
error("Character::loadSettings: Can't open %s", path.toString(Common::Path::kNativeSeparator).c_str());
const int64 bufsize = xmlFile.size();
char *buf = new char[bufsize+1];
buf[bufsize] = '\0';
xmlFile.read(buf, bufsize);
Common::String fixedbuf(buf);
delete [] buf;
size_t offset = fixedbuf.find("------------");
while (offset != Common::String::npos) {
fixedbuf.replace(offset, 12, "--");
offset = fixedbuf.find("------------");
}
// Big HACK: Remove the embedded comment in this config.
offset = fixedbuf.find("<!--<walk>");
if (offset != Common::String::npos) {
size_t endOffset = fixedbuf.find(" -->", offset);
if (endOffset != Common::String::npos) {
size_t realEndOffset = fixedbuf.find("walk>-->", endOffset);
if (realEndOffset != Common::String::npos && realEndOffset > endOffset) {
fixedbuf.replace(offset, endOffset - offset, "<!-- ");
}
}
}
if (!parser.loadBuffer((const byte *)fixedbuf.c_str(), bufsize))
error("Character::loadSettings: Can't open %s", path.toString(Common::Path::kNativeSeparator).c_str());
if (!parser.parse())
error("Character::loadSettings: Can't parse %s", path.toString(Common::Path::kNativeSeparator).c_str());
return true;
}
bool Character::onBonesUpdate(const Common::String &boneName, TeMatrix4x4 &boneMatrix) {
if (!_model || !_model->anim())
return false;
Game *game = g_engine->getGame();
if (boneName == rootBone()) {
const Common::String animfile = _model->anim()->loadedPath().baseName();
bool resetX = false;
if (game->scene()._character == this) {
for (const auto &walkSettings : _characterSettings._walkSettings) {
if (walkSettings._key.contains("Walk") || walkSettings._key.contains("Jog")) {
resetX |= (walkSettings._value._walkParts[0]._file.contains(animfile)
|| walkSettings._value._walkParts[1]._file.contains(animfile)
|| walkSettings._value._walkParts[2]._file.contains(animfile)
|| walkSettings._value._walkParts[3]._file.contains(animfile));
}
}
resetX |= _characterSettings._idleAnimFileName.contains(animfile);
} else {
resetX = (_characterSettings._idleAnimFileName.contains(animfile) ||
walkAnim(WalkPart_Start).contains(animfile) ||
walkAnim(WalkPart_Loop).contains(animfile) ||
walkAnim(WalkPart_EndD).contains(animfile) ||
walkAnim(WalkPart_EndG).contains(animfile));
}
if (resetX) {
boneMatrix.setValue(0, 3, 0.0f);
boneMatrix.setValue(2, 3, 0.0f);
}
}
if (boneName.contains("Bip01 Head")) {
if (_hasAnchor) {
game->scene().currentCamera()->apply();
_lastHeadRotation = _headRotation;
TeQuaternion rot1 = TeQuaternion::fromAxisAndAngle(TeVector3f32(-1, 0, 0), _lastHeadRotation.getX());
TeQuaternion rot2 = TeQuaternion::fromAxisAndAngle(TeVector3f32(0, 0, 1), _lastHeadRotation.getY());
boneMatrix.rotate(rot1);
boneMatrix.rotate(rot2);
} else {
// Return the head to the centerpoint if there is no anchor.
const float lastHeadX = _lastHeadRotation.getX();
const float headXAdjust = (lastHeadX > 0) ? -0.1 : 0.1;
const float newX = (fabs(headXAdjust) > fabs(lastHeadX)) ? 0.0 : lastHeadX + headXAdjust;
_lastHeadRotation.setX(newX);
const float lastHeadY = _lastHeadRotation.getY();
const float headYAdjust = (lastHeadY > 0) ? -0.1 : 0.1;
const float newY = (fabs(headYAdjust) > fabs(lastHeadY)) ? 0.0 : lastHeadY + headYAdjust;
_lastHeadRotation.setY(newY);
_headRotation = _lastHeadRotation;
const TeQuaternion rot1 = TeQuaternion::fromAxisAndAngle(TeVector3f32(-1, 0, 0), _lastHeadRotation.getX());
const TeQuaternion rot2 = TeQuaternion::fromAxisAndAngle(TeVector3f32(0, 0, 1), _lastHeadRotation.getY());
boneMatrix.rotate(rot1);
boneMatrix.rotate(rot2);
_lastHeadBoneTrans = boneMatrix.translation();
}
}
if (boneName.contains("Bip01 L Foot") || boneName.contains("Bip01 R Foot")) {
TeVector3f32 trans = boneMatrix.translation();
trans.rotate(_model->rotation());
const TeVector3f32 modelScale = _model->scale();
trans.x() *= modelScale.x();
trans.y() = 0.0;
trans.z() *= modelScale.z();
TeVector3f32 pos = _model->position() + trans;
if (_freeMoveZone) {
bool flag;
pos = _freeMoveZone->correctCharacterPosition(pos, &flag, true);
}
int shadowNo = boneName.contains("Bip01 L Foot") ? 0 : 1;
if (_shadowModel[shadowNo]) {
_shadowModel[shadowNo]->setPosition(pos);
_shadowModel[shadowNo]->setRotation(_model->rotation());
_shadowModel[shadowNo]->setScale(_model->scale());
}
}
// Move any objects attached to the bone
for (Object3D *obj : game->scene().object3Ds()) {
if (obj->_onCharName == _model->name() && boneName == obj->_onCharBone) {
if (_model->anim()->curFrame2() >= obj->_startFrame
&& _model->anim()->curFrame2() <= obj->_endFrame) {
obj->model()->setVisible(true);
if (!obj->_moveAnim._runTimer.running()) {
obj->_lastMatrix = boneMatrix;
obj->_lastMatrix.scale(obj->_objScale);
obj->_lastMatrix.rotate(obj->_objRotation);
obj->_lastMatrix.translate(obj->_objTranslation);
obj->model()->forceMatrix(obj->_lastMatrix);
obj->model()->setRotation(_model->rotation());
obj->model()->setPosition(_model->position());
obj->model()->setScale(_model->scale());
} else {
obj->model()->forceMatrix(obj->_lastMatrix);
obj->model()->setRotation(_model->rotation());
obj->model()->setPosition(_model->position() + obj->_curMovePos);
obj->model()->setScale(_model->scale());
}
} else {
obj->model()->setVisible(false);
}
}
}
return true;
}
bool Character::onModelAnimationFinished() {
// this shouldn't happen but check to be sure..
if (!_model || !_model->anim())
return false;
const Common::Path loadedPath = _model->anim()->loadedPath();
const Common::String animfile = loadedPath.baseName();
bool shouldAdjust = true;
for (const auto &unrecal : g_engine->getApplication()->unrecalAnims()) {
if (animfile.contains(unrecal))
shouldAdjust = false;
}
Game *game = g_engine->getGame();
bool isWalkAnim = false;
if (game->scene()._character == this) {
for (const auto &walkSettings : _characterSettings._walkSettings) {
if (walkSettings._key.contains("Walk") || walkSettings._key.contains("Jog")) {
isWalkAnim |= (walkSettings._value._walkParts[0]._file.contains(animfile)
|| walkSettings._value._walkParts[1]._file.contains(animfile)
|| walkSettings._value._walkParts[2]._file.contains(animfile)
|| walkSettings._value._walkParts[3]._file.contains(animfile));
}
}
isWalkAnim |= animfile.contains(_characterSettings._idleAnimFileName);
} else {
isWalkAnim = (_characterSettings._idleAnimFileName.contains(animfile)
|| walkAnim(WalkPart_Start).contains(animfile)
|| walkAnim(WalkPart_Loop).contains(animfile)
|| walkAnim(WalkPart_EndD).contains(animfile)
|| walkAnim(WalkPart_EndG).contains(animfile));
}
if (!isWalkAnim && shouldAdjust) {
int pereBone = _curModelAnim->findBone(rootBone());
const TeTRS endTRS = trsFromAnim(*_curModelAnim, pereBone, _curModelAnim->lastFrame());
TeVector3f32 trans = endTRS.getTranslation();
trans.x() = -trans.x();
TeVector3f32 newpos;
if (!_recallageY) {
const TeTRS startTRS = trsFromAnim(*_curModelAnim, pereBone, _curModelAnim->firstFrame());
trans = trans - startTRS.getTranslation();
const TeTRS nearEndTRS = trsFromAnim(*_curModelAnim, pereBone, _curModelAnim->lastFrame() - 1);
trans = trans + (endTRS.getTranslation() - nearEndTRS.getTranslation());
newpos = _model->worldTransformationMatrix() * trans;
} else if (!_freeMoveZone) {
trans.x() = -trans.x();
trans.y() = 0.0;
newpos = _model->worldTransformationMatrix() * trans;
} else {
newpos = correctPosition(_model->worldTransformationMatrix() * trans);
}
_model->setPosition(newpos);
}
if (game->scene()._character == this) {
_characterAnimPlayerFinishedSignal.call(loadedPath.toString());
} else {
_onCharacterAnimFinishedSignal.call(_model->name());
}
Common::Path setAnimNamePath(_setAnimName);
Common::String setAnimNameFile = setAnimNamePath.baseName();
Common::String loadedPathFile = loadedPath.baseName();
if (_returnToIdleAnim && loadedPathFile.contains(setAnimNameFile)) {
_notWalkAnim = false;
_returnToIdleAnim = false;
setAnimation(_characterSettings._idleAnimFileName, true);
}
return false;
}
void Character::permanentUpdate() {
assert(_model->anim());
const Common::Path &animPath = _model->anim()->loadedPath();
int curFrame = _model->anim()->curFrame2();
Game *game = g_engine->getGame();
_callbacksChanged = false;
// Diverge from original - just use filename for anim callbacks as the
// original does werid things with paths.
Common::String animFile = animPath.baseName();
if (g_engine->gameType() == TetraedgeEngine::kSyberia2)
animFile.toLowercase();
//if (!_callbacks.empty())
// debug("%s: check cbs for %s frame %d speed %.02f", _model->name().c_str(),
// animFile.c_str(), curFrame, _model->anim()->speed());
if (_callbacks.contains(animFile)) {
Common::Array<Callback *> &cbs = _callbacks.getVal(animFile);
for (Callback *cb : cbs) {
//debug("%s: check cb for %s frame %d against %d. speed %.02f", _model->name().c_str(),
// animFile.c_str(), curFrame, cb->_triggerFrame, _model->anim()->speed());
if (cb->_triggerFrame > cb->_lastCheckFrame && curFrame >= cb->_triggerFrame){
int callsMade = cb->_callsMade;
cb->_callsMade++;
if (callsMade >= cb->_maxCalls)
continue;
cb->_lastCheckFrame = curFrame;
game->luaScript().execute(cb->_luaFn);
if (_callbacksChanged)
break;
}
cb->_lastCheckFrame = curFrame;
}
}
if (animFile.contains("ka_esc_h")) {
if (_lastAnimFrame < 7 && _model->anim()->curFrame2() > 6) {
game->playSound("sounds/SFX/PAS_F_PAVE1.ogg", 1, 1.0f);
} else if (_lastAnimFrame < 22 && _model->anim()->curFrame2() > 21) {
game->playSound("sounds/SFX/PAS_F_PAVE2.ogg", 1, 1.0f);
}
} else if (animFile.contains("ka_esc_b")) {
if (_lastAnimFrame < 12 && _model->anim()->curFrame2() > 11) {
game->playSound("sounds/SFX/PAS_F_PAVE1.ogg", 1, 1.0f);
} else if (_lastAnimFrame < 27 && _model->anim()->curFrame2() > 26) {
game->playSound("sounds/SFX/PAS_F_PAVE2.ogg", 1, 1.0f);
}
}
updateAnimFrame();
}
void Character::placeOnCurve(TeIntrusivePtr<TeBezierCurve> &curve) {
_curve = curve;
updatePosition(_walkCurveStart);
}
void Character::removeAnim() {
if (_curModelAnim) {
_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
_curModelAnim->unbind();
_curModelAnim->reset();
}
_model->removeAnim();
if (_curModelAnim) {
_curModelAnim.release();
}
}
void Character::removeFromCurve() {
_curve.release();
}
Common::String Character::rootBone() const {
if (g_engine->gameType() != TetraedgeEngine::kSyberia2 || _model->name() != "Youki")
return "Pere";
else
return "Bip01";
}
bool Character::setAnimation(const Common::String &aname, bool repeat, bool returnToIdle, bool unused, int startFrame, int endFrame) {
if (aname.empty())
return false;
Common::Path animPath("models/Anims");
animPath.joinInPlace(aname);
bool isWalkAnim = (aname.contains(_characterSettings._idleAnimFileName) ||
aname.contains(walkAnim(WalkPart_Start)) ||
aname.contains(walkAnim(WalkPart_Loop)) ||
aname.contains(walkAnim(WalkPart_EndD)) ||
aname.contains(walkAnim(WalkPart_EndG)));
_notWalkAnim = !isWalkAnim;
if (_curModelAnim) {
_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
_curModelAnim->unbind();
_curModelAnim->reset();
}
_curModelAnim = animCacheLoad(animPath);
_curModelAnim->reset();
_curModelAnim->onFinished().add(this, &Character::onModelAnimationFinished);
_curModelAnim->bind(_model);
_curModelAnim->setFrameLimits(startFrame, endFrame);
_model->setAnim(_curModelAnim, repeat);
_lastFrame = -1;
_curModelAnim->play();
_setAnimName = aname;
_curAnimName = aname;
_returnToIdleAnim = !repeat && returnToIdle;
return true;
}
void Character::setAnimationSound(const Common::String &sname, uint offset) {
warning("TODO: Set field 0x2f8 to 0 in Character::setAnimationSound.");
_animSound = sname;
_animSoundOffset = offset;
}
void Character::setCurveOffset(float offset) {
_walkCurveStart = offset;
updatePosition(offset);
}
void Character::setFreeMoveZone(TeFreeMoveZone *zone) {
_freeMoveZone = zone;
}
void Character::setStepSound(const Common::String &stepSound1, const Common::String &stepSound2) {
_stepSound1 = stepSound1;
_stepSound2 = stepSound2;
}
bool Character::setShadowVisible(bool visible) {
if (_shadowModel[0]) {
_shadowModel[0]->setVisible(visible);
_shadowModel[1]->setVisible(visible);
}
return false;
}
float Character::speedFromAnim(double msFromStart) {
if (!_model)
return 0.0f;
TeIntrusivePtr<TeModelAnimation> modelAnim;
if (_model->boneBlenders().empty()) {
modelAnim = _model->anim();
} else {
modelAnim = _model->boneBlenders().back()->_anim;
}
if (!modelAnim)
return 0.0f;
const int pereBone = modelAnim->findBone(rootBone());
int curFrame = modelAnim->calcCurrentFrame(msFromStart);
float result;
if (_lastFrame == -1) {
const TeVector3f32 nowvec = translationVectorFromAnim(*modelAnim, pereBone, 0);
const TeVector3f32 lastvec = translationVectorFromAnim(*modelAnim, pereBone, 1);
result = lastvec.z() - nowvec.z();
} else {
const TeVector3f32 nowvec = translationVectorFromAnim(*modelAnim, pereBone, curFrame);
const TeVector3f32 lastvec = translationVectorFromAnim(*modelAnim, pereBone, _lastFrame);
result = nowvec.z() - lastvec.z();
if (curFrame < _lastFrame) {
result += animLength(*modelAnim, pereBone, 9999);
}
}
_lastFrame = curFrame;
result *= _model->scale().z();
return result;
}
float Character::translationFromAnim(const TeModelAnimation &anim, int bone, int frame) {
return translationVectorFromAnim(anim, bone, frame).z();
}
TeVector3f32 Character::translationVectorFromAnim(const TeModelAnimation &anim, int bone, int frame) {
const TeTRS trs = trsFromAnim(anim, bone, frame);
return trs.getTranslation();
}
TeTRS Character::trsFromAnim(const TeModelAnimation &anim, int bone, int frame) {
if (bone == -1)
return TeTRS();
return anim.getTRS(bone, frame, false);
}
void Character::update(double msFromStart) {
if (!_curve || !_runTimer.running())
return;
_walkCurveNextLength = speedFromAnim(msFromStart) * _walkCurveIncrement + _walkCurveNextLength;
if (_curve->controlPoints().size() < 2) {
blendAnimation(_characterSettings._idleAnimFileName, 0.0667f, true, false);
endMove();
return;
}
const float baseAngle = (_walkCurveStart > _walkCurveEnd ? M_PI : 0);
const float sign = (_walkCurveStart > _walkCurveEnd ? -1 : 1);
updatePosition(_walkCurveLast);
float lastWalkedLength = _walkedLength;
TeVector3f32 lastNextPos = _model->position();
TeVector3f32 nextPos = _model->position();
TeVector3f32 newPos = _model->position();
float lastOffset = _walkCurveLast;
// First do a coarse search for the position, then back up 1 step and do a finer
// search.
const float coarseStep = (4.0 / _curve->numIterations()) * sign;
const float fineStep = (1.0 / _curve->numIterations()) * sign;
float offset = _walkCurveLast;
while (_walkedLength < _walkCurveNextLength) {
lastOffset = offset;
lastWalkedLength = _walkedLength;
lastNextPos = nextPos;
offset = CLIP(lastOffset + coarseStep, 0.0f, 1.0f);
newPos = _curve->retrievePoint(offset) + _curveStartLocation;
const TeVector2f32 dist = TeVector2f32(nextPos.x(), nextPos.z()) - TeVector2f32(newPos.x(), newPos.z());
_walkedLength += dist.length();
nextPos = newPos;
if (offset == 1.0 || offset == 0.0)
break;
}
_walkedLength = lastWalkedLength;
nextPos = lastNextPos;
offset = lastOffset;
while (_walkedLength < _walkCurveNextLength) {
offset = CLIP(offset + fineStep, 0.0f, 1.0f);
newPos = _curve->retrievePoint(offset) + _curveStartLocation;
const TeVector2f32 dist = TeVector2f32(nextPos.x(), nextPos.z()) - TeVector2f32(newPos.x(), newPos.z());
_walkedLength += dist.length();
nextPos = newPos;
if (offset == 1.0 || offset == 0.0)
break;
}
if (_freeMoveZone) {
bool correctflag;
newPos = _freeMoveZone->correctCharacterPosition(newPos, &correctflag, true);
}
//debug("Character::update %4d %.04f %s -> %s %.4f", (int)msFromStart, offset, _model->position().dump().c_str(),
// newPos.dump().c_str(), (newPos - _model->position()).length());
_walkCurveLast = offset;
_model->setPosition(newPos);
TeVector3f32 t1;
TeVector3f32 t2;
_curve->pseudoTangent(offset, t1, t2);
const TeVector3f32 normalizedTangent = (t2 - t1).getNormalized();
float angle = TeVector3f32(0.0, 0.0, 1.0).dotProduct(normalizedTangent);
angle = acos(angle);
TeVector3f32 crossprod = TeVector3f32::crossProduct(TeVector3f32(0.0, 0.0, 1.0), normalizedTangent);
if (crossprod.y() >= 0.0f) {
angle = -angle;
}
//debug("update: curve offset %f - angle %f (base %f)", offset, angle, baseAngle);
TeQuaternion rot = TeQuaternion::fromAxisAndAngle(TeVector3f32(0.0, 1.0, 0.0), baseAngle + angle);
_model->setRotation(rot);
const Common::String endGAnim = walkAnim(WalkPart_EndG);
if (_walkCurveLast == _walkCurveEnd || fabs(_walkCurveEnd - _walkCurveStart) < fabs(_walkCurveLast - _walkCurveStart)) {
if (_walkToFlag) {
_walkToFlag = false;
endMove();
}
if (endGAnim.empty()) {
blendAnimation(_characterSettings._idleAnimFileName, 0.0667f, true, false);
endMove();
}
}
if (!endGAnim.empty() && _curAnimName == walkAnim(WalkPart_Loop) &&
((_curModelAnim->speed() * (msFromStart / 1000.0)) >= _walkTotalFrames)) {
if (_walkToFlag) {
_walkToFlag = false;
endMove();
} else {
if (_walkEndAnimG)
setAnimation(walkAnim(WalkPart_EndG), false);
else
setAnimation(walkAnim(WalkPart_EndD), false);
}
}
// Note:
// The game does a bunch of extra things here (line 252 on)
// that seem to have no actual effect??
updateAnimFrame();
}
void Character::updateAnimFrame() {
if (_model->anim()) {
_lastAnimFrame = _model->anim()->curFrame2();
}
}
void Character::updatePosition(float curveOffset) {
assert(_curve);
if (!_curve->controlPoints().empty()) {
TeVector3f32 pt = _curve->retrievePoint(curveOffset) + _curveStartLocation;
if (_freeMoveZone) {
bool flag;
pt = _freeMoveZone->correctCharacterPosition(pt, &flag, true);
}
_model->setPosition(pt);
}
}
Common::String Character::walkAnim(Character::WalkPart part) {
Common::String result;
const Character::WalkSettings *settings = getCurrentWalkFiles();
if (settings) {
return settings->_walkParts[(int)part]._file;
}
return result;
}
void Character::walkMode(const Common::String &mode) {
if (_walkModeStr != mode)
_walkModeStr = mode;
_walkStartAnimLen = animLengthFromFile(walkAnim(WalkPart_Start), &_walkStartAnimFrameCount);
_walkEndGAnimLen = animLengthFromFile(walkAnim(WalkPart_EndG), &_walkEndGAnimFrameCount);
_walkLoopAnimLen = animLengthFromFile(walkAnim(WalkPart_Loop), &_walkLoopAnimFrameCount);
}
void Character::walkTo(float curveEnd, bool walkFlag) {
_walkToFlag = walkFlag;
stop();
_walkCurveEnd = curveEnd;
_walkCurveLast = _walkCurveStart;
_walkCurveNextLength = 0.0f;
_walkedLength = 0.0f;
if (!_curve)
warning("_curve not set in Character::walkTo");
if (_curve && _curve->controlPoints().size()) {
const float walkEndLen = (walkFlag ? 0 : _walkEndGAnimLen);
_walkCurveLen = _curve->length();
_walkEndAnimG = false;
const float nloops = (_walkCurveLen - (walkEndLen + _walkStartAnimLen)) / _walkLoopAnimLen;
float animLen;
if (nloops >= 0) {
Game *game = g_engine->getGame();
if (game->scene()._character == this && _walkModeStr == "Walk") {
int looplen = (int)(nloops * _walkLoopAnimFrameCount);
int repeats = looplen / _walkLoopAnimFrameCount;
uint32 remainder = looplen % _walkLoopAnimFrameCount;
uint framecounts[4];
if (repeats == 0)
framecounts[0] = INT_MAX;
else
framecounts[0] = (repeats - 1) * _walkLoopAnimFrameCount + 29;
framecounts[1] = _walkLoopAnimFrameCount * repeats + 13;
framecounts[2] = _walkLoopAnimFrameCount * repeats + 29;
framecounts[3] = _walkLoopAnimFrameCount * (repeats + 1) + 13;
for (int i = 0; i < 4; i++) {
framecounts[i] = abs((int)(framecounts[i] - (int)(nloops * _walkLoopAnimFrameCount)));
}
int minoffset = 0;
for (int i = 0; i < 4; i++) {
if (framecounts[i] < framecounts[minoffset])
minoffset = i;
}
switch(minoffset) {
case 0:
remainder = 29;
_walkEndAnimG = true;
repeats--;
break;
case 1:
remainder = 13;
break;
case 2:
remainder = 29;
_walkEndAnimG = true;
break;
case 3:
remainder = 13;
repeats++;
break;
}
_walkTotalFrames = _walkLoopAnimFrameCount * repeats + _walkStartAnimFrameCount + remainder;
const float loopAnimLen = animLengthFromFile(walkAnim(WalkPart_Loop), &remainder, remainder);
_walkCurveIncrement = _walkCurveLen / (repeats * _walkLoopAnimLen + walkEndLen + _walkStartAnimLen + loopAnimLen);
play();
return; // NOTE: early return here.
} else {
// Run or NPC walk
double intpart;
double remainder = modf(nloops, &intpart);
if (remainder >= 0.5) {
_walkEndAnimG = true;
intpart += 0.75;
} else {
intpart += 0.25;
}
_walkTotalFrames = (int)(_walkLoopAnimFrameCount * intpart) + _walkStartAnimFrameCount;
animLen = walkEndLen + (float)_walkStartAnimLen + intpart * _walkLoopAnimLen;
}
} else {
_walkTotalFrames = _walkStartAnimFrameCount;
animLen = (float)(_walkStartAnimLen + _walkEndGAnimLen);
}
_walkCurveIncrement = _walkCurveLen / animLen;
}
play();
}
Character::Water::Water() {
_model = new TeModel();
_model->setName("Water");
TeIntrusivePtr<TeCamera> cam = g_engine->getGame()->scene().currentCamera();
if (!cam)
error("No active camera when constructing water");
TeMatrix4x4 camMatrix = cam->worldTransformationMatrix();
Common::Array<TeVector3f32> quad;
quad.resize(4);
quad[0] = camMatrix.mult3x3(TeVector3f32(-0.1f, 0.0f, 0.1f));
quad[1] = camMatrix.mult3x3(TeVector3f32( 0.1f, 0.0f, 0.1f));
quad[2] = camMatrix.mult3x3(TeVector3f32(-0.1f, 0.0f, -0.1f));
quad[3] = camMatrix.mult3x3(TeVector3f32( 0.1f, 0.0f, -0.1f));
const TeQuaternion noRot = TeQuaternion::fromEuler(TeVector3f32(0, 0, 0));
TeIntrusivePtr<Te3DTexture> tex = Te3DTexture::makeInstance();
tex->load(g_engine->getCore()->findFile("texturesIngame/EauOndine1.tga"));
_model->setQuad(tex, quad, TeColor(255, 0, 0, 0));
_model->setRotation(noRot);
_model->setScale(TeVector3f32(0.5, 0.5, 0.5));
_colorAnim._duration = 2000.0f;
TeColor col = _model->color();
col.a() = 100;
_colorAnim._startVal = col;
col.a() = 0;
_colorAnim._endVal = col;
Common::Array<float> curve;
curve.push_back(0);
curve.push_back(1);
_colorAnim.setCurve(curve);
_colorAnim._callbackObj = _model.get();
_colorAnim._callbackMethod = &TeModel::setColor;
_colorAnim.play();
_scaleAnim._duration = 2000.0f;
_scaleAnim._startVal = _model->scale();
_scaleAnim._endVal = TeVector3f32(3.0f, 3.0f, 3.0f);
_scaleAnim.setCurve(curve);
_scaleAnim._callbackObj = _model.get();
_scaleAnim._callbackMethod = &TeModel::setScale;
}
} // end namespace Tetraedge