scummvm/engines/twp/scenegraph.cpp
2024-03-07 20:08:26 +01:00

673 lines
20 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 "math/matrix3.h"
#include "math/vector3d.h"
#include "common/algorithm.h"
#include "twp/scenegraph.h"
#include "twp/twp.h"
#include "twp/gfx.h"
#include "twp/object.h"
#include "twp/util.h"
namespace Twp {
#define DEFAULT_FPS 10.f
#define NUMOBJECTSBYROW 4
#define MARGIN 8.f
#define MARGINBOTTOM 10.f
#define BACKOFFSET 7.f
#define ARROWWIDTH 56.f
#define ARROWHEIGHT 86.f
#define BACKWIDTH 137.f
#define BACKHEIGHT 75.f
static float _getFps(float fps, float animFps) {
if (fps != 0.f)
return fps;
return (animFps < 1e-3) ? DEFAULT_FPS : animFps;
}
Node::Node(const Common::String &name, Math::Vector2d scale, Color color)
: _name(name),
_color(color),
_computedColor(color),
_scale(scale) {
}
Node::~Node() {}
void Node::addChild(Node *child) {
if(child->_parent == this) return;
if (child->_parent) {
child->_pos -= getAbsPos();
child->remove();
}
_children.push_back(child);
child->_parent = this;
child->updateColor();
child->updateAlpha();
}
const Node *Node::getRoot() const {
const Node *result = this;
while (result->_parent != NULL) {
result = result->_parent;
}
return result;
}
int Node::find(Node *other) {
for (int i = 0; i < _children.size(); i++) {
if (_children[i] == other) {
return i;
}
}
return -1;
}
void Node::removeChild(Node *child) {
int i = find(child);
_children.remove_at(i);
child->_parent = nullptr;
}
void Node::clear() {
if (_children.size() > 0) {
Common::Array<Node *> children(_children);
for (int i = 0; i < children.size(); i++) {
children[i]->remove();
}
}
_children.clear();
}
void Node::remove() {
if (_parent != NULL)
_parent->removeChild(this);
}
void Node::setColor(Color c) {
_color.rgba.r = c.rgba.r;
_color.rgba.g = c.rgba.g;
_color.rgba.b = c.rgba.b;
_computedColor.rgba.r = c.rgba.r;
_computedColor.rgba.g = c.rgba.g;
_computedColor.rgba.b = c.rgba.b;
updateColor();
}
void Node::setAlpha(float alpha) {
_color.rgba.a = alpha;
_computedColor.rgba.a = alpha;
updateAlpha();
}
void Node::updateColor() {
Color parentColor = !_parent ? Color(1.f, 1.f, 1.f) : _parent->_computedColor;
updateColor(parentColor);
}
void Node::updateAlpha() {
float parentOpacity = !_parent ? 1.f : _parent->_computedColor.rgba.a;
updateAlpha(parentOpacity);
}
void Node::updateColor(Color parentColor) {
_computedColor.rgba.r = _color.rgba.r * parentColor.rgba.r;
_computedColor.rgba.g = _color.rgba.g * parentColor.rgba.g;
_computedColor.rgba.b = _color.rgba.b * parentColor.rgba.b;
onColorUpdated(_computedColor);
for (int i = 0; i < _children.size(); i++) {
Node *child = _children[i];
child->updateColor(_computedColor);
}
}
void Node::updateAlpha(float parentAlpha) {
_computedColor.rgba.a = _color.rgba.a * parentAlpha;
onColorUpdated(_computedColor);
for (int i = 0; i < _children.size(); i++) {
Node *child = _children[i];
child->updateAlpha(_computedColor.rgba.a);
}
}
void Node::setAnchor(Math::Vector2d anchor) {
if (_anchor != anchor) {
_anchorNorm = anchor / _size;
_anchor = anchor;
}
}
void Node::setAnchorNorm(Math::Vector2d anchorNorm) {
if (_anchorNorm != anchorNorm) {
_anchorNorm = anchorNorm;
_anchor = _size * _anchorNorm;
}
}
void Node::setSize(Math::Vector2d size) {
if (_size != size) {
_size = size;
_anchor = size * _anchorNorm;
}
}
static int cmpNodes(const Node *x, const Node *y) {
return y->getZSort() < x->getZSort();
}
void Node::draw(Math::Matrix4 parent) {
if (_visible) {
Math::Matrix4 trsf = getTrsf(parent);
Math::Matrix4 myTrsf(trsf);
myTrsf.translate(Math::Vector3d(-_anchor.getX(), _anchor.getY(), 0.f));
Common::Array<Node *> children(_children);
Common::sort(children.begin(), children.end(), cmpNodes);
drawCore(myTrsf);
for (int i = 0; i < children.size(); i++) {
Node *child = children[i];
child->draw(trsf);
}
}
}
Math::Vector2d Node::getAbsPos() const {
return !_parent ? _pos : _parent->getAbsPos() + _pos;
}
Math::Matrix4 Node::getTrsf(Math::Matrix4 parentTrsf) {
return parentTrsf * getLocalTrsf();
}
Math::Matrix4 Node::getLocalTrsf() {
Math::Vector2d p = _pos + _offset + _shakeOffset;
Math::Matrix4 m1;
m1.translate(Math::Vector3d(p.getX(), p.getY(), 0.f));
Math::Matrix3 mRot;
mRot.buildAroundZ(Math::Angle(-_rotation + _rotationOffset));
Math::Matrix4 m2;
m2.setRotation(mRot);
scale(m2, getScale());
Math::Matrix4 m3;
m3.translate(Math::Vector3d(_renderOffset.getX(), _renderOffset.getY(), 0.f));
return m1 * m2 * m3;
}
Rectf Node::getRect() const {
Math::Vector2d size = _size * getScale();
return Rectf::fromPosAndSize(getAbsPos(), Math::Vector2d(-size.getX(), size.getY()) * _anchorNorm * _size);
}
ParallaxNode::ParallaxNode(const Math::Vector2d &parallax, const Common::String &sheet, const Common::StringArray &frames)
: Node("parallax"),
_parallax(parallax),
_sheet(sheet),
_frames(frames) {
}
ParallaxNode::~ParallaxNode() {}
Math::Matrix4 ParallaxNode::getTrsf(Math::Matrix4 parentTrsf) {
Gfx &gfx = g_engine->getGfx();
Math::Matrix4 trsf = Node::getTrsf(parentTrsf);
Math::Vector2d camPos = gfx.cameraPos();
Math::Vector2d p = Math::Vector2d(-camPos.getX() * _parallax.getX(), -camPos.getY() * _parallax.getY());
trsf.translate(Math::Vector3d(p.getX(), p.getY(), 0.0f));
return trsf;
}
void ParallaxNode::drawCore(Math::Matrix4 trsf) {
Gfx &gfx = g_engine->getGfx();
SpriteSheet *sheet = g_engine->_resManager.spriteSheet(_sheet);
Texture *texture = g_engine->_resManager.texture(sheet->meta.image);
Math::Matrix4 t = trsf;
float x = 0.f;
for (int i = 0; i < _frames.size(); i++) {
const SpriteSheetFrame &frame = sheet->frameTable[_frames[i]];
Math::Matrix4 myTrsf = t;
myTrsf.translate(Math::Vector3d(x + frame.spriteSourceSize.left, frame.sourceSize.getY() - frame.spriteSourceSize.height() - frame.spriteSourceSize.top, 0.0f));
gfx.drawSprite(frame.frame, *texture, getColor(), myTrsf);
t = trsf;
x += frame.frame.width();
}
}
Anim::Anim(Object *obj)
: Node("anim") {
_obj = obj;
_zOrder = 1000;
}
void Anim::clearFrames() {
_frames.clear();
}
void Anim::setAnim(const ObjectAnimation *anim, float fps, bool loop, bool instant) {
_anim = anim;
_disabled = false;
setName(anim->name);
_sheet = anim->sheet;
_frames = anim->frames;
_frameIndex = instant && _frames.size() > 0 ? _frames.size() - 1 : 0;
_frameDuration = 1.0 / _getFps(fps, anim->fps);
_loop = loop || anim->loop;
_instant = instant;
clear();
for (int i = 0; i < _anim->layers.size(); i++) {
const ObjectAnimation &layer = _anim->layers[i];
Anim *node = new Anim(_obj);
node->setAnim(&layer, fps, loop, instant);
addChild(node);
}
}
void Anim::trigSound() {
if ((_anim->triggers.size() > 0) && _frameIndex < _anim->triggers.size()) {
const Common::String &trigger = _anim->triggers[_frameIndex];
if ((trigger.size() > 0) && trigger != "null") {
_obj->trig(trigger);
}
}
}
void Anim::update(float elapsed) {
if (_anim)
setVisible(Twp::find(_obj->_hiddenLayers, _anim->name) == -1);
if (_instant)
disable();
else if (_frames.size() != 0) {
_elapsed += elapsed;
if (_elapsed > _frameDuration) {
_elapsed = 0;
if (_frameIndex < _frames.size() - 1) {
_frameIndex++;
trigSound();
} else if (_loop) {
_frameIndex = 0;
trigSound();
} else {
disable();
}
}
if (_anim->offsets.size() > 0) {
Math::Vector2d off = _frameIndex < _anim->offsets.size() ? _anim->offsets[_frameIndex] : Math::Vector2d();
if (_obj->getFacing() == FACE_LEFT) {
off.setX(-off.getX());
}
_offset = off;
}
} else if (_children.size() != 0) {
bool disabled = true;
for (int i = 0; i < _children.size(); i++) {
Anim *layer = static_cast<Anim *>(_children[i]);
layer->update(elapsed);
disabled = disabled && layer->_disabled;
}
if (disabled) {
disable();
}
} else {
disable();
}
}
void Anim::drawCore(Math::Matrix4 trsf) {
if (_frameIndex < _frames.size()) {
const Common::String &frame = _frames[_frameIndex];
bool flipX = _obj->getFacing() == FACE_LEFT;
if (_sheet.size() == 0) {
_sheet = _obj->_sheet;
if (_sheet.size() == 0 && _obj->_room) {
_sheet = _obj->_room->_sheet;
}
}
if (_sheet == "raw") {
Texture *texture = g_engine->_resManager.texture(frame);
Math::Vector3d pos(-texture->width / 2.f, -texture->height / 2.f, 0.f);
trsf.translate(pos);
g_engine->getGfx().drawSprite(Common::Rect(texture->width, texture->height), *texture, getComputedColor(), trsf, flipX);
} else {
SpriteSheet *sheet = g_engine->_resManager.spriteSheet(_sheet);
const SpriteSheetFrame &sf = sheet->frameTable[frame];
Texture *texture = g_engine->_resManager.texture(sheet->meta.image);
float x = flipX ? -0.5f * (-1.f + sf.sourceSize.getX()) + sf.frame.width() + sf.spriteSourceSize.left : 0.5f * (sf.sourceSize.getX()) - sf.spriteSourceSize.left;
float y = 0.5f * (sf.sourceSize.getY()) - sf.spriteSourceSize.height() - sf.spriteSourceSize.top;
Math::Vector3d pos(int(-x), int(y), 0.f);
trsf.translate(pos);
g_engine->getGfx().drawSprite(sf.frame, *texture, getComputedColor(), trsf, flipX);
}
}
}
ActorNode::ActorNode(Object *obj)
: Node(obj->_key), _object(obj) {
}
int ActorNode::getZSort() const { return getPos().getY(); }
Math::Vector2d ActorNode::getScale() const {
float y = _object->_room->getScaling(_object->_node->getPos().getY());
return Math::Vector2d(y, y);
}
TextNode::TextNode() : Node("text") {
}
TextNode::~TextNode() {}
void TextNode::setText(Text text) {
_text = text;
setSize(text.getBounds());
}
void TextNode::updateBounds() {
setSize(_text.getBounds());
}
Rectf TextNode::getRect() const {
Math::Vector2d size = _size * getScale();
return Rectf::fromPosAndSize(getAbsPos() + Math::Vector2d(0, -size.getY()) + Math::Vector2d(-size.getX(), size.getY()) * _anchorNorm, size);
}
void TextNode::onColorUpdated(Color color) {
_text.setColor(color);
}
void TextNode::drawCore(Math::Matrix4 trsf) {
_text.draw(g_engine->getGfx(), trsf);
}
Scene::Scene() : Node("Scene") {
_zOrder = -100;
}
Scene::~Scene() {}
InputState::InputState() : Node("InputState") {
_zOrder = -100;
}
InputState::~InputState() {}
void InputState::drawCore(Math::Matrix4 trsf) {
// draw cursor
SpriteSheet *gameSheet = g_engine->_resManager.spriteSheet("GameSheet");
Texture *texture = g_engine->_resManager.texture(gameSheet->meta.image);
// TODO: if prefs(ClassicSentence) and self.hotspot:
Common::String cursorName = _hotspot ? Common::String::format("hotspot_%s", _cursorName.c_str()) : _cursorName;
const SpriteSheetFrame &sf = gameSheet->frameTable[cursorName];
Math::Vector3d pos(sf.spriteSourceSize.left - sf.sourceSize.getX() / 2.f, -sf.spriteSourceSize.height() - sf.spriteSourceSize.top + sf.sourceSize.getY() / 2.f, 0.f);
trsf.translate(pos * 2.f);
scale(trsf, Math::Vector2d(2.f, 2.f));
g_engine->getGfx().drawSprite(sf.frame, *texture, getComputedColor(), trsf);
}
InputStateFlag InputState::getState() const {
int tmp = 0;
tmp |= (_inputActive ? UI_INPUT_ON : UI_INPUT_OFF);
tmp |= (_inputVerbsActive ? UI_VERBS_ON : UI_VERBS_OFF);
tmp |= (_showCursor ? UI_CURSOR_ON : UI_CURSOR_OFF);
tmp |= (_inputHUD ? UI_HUDOBJECTS_ON : UI_HUDOBJECTS_OFF);
return (InputStateFlag)tmp;
}
void InputState::setState(InputStateFlag state) {
if ((UI_INPUT_ON & state) == UI_INPUT_ON)
_inputActive = true;
if ((UI_INPUT_OFF & state) == UI_INPUT_OFF)
_inputActive = false;
if ((UI_VERBS_ON & state) == UI_VERBS_ON)
_inputVerbsActive = true;
if ((UI_VERBS_OFF & state) == UI_VERBS_OFF)
_inputVerbsActive = false;
if ((UI_CURSOR_ON & state) == UI_CURSOR_ON) {
_showCursor = true;
_visible = true;
}
if ((UI_CURSOR_OFF & state) == UI_CURSOR_OFF) {
_showCursor = false;
_visible = false;
}
if ((UI_HUDOBJECTS_ON & state) == UI_HUDOBJECTS_ON)
_inputHUD = true;
if ((UI_HUDOBJECTS_OFF & state) == UI_HUDOBJECTS_OFF)
_inputHUD = false;
}
void InputState::setCursorShape(CursorShape shape) {
if (_cursorShape != shape) {
_cursorShape = shape;
switch (shape) {
case CursorShape::Normal:
_cursorName = "cursor";
break;
case CursorShape::Left:
_cursorName = "cursor_left";
break;
case CursorShape::Right:
_cursorName = "cursor_right";
break;
case CursorShape::Front:
_cursorName = "cursor_front";
break;
case CursorShape::Back:
_cursorName = "cursor_back";
break;
case CursorShape::Pause:
_cursorName = "cursor_pause";
break;
}
}
}
OverlayNode::OverlayNode() : Node("overlay") {
_ovlColor = Color(0.f, 0.f, 0.f, 0.f); // transparent
_zOrder = INT_MIN;
}
void OverlayNode::drawCore(Math::Matrix4 trsf) {
Math::Vector2d size = g_engine->getGfx().camera();
g_engine->getGfx().drawQuad(size, _ovlColor);
}
static bool hasUpArrow(Object *actor) {
return actor->_inventoryOffset != 0;
}
static bool hasDownArrow(Object *actor) {
return actor->_inventory.size() > (actor->_inventoryOffset * NUMOBJECTSBYROW + NUMOBJECTS);
}
Inventory::Inventory() : Node("Inventory") {
for (int i = 0; i < NUMOBJECTS; i++) {
float x = SCREEN_WIDTH / 2.f + ARROWWIDTH + MARGIN + ((i % NUMOBJECTSBYROW) * (BACKWIDTH + BACKOFFSET));
float y = MARGINBOTTOM + BACKHEIGHT + BACKOFFSET - ((i / NUMOBJECTSBYROW) * (BACKHEIGHT + BACKOFFSET));
_itemRects[i] = Common::Rect(x, y, x + BACKWIDTH, y + BACKHEIGHT);
}
}
Math::Vector2d Inventory::getPos(Object *inv) const {
if (_actor) {
int i = Twp::find(_actor->_inventory, inv) - _actor->_inventoryOffset * NUMOBJECTSBYROW;
return Math::Vector2d(_itemRects[i].left + _itemRects[i].width() / 2.f, _itemRects[i].bottom + _itemRects[i].height() / 2.f);
}
return {};
}
void Inventory::drawSprite(SpriteSheetFrame &sf, Texture *texture, Color color, Math::Matrix4 trsf) {
Math::Vector3d pos(sf.spriteSourceSize.left - sf.sourceSize.getX() / 2.f, -sf.spriteSourceSize.height() - sf.spriteSourceSize.top + sf.sourceSize.getY() / 2.f, 0.f);
trsf.translate(pos);
g_engine->getGfx().drawSprite(sf.frame, *texture, color, trsf);
}
void Inventory::drawArrows(Math::Matrix4 trsf) {
// TODO: bool isRetro = prefs(RetroVerbs);
bool isRetro = false;
SpriteSheet *gameSheet = g_engine->_resManager.spriteSheet("GameSheet");
Texture *texture = g_engine->_resManager.texture(gameSheet->meta.image);
SpriteSheetFrame *arrowUp = &gameSheet->frameTable[isRetro ? "scroll_up_retro" : "scroll_up"];
SpriteSheetFrame *arrowDn = &gameSheet->frameTable[isRetro ? "scroll_down_retro" : "scroll_down"];
float alphaUp = hasUpArrow(_actor) ? 1.f : 0.f;
float alphaDn = hasDownArrow(_actor) ? 1.f : 0.f;
Math::Matrix4 tUp(trsf);
tUp.translate(Math::Vector3d(SCREEN_WIDTH / 2.f + ARROWWIDTH / 2.f + MARGIN, 1.5f * ARROWHEIGHT + BACKOFFSET, 0.f));
Math::Matrix4 tDn(trsf);
tDn.translate(Math::Vector3d(SCREEN_WIDTH / 2.f + ARROWWIDTH / 2.f + MARGIN, 0.5f * ARROWHEIGHT, 0.f));
drawSprite(*arrowUp, texture, Color::withAlpha(_verbNormal, alphaUp), tUp);
drawSprite(*arrowDn, texture, Color::withAlpha(_verbNormal, alphaDn), tDn);
}
void Inventory::drawBack(Math::Matrix4 trsf) {
SpriteSheet *gameSheet = g_engine->_resManager.spriteSheet("GameSheet");
Texture *texture = g_engine->_resManager.texture(gameSheet->meta.image);
SpriteSheetFrame *back = &gameSheet->frameTable["inventory_background"];
float startOffsetX = SCREEN_WIDTH / 2.f + ARROWWIDTH + MARGIN + back->sourceSize.getX() / 2.f;
float offsetX = startOffsetX;
float offsetY = 3.f * back->sourceSize.getY() / 2.f + MARGINBOTTOM + BACKOFFSET;
for (int i = 0; i < 4; i++) {
Math::Matrix4 t(trsf);
t.translate(Math::Vector3d(offsetX, offsetY, 0.f));
drawSprite(*back, texture, _backColor, t);
offsetX += back->sourceSize.getX() + BACKOFFSET;
}
offsetX = startOffsetX;
offsetY = back->sourceSize.getY() / 2.f + MARGINBOTTOM;
for (int i = 0; i < 4; i++) {
Math::Matrix4 t(trsf);
t.translate(Math::Vector3d(offsetX, offsetY, 0.f));
drawSprite(*back, texture, _backColor, t);
offsetX += back->sourceSize.getX() + BACKOFFSET;
}
}
void Inventory::drawItems(Math::Matrix4 trsf) {
float startOffsetX = SCREEN_WIDTH / 2.f + ARROWWIDTH + MARGIN + BACKWIDTH / 2.f;
float startOffsetY = MARGINBOTTOM + 1.5f * BACKHEIGHT + BACKOFFSET;
SpriteSheet *itemsSheet = g_engine->_resManager.spriteSheet("InventoryItems");
Texture *texture = g_engine->_resManager.texture(itemsSheet->meta.image);
int count = MIN(NUMOBJECTS, (int)_actor->_inventory.size() - _actor->_inventoryOffset * NUMOBJECTSBYROW);
for (int i = 0; i < count; i++) {
Object *obj = _actor->_inventory[_actor->_inventoryOffset * NUMOBJECTSBYROW + i];
Common::String icon = obj->getIcon();
if (itemsSheet->frameTable.contains(icon)) {
SpriteSheetFrame *itemFrame = &itemsSheet->frameTable[icon];
Math::Vector2d pos(startOffsetX + ((float)(i % NUMOBJECTSBYROW) * (BACKWIDTH + BACKOFFSET)), startOffsetY - ((float)(i / NUMOBJECTSBYROW) * (BACKHEIGHT + BACKOFFSET)));
Math::Matrix4 t(trsf);
t.translate(Math::Vector3d(pos.getX(), pos.getY(), 0.f));
float s = obj->getScale();
Twp::scale(t, Math::Vector2d(s, s));
drawSprite(*itemFrame, texture, Color(), t);
}
}
}
void Inventory::drawCore(Math::Matrix4 trsf) {
if (_actor) {
drawArrows(trsf);
drawBack(trsf);
drawItems(trsf);
}
}
void Inventory::update(float elapsed, Object *actor, Color backColor, Color verbNormal) {
static Common::Rect gArrowUpRect(SCREEN_WIDTH / 2.f, ARROWHEIGHT + MARGINBOTTOM + BACKOFFSET, SCREEN_WIDTH / 2.f + ARROWWIDTH, ARROWHEIGHT + MARGINBOTTOM + BACKOFFSET + ARROWHEIGHT);
static Common::Rect gArrowDnRect(SCREEN_WIDTH / 2.f, MARGINBOTTOM, SCREEN_WIDTH / 2.f + ARROWWIDTH, MARGINBOTTOM + ARROWHEIGHT);
// udate colors
_actor = actor;
_backColor = backColor;
_verbNormal = verbNormal;
_obj = nullptr;
if (_actor) {
Math::Vector2d scrPos = g_engine->winToScreen(g_engine->_cursor.pos);
// update mouse click
bool down = g_engine->_cursor.leftDown;
if (_down && down) {
_down = true;
if (gArrowUpRect.contains(scrPos.getX(), scrPos.getY())) {
_actor->_inventoryOffset -= 1;
if (_actor->_inventoryOffset < 0)
_actor->_inventoryOffset = clamp(_actor->_inventoryOffset, 0, ((int)_actor->_inventory.size() - 5) / 4);
} else if (gArrowDnRect.contains(scrPos.getX(), scrPos.getY())) {
_actor->_inventoryOffset++;
_actor->_inventoryOffset = clamp(_actor->_inventoryOffset, 0, ((int)_actor->_inventory.size() - 5) / 4);
}
} else if (!down) {
_down = false;
}
for (int i = 0; i < NUMOBJECTS; i++) {
const Common::Rect &item = _itemRects[i];
if (item.contains(scrPos.getX(), scrPos.getY())) {
int index = _actor->_inventoryOffset * NUMOBJECTSBYROW + i;
if (index < _actor->_inventory.size())
_obj = _actor->_inventory[index];
break;
}
}
for (int i = 0; i < _actor->_inventory.size(); i++) {
Object *obj = _actor->_inventory[i];
obj->update(elapsed);
}
}
}
SentenceNode::SentenceNode() : Node("Sentence") {
_zOrder = -100;
}
SentenceNode::~SentenceNode() {
}
void SentenceNode::setText(const Common::String &text) {
_text = text;
}
void SentenceNode::drawCore(Math::Matrix4 trsf) {
Text text("sayline", _text);
float x, y;
// if prefs(ClassicSentence):
// x = (ScreenWidth - text.bounds.x) / 2f;
// y = 208f;
// else:
x = MAX(_pos.getX() - text.getBounds().getX() / 2.f, MARGIN);
x = MIN(x, SCREEN_WIDTH - text.getBounds().getX() - MARGIN);
y = _pos.getY() + 2.f * 38.f;
if (y >= SCREEN_HEIGHT)
y = _pos.getY() - 38.f;
Math::Matrix4 t;
t.translate(Math::Vector3d(x, y, 0.f));
text.draw(g_engine->getGfx(), t);
}
} // namespace Twp