mirror of
https://github.com/libretro/scummvm.git
synced 2025-02-12 22:51:11 +00:00
569 lines
17 KiB
C++
569 lines
17 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 "twp/twp.h"
|
|
#include "twp/detection.h"
|
|
#include "twp/resmanager.h"
|
|
#include "twp/object.h"
|
|
#include "twp/room.h"
|
|
#include "twp/squtil.h"
|
|
#include "twp/clipper/clipper.hpp"
|
|
|
|
namespace Twp {
|
|
|
|
static ObjectType toObjectType(const Common::JSONObject &jObject) {
|
|
if (toBool(jObject, "prop"))
|
|
return otProp;
|
|
if (toBool(jObject, "spot"))
|
|
return otSpot;
|
|
if (toBool(jObject, "trigger"))
|
|
return otTrigger;
|
|
return otNone;
|
|
}
|
|
|
|
static Direction parseUseDir(const Common::String &s) {
|
|
if (s == "DIR_FRONT")
|
|
return dFront;
|
|
if (s == "DIR_BACK")
|
|
return dBack;
|
|
if (s == "DIR_LEFT")
|
|
return dLeft;
|
|
if (s == "DIR_RIGHT")
|
|
return dRight;
|
|
error("invalid use direction: %s", s.c_str());
|
|
}
|
|
|
|
static Math::Vector2d parseParallax(const Common::JSONValue &v) {
|
|
if (v.isIntegerNumber()) {
|
|
return {(float)v.asIntegerNumber(), 1};
|
|
}
|
|
if (v.isNumber()) {
|
|
return {(float)v.asNumber(), 1};
|
|
}
|
|
if (v.isString()) {
|
|
return parseVec2(v.asString());
|
|
}
|
|
error("parseParallax expected a float, int or string, not this: %s", v.stringify().c_str());
|
|
}
|
|
|
|
static Walkbox parseWalkbox(const Common::String &text) {
|
|
Common::Array<Vector2i> points;
|
|
size_t i = 1;
|
|
size_t endPos;
|
|
do {
|
|
uint32 commaPos = text.find(',', i);
|
|
int x = (int)strtol(text.substr(i, commaPos - i).c_str(), nullptr, 10);
|
|
endPos = text.find('}', commaPos + 1);
|
|
int y = (int)strtol(text.substr(commaPos + 1, endPos - commaPos - 1).c_str(), nullptr, 10);
|
|
i = endPos + 3;
|
|
points.push_back({x, y});
|
|
} while ((text.size() - 1) != endPos);
|
|
return Walkbox(points);
|
|
}
|
|
|
|
static Scaling parseScaling(const Common::JSONArray &jScalings) {
|
|
float scale;
|
|
int y;
|
|
Scaling result;
|
|
for (auto it = jScalings.begin(); it != jScalings.end(); it++) {
|
|
const Common::String &v = (*it)->asString();
|
|
sscanf(v.c_str(), "%f@%d", &scale, &y);
|
|
result.values.push_back(ScalingValue{scale, y});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static ClipperLib::Path toPolygon(const Walkbox &walkbox) {
|
|
ClipperLib::Path path;
|
|
const Common::Array<Vector2i> &points = walkbox.getPoints();
|
|
for (size_t i = 0; i < points.size(); i++) {
|
|
path.push_back(ClipperLib::IntPoint(points[i].x, points[i].y));
|
|
}
|
|
return path;
|
|
}
|
|
|
|
static Walkbox toWalkbox(const ClipperLib::Path &path) {
|
|
Common::Array<Vector2i> pts;
|
|
for (size_t i = 0; i < path.size(); i++) {
|
|
const ClipperLib::IntPoint &pt = path[i];
|
|
pts.push_back(Vector2i(static_cast<int>(pt.X), static_cast<int>(pt.Y)));
|
|
}
|
|
return Walkbox(pts, ClipperLib::Orientation(path));
|
|
}
|
|
|
|
static Common::Array<Walkbox> merge(const Common::Array<Walkbox> &walkboxes) {
|
|
Common::Array<Walkbox> result;
|
|
if (walkboxes.size() > 0) {
|
|
ClipperLib::Paths subjects, clips;
|
|
for (size_t i = 0; i < walkboxes.size(); i++) {
|
|
const Walkbox &wb = walkboxes[i];
|
|
if (wb.isVisible()) {
|
|
subjects.push_back(toPolygon(wb));
|
|
}
|
|
}
|
|
|
|
ClipperLib::Paths solutions;
|
|
ClipperLib::Clipper c;
|
|
c.AddPaths(subjects, ClipperLib::ptSubject, true);
|
|
c.Execute(ClipperLib::ClipType::ctUnion, solutions, ClipperLib::pftEvenOdd);
|
|
|
|
ClipperLib::Paths solutions2;
|
|
ClipperLib::Clipper c2;
|
|
c2.AddPaths(solutions, ClipperLib::ptSubject, true);
|
|
c2.AddPaths(clips, ClipperLib::ptClip, true);
|
|
c2.Execute(ClipperLib::ClipType::ctDifference, solutions2, ClipperLib::pftEvenOdd);
|
|
|
|
for (size_t i = 0; i < solutions2.size(); i++) {
|
|
result.push_back(toWalkbox(solutions2[i]));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Room::Room(const Common::String &name, HSQOBJECT &table) : _table(table) {
|
|
setId(_table, g_twp->_resManager->newRoomId());
|
|
_name = name;
|
|
_scene = Common::SharedPtr<Scene>(new Scene());
|
|
_scene->addChild(&_overlayNode);
|
|
}
|
|
|
|
Room::~Room() {
|
|
}
|
|
|
|
Common::SharedPtr<Object> Room::createObject(const Common::String &sheet, const Common::Array<Common::String> &frames) {
|
|
Common::SharedPtr<Object> obj(new Object());
|
|
obj->_temporary = true;
|
|
|
|
HSQUIRRELVM v = g_twp->getVm();
|
|
|
|
// create a table for this object
|
|
sq_newtable(v);
|
|
sq_getstackobj(v, -1, &obj->_table);
|
|
sq_addref(v, &obj->_table);
|
|
sq_pop(v, 1);
|
|
|
|
// assign an id
|
|
const int id = g_twp->_resManager->newObjId();
|
|
setId(obj->_table, id);
|
|
g_twp->_resManager->_allObjects[id] = obj;
|
|
Common::String name = frames.size() > 0 ? frames[0] : "noname";
|
|
sqsetf(obj->_table, "name", name);
|
|
obj->_key = name;
|
|
obj->_node->setName(name);
|
|
debugC(kDebugGame, "Create object with new table: %s #%d", obj->_name.c_str(), obj->getId());
|
|
|
|
obj->_sheet = sheet;
|
|
|
|
// create anim if any
|
|
if (frames.size() > 0) {
|
|
ObjectAnimation objAnim;
|
|
objAnim.name = "state0";
|
|
objAnim.frames.push_back(frames);
|
|
obj->_anims.push_back(objAnim);
|
|
}
|
|
|
|
obj->_node->setZSort(1);
|
|
layer(0)->_objects.push_back(obj);
|
|
layer(0)->_node->addChild(obj->_node.get());
|
|
obj->_layer = layer(0);
|
|
obj->setState(0);
|
|
|
|
return obj;
|
|
}
|
|
|
|
Common::SharedPtr<Object> Room::createTextObject(const Common::String &fontName, const Common::String &text, TextHAlignment hAlign, TextVAlignment vAlign, float maxWidth) {
|
|
Common::SharedPtr<Object> obj(new Object());
|
|
obj->_temporary = true;
|
|
|
|
HSQUIRRELVM v = g_twp->getVm();
|
|
|
|
// create a table for this object
|
|
sq_newtable(v);
|
|
sq_getstackobj(v, -1, &obj->_table);
|
|
sq_addref(v, &obj->_table);
|
|
sq_pop(v, 1);
|
|
|
|
// assign an id
|
|
const int id = g_twp->_resManager->newObjId();
|
|
setId(obj->_table, id);
|
|
g_twp->_resManager->_allObjects[id] = obj;
|
|
debugC(kDebugGame, "Create object with new table: %s #%d", obj->_name.c_str(), obj->getId());
|
|
obj->_name = Common::String::format("text#%d: %s", obj->getId(), text.c_str());
|
|
|
|
Text txt(fontName, text, hAlign, vAlign, maxWidth);
|
|
|
|
Common::SharedPtr<TextNode> node(new TextNode());
|
|
node->setName(obj->_name);
|
|
node->setText(txt);
|
|
float y = 0.5f;
|
|
switch (vAlign) {
|
|
case tvTop:
|
|
y = 0.f;
|
|
break;
|
|
case tvCenter:
|
|
y = 0.5f;
|
|
break;
|
|
case tvBottom:
|
|
y = 1.f;
|
|
break;
|
|
}
|
|
switch (hAlign) {
|
|
case thLeft:
|
|
node->setAnchorNorm(Math::Vector2d(0.f, y));
|
|
break;
|
|
case thCenter:
|
|
node->setAnchorNorm(Math::Vector2d(0.5f, y));
|
|
break;
|
|
case thRight:
|
|
node->setAnchorNorm(Math::Vector2d(1.f, y));
|
|
break;
|
|
}
|
|
obj->_nodeAnim = nullptr;
|
|
obj->_node = node;
|
|
layer(0)->_objects.push_back(obj);
|
|
layer(0)->_node->addChild(obj->_node.get());
|
|
obj->_layer = layer(0);
|
|
|
|
return obj;
|
|
}
|
|
|
|
void Room::load(Common::SharedPtr<Room> room, Common::SeekableReadStream &s) {
|
|
GGHashMapDecoder d;
|
|
Common::ScopedPtr<Common::JSONValue> value(d.open(&s));
|
|
// debugC(kDebugGame, "Room: %s", value->stringify().c_str());
|
|
const Common::JSONObject &jRoom = value->asObject();
|
|
|
|
room->_name = jRoom["name"]->asString();
|
|
room->_sheet = jRoom["sheet"]->asString();
|
|
|
|
room->_roomSize = parseVec2(jRoom["roomsize"]->asString());
|
|
room->_height = jRoom.contains("height") ? jRoom["height"]->asIntegerNumber() : room->_roomSize.getY();
|
|
room->_fullscreen = jRoom.contains("fullscreen") ? jRoom["fullscreen"]->asIntegerNumber() : 0;
|
|
|
|
// backgrounds
|
|
Common::StringArray backNames;
|
|
if (jRoom["background"]->isString()) {
|
|
backNames.push_back(jRoom["background"]->asString());
|
|
} else {
|
|
const Common::JSONArray &jBacks = jRoom["background"]->asArray();
|
|
for (size_t i = 0; i < jBacks.size(); i++) {
|
|
backNames.push_back(jBacks[i]->asString());
|
|
}
|
|
}
|
|
|
|
{
|
|
Common::SharedPtr<Layer> layer(new Layer(backNames, Math::Vector2d(1, 1), 0));
|
|
room->_layers.push_back(layer);
|
|
}
|
|
|
|
// layers
|
|
if (jRoom.contains("layers")) {
|
|
const Common::JSONArray &jLayers = jRoom["layers"]->asArray();
|
|
for (size_t i = 0; i < jLayers.size(); i++) {
|
|
Common::StringArray names;
|
|
const Common::JSONObject &jLayer = jLayers[i]->asObject();
|
|
if (jLayer["name"]->isArray()) {
|
|
const Common::JSONArray &jNames = jLayer["name"]->asArray();
|
|
for (size_t j = 0; j < jNames.size(); j++) {
|
|
names.push_back(jNames[j]->asString());
|
|
}
|
|
} else if (jLayer["name"]->isString()) {
|
|
names.push_back(jLayer["name"]->asString());
|
|
}
|
|
Math::Vector2d parallax = parseParallax(*jLayer["parallax"]);
|
|
int zsort = jLayer["zsort"]->asIntegerNumber();
|
|
|
|
Common::SharedPtr<Layer> layer(new Layer(names, parallax, zsort));
|
|
room->_layers.push_back(layer);
|
|
}
|
|
}
|
|
|
|
// walkboxes
|
|
if (jRoom.contains("walkboxes")) {
|
|
const Common::JSONArray &jWalkboxes = jRoom["walkboxes"]->asArray();
|
|
for (auto it = jWalkboxes.begin(); it != jWalkboxes.end(); it++) {
|
|
const Common::JSONObject &jWalkbox = (*it)->asObject();
|
|
Walkbox walkbox = parseWalkbox(jWalkbox["polygon"]->asString());
|
|
if (jWalkbox.contains("name") && jWalkbox["name"]->isString()) {
|
|
walkbox._name = jWalkbox["name"]->asString();
|
|
}
|
|
room->_walkboxes.push_back(walkbox);
|
|
}
|
|
}
|
|
|
|
// objects
|
|
if (jRoom.contains("objects")) {
|
|
const Common::JSONArray &jobjects = jRoom["objects"]->asArray();
|
|
for (auto it = jobjects.begin(); it != jobjects.end(); it++) {
|
|
const Common::JSONObject &jObject = (*it)->asObject();
|
|
Common::SharedPtr<Object> obj(new Object());
|
|
const int id = g_twp->_resManager->newObjId();
|
|
Twp::setId(obj->_table, id);
|
|
g_twp->_resManager->_allObjects[id] = obj;
|
|
obj->_key = jObject["name"]->asString();
|
|
obj->_node->setName(obj->_key.c_str());
|
|
obj->_node->setPos(Math::Vector2d(parseVec2(jObject["pos"]->asString())));
|
|
obj->_node->setZSort(jObject["zsort"]->asIntegerNumber());
|
|
obj->_usePos = parseVec2(jObject["usepos"]->asString());
|
|
if (jObject.contains("usedir")) {
|
|
obj->_useDir = parseUseDir(jObject["usedir"]->asString());
|
|
} else {
|
|
obj->_useDir = dNone;
|
|
}
|
|
obj->_hotspot = parseRect(jObject["hotspot"]->asString());
|
|
obj->_objType = toObjectType(jObject);
|
|
if (jObject.contains("parent"))
|
|
obj->_parent = jObject["parent"]->asString();
|
|
obj->_room = room;
|
|
if (jObject.contains("animations")) {
|
|
parseObjectAnimations(jObject["animations"]->asArray(), obj->_anims);
|
|
}
|
|
obj->_layer = room->layer(0);
|
|
room->layer(0)->_objects.push_back(obj);
|
|
}
|
|
}
|
|
|
|
// scalings
|
|
if (jRoom.contains("scaling")) {
|
|
const Common::JSONArray &jScalings = jRoom["scaling"]->asArray();
|
|
if (jScalings[0]->isString()) {
|
|
room->_scalings.push_back(parseScaling(jScalings));
|
|
} else {
|
|
for (auto it = jScalings.begin(); it != jScalings.end(); it++) {
|
|
const Common::JSONObject &jScaling = (*it)->asObject();
|
|
Scaling scaling = parseScaling(jScaling["scaling"]->asArray());
|
|
if (jScaling.contains("trigger") && jScaling["trigger"]->isString())
|
|
scaling.trigger = jScaling["trigger"]->asString();
|
|
room->_scalings.push_back(scaling);
|
|
}
|
|
}
|
|
room->_scaling = room->_scalings[0];
|
|
}
|
|
|
|
room->_mergedPolygon = merge(room->_walkboxes);
|
|
|
|
// Fix room size (why ?)
|
|
int width = 0;
|
|
for (size_t i = 0; i < backNames.size(); i++) {
|
|
Common::String name = backNames[i];
|
|
width += g_twp->_resManager->spriteSheet(room->_sheet)->getFrame(name).sourceSize.getX();
|
|
}
|
|
room->_roomSize.setX(width);
|
|
}
|
|
|
|
Common::SharedPtr<Layer> Room::layer(int zsort) {
|
|
for (size_t i = 0; i < _layers.size(); i++) {
|
|
Common::SharedPtr<Layer> l = _layers[i];
|
|
if (l->_zsort == zsort)
|
|
return l;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
Math::Vector2d Room::getScreenSize() {
|
|
switch (_height) {
|
|
case 128:
|
|
return {320, 180};
|
|
case 172:
|
|
return {428, 240};
|
|
case 256:
|
|
return {640, 360};
|
|
default:
|
|
return {_roomSize.getX(), (float)_height};
|
|
}
|
|
}
|
|
|
|
Common::SharedPtr<Object> Room::getObj(const Common::String &key) {
|
|
for (size_t i = 0; i < _layers.size(); i++) {
|
|
Common::SharedPtr<Layer> layer = _layers[i];
|
|
for (size_t j = 0; j < layer->_objects.size(); j++) {
|
|
Common::SharedPtr<Object> obj = layer->_objects[j];
|
|
if (obj->_key == key)
|
|
return obj;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Light *Room::createLight(const Color &color, const Math::Vector2d &pos) {
|
|
Light *result = &_lights._lights[_lights._numLights];
|
|
result->id = 100000 + _lights._numLights;
|
|
result->on = true;
|
|
result->color = color;
|
|
result->pos = pos;
|
|
_lights._numLights++;
|
|
return result;
|
|
}
|
|
|
|
float Room::getScaling(float yPos) {
|
|
return _scaling.getScaling(yPos);
|
|
}
|
|
|
|
void Room::objectParallaxLayer(Common::SharedPtr<Object> obj, int zsort) {
|
|
Common::SharedPtr<Layer> l = layer(zsort);
|
|
if (obj->_layer != l) {
|
|
// removes object from old layer
|
|
if (obj->_layer) {
|
|
int i = find(obj->_layer->_objects, obj);
|
|
obj->_layer->_node->removeChild(obj->_node.get());
|
|
obj->_layer->_objects.remove_at(i);
|
|
}
|
|
// adds object to the new one
|
|
l->_objects.push_back(obj);
|
|
// update scenegraph
|
|
l->_node->addChild(obj->_node.get());
|
|
obj->_layer = l;
|
|
}
|
|
}
|
|
|
|
void Room::setOverlay(const Color &color) {
|
|
_overlayNode.setOverlayColor(color);
|
|
}
|
|
|
|
Color Room::getOverlay() const {
|
|
return _overlayNode.getOverlayColor();
|
|
}
|
|
|
|
void Room::update(float elapsed) {
|
|
if (_overlayTo)
|
|
_overlayTo->update(elapsed);
|
|
if (_rotateTo)
|
|
_rotateTo->update(elapsed);
|
|
for (size_t j = 0; j < _layers.size(); j++) {
|
|
Common::SharedPtr<Layer> layer = _layers[j];
|
|
for (size_t k = 0; k < layer->_objects.size(); k++) {
|
|
Common::SharedPtr<Object> obj = layer->_objects[k];
|
|
obj->update(elapsed);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Room::walkboxHidden(const Common::String &name, bool hidden) {
|
|
for (size_t i = 0; i < _walkboxes.size(); i++) {
|
|
Walkbox &wb = _walkboxes[i];
|
|
if (wb._name == name) {
|
|
wb.setVisible(!hidden);
|
|
// 1 walkbox has changed so update merged polygon
|
|
_pathFinder.setDirty(true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
Common::Array<Math::Vector2d> Room::calculatePath(const Math::Vector2d &frm, const Math::Vector2d &to) {
|
|
if (_mergedPolygon.size() > 0) {
|
|
if (_pathFinder.isDirty()) {
|
|
_mergedPolygon = merge(_walkboxes);
|
|
_pathFinder.setWalkboxes(_mergedPolygon);
|
|
_pathFinder.setDirty(false);
|
|
}
|
|
return _pathFinder.calculatePath(frm, to);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
Layer::Layer(const Common::String &name, const Math::Vector2d ¶llax, int zsort) {
|
|
_names.push_back(name);
|
|
_parallax = parallax;
|
|
_zsort = zsort;
|
|
}
|
|
|
|
Layer::Layer(const Common::StringArray &name, const Math::Vector2d ¶llax, int zsort) {
|
|
_names.push_back(name);
|
|
_parallax = parallax;
|
|
_zsort = zsort;
|
|
}
|
|
|
|
Walkbox::Walkbox(const Common::Array<Vector2i> &polygon, bool visible)
|
|
: _polygon(polygon), _visible(visible) {
|
|
}
|
|
|
|
bool Walkbox::concave(int vertex) const {
|
|
Math::Vector2d current = (Math::Vector2d)_polygon[vertex];
|
|
Math::Vector2d next = (Math::Vector2d)_polygon[(vertex + 1) % _polygon.size()];
|
|
Math::Vector2d previous = (Math::Vector2d)_polygon[vertex == 0 ? _polygon.size() - 1 : vertex - 1];
|
|
|
|
Math::Vector2d left{current.getX() - previous.getX(), current.getY() - previous.getY()};
|
|
Math::Vector2d right{next.getX() - current.getX(), next.getY() - current.getY()};
|
|
|
|
float cross = (left.getX() * right.getY()) - (left.getY() * right.getX());
|
|
return cross < 0;
|
|
}
|
|
|
|
bool Walkbox::contains(const Math::Vector2d &position, bool toleranceOnOutside) const {
|
|
Math::Vector2d point = position;
|
|
const float epsilon = 2.0f;
|
|
bool result = false;
|
|
|
|
// Must have 3 or more edges
|
|
if (_polygon.size() < 3)
|
|
return false;
|
|
|
|
Math::Vector2d oldPoint(_polygon[_polygon.size() - 1]);
|
|
float oldSqDist = distanceSquared(oldPoint, point);
|
|
|
|
for (size_t i = 0; i < _polygon.size(); i++) {
|
|
Math::Vector2d newPoint = (Math::Vector2d)_polygon[i];
|
|
float newSqDist = distanceSquared(newPoint, point);
|
|
|
|
if (oldSqDist + newSqDist + 2.0f * sqrt(oldSqDist * newSqDist) - distanceSquared(newPoint, oldPoint) < epsilon)
|
|
return toleranceOnOutside;
|
|
|
|
Math::Vector2d left;
|
|
Math::Vector2d right;
|
|
if (newPoint.getX() > oldPoint.getX()) {
|
|
left = oldPoint;
|
|
right = newPoint;
|
|
} else {
|
|
left = newPoint;
|
|
right = oldPoint;
|
|
}
|
|
|
|
if ((left.getX() < point.getX()) && (point.getX() <= right.getX()) && ((point.getY() - left.getY()) * (right.getX() - left.getX())) < ((right.getY() - left.getY()) * (point.getX() - left.getX())))
|
|
result = !result;
|
|
|
|
oldPoint = Common::move(newPoint);
|
|
oldSqDist = newSqDist;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
float Scaling::getScaling(float yPos) {
|
|
if (values.size() == 0)
|
|
return 1.0f;
|
|
for (size_t i = 0; i < values.size(); i++) {
|
|
ScalingValue scaling = values[i];
|
|
if (yPos < scaling.y) {
|
|
if (i == 0)
|
|
return values[i].scale;
|
|
ScalingValue prevScaling = values[i - 1];
|
|
float dY = scaling.y - prevScaling.y;
|
|
float dScale = scaling.scale - prevScaling.scale;
|
|
float p = (yPos - prevScaling.y) / dY;
|
|
float scale = prevScaling.scale + (p * dScale);
|
|
return scale;
|
|
}
|
|
}
|
|
return values[values.size() - 1].scale;
|
|
}
|
|
|
|
} // namespace Twp
|