最终整理版
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#include "MapAtmosphere.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string Trim(std::string value)
|
||||
{
|
||||
auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch);
|
||||
});
|
||||
auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
|
||||
return std::isspace(ch);
|
||||
}).base();
|
||||
if (first >= last) {
|
||||
return {};
|
||||
}
|
||||
return std::string(first, last);
|
||||
}
|
||||
|
||||
std::string LowerCopy(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
float Clamp01(float value)
|
||||
{
|
||||
return std::clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float ParseIntensity(const TmxMap& map, float fallback)
|
||||
{
|
||||
const auto it = map.properties.find("ambientintensity");
|
||||
if (it == map.properties.end()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const std::string text = Trim(it->second);
|
||||
if (text.empty()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
const float value = std::strtof(text.c_str(), &end);
|
||||
if (end == text.c_str()) {
|
||||
return fallback;
|
||||
}
|
||||
return Clamp01(value);
|
||||
}
|
||||
|
||||
MapAmbientKind ParseKind(const std::string& ambientName)
|
||||
{
|
||||
const std::string lower = LowerCopy(ambientName);
|
||||
if (lower == "lighting") {
|
||||
return MapAmbientKind::Lighting;
|
||||
}
|
||||
if (lower == "cloud") {
|
||||
return MapAmbientKind::Cloud;
|
||||
}
|
||||
if (lower == "cloudlight") {
|
||||
return MapAmbientKind::CloudLight;
|
||||
}
|
||||
if (lower == "neutral") {
|
||||
return MapAmbientKind::Neutral;
|
||||
}
|
||||
return MapAmbientKind::Unknown;
|
||||
}
|
||||
|
||||
float LightingOverlayAlpha(float intensity)
|
||||
{
|
||||
return std::clamp(0.10f + Clamp01(intensity) * 0.40f, 0.10f, 0.50f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MapAtmosphere ResolveMapAtmosphere(const TmxMap& map)
|
||||
{
|
||||
const auto ambientIt = map.properties.find("ambient");
|
||||
if (ambientIt == map.properties.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string ambientName = Trim(ambientIt->second);
|
||||
if (ambientName.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
MapAtmosphere atmosphere;
|
||||
atmosphere.ambientName = ambientName;
|
||||
atmosphere.kind = ParseKind(ambientName);
|
||||
|
||||
switch (atmosphere.kind) {
|
||||
case MapAmbientKind::Lighting:
|
||||
atmosphere.intensity = ParseIntensity(map, 0.5f);
|
||||
atmosphere.overlayTint = MapAtmosphereColor{0.10f, 0.08f, 0.15f, LightingOverlayAlpha(atmosphere.intensity)};
|
||||
atmosphere.darkens = true;
|
||||
break;
|
||||
case MapAmbientKind::Cloud:
|
||||
atmosphere.intensity = ParseIntensity(map, 0.0f);
|
||||
atmosphere.overlayTint = MapAtmosphereColor{1.0f, 1.0f, 1.0f, 0.03f};
|
||||
break;
|
||||
case MapAmbientKind::CloudLight:
|
||||
atmosphere.intensity = ParseIntensity(map, 0.0f);
|
||||
atmosphere.overlayTint = MapAtmosphereColor{1.0f, 0.98f, 0.90f, 0.04f};
|
||||
break;
|
||||
case MapAmbientKind::Neutral:
|
||||
return {};
|
||||
case MapAmbientKind::Unknown:
|
||||
atmosphere.intensity = ParseIntensity(map, 0.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
return atmosphere;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "TmxMap.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
enum class MapAmbientKind {
|
||||
Neutral,
|
||||
Lighting,
|
||||
Cloud,
|
||||
CloudLight,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct MapAtmosphereColor {
|
||||
float r = 1.0f;
|
||||
float g = 1.0f;
|
||||
float b = 1.0f;
|
||||
float a = 0.0f;
|
||||
};
|
||||
|
||||
struct MapAtmosphere {
|
||||
std::string ambientName = "Neutral";
|
||||
MapAmbientKind kind = MapAmbientKind::Neutral;
|
||||
float intensity = 0.0f;
|
||||
MapAtmosphereColor overlayTint;
|
||||
bool darkens = false;
|
||||
};
|
||||
|
||||
MapAtmosphere ResolveMapAtmosphere(const TmxMap& map);
|
||||
@@ -0,0 +1,473 @@
|
||||
#include "TmxMap.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <pugixml.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t FlippedHorizontally = 0x80000000u;
|
||||
constexpr std::uint32_t FlippedVertically = 0x40000000u;
|
||||
constexpr std::uint32_t FlippedDiagonally = 0x20000000u;
|
||||
constexpr std::uint32_t FlippedHex120 = 0x10000000u;
|
||||
|
||||
std::string Trim(std::string value)
|
||||
{
|
||||
auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); });
|
||||
auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch); }).base();
|
||||
if (first >= last) {
|
||||
return {};
|
||||
}
|
||||
return std::string(first, last);
|
||||
}
|
||||
|
||||
std::string LowerCopy(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::filesystem::path NormalizePath(const std::filesystem::path& base, const char* relative)
|
||||
{
|
||||
return std::filesystem::weakly_canonical(base / relative);
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> ReadProperties(const pugi::xml_node& node)
|
||||
{
|
||||
std::map<std::string, std::string> result;
|
||||
for (pugi::xml_node property : node.child("properties").children("property")) {
|
||||
const std::string name = property.attribute("name").as_string();
|
||||
const char* valueAttr = property.attribute("value").as_string(nullptr);
|
||||
if (valueAttr) {
|
||||
result[name] = valueAttr;
|
||||
} else {
|
||||
result[name] = property.text().as_string();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<TmxPoint> ParsePoints(const std::string& points);
|
||||
|
||||
std::vector<TmxCollisionShape> ParseCollisionShapes(const pugi::xml_node& tile)
|
||||
{
|
||||
std::vector<TmxCollisionShape> shapes;
|
||||
for (pugi::xml_node object : tile.child("objectgroup").children("object")) {
|
||||
TmxCollisionShape shape;
|
||||
shape.x = object.attribute("x").as_float();
|
||||
shape.y = object.attribute("y").as_float();
|
||||
shape.width = object.attribute("width").as_float();
|
||||
shape.height = object.attribute("height").as_float();
|
||||
if (pugi::xml_node polygon = object.child("polygon")) {
|
||||
shape.polygon = ParsePoints(polygon.attribute("points").as_string());
|
||||
} else if (pugi::xml_node polyline = object.child("polyline")) {
|
||||
shape.polygon = ParsePoints(polyline.attribute("points").as_string());
|
||||
}
|
||||
if (!shape.polygon.empty() || shape.width > 0.0f || shape.height > 0.0f) {
|
||||
shapes.push_back(std::move(shape));
|
||||
}
|
||||
}
|
||||
return shapes;
|
||||
}
|
||||
|
||||
TmxTileset ParseTilesetNode(const pugi::xml_node& node, std::uint32_t firstGid, const std::filesystem::path& baseDir)
|
||||
{
|
||||
TmxTileset tileset;
|
||||
tileset.firstGid = firstGid;
|
||||
tileset.name = node.attribute("name").as_string();
|
||||
tileset.tileWidth = node.attribute("tilewidth").as_int();
|
||||
tileset.tileHeight = node.attribute("tileheight").as_int();
|
||||
tileset.tileCount = node.attribute("tilecount").as_int();
|
||||
tileset.columns = node.attribute("columns").as_int();
|
||||
|
||||
pugi::xml_node image = node.child("image");
|
||||
if (!image) {
|
||||
throw std::runtime_error("Tileset has no image: " + tileset.name);
|
||||
}
|
||||
tileset.imagePath = NormalizePath(baseDir, image.attribute("source").as_string());
|
||||
for (pugi::xml_node tile : node.children("tile")) {
|
||||
const auto tileId = static_cast<std::uint32_t>(tile.attribute("id").as_uint());
|
||||
std::vector<TmxCollisionShape> collisionShapes = ParseCollisionShapes(tile);
|
||||
if (!collisionShapes.empty()) {
|
||||
tileset.collisionShapes[tileId] = std::move(collisionShapes);
|
||||
}
|
||||
|
||||
pugi::xml_node animation = tile.child("animation");
|
||||
if (!animation) {
|
||||
continue;
|
||||
}
|
||||
std::vector<TmxAnimationFrame> frames;
|
||||
for (pugi::xml_node frame : animation.children("frame")) {
|
||||
frames.push_back(TmxAnimationFrame{
|
||||
static_cast<std::uint32_t>(frame.attribute("tileid").as_uint()),
|
||||
frame.attribute("duration").as_int(),
|
||||
});
|
||||
}
|
||||
if (!frames.empty()) {
|
||||
tileset.animations[tileId] = std::move(frames);
|
||||
}
|
||||
}
|
||||
return tileset;
|
||||
}
|
||||
|
||||
TmxTileset ParseTileset(const pugi::xml_node& node, const std::filesystem::path& mapDir)
|
||||
{
|
||||
const auto firstGid = static_cast<std::uint32_t>(node.attribute("firstgid").as_uint());
|
||||
const char* source = node.attribute("source").as_string(nullptr);
|
||||
if (!source) {
|
||||
return ParseTilesetNode(node, firstGid, mapDir);
|
||||
}
|
||||
|
||||
const std::filesystem::path tsxPath = NormalizePath(mapDir, source);
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result ok = doc.load_file(tsxPath.c_str());
|
||||
if (!ok) {
|
||||
throw std::runtime_error("Could not parse TSX: " + tsxPath.string());
|
||||
}
|
||||
return ParseTilesetNode(doc.child("tileset"), firstGid, tsxPath.parent_path());
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> ParseCsvGids(const std::string& csv)
|
||||
{
|
||||
std::vector<std::uint32_t> gids;
|
||||
std::stringstream stream(csv);
|
||||
std::string item;
|
||||
while (std::getline(stream, item, ',')) {
|
||||
item = Trim(item);
|
||||
if (!item.empty()) {
|
||||
gids.push_back(static_cast<std::uint32_t>(std::stoul(item)));
|
||||
}
|
||||
}
|
||||
return gids;
|
||||
}
|
||||
|
||||
std::vector<TmxPoint> ParsePoints(const std::string& points)
|
||||
{
|
||||
std::vector<TmxPoint> out;
|
||||
std::stringstream stream(points);
|
||||
std::string pair;
|
||||
while (stream >> pair) {
|
||||
const std::size_t comma = pair.find(',');
|
||||
if (comma == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
TmxPoint point;
|
||||
point.x = std::stof(pair.substr(0, comma));
|
||||
point.y = std::stof(pair.substr(comma + 1));
|
||||
out.push_back(point);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
|
||||
std::map<std::string, TmxMap>& TmxMapCache()
|
||||
{
|
||||
static std::map<std::string, TmxMap> cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::uint32_t CleanGid(std::uint32_t gid)
|
||||
{
|
||||
return gid & ~(FlippedHorizontally | FlippedVertically | FlippedDiagonally | FlippedHex120);
|
||||
}
|
||||
|
||||
TmxMap LoadTmxMap(const std::filesystem::path& path)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result ok = doc.load_file(path.c_str());
|
||||
if (!ok) {
|
||||
throw std::runtime_error("Could not parse TMX: " + path.string());
|
||||
}
|
||||
|
||||
pugi::xml_node root = doc.child("map");
|
||||
if (!root) {
|
||||
throw std::runtime_error("TMX file has no map root: " + path.string());
|
||||
}
|
||||
|
||||
TmxMap map;
|
||||
map.sourcePath = std::filesystem::weakly_canonical(path);
|
||||
map.width = root.attribute("width").as_int();
|
||||
map.height = root.attribute("height").as_int();
|
||||
map.tileWidth = root.attribute("tilewidth").as_int();
|
||||
map.tileHeight = root.attribute("tileheight").as_int();
|
||||
map.properties = ReadProperties(root);
|
||||
|
||||
const std::filesystem::path mapDir = map.sourcePath.parent_path();
|
||||
for (pugi::xml_node child : root.children()) {
|
||||
const std::string nodeName = child.name();
|
||||
if (nodeName == "tileset") {
|
||||
map.tilesets.push_back(ParseTileset(child, mapDir));
|
||||
} else if (nodeName == "layer") {
|
||||
pugi::xml_node data = child.child("data");
|
||||
const std::string encoding = data.attribute("encoding").as_string();
|
||||
if (encoding != "csv") {
|
||||
continue;
|
||||
}
|
||||
TmxLayer layer;
|
||||
layer.name = child.attribute("name").as_string();
|
||||
layer.width = child.attribute("width").as_int();
|
||||
layer.height = child.attribute("height").as_int();
|
||||
layer.visible = child.attribute("visible").as_int(1) != 0;
|
||||
layer.opacity = child.attribute("opacity").as_float(1.0f);
|
||||
layer.gids = ParseCsvGids(data.text().as_string());
|
||||
map.layers.push_back(std::move(layer));
|
||||
} else if (nodeName == "objectgroup") {
|
||||
for (pugi::xml_node object : child.children("object")) {
|
||||
TmxObject out;
|
||||
out.id = object.attribute("id").as_int();
|
||||
out.name = object.attribute("name").as_string();
|
||||
out.type = object.attribute("type").as_string();
|
||||
out.x = object.attribute("x").as_float();
|
||||
out.y = object.attribute("y").as_float();
|
||||
out.width = object.attribute("width").as_float();
|
||||
out.height = object.attribute("height").as_float();
|
||||
if (pugi::xml_node polygon = object.child("polygon")) {
|
||||
out.polygon = ParsePoints(polygon.attribute("points").as_string());
|
||||
} else if (pugi::xml_node polyline = object.child("polyline")) {
|
||||
out.polygon = ParsePoints(polyline.attribute("points").as_string());
|
||||
}
|
||||
out.properties = ReadProperties(object);
|
||||
map.objects.push_back(std::move(out));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(map.tilesets.begin(), map.tilesets.end(), [](const TmxTileset& a, const TmxTileset& b) {
|
||||
return a.firstGid < b.firstGid;
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
const TmxMap& LoadTmxMapCached(const std::filesystem::path& path)
|
||||
{
|
||||
const std::filesystem::path canonical = std::filesystem::weakly_canonical(path);
|
||||
const std::string key = canonical.string();
|
||||
std::map<std::string, TmxMap>& cache = TmxMapCache();
|
||||
auto [it, inserted] = cache.try_emplace(key);
|
||||
if (inserted) {
|
||||
it->second = LoadTmxMap(canonical);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void ClearTmxMapCache()
|
||||
{
|
||||
TmxMapCache().clear();
|
||||
}
|
||||
|
||||
const TmxLayer* FindLayer(const TmxMap& map, const std::string& name)
|
||||
{
|
||||
auto it = std::find_if(map.layers.begin(), map.layers.end(), [&](const TmxLayer& layer) {
|
||||
return layer.name == name;
|
||||
});
|
||||
return it == map.layers.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
const TmxObject* FindObject(const TmxMap& map, const std::string& name)
|
||||
{
|
||||
auto it = std::find_if(map.objects.begin(), map.objects.end(), [&](const TmxObject& object) {
|
||||
return object.name == name;
|
||||
});
|
||||
return it == map.objects.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
const TmxTileset* FindTilesetForGid(const TmxMap& map, std::uint32_t gid)
|
||||
{
|
||||
gid = CleanGid(gid);
|
||||
const TmxTileset* found = nullptr;
|
||||
for (const TmxTileset& tileset : map.tilesets) {
|
||||
if (gid >= tileset.firstGid) {
|
||||
found = &tileset;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint32_t LayerGidAt(const TmxLayer& layer, int x, int y)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= layer.width || y >= layer.height) {
|
||||
return 0;
|
||||
}
|
||||
const std::size_t index = static_cast<std::size_t>(y * layer.width + x);
|
||||
return index < layer.gids.size() ? CleanGid(layer.gids[index]) : 0;
|
||||
}
|
||||
|
||||
bool PointInRect(float x, float y, const TmxCollisionShape& shape)
|
||||
{
|
||||
return x >= shape.x
|
||||
&& y >= shape.y
|
||||
&& x < shape.x + shape.width
|
||||
&& y < shape.y + shape.height;
|
||||
}
|
||||
|
||||
bool PointOnSegment(float px, float py, TmxPoint a, TmxPoint b)
|
||||
{
|
||||
const float cross = (px - a.x) * (b.y - a.y) - (py - a.y) * (b.x - a.x);
|
||||
if (std::abs(cross) > 0.001f) {
|
||||
return false;
|
||||
}
|
||||
return px >= std::min(a.x, b.x) - 0.001f
|
||||
&& px <= std::max(a.x, b.x) + 0.001f
|
||||
&& py >= std::min(a.y, b.y) - 0.001f
|
||||
&& py <= std::max(a.y, b.y) + 0.001f;
|
||||
}
|
||||
|
||||
bool PointInPolygon(float x, float y, const TmxCollisionShape& shape)
|
||||
{
|
||||
if (shape.polygon.size() < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<TmxPoint> points;
|
||||
points.reserve(shape.polygon.size());
|
||||
for (const TmxPoint& point : shape.polygon) {
|
||||
points.push_back({shape.x + point.x, shape.y + point.y});
|
||||
}
|
||||
|
||||
bool inside = false;
|
||||
for (std::size_t i = 0, j = points.size() - 1; i < points.size(); j = i++) {
|
||||
const TmxPoint a = points[i];
|
||||
const TmxPoint b = points[j];
|
||||
if (PointOnSegment(x, y, a, b)) {
|
||||
return true;
|
||||
}
|
||||
const bool crosses = ((a.y > y) != (b.y > y))
|
||||
&& (x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x);
|
||||
if (crosses) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
bool PointInCollisionShape(float x, float y, const TmxCollisionShape& shape)
|
||||
{
|
||||
if (!shape.polygon.empty()) {
|
||||
return PointInPolygon(x, y, shape);
|
||||
}
|
||||
return PointInRect(x, y, shape);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsPointBlockedByCollision(const TmxMap& map, float worldX, float worldY)
|
||||
{
|
||||
if (worldX < 0.0f || worldY < 0.0f
|
||||
|| worldX >= static_cast<float>(map.width * map.tileWidth)
|
||||
|| worldY >= static_cast<float>(map.height * map.tileHeight)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (map.tileWidth <= 0 || map.tileHeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int tileX = static_cast<int>(std::floor(worldX / static_cast<float>(map.tileWidth)));
|
||||
const int tileY = static_cast<int>(std::floor(worldY / static_cast<float>(map.tileHeight)));
|
||||
for (const TmxLayer& layer : map.layers) {
|
||||
if (!layer.visible || !IsTmxCollisionLayer(layer.name)) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t gid = LayerGidAt(layer, tileX, tileY);
|
||||
if (gid == 0) {
|
||||
continue;
|
||||
}
|
||||
const TmxTileset* tileset = FindTilesetForGid(map, gid);
|
||||
if (!tileset) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t localTileId = gid - tileset->firstGid;
|
||||
const auto shapes = tileset->collisionShapes.find(localTileId);
|
||||
if (shapes == tileset->collisionShapes.end() || shapes->second.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const float localX = (worldX - static_cast<float>(tileX * map.tileWidth))
|
||||
* static_cast<float>(std::max(1, tileset->tileWidth)) / static_cast<float>(map.tileWidth);
|
||||
const float localY = (worldY - static_cast<float>(tileY * map.tileHeight))
|
||||
* static_cast<float>(std::max(1, tileset->tileHeight)) / static_cast<float>(map.tileHeight);
|
||||
for (const TmxCollisionShape& shape : shapes->second) {
|
||||
if (PointInCollisionShape(localX, localY, shape)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsTmxCollisionLayer(const std::string& layerName)
|
||||
{
|
||||
return LowerCopy(layerName).find("collision") != std::string::npos;
|
||||
}
|
||||
|
||||
bool IsTmxOverlayLayer(const std::string& layerName)
|
||||
{
|
||||
const std::string lower = LowerCopy(layerName);
|
||||
return lower.find("fringe") != std::string::npos || lower.find("over") != std::string::npos;
|
||||
}
|
||||
|
||||
std::uint32_t AnimatedLocalTileId(const TmxTileset& tileset, std::uint32_t localTileId, int elapsedMs)
|
||||
{
|
||||
const auto it = tileset.animations.find(localTileId);
|
||||
if (it == tileset.animations.end() || it->second.empty()) {
|
||||
return localTileId;
|
||||
}
|
||||
|
||||
int totalDuration = 0;
|
||||
for (const TmxAnimationFrame& frame : it->second) {
|
||||
totalDuration += std::max(0, frame.durationMs);
|
||||
}
|
||||
if (totalDuration <= 0) {
|
||||
return localTileId;
|
||||
}
|
||||
|
||||
int cursor = std::max(0, elapsedMs) % totalDuration;
|
||||
for (const TmxAnimationFrame& frame : it->second) {
|
||||
const int duration = std::max(0, frame.durationMs);
|
||||
if (cursor < duration) {
|
||||
return frame.tileId;
|
||||
}
|
||||
cursor -= duration;
|
||||
}
|
||||
return it->second.back().tileId;
|
||||
}
|
||||
|
||||
TileBounds VisibleTileBounds(
|
||||
const TmxMap& map,
|
||||
float cameraTargetX,
|
||||
float cameraTargetY,
|
||||
float screenWidth,
|
||||
float screenHeight,
|
||||
float zoom,
|
||||
int paddingTiles)
|
||||
{
|
||||
const float safeZoom = std::max(0.1f, zoom);
|
||||
const float halfWorldWidth = screenWidth / safeZoom * 0.5f;
|
||||
const float halfWorldHeight = screenHeight / safeZoom * 0.5f;
|
||||
const float minWorldX = cameraTargetX - halfWorldWidth;
|
||||
const float maxWorldX = cameraTargetX + halfWorldWidth;
|
||||
const float minWorldY = cameraTargetY - halfWorldHeight;
|
||||
const float maxWorldY = cameraTargetY + halfWorldHeight;
|
||||
|
||||
TileBounds bounds;
|
||||
bounds.minX = std::clamp(static_cast<int>(std::floor(minWorldX / static_cast<float>(map.tileWidth))) - paddingTiles, 0, std::max(0, map.width - 1));
|
||||
bounds.maxX = std::clamp(static_cast<int>(std::ceil(maxWorldX / static_cast<float>(map.tileWidth))) + paddingTiles, 0, std::max(0, map.width - 1));
|
||||
bounds.minY = std::clamp(static_cast<int>(std::floor(minWorldY / static_cast<float>(map.tileHeight))) - paddingTiles, 0, std::max(0, map.height - 1));
|
||||
bounds.maxY = std::clamp(static_cast<int>(std::ceil(maxWorldY / static_cast<float>(map.tileHeight))) + paddingTiles, 0, std::max(0, map.height - 1));
|
||||
return bounds;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct TmxAnimationFrame {
|
||||
std::uint32_t tileId = 0;
|
||||
int durationMs = 0;
|
||||
};
|
||||
|
||||
struct TmxPoint {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
};
|
||||
|
||||
struct TmxCollisionShape {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
std::vector<TmxPoint> polygon;
|
||||
};
|
||||
|
||||
struct TmxTileset {
|
||||
std::uint32_t firstGid = 0;
|
||||
std::string name;
|
||||
int tileWidth = 0;
|
||||
int tileHeight = 0;
|
||||
int tileCount = 0;
|
||||
int columns = 0;
|
||||
std::filesystem::path imagePath;
|
||||
std::map<std::uint32_t, std::vector<TmxAnimationFrame>> animations;
|
||||
std::map<std::uint32_t, std::vector<TmxCollisionShape>> collisionShapes;
|
||||
};
|
||||
|
||||
struct TmxLayer {
|
||||
std::string name;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool visible = true;
|
||||
float opacity = 1.0f;
|
||||
std::vector<std::uint32_t> gids;
|
||||
};
|
||||
|
||||
struct TmxObject {
|
||||
int id = 0;
|
||||
std::string name;
|
||||
std::string type;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
std::vector<TmxPoint> polygon;
|
||||
std::map<std::string, std::string> properties;
|
||||
};
|
||||
|
||||
struct TmxMap {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int tileWidth = 0;
|
||||
int tileHeight = 0;
|
||||
std::filesystem::path sourcePath;
|
||||
std::map<std::string, std::string> properties;
|
||||
std::vector<TmxTileset> tilesets;
|
||||
std::vector<TmxLayer> layers;
|
||||
std::vector<TmxObject> objects;
|
||||
};
|
||||
|
||||
struct TileBounds {
|
||||
int minX = 0;
|
||||
int maxX = 0;
|
||||
int minY = 0;
|
||||
int maxY = 0;
|
||||
};
|
||||
|
||||
TmxMap LoadTmxMap(const std::filesystem::path& path);
|
||||
const TmxMap& LoadTmxMapCached(const std::filesystem::path& path);
|
||||
void ClearTmxMapCache();
|
||||
const TmxLayer* FindLayer(const TmxMap& map, const std::string& name);
|
||||
const TmxObject* FindObject(const TmxMap& map, const std::string& name);
|
||||
const TmxTileset* FindTilesetForGid(const TmxMap& map, std::uint32_t gid);
|
||||
std::uint32_t CleanGid(std::uint32_t gid);
|
||||
bool IsTmxCollisionLayer(const std::string& layerName);
|
||||
bool IsTmxOverlayLayer(const std::string& layerName);
|
||||
bool IsPointBlockedByCollision(const TmxMap& map, float worldX, float worldY);
|
||||
std::uint32_t AnimatedLocalTileId(const TmxTileset& tileset, std::uint32_t localTileId, int elapsedMs);
|
||||
TileBounds VisibleTileBounds(
|
||||
const TmxMap& map,
|
||||
float cameraTargetX,
|
||||
float cameraTargetY,
|
||||
float screenWidth,
|
||||
float screenHeight,
|
||||
float zoom,
|
||||
int paddingTiles);
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "TmxMapPool.h"
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::string> PoolTargets(const TmxWorldIndex& index, const std::string& currentMapName, int maxSize)
|
||||
{
|
||||
std::vector<std::string> targets;
|
||||
if (maxSize <= 0 || !index.mapsByName.contains(currentMapName)) {
|
||||
return targets;
|
||||
}
|
||||
|
||||
targets.push_back(currentMapName);
|
||||
for (const std::string& adjacent : AdjacentMaps(index, currentMapName)) {
|
||||
if (static_cast<int>(targets.size()) >= maxSize) {
|
||||
break;
|
||||
}
|
||||
if (adjacent != currentMapName) {
|
||||
targets.push_back(adjacent);
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
std::vector<std::string> PoolTargets(
|
||||
const TmxWorldIndex& index,
|
||||
const std::vector<WarpEdge>& warpGraph,
|
||||
const std::string& currentMapName,
|
||||
int maxSize)
|
||||
{
|
||||
std::vector<std::string> targets;
|
||||
if (maxSize <= 0 || !index.mapsByName.contains(currentMapName)) {
|
||||
return targets;
|
||||
}
|
||||
|
||||
targets.push_back(currentMapName);
|
||||
for (const std::string& adjacent : AdjacentMaps(index, warpGraph, currentMapName)) {
|
||||
if (static_cast<int>(targets.size()) >= maxSize) {
|
||||
break;
|
||||
}
|
||||
if (adjacent != currentMapName) {
|
||||
targets.push_back(adjacent);
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
void RefreshTmxMapPoolTargets(
|
||||
TmxMapPool& pool,
|
||||
const TmxWorldIndex& index,
|
||||
const std::vector<std::string>& targets)
|
||||
{
|
||||
const std::set<std::string> keep(targets.begin(), targets.end());
|
||||
|
||||
for (auto it = pool.maps.begin(); it != pool.maps.end();) {
|
||||
if (!keep.contains(it->first)) {
|
||||
it = pool.maps.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& mapName : targets) {
|
||||
if (!pool.maps.contains(mapName)) {
|
||||
pool.maps[mapName] = LoadNamedMap(index, mapName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RefreshTmxMapPool(TmxMapPool& pool, const TmxWorldIndex& index, const std::string& currentMapName, int maxSize)
|
||||
{
|
||||
const std::vector<std::string> targets = PoolTargets(index, currentMapName, maxSize);
|
||||
RefreshTmxMapPoolTargets(pool, index, targets);
|
||||
}
|
||||
|
||||
void RefreshTmxMapPool(
|
||||
TmxMapPool& pool,
|
||||
const TmxWorldIndex& index,
|
||||
const std::vector<WarpEdge>& warpGraph,
|
||||
const std::string& currentMapName,
|
||||
int maxSize)
|
||||
{
|
||||
const std::vector<std::string> targets = PoolTargets(index, warpGraph, currentMapName, maxSize);
|
||||
RefreshTmxMapPoolTargets(pool, index, targets);
|
||||
}
|
||||
|
||||
void RefreshTmxMapPool(
|
||||
TmxMapPool& pool,
|
||||
const TmxWorldIndex& index,
|
||||
const std::string& currentMapName,
|
||||
int maxSize,
|
||||
const std::vector<WarpEdge>& warpGraph)
|
||||
{
|
||||
RefreshTmxMapPool(pool, index, warpGraph, currentMapName, maxSize);
|
||||
}
|
||||
|
||||
const TmxMap* FindPooledMap(const TmxMapPool& pool, const std::string& mapName)
|
||||
{
|
||||
const auto it = pool.maps.find(mapName);
|
||||
return it == pool.maps.end() ? nullptr : &it->second;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "TmxMap.h"
|
||||
#include "TmxWorld.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct TmxMapPool {
|
||||
std::map<std::string, TmxMap> maps;
|
||||
};
|
||||
|
||||
void RefreshTmxMapPool(TmxMapPool& pool, const TmxWorldIndex& index, const std::string& currentMapName, int maxSize);
|
||||
void RefreshTmxMapPool(
|
||||
TmxMapPool& pool,
|
||||
const TmxWorldIndex& index,
|
||||
const std::vector<WarpEdge>& warpGraph,
|
||||
const std::string& currentMapName,
|
||||
int maxSize);
|
||||
void RefreshTmxMapPool(
|
||||
TmxMapPool& pool,
|
||||
const TmxWorldIndex& index,
|
||||
const std::string& currentMapName,
|
||||
int maxSize,
|
||||
const std::vector<WarpEdge>& warpGraph);
|
||||
const TmxMap* FindPooledMap(const TmxMapPool& pool, const std::string& mapName);
|
||||
@@ -0,0 +1,442 @@
|
||||
#include "TmxWorld.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string PropertyOr(const TmxObject& object, const std::string& key, const std::string& fallback = {})
|
||||
{
|
||||
const auto it = object.properties.find(key);
|
||||
return it == object.properties.end() ? fallback : it->second;
|
||||
}
|
||||
|
||||
int PropertyIntOr(const TmxObject& object, const std::string& key, int fallback = 0)
|
||||
{
|
||||
const std::string value = PropertyOr(object, key);
|
||||
return value.empty() ? fallback : std::stoi(value);
|
||||
}
|
||||
|
||||
float PropertyFloatOr(const TmxObject& object, const std::string& key, float fallback)
|
||||
{
|
||||
const std::string value = PropertyOr(object, key);
|
||||
return value.empty() ? fallback : std::stof(value);
|
||||
}
|
||||
|
||||
std::string LowerCopy(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
bool PropertyBoolOr(const TmxObject& object, const std::string& key, bool fallback)
|
||||
{
|
||||
const std::string value = LowerCopy(PropertyOr(object, key));
|
||||
if (value == "true" || value == "1") {
|
||||
return true;
|
||||
}
|
||||
if (value == "false" || value == "0") {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool ContainsText(const std::string& text, const std::string& needle)
|
||||
{
|
||||
return text.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
bool LooksLikeIndoorMapName(const std::string& mapName)
|
||||
{
|
||||
if (mapName == "Indoor") {
|
||||
return true;
|
||||
}
|
||||
return !mapName.empty() && std::all_of(mapName.begin(), mapName.end(), [](unsigned char ch) {
|
||||
return std::isdigit(ch) || ch == '-';
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string MapDisplayName(const TmxMap& map)
|
||||
{
|
||||
const auto it = map.properties.find("name");
|
||||
if (it != map.properties.end() && !it->second.empty()) {
|
||||
return it->second;
|
||||
}
|
||||
return map.sourcePath.stem().string();
|
||||
}
|
||||
|
||||
TmxWorldIndex BuildTmxWorldIndex(const std::filesystem::path& mapsRoot)
|
||||
{
|
||||
TmxWorldIndex index;
|
||||
for (const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator(mapsRoot)) {
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".tmx") {
|
||||
continue;
|
||||
}
|
||||
const TmxMap& map = LoadTmxMapCached(entry.path());
|
||||
index.mapsByName[MapDisplayName(map)] = std::filesystem::weakly_canonical(entry.path());
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
TmxMap LoadNamedMap(const TmxWorldIndex& index, const std::string& mapName)
|
||||
{
|
||||
const auto it = index.mapsByName.find(mapName);
|
||||
if (it == index.mapsByName.end()) {
|
||||
throw std::runtime_error("Unknown TMX map: " + mapName);
|
||||
}
|
||||
return LoadTmxMapCached(it->second);
|
||||
}
|
||||
|
||||
bool IsSpawnObject(const TmxObject& object)
|
||||
{
|
||||
return LowerCopy(object.type) == "spawn";
|
||||
}
|
||||
|
||||
bool IsWarpObject(const TmxObject& object)
|
||||
{
|
||||
const std::string type = LowerCopy(object.type);
|
||||
return type == "warp" || type == "port";
|
||||
}
|
||||
|
||||
bool IsCollectibleSpawnObject(const TmxObject& object)
|
||||
{
|
||||
if (!IsSpawnObject(object)) {
|
||||
return false;
|
||||
}
|
||||
const std::string script = PropertyOr(object, "player_script") + " " + PropertyOr(object, "own_script");
|
||||
return ContainsText(object.name, "Chest")
|
||||
|| ContainsText(object.name, "Key")
|
||||
|| ContainsText(object.name, "Letter")
|
||||
|| ContainsText(object.name, "Stash")
|
||||
|| ContainsText(object.name, "Clue")
|
||||
|| ContainsText(object.name, "Pond")
|
||||
|| ContainsText(object.name, "Well")
|
||||
|| ContainsText(script, "Chest")
|
||||
|| ContainsText(script, "MineKey");
|
||||
}
|
||||
|
||||
std::string SpawnDisplayName(const TmxObject& object)
|
||||
{
|
||||
return PropertyOr(object, "nick", object.name);
|
||||
}
|
||||
|
||||
std::string SpawnStateName(const TmxObject& object)
|
||||
{
|
||||
std::string state = LowerCopy(PropertyOr(object, "state"));
|
||||
return state == "walk" || state == "sit" || state == "attack" ? state : "idle";
|
||||
}
|
||||
|
||||
TmxObjectPosition SpawnObjectPosition(const TmxObject& object)
|
||||
{
|
||||
return {
|
||||
object.x + std::max(0.0f, object.width) * 0.5f,
|
||||
object.y + std::max(0.0f, object.height) * 0.5f,
|
||||
};
|
||||
}
|
||||
|
||||
bool WarpAutoWarps(const TmxObject& warp)
|
||||
{
|
||||
return PropertyBoolOr(warp, "auto_warp", true);
|
||||
}
|
||||
|
||||
float TriggerRadiusForObject(const TmxObject& object, float fallback)
|
||||
{
|
||||
return PropertyFloatOr(object, "trigger_radius", fallback);
|
||||
}
|
||||
|
||||
TmxObjectBounds WarpTriggerBounds(const TmxObject& warp)
|
||||
{
|
||||
TmxObjectBounds bounds{warp.x, warp.y, warp.width, warp.height};
|
||||
const int triggerTilesX = PropertyIntOr(warp, "trigger_x", 0);
|
||||
const int triggerTilesY = PropertyIntOr(warp, "trigger_y", 0);
|
||||
if (triggerTilesX <= 0 || triggerTilesY <= 0) {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
const float tileWidth = warp.width > 0.0f ? warp.width : 32.0f;
|
||||
const float tileHeight = warp.height > 0.0f ? warp.height : 32.0f;
|
||||
bounds.width = static_cast<float>(triggerTilesX) * tileWidth;
|
||||
bounds.height = static_cast<float>(triggerTilesY) * tileHeight;
|
||||
bounds.x = warp.x + (warp.width - bounds.width) * 0.5f;
|
||||
bounds.y = warp.y + (warp.height - bounds.height) * 0.5f;
|
||||
return bounds;
|
||||
}
|
||||
|
||||
WarpTarget ReadWarpTarget(const TmxObject& warp)
|
||||
{
|
||||
WarpTarget target;
|
||||
target.mapName = PropertyOr(warp, "dest_map");
|
||||
if (target.mapName.empty()) {
|
||||
target.mapName = PropertyOr(warp, "map");
|
||||
}
|
||||
if (LowerCopy(warp.type) == "port" && !PropertyOr(warp, "sail_pos_x").empty() && !PropertyOr(warp, "sail_pos_y").empty()) {
|
||||
target.tileX = PropertyIntOr(warp, "sail_pos_x");
|
||||
target.tileY = PropertyIntOr(warp, "sail_pos_y");
|
||||
} else {
|
||||
target.tileX = PropertyIntOr(warp, "dest_pos_x", PropertyIntOr(warp, "dest_x"));
|
||||
target.tileY = PropertyIntOr(warp, "dest_pos_y", PropertyIntOr(warp, "dest_y"));
|
||||
}
|
||||
target.originalMapName = target.mapName;
|
||||
return target;
|
||||
}
|
||||
|
||||
bool WarpHasExplicitTileDestination(const TmxObject& warp)
|
||||
{
|
||||
if (LowerCopy(warp.type) == "port"
|
||||
&& !PropertyOr(warp, "sail_pos_x").empty()
|
||||
&& !PropertyOr(warp, "sail_pos_y").empty()) {
|
||||
return true;
|
||||
}
|
||||
return (!PropertyOr(warp, "dest_pos_x").empty() && !PropertyOr(warp, "dest_pos_y").empty())
|
||||
|| (!PropertyOr(warp, "dest_x").empty() && !PropertyOr(warp, "dest_y").empty());
|
||||
}
|
||||
|
||||
bool WarpTargetResolved(const TmxWorldIndex& index, const TmxObject& warp)
|
||||
{
|
||||
const WarpTarget target = ReadWarpTarget(warp);
|
||||
if (target.mapName == "Indoor") {
|
||||
return index.mapsByName.contains(IndoorAliasMapName());
|
||||
}
|
||||
return !target.mapName.empty() && index.mapsByName.contains(target.mapName);
|
||||
}
|
||||
|
||||
bool PointLooksLikeTonoriBuildingEntrance(const TmxMap& map, float x, float y)
|
||||
{
|
||||
if (map.tileWidth <= 0 || map.tileHeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
const int tileX = static_cast<int>(x / static_cast<float>(map.tileWidth));
|
||||
const int tileY = static_cast<int>(y / static_cast<float>(map.tileHeight));
|
||||
if (tileX < 0 || tileY < 0 || tileX >= map.width || tileY >= map.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> houseFirstGids;
|
||||
for (const TmxTileset& tileset : map.tilesets) {
|
||||
if (tileset.name == "House") {
|
||||
houseFirstGids.push_back(tileset.firstGid);
|
||||
}
|
||||
}
|
||||
if (houseFirstGids.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto isHouseEntranceGid = [&](std::uint32_t rawGid) {
|
||||
const std::uint32_t gid = CleanGid(rawGid);
|
||||
if (gid == 0) {
|
||||
return false;
|
||||
}
|
||||
for (const std::uint32_t firstGid : houseFirstGids) {
|
||||
if (gid < firstGid) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t localId = gid - firstGid;
|
||||
// House.tsx rows used by Tonori exterior door thresholds and ladder entries.
|
||||
if (localId == 48 || localId == 49 || localId == 50 || localId == 51 || localId == 112) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const TmxLayer& layer : map.layers) {
|
||||
if (!layer.visible || IsTmxCollisionLayer(layer.name)) {
|
||||
continue;
|
||||
}
|
||||
if (tileX >= layer.width || tileY >= layer.height) {
|
||||
continue;
|
||||
}
|
||||
const std::size_t index = static_cast<std::size_t>(tileY * layer.width + tileX);
|
||||
if (index < layer.gids.size() && isHouseEntranceGid(layer.gids[index])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string IndoorAliasMapName()
|
||||
{
|
||||
return "Tulimshar West Chamber";
|
||||
}
|
||||
|
||||
std::vector<MapSummary> ListMapsSorted(const TmxWorldIndex& index)
|
||||
{
|
||||
std::vector<MapSummary> summaries;
|
||||
summaries.reserve(index.mapsByName.size());
|
||||
for (const auto& [name, path] : index.mapsByName) {
|
||||
const TmxMap& map = LoadTmxMapCached(path);
|
||||
MapSummary summary;
|
||||
summary.name = name;
|
||||
summary.path = path;
|
||||
summary.widthPixels = map.width * map.tileWidth;
|
||||
summary.heightPixels = map.height * map.tileHeight;
|
||||
for (const TmxObject& object : map.objects) {
|
||||
if (IsSpawnObject(object)) {
|
||||
++summary.spawnCount;
|
||||
} else if (IsWarpObject(object)) {
|
||||
++summary.warpCount;
|
||||
}
|
||||
}
|
||||
summaries.push_back(std::move(summary));
|
||||
}
|
||||
std::sort(summaries.begin(), summaries.end(), [](const MapSummary& a, const MapSummary& b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
return summaries;
|
||||
}
|
||||
|
||||
std::vector<WarpEdge> BuildWarpGraph(const TmxWorldIndex& index)
|
||||
{
|
||||
std::vector<WarpEdge> edges;
|
||||
for (const auto& [name, path] : index.mapsByName) {
|
||||
const TmxMap& map = LoadTmxMapCached(path);
|
||||
for (const TmxObject& object : map.objects) {
|
||||
if (!IsWarpObject(object)) {
|
||||
continue;
|
||||
}
|
||||
const WarpTarget target = ReadWarpTarget(object);
|
||||
if (target.mapName.empty()) {
|
||||
continue;
|
||||
}
|
||||
WarpEdge edge;
|
||||
edge.fromMap = name;
|
||||
edge.toMap = target.mapName;
|
||||
edge.objectName = object.name;
|
||||
edge.tileX = target.tileX;
|
||||
edge.tileY = target.tileY;
|
||||
edge.resolved = WarpTargetResolved(index, object);
|
||||
edges.push_back(std::move(edge));
|
||||
}
|
||||
}
|
||||
std::sort(edges.begin(), edges.end(), [](const WarpEdge& a, const WarpEdge& b) {
|
||||
if (a.fromMap != b.fromMap) {
|
||||
return a.fromMap < b.fromMap;
|
||||
}
|
||||
if (a.toMap != b.toMap) {
|
||||
return a.toMap < b.toMap;
|
||||
}
|
||||
return a.objectName < b.objectName;
|
||||
});
|
||||
return edges;
|
||||
}
|
||||
|
||||
std::vector<WarpEdge> MissingWarpEdges(const TmxWorldIndex& index)
|
||||
{
|
||||
return MissingWarpEdges(BuildWarpGraph(index));
|
||||
}
|
||||
|
||||
std::vector<WarpEdge> MissingWarpEdges(const std::vector<WarpEdge>& graph)
|
||||
{
|
||||
std::vector<WarpEdge> missing;
|
||||
for (const WarpEdge& edge : graph) {
|
||||
if (!edge.resolved) {
|
||||
missing.push_back(edge);
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
std::vector<std::string> ReachableMaps(const TmxWorldIndex& index, const std::vector<WarpEdge>& graph, const std::string& startMapName)
|
||||
{
|
||||
std::map<std::string, std::vector<std::string>> adjacency;
|
||||
for (const WarpEdge& edge : graph) {
|
||||
if (edge.resolved && index.mapsByName.contains(edge.toMap)) {
|
||||
adjacency[edge.fromMap].push_back(edge.toMap);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> reachable;
|
||||
if (!index.mapsByName.contains(startMapName)) {
|
||||
return reachable;
|
||||
}
|
||||
|
||||
std::set<std::string> seen;
|
||||
std::queue<std::string> pending;
|
||||
seen.insert(startMapName);
|
||||
pending.push(startMapName);
|
||||
while (!pending.empty()) {
|
||||
const std::string current = pending.front();
|
||||
pending.pop();
|
||||
reachable.push_back(current);
|
||||
for (const std::string& next : adjacency[current]) {
|
||||
if (seen.insert(next).second) {
|
||||
pending.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(reachable.begin(), reachable.end());
|
||||
return reachable;
|
||||
}
|
||||
|
||||
std::vector<std::string> ReachableMaps(const TmxWorldIndex& index, const std::string& startMapName)
|
||||
{
|
||||
return ReachableMaps(index, BuildWarpGraph(index), startMapName);
|
||||
}
|
||||
|
||||
std::vector<std::string> AdjacentMaps(const TmxWorldIndex& index, const std::vector<WarpEdge>& graph, const std::string& mapName)
|
||||
{
|
||||
std::set<std::string> adjacent;
|
||||
for (const WarpEdge& edge : graph) {
|
||||
if (!edge.resolved) {
|
||||
continue;
|
||||
}
|
||||
if (edge.fromMap == mapName) {
|
||||
if (index.mapsByName.contains(edge.toMap)) {
|
||||
adjacent.insert(edge.toMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {adjacent.begin(), adjacent.end()};
|
||||
}
|
||||
|
||||
std::vector<std::string> AdjacentMaps(const TmxWorldIndex& index, const std::string& mapName, const std::vector<WarpEdge>& graph)
|
||||
{
|
||||
return AdjacentMaps(index, graph, mapName);
|
||||
}
|
||||
|
||||
std::vector<std::string> AdjacentMaps(const TmxWorldIndex& index, const std::string& mapName)
|
||||
{
|
||||
return AdjacentMaps(index, BuildWarpGraph(index), mapName);
|
||||
}
|
||||
|
||||
WorldAudit BuildWorldAudit(const TmxWorldIndex& index)
|
||||
{
|
||||
WorldAudit audit;
|
||||
audit.mapCount = static_cast<int>(index.mapsByName.size());
|
||||
for (const auto& [_, path] : index.mapsByName) {
|
||||
const TmxMap& map = LoadTmxMapCached(path);
|
||||
for (const TmxObject& object : map.objects) {
|
||||
if (IsSpawnObject(object)) {
|
||||
++audit.spawnCount;
|
||||
continue;
|
||||
}
|
||||
if (!IsWarpObject(object)) {
|
||||
continue;
|
||||
}
|
||||
++audit.warpCount;
|
||||
if (!WarpAutoWarps(object)) {
|
||||
++audit.manualWarpCount;
|
||||
}
|
||||
const WarpTarget target = ReadWarpTarget(object);
|
||||
if (target.mapName.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (WarpTargetResolved(index, object)) {
|
||||
++audit.resolvedWarpCount;
|
||||
} else {
|
||||
++audit.missingWarpCount;
|
||||
if (LooksLikeIndoorMapName(target.mapName)) {
|
||||
++audit.missingIndoorWarpCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return audit;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "TmxMap.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct TmxWorldIndex {
|
||||
std::map<std::string, std::filesystem::path> mapsByName;
|
||||
};
|
||||
|
||||
struct MapSummary {
|
||||
std::string name;
|
||||
std::filesystem::path path;
|
||||
int widthPixels = 0;
|
||||
int heightPixels = 0;
|
||||
int spawnCount = 0;
|
||||
int warpCount = 0;
|
||||
};
|
||||
|
||||
struct WarpEdge {
|
||||
std::string fromMap;
|
||||
std::string toMap;
|
||||
std::string objectName;
|
||||
int tileX = 0;
|
||||
int tileY = 0;
|
||||
bool resolved = false;
|
||||
};
|
||||
|
||||
struct WarpTarget {
|
||||
std::string mapName;
|
||||
std::string originalMapName;
|
||||
int tileX = 0;
|
||||
int tileY = 0;
|
||||
bool aliased = false;
|
||||
};
|
||||
|
||||
struct TmxObjectBounds {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
};
|
||||
|
||||
struct TmxObjectPosition {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
};
|
||||
|
||||
struct WorldAudit {
|
||||
int mapCount = 0;
|
||||
int spawnCount = 0;
|
||||
int warpCount = 0;
|
||||
int resolvedWarpCount = 0;
|
||||
int missingWarpCount = 0;
|
||||
int manualWarpCount = 0;
|
||||
int missingIndoorWarpCount = 0;
|
||||
};
|
||||
|
||||
TmxWorldIndex BuildTmxWorldIndex(const std::filesystem::path& mapsRoot);
|
||||
TmxMap LoadNamedMap(const TmxWorldIndex& index, const std::string& mapName);
|
||||
bool IsSpawnObject(const TmxObject& object);
|
||||
bool IsWarpObject(const TmxObject& object);
|
||||
bool IsCollectibleSpawnObject(const TmxObject& object);
|
||||
std::string SpawnDisplayName(const TmxObject& object);
|
||||
std::string SpawnStateName(const TmxObject& object);
|
||||
TmxObjectPosition SpawnObjectPosition(const TmxObject& object);
|
||||
bool WarpAutoWarps(const TmxObject& warp);
|
||||
float TriggerRadiusForObject(const TmxObject& object, float fallback);
|
||||
TmxObjectBounds WarpTriggerBounds(const TmxObject& warp);
|
||||
WarpTarget ReadWarpTarget(const TmxObject& warp);
|
||||
bool WarpHasExplicitTileDestination(const TmxObject& warp);
|
||||
std::string IndoorAliasMapName();
|
||||
bool WarpTargetResolved(const TmxWorldIndex& index, const TmxObject& warp);
|
||||
bool PointLooksLikeTonoriBuildingEntrance(const TmxMap& map, float x, float y);
|
||||
std::string MapDisplayName(const TmxMap& map);
|
||||
std::vector<MapSummary> ListMapsSorted(const TmxWorldIndex& index);
|
||||
std::vector<WarpEdge> BuildWarpGraph(const TmxWorldIndex& index);
|
||||
std::vector<WarpEdge> MissingWarpEdges(const std::vector<WarpEdge>& graph);
|
||||
std::vector<WarpEdge> MissingWarpEdges(const TmxWorldIndex& index);
|
||||
std::vector<std::string> ReachableMaps(const TmxWorldIndex& index, const std::vector<WarpEdge>& graph, const std::string& startMapName);
|
||||
std::vector<std::string> ReachableMaps(const TmxWorldIndex& index, const std::string& startMapName);
|
||||
std::vector<std::string> AdjacentMaps(const TmxWorldIndex& index, const std::vector<WarpEdge>& graph, const std::string& mapName);
|
||||
std::vector<std::string> AdjacentMaps(const TmxWorldIndex& index, const std::string& mapName, const std::vector<WarpEdge>& graph);
|
||||
std::vector<std::string> AdjacentMaps(const TmxWorldIndex& index, const std::string& mapName);
|
||||
WorldAudit BuildWorldAudit(const TmxWorldIndex& index);
|
||||
@@ -0,0 +1,187 @@
|
||||
#include "TmxWorldLayout.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ReadTextFile(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream in(path);
|
||||
if (!in) {
|
||||
return {};
|
||||
}
|
||||
std::ostringstream buffer;
|
||||
buffer << in.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::string JsonStringField(const std::string& block, const std::string& key)
|
||||
{
|
||||
const std::regex field("\\\"" + key + "\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"");
|
||||
std::smatch match;
|
||||
return std::regex_search(block, match, field) ? match[1].str() : "";
|
||||
}
|
||||
|
||||
int JsonIntField(const std::string& block, const std::string& key)
|
||||
{
|
||||
const std::regex field("\\\"" + key + "\\\"\\s*:\\s*(-?\\d+)");
|
||||
std::smatch match;
|
||||
return std::regex_search(block, match, field) ? std::stoi(match[1].str()) : 0;
|
||||
}
|
||||
|
||||
std::string MapNameForPath(const TmxWorldIndex& index, const std::filesystem::path& path)
|
||||
{
|
||||
const std::filesystem::path canonical = std::filesystem::weakly_canonical(path);
|
||||
for (const auto& [name, indexedPath] : index.mapsByName) {
|
||||
if (std::filesystem::weakly_canonical(indexedPath) == canonical) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ContainsPoint(const TmxWorldPlacement& placement, float worldX, float worldY)
|
||||
{
|
||||
return worldX >= static_cast<float>(placement.x)
|
||||
&& worldY >= static_cast<float>(placement.y)
|
||||
&& worldX < static_cast<float>(placement.x + placement.width)
|
||||
&& worldY < static_cast<float>(placement.y + placement.height);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TmxWorldLayout LoadTmxWorldLayout(const std::filesystem::path& worldFile, const TmxWorldIndex& index)
|
||||
{
|
||||
TmxWorldLayout layout;
|
||||
const std::string text = ReadTextFile(worldFile);
|
||||
if (text.empty()) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const std::regex mapBlockRegex(R"(\{[^{}]*"fileName"[^{}]*\})");
|
||||
for (auto it = std::sregex_iterator(text.begin(), text.end(), mapBlockRegex); it != std::sregex_iterator(); ++it) {
|
||||
const std::string block = it->str();
|
||||
const std::string fileName = JsonStringField(block, "fileName");
|
||||
if (fileName.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::filesystem::path mapPath = worldFile.parent_path() / fileName;
|
||||
const std::string mapName = MapNameForPath(index, mapPath);
|
||||
if (mapName.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TmxWorldPlacement placement;
|
||||
placement.mapName = mapName;
|
||||
placement.path = std::filesystem::weakly_canonical(mapPath);
|
||||
placement.x = JsonIntField(block, "x");
|
||||
placement.y = JsonIntField(block, "y");
|
||||
placement.width = JsonIntField(block, "width");
|
||||
placement.height = JsonIntField(block, "height");
|
||||
layout.placements.push_back(std::move(placement));
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
TmxWorldLayout BuildCompleteWorldLayout(const std::filesystem::path& worldFile, const TmxWorldIndex& index)
|
||||
{
|
||||
TmxWorldLayout layout = LoadTmxWorldLayout(worldFile, index);
|
||||
|
||||
std::vector<MapSummary> unplaced;
|
||||
int maxWidth = 0;
|
||||
int maxHeight = 0;
|
||||
for (const MapSummary& summary : ListMapsSorted(index)) {
|
||||
if (FindWorldPlacement(layout, summary.name) != nullptr) {
|
||||
continue;
|
||||
}
|
||||
unplaced.push_back(summary);
|
||||
maxWidth = std::max(maxWidth, summary.widthPixels);
|
||||
maxHeight = std::max(maxHeight, summary.heightPixels);
|
||||
}
|
||||
if (unplaced.empty()) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
constexpr int gap = 512;
|
||||
constexpr int columns = 4;
|
||||
const TmxWorldBounds bounds = WorldLayoutBounds(layout);
|
||||
const int shelfX = bounds.x + bounds.width + gap;
|
||||
const int shelfY = bounds.y;
|
||||
const int cellWidth = std::max(1, maxWidth) + gap;
|
||||
const int cellHeight = std::max(1, maxHeight) + gap;
|
||||
|
||||
for (std::size_t i = 0; i < unplaced.size(); ++i) {
|
||||
const MapSummary& summary = unplaced[i];
|
||||
TmxWorldPlacement placement;
|
||||
placement.mapName = summary.name;
|
||||
placement.path = summary.path;
|
||||
placement.x = shelfX + static_cast<int>(i % columns) * cellWidth;
|
||||
placement.y = shelfY + static_cast<int>(i / columns) * cellHeight;
|
||||
placement.width = summary.widthPixels;
|
||||
placement.height = summary.heightPixels;
|
||||
layout.placements.push_back(std::move(placement));
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
const TmxWorldPlacement* FindWorldPlacement(const TmxWorldLayout& layout, const std::string& mapName)
|
||||
{
|
||||
for (const TmxWorldPlacement& placement : layout.placements) {
|
||||
if (placement.mapName == mapName) {
|
||||
return &placement;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const TmxWorldPlacement* FindWorldPlacementAt(
|
||||
const TmxWorldLayout& layout,
|
||||
float worldX,
|
||||
float worldY,
|
||||
const std::string& excludedMapName)
|
||||
{
|
||||
for (const TmxWorldPlacement& placement : layout.placements) {
|
||||
if (!excludedMapName.empty() && placement.mapName == excludedMapName) {
|
||||
continue;
|
||||
}
|
||||
if (ContainsPoint(placement, worldX, worldY)) {
|
||||
return &placement;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TmxWorldPoint LocalToWorld(const TmxWorldPlacement& placement, float localX, float localY)
|
||||
{
|
||||
return {static_cast<float>(placement.x) + localX, static_cast<float>(placement.y) + localY};
|
||||
}
|
||||
|
||||
TmxWorldPoint WorldToLocal(const TmxWorldPlacement& placement, float worldX, float worldY)
|
||||
{
|
||||
return {worldX - static_cast<float>(placement.x), worldY - static_cast<float>(placement.y)};
|
||||
}
|
||||
|
||||
TmxWorldBounds WorldLayoutBounds(const TmxWorldLayout& layout)
|
||||
{
|
||||
if (layout.placements.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int minX = std::numeric_limits<int>::max();
|
||||
int minY = std::numeric_limits<int>::max();
|
||||
int maxX = std::numeric_limits<int>::min();
|
||||
int maxY = std::numeric_limits<int>::min();
|
||||
for (const TmxWorldPlacement& placement : layout.placements) {
|
||||
minX = std::min(minX, placement.x);
|
||||
minY = std::min(minY, placement.y);
|
||||
maxX = std::max(maxX, placement.x + placement.width);
|
||||
maxY = std::max(maxY, placement.y + placement.height);
|
||||
}
|
||||
return {minX, minY, maxX - minX, maxY - minY};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "TmxWorld.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct TmxWorldPoint {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
};
|
||||
|
||||
struct TmxWorldPlacement {
|
||||
std::string mapName;
|
||||
std::filesystem::path path;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct TmxWorldBounds {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct TmxWorldLayout {
|
||||
std::vector<TmxWorldPlacement> placements;
|
||||
};
|
||||
|
||||
TmxWorldLayout LoadTmxWorldLayout(const std::filesystem::path& worldFile, const TmxWorldIndex& index);
|
||||
TmxWorldLayout BuildCompleteWorldLayout(const std::filesystem::path& worldFile, const TmxWorldIndex& index);
|
||||
const TmxWorldPlacement* FindWorldPlacement(const TmxWorldLayout& layout, const std::string& mapName);
|
||||
const TmxWorldPlacement* FindWorldPlacementAt(
|
||||
const TmxWorldLayout& layout,
|
||||
float worldX,
|
||||
float worldY,
|
||||
const std::string& excludedMapName);
|
||||
TmxWorldPoint LocalToWorld(const TmxWorldPlacement& placement, float localX, float localY);
|
||||
TmxWorldPoint WorldToLocal(const TmxWorldPlacement& placement, float worldX, float worldY);
|
||||
TmxWorldBounds WorldLayoutBounds(const TmxWorldLayout& layout);
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "WarpAuditReport.h"
|
||||
|
||||
#include "TmxMap.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
bool WarpAuditTargetLooksIndoor(std::string_view mapName)
|
||||
{
|
||||
if (mapName == "Indoor") {
|
||||
return true;
|
||||
}
|
||||
return WarpAuditTargetLooksLegacyNumericIndoor(mapName);
|
||||
}
|
||||
|
||||
bool WarpAuditTargetLooksLegacyNumericIndoor(std::string_view mapName)
|
||||
{
|
||||
if (mapName.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasDigit = false;
|
||||
bool hasDash = false;
|
||||
for (const unsigned char ch : mapName) {
|
||||
if (std::isdigit(ch)) {
|
||||
hasDigit = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '-') {
|
||||
hasDash = true;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return hasDigit && hasDash;
|
||||
}
|
||||
|
||||
std::string WarpAuditMissingCategory(std::string_view mapName)
|
||||
{
|
||||
if (WarpAuditTargetLooksLegacyNumericIndoor(mapName)) {
|
||||
return "legacy_numeric_indoor";
|
||||
}
|
||||
if (mapName == "Indoor") {
|
||||
return "indoor_alias";
|
||||
}
|
||||
return "unknown_map";
|
||||
}
|
||||
|
||||
WarpAuditReport BuildWarpAuditReport(const TmxWorldIndex& index)
|
||||
{
|
||||
WarpAuditReport report;
|
||||
|
||||
const std::vector<WarpEdge> graph = BuildWarpGraph(index);
|
||||
report.totalWarpCount = static_cast<int>(graph.size());
|
||||
for (const WarpEdge& edge : graph) {
|
||||
if (edge.resolved) {
|
||||
report.resolvedEdges.push_back(edge);
|
||||
continue;
|
||||
}
|
||||
report.missingEdges.push_back(edge);
|
||||
++report.missingByFromMap[edge.fromMap];
|
||||
++report.missingByToMap[edge.toMap];
|
||||
++report.missingByCategory[WarpAuditMissingCategory(edge.toMap)];
|
||||
if (WarpAuditTargetLooksIndoor(edge.toMap)) {
|
||||
++report.missingIndoorWarpCount;
|
||||
}
|
||||
if (WarpAuditTargetLooksLegacyNumericIndoor(edge.toMap)) {
|
||||
++report.missingLegacyNumericIndoorWarpCount;
|
||||
}
|
||||
}
|
||||
|
||||
report.resolvedWarpCount = static_cast<int>(report.resolvedEdges.size());
|
||||
report.missingWarpCount = static_cast<int>(report.missingEdges.size());
|
||||
|
||||
for (const auto& [_, path] : index.mapsByName) {
|
||||
const TmxMap map = LoadTmxMap(path);
|
||||
report.manualWarpCount += static_cast<int>(std::count_if(map.objects.begin(), map.objects.end(), [](const TmxObject& object) {
|
||||
return IsWarpObject(object) && !WarpAutoWarps(object);
|
||||
}));
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "TmxWorld.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
struct WarpAuditReport {
|
||||
int totalWarpCount = 0;
|
||||
int resolvedWarpCount = 0;
|
||||
int missingWarpCount = 0;
|
||||
int manualWarpCount = 0;
|
||||
int missingIndoorWarpCount = 0;
|
||||
int missingLegacyNumericIndoorWarpCount = 0;
|
||||
std::vector<WarpEdge> resolvedEdges;
|
||||
std::vector<WarpEdge> missingEdges;
|
||||
std::map<std::string, int> missingByFromMap;
|
||||
std::map<std::string, int> missingByToMap;
|
||||
std::map<std::string, int> missingByCategory;
|
||||
};
|
||||
|
||||
bool WarpAuditTargetLooksIndoor(std::string_view mapName);
|
||||
bool WarpAuditTargetLooksLegacyNumericIndoor(std::string_view mapName);
|
||||
std::string WarpAuditMissingCategory(std::string_view mapName);
|
||||
WarpAuditReport BuildWarpAuditReport(const TmxWorldIndex& index);
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "WarpInteraction.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool LooksLikeLegacyIndoorId(const std::string& mapName)
|
||||
{
|
||||
if (mapName.empty() || mapName == "Indoor") {
|
||||
return false;
|
||||
}
|
||||
bool hasDigit = false;
|
||||
bool hasDash = false;
|
||||
for (const unsigned char ch : mapName) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
hasDigit = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '-') {
|
||||
hasDash = true;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return hasDigit && hasDash;
|
||||
}
|
||||
|
||||
bool LooksLikeUnavailableIndoorTarget(const std::string& mapName)
|
||||
{
|
||||
return mapName == "Indoor" || LooksLikeLegacyIndoorId(mapName);
|
||||
}
|
||||
|
||||
std::string WarpFieldLabel(const TmxObject& warp, const WarpTarget& target)
|
||||
{
|
||||
(void)warp;
|
||||
if (LooksLikeUnavailableIndoorTarget(target.mapName)) {
|
||||
return "室内入口";
|
||||
}
|
||||
return "未知入口";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WarpInteraction BuildWarpInteraction(const TmxWorldIndex& index, const TmxObject& warp)
|
||||
{
|
||||
WarpInteraction interaction;
|
||||
interaction.target = ReadWarpTarget(warp);
|
||||
interaction.autoWarp = WarpAutoWarps(warp);
|
||||
interaction.resolved = WarpTargetResolved(index, warp);
|
||||
interaction.fieldLabel = WarpFieldLabel(warp, interaction.target);
|
||||
|
||||
if (interaction.target.mapName.empty()) {
|
||||
interaction.prompt = "传送目标缺失";
|
||||
} else if (!interaction.resolved) {
|
||||
interaction.prompt = LooksLikeLegacyIndoorId(interaction.target.mapName)
|
||||
? "旧室内入口暂不可用"
|
||||
: "室内入口暂不可用";
|
||||
} else if (interaction.autoWarp) {
|
||||
interaction.fieldLabel = "入口";
|
||||
interaction.prompt = "进入入口";
|
||||
} else {
|
||||
interaction.fieldLabel = "入口";
|
||||
interaction.prompt = "进入入口";
|
||||
}
|
||||
return interaction;
|
||||
}
|
||||
|
||||
bool CanOpenWarpConfirmation(const WarpInteraction& interaction)
|
||||
{
|
||||
return !interaction.target.mapName.empty() && interaction.resolved;
|
||||
}
|
||||
|
||||
bool WarpNameLooksLikeBuildingEntrance(const std::string& name)
|
||||
{
|
||||
return name.find("House") != std::string::npos
|
||||
|| name.find("Building") != std::string::npos
|
||||
|| name.find("Barracks") != std::string::npos
|
||||
|| name.find("Tower") != std::string::npos
|
||||
|| name.find("Inn") != std::string::npos
|
||||
|| name.find("Shop") != std::string::npos
|
||||
|| name.find("Store") != std::string::npos;
|
||||
}
|
||||
|
||||
bool WarpShouldUseIndoorAlias(const TmxWorldIndex& index, const WarpInteraction& interaction)
|
||||
{
|
||||
return interaction.target.originalMapName == "Indoor"
|
||||
&& index.mapsByName.contains(IndoorAliasMapName());
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "TmxWorld.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
struct WarpInteraction {
|
||||
WarpTarget target;
|
||||
bool autoWarp = true;
|
||||
bool resolved = false;
|
||||
std::string fieldLabel;
|
||||
std::string prompt;
|
||||
};
|
||||
|
||||
WarpInteraction BuildWarpInteraction(const TmxWorldIndex& index, const TmxObject& warp);
|
||||
bool CanOpenWarpConfirmation(const WarpInteraction& interaction);
|
||||
bool WarpNameLooksLikeBuildingEntrance(const std::string& name);
|
||||
bool WarpShouldUseIndoorAlias(const TmxWorldIndex& index, const WarpInteraction& interaction);
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "WildLevelZone.h"
|
||||
|
||||
#include "PetGrowth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
WildSpeciesSpawnRule Rule(std::string species, int weight, WildLevelBand band)
|
||||
{
|
||||
return {std::move(species), std::max(1, weight), NormalizeWildLevelBand(band)};
|
||||
}
|
||||
|
||||
const MapWildZone& DefaultZone()
|
||||
{
|
||||
static const MapWildZone zone = {
|
||||
"",
|
||||
{3, 15},
|
||||
20,
|
||||
3,
|
||||
{
|
||||
Rule("Piou", 18, {3, 12}),
|
||||
Rule("Bird", 18, {3, 12}),
|
||||
Rule("Fluffy", 16, {3, 12}),
|
||||
Rule("Pinkie", 12, {3, 12}),
|
||||
Rule("Maggot", 14, {4, 14}),
|
||||
Rule("Snake", 12, {6, 15}),
|
||||
Rule("Lizzy", 10, {8, 15}),
|
||||
Rule("Peyote", 8, {6, 15}),
|
||||
Rule("Spiky Mushroom", 6, {8, 15}),
|
||||
Rule("Ratto", 4, {3, 10}),
|
||||
},
|
||||
};
|
||||
return zone;
|
||||
}
|
||||
|
||||
const std::vector<MapWildZone>& ExactZones()
|
||||
{
|
||||
static const std::vector<MapWildZone> zones = {
|
||||
{"Overworld", {3, 18}, 22, 0, {Rule("Bird", 18, {3, 15}), Rule("Piou", 16, {3, 12}), Rule("Fluffy", 14, {3, 12}), Rule("Pinkie", 10, {3, 12}), Rule("Maggot", 12, {4, 14}), Rule("Snake", 12, {6, 18}), Rule("Lizzy", 10, {8, 18}), Rule("Peyote", 8, {6, 16}), Rule("Spiky Mushroom", 6, {8, 18}), Rule("Lulea", 4, {8, 18})}},
|
||||
{"Tulimshar", {1, 8}, 0, 3, {Rule("Maggot", 62, {1, 8}), Rule("Peyote", 38, {2, 8})}},
|
||||
{"Tulimshar Center", {1, 4}, 0, 3, {Rule("Piou", 25, {1, 3}), Rule("Fluffy", 30, {2, 4}), Rule("Ratto", 30, {2, 4}), Rule("Pinkie", 15, {2, 4})}},
|
||||
{"Ship First Deck", {1, 4}, 0, 3, {Rule("Ratto", 70, {1, 4}), Rule("Piou", 30, {1, 3})}},
|
||||
{"Ship Nard's Room", {1, 4}, 0, 2, {Rule("Ratto", 100, {1, 4})}},
|
||||
{"Manayir Beach", {10, 32}, 22, 0, {Rule("Lulea", 20, {10, 24}), Rule("Turtle", 18, {15, 30}), Rule("Salt Slime", 16, {14, 30}), Rule("Slime", 15, {10, 26}), Rule("Croc", 10, {22, 32}), Rule("Bird", 8, {10, 24}), Rule("Piou", 6, {8, 18}), Rule("Spiky Mushroom", 4, {12, 26}), Rule("Peyote", 3, {10, 22})}},
|
||||
{"Tulimshar Beach", {8, 30}, 22, 0, {Rule("Lulea", 18, {8, 22}), Rule("Turtle", 16, {12, 28}), Rule("Salt Slime", 15, {12, 28}), Rule("Slime", 14, {8, 24}), Rule("Bird", 12, {6, 20}), Rule("Piou", 10, {5, 16}), Rule("Fluffy", 8, {5, 14}), Rule("Maggot", 6, {5, 14}), Rule("Peyote", 6, {6, 18}), Rule("Spiky Mushroom", 5, {8, 20}), Rule("Croc", 4, {20, 30})}},
|
||||
{"Tulimshar Western Cave", {18, 40}, 26, 0, {Rule("Bat", 24, {18, 36}), Rule("Skeleton", 18, {22, 40}), Rule("Sludge Slime", 16, {20, 38}), Rule("Drain Slime", 14, {22, 40}), Rule("Scorpion", 12, {18, 34}), Rule("Giant Maggot", 10, {24, 40}), Rule("Evil Mushroom", 8, {20, 38}), Rule("Spiky Mushroom", 6, {18, 32})}},
|
||||
{"Sandstorm", {8, 32}, 30, 0, {Rule("Scorpion", 16, {10, 28}), Rule("Maggot", 14, {8, 22}), Rule("Peyote", 12, {8, 26}), Rule("Sand Snake", 12, {10, 30}), Rule("Snake", 9, {8, 24}), Rule("Fire Goblin", 9, {16, 32}), Rule("Lizzy", 8, {8, 24}), Rule("Bird", 7, {6, 22}), Rule("Piou", 6, {5, 18}), Rule("Fluffy", 6, {5, 18}), Rule("Pinkie", 5, {5, 18}), Rule("Spiky Mushroom", 5, {10, 24}), Rule("Evil Mushroom", 4, {14, 28}), Rule("Lynx", 4, {18, 32}), Rule("Giant Maggot", 3, {22, 32})}},
|
||||
{"Desert Mines", {18, 40}, 28, 0, {Rule("Bat", 18, {18, 34}), Rule("Skeleton", 20, {22, 40}), Rule("Scorpion", 16, {18, 36}), Rule("Giant Maggot", 16, {24, 40}), Rule("Sludge Slime", 12, {22, 40}), Rule("Fire Goblin", 10, {22, 40}), Rule("Drain Slime", 5, {24, 40}), Rule("Evil Mushroom", 3, {20, 36})}},
|
||||
{"Desert Abandoned Level", {25, 55}, 30, 0, {Rule("Fire Skull", 18, {35, 55}), Rule("Skeleton", 20, {25, 55}), Rule("Giant Maggot", 16, {25, 50}), Rule("Fire Goblin", 12, {28, 52}), Rule("Lynx", 10, {35, 55}), Rule("Croc", 8, {35, 55}), Rule("Sludge Slime", 12, {28, 50}), Rule("Drain Slime", 4, {32, 52})}},
|
||||
{"Desert Deep Level", {45, 75}, 30, 0, {Rule("Splatyna", 10, {55, 75}), Rule("Lizandra", 14, {55, 75}), Rule("Fire Skull", 22, {45, 75}), Rule("Skeleton", 22, {45, 75}), Rule("Lynx", 18, {50, 75}), Rule("Fire Goblin", 8, {48, 72}), Rule("Croc", 6, {50, 72})}},
|
||||
{"Splatyna Cave Entrance", {45, 75}, 22, 0, {Rule("Splatyna", 20, {55, 80}), Rule("Drain Slime", 25, {45, 70}), Rule("Sludge Slime", 25, {45, 70}), Rule("Skeleton", 20, {45, 75}), Rule("Lizandra", 10, {60, 80})}},
|
||||
{"Splatyna's Chamber", {75, 100}, 16, 0, {Rule("Splatyna", 35, {75, 100}), Rule("Lizandra", 25, {75, 100}), Rule("Fire Goblin", 20, {75, 100}), Rule("Fire Skull", 20, {75, 100})}},
|
||||
};
|
||||
return zones;
|
||||
}
|
||||
|
||||
bool ContainsCaseInsensitive(const std::string& text, const std::string& needle)
|
||||
{
|
||||
const auto lower = [](std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
};
|
||||
return lower(text).find(lower(needle)) != std::string::npos;
|
||||
}
|
||||
|
||||
MapWildZone PatternZone(const std::string& mapName)
|
||||
{
|
||||
if (ContainsCaseInsensitive(mapName, "Cave") || ContainsCaseInsensitive(mapName, "Mine") || ContainsCaseInsensitive(mapName, "Sewer")) {
|
||||
return {"", {18, 40}, 26, 0, {Rule("Bat", 22, {18, 36}), Rule("Skeleton", 18, {22, 40}), Rule("Scorpion", 16, {18, 36}), Rule("Sludge Slime", 16, {20, 40}), Rule("Giant Maggot", 14, {24, 42}), Rule("Drain Slime", 8, {22, 40}), Rule("Fire Goblin", 4, {24, 42}), Rule("Evil Mushroom", 4, {20, 38})}};
|
||||
}
|
||||
if (ContainsCaseInsensitive(mapName, "Beach") || ContainsCaseInsensitive(mapName, "Ocean") || ContainsCaseInsensitive(mapName, "Bay")) {
|
||||
return {"", {8, 32}, 22, 0, {Rule("Lulea", 18, {8, 24}), Rule("Turtle", 16, {12, 30}), Rule("Salt Slime", 15, {12, 30}), Rule("Slime", 14, {8, 26}), Rule("Croc", 8, {22, 32}), Rule("Bird", 8, {6, 22}), Rule("Piou", 7, {5, 18}), Rule("Peyote", 5, {6, 20}), Rule("Spiky Mushroom", 4, {8, 22}), Rule("Maggot", 4, {5, 16})}};
|
||||
}
|
||||
if (ContainsCaseInsensitive(mapName, "Tulimshar") && !ContainsCaseInsensitive(mapName, "Hills")) {
|
||||
return {"", {1, 8}, 0, 3, {Rule("Maggot", 62, {1, 8}), Rule("Peyote", 38, {2, 8})}};
|
||||
}
|
||||
if (ContainsCaseInsensitive(mapName, "Desert") || ContainsCaseInsensitive(mapName, "Hills") || ContainsCaseInsensitive(mapName, "Sandstorm")) {
|
||||
return {"", {8, 32}, 30, 0, {Rule("Scorpion", 16, {10, 28}), Rule("Sand Snake", 14, {10, 30}), Rule("Maggot", 12, {8, 22}), Rule("Peyote", 12, {8, 26}), Rule("Snake", 9, {8, 24}), Rule("Lizzy", 8, {8, 24}), Rule("Bird", 7, {6, 22}), Rule("Piou", 6, {5, 18}), Rule("Fluffy", 6, {5, 18}), Rule("Pinkie", 5, {5, 18}), Rule("Spiky Mushroom", 5, {10, 24}), Rule("Evil Mushroom", 4, {14, 28}), Rule("Lulea", 4, {10, 26}), Rule("Turtle", 4, {14, 30}), Rule("Salt Slime", 3, {12, 28}), Rule("Fire Goblin", 3, {18, 32}), Rule("Lynx", 3, {18, 32}), Rule("Giant Maggot", 2, {22, 32})}};
|
||||
}
|
||||
return DefaultZone();
|
||||
}
|
||||
|
||||
std::string Trim(std::string value)
|
||||
{
|
||||
const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); });
|
||||
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch); }).base();
|
||||
if (first >= last) {
|
||||
return {};
|
||||
}
|
||||
return {first, last};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WildLevelBand NormalizeWildLevelBand(WildLevelBand band)
|
||||
{
|
||||
band.minLevel = ClampPetLevel(band.minLevel);
|
||||
band.maxLevel = ClampPetLevel(band.maxLevel);
|
||||
if (band.minLevel > band.maxLevel) {
|
||||
std::swap(band.minLevel, band.maxLevel);
|
||||
}
|
||||
return band;
|
||||
}
|
||||
|
||||
const MapWildZone& WildZoneForMap(const std::string& mapName)
|
||||
{
|
||||
for (const MapWildZone& zone : ExactZones()) {
|
||||
if (zone.mapName == mapName) {
|
||||
return zone;
|
||||
}
|
||||
}
|
||||
static std::map<std::string, MapWildZone> cache;
|
||||
auto [it, _] = cache.try_emplace(mapName, PatternZone(mapName));
|
||||
it->second.mapName = mapName;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
int RandomLevelInBand(WildLevelBand band, std::mt19937& rng)
|
||||
{
|
||||
band = NormalizeWildLevelBand(band);
|
||||
std::uniform_int_distribution<int> dist(band.minLevel, band.maxLevel);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
std::string ChooseWildSpeciesFromRules(const std::vector<WildSpeciesSpawnRule>& rules, std::mt19937& rng)
|
||||
{
|
||||
if (rules.empty()) {
|
||||
return "Piou";
|
||||
}
|
||||
int totalWeight = 0;
|
||||
for (const WildSpeciesSpawnRule& rule : rules) {
|
||||
totalWeight += std::max(1, rule.weight);
|
||||
}
|
||||
std::uniform_int_distribution<int> dist(1, std::max(1, totalWeight));
|
||||
int roll = dist(rng);
|
||||
for (const WildSpeciesSpawnRule& rule : rules) {
|
||||
roll -= std::max(1, rule.weight);
|
||||
if (roll <= 0) {
|
||||
return rule.speciesName;
|
||||
}
|
||||
}
|
||||
return rules.back().speciesName;
|
||||
}
|
||||
|
||||
std::string ChooseWildSpecies(const MapWildZone& zone, std::mt19937& rng)
|
||||
{
|
||||
return ChooseWildSpeciesFromRules(zone.species, rng);
|
||||
}
|
||||
|
||||
WildLevelBand LevelBandForWildSpecies(const MapWildZone& zone, const std::string& speciesName)
|
||||
{
|
||||
for (const WildSpeciesSpawnRule& rule : zone.species) {
|
||||
if (rule.speciesName == speciesName) {
|
||||
return NormalizeWildLevelBand(rule.levelBand);
|
||||
}
|
||||
}
|
||||
return NormalizeWildLevelBand(zone.defaultBand);
|
||||
}
|
||||
|
||||
std::vector<WildSpeciesSpawnRule> ParseSpeciesPool(const std::string& text, WildLevelBand fallbackBand)
|
||||
{
|
||||
std::vector<WildSpeciesSpawnRule> rules;
|
||||
std::stringstream stream(text);
|
||||
std::string part;
|
||||
while (std::getline(stream, part, ',')) {
|
||||
part = Trim(part);
|
||||
if (part.empty()) {
|
||||
continue;
|
||||
}
|
||||
int weight = 1;
|
||||
const std::size_t marker = part.rfind(':');
|
||||
if (marker != std::string::npos && marker + 1 < part.size()) {
|
||||
weight = std::max(1, std::stoi(part.substr(marker + 1)));
|
||||
part = Trim(part.substr(0, marker));
|
||||
}
|
||||
if (!part.empty()) {
|
||||
rules.push_back(Rule(part, weight, fallbackBand));
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
int FallbackCountForWildZone(const MapWildZone& zone)
|
||||
{
|
||||
if (zone.targetOutdoorCount > 0) {
|
||||
return zone.targetOutdoorCount;
|
||||
}
|
||||
return zone.targetCityCount;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct WildLevelBand {
|
||||
int minLevel = 1;
|
||||
int maxLevel = 1;
|
||||
};
|
||||
|
||||
struct WildSpeciesSpawnRule {
|
||||
std::string speciesName;
|
||||
int weight = 1;
|
||||
WildLevelBand levelBand;
|
||||
};
|
||||
|
||||
struct MapWildZone {
|
||||
std::string mapName;
|
||||
WildLevelBand defaultBand;
|
||||
int targetOutdoorCount = 0;
|
||||
int targetCityCount = 0;
|
||||
std::vector<WildSpeciesSpawnRule> species;
|
||||
};
|
||||
|
||||
WildLevelBand NormalizeWildLevelBand(WildLevelBand band);
|
||||
const MapWildZone& WildZoneForMap(const std::string& mapName);
|
||||
int RandomLevelInBand(WildLevelBand band, std::mt19937& rng);
|
||||
std::string ChooseWildSpecies(const MapWildZone& zone, std::mt19937& rng);
|
||||
WildLevelBand LevelBandForWildSpecies(const MapWildZone& zone, const std::string& speciesName);
|
||||
std::vector<WildSpeciesSpawnRule> ParseSpeciesPool(const std::string& text, WildLevelBand fallbackBand);
|
||||
std::string ChooseWildSpeciesFromRules(const std::vector<WildSpeciesSpawnRule>& rules, std::mt19937& rng);
|
||||
int FallbackCountForWildZone(const MapWildZone& zone);
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "WildSpawn.h"
|
||||
|
||||
#include "WildLevelZone.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kMinimumSpawnExtent = 1.0f;
|
||||
constexpr float kDefaultRespawnDelaySeconds = 30.0f;
|
||||
constexpr int kMaximumSpawnCount = 256;
|
||||
|
||||
void HashText(std::uint32_t& hash, const std::string& text)
|
||||
{
|
||||
for (const unsigned char ch : text) {
|
||||
hash ^= ch;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
hash ^= 0xffu;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
|
||||
bool ContainsText(const std::string& text, const std::string& needle)
|
||||
{
|
||||
return text.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
bool AllowsFallbackZone(const std::string& mapName)
|
||||
{
|
||||
return mapName == "Overworld"
|
||||
|| mapName == "Tulimshar"
|
||||
|| mapName == "Ship First Deck"
|
||||
|| mapName == "Ship Nard's Room"
|
||||
|| mapName == "Splatyna Cave Entrance"
|
||||
|| mapName == "Desert Deep Level"
|
||||
|| mapName == "Candor Arena"
|
||||
|| ContainsText(mapName, "Beach")
|
||||
|| ContainsText(mapName, "Hills")
|
||||
|| ContainsText(mapName, "Cave")
|
||||
|| ContainsText(mapName, "Desert")
|
||||
|| ContainsText(mapName, "Sandstorm")
|
||||
|| ContainsText(mapName, "Sewer")
|
||||
|| ContainsText(mapName, "Ocean");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpawnArea EffectiveSpawnArea(SpawnArea area)
|
||||
{
|
||||
area.width = std::max(kMinimumSpawnExtent, area.width);
|
||||
area.height = std::max(kMinimumSpawnExtent, area.height);
|
||||
return area;
|
||||
}
|
||||
|
||||
SpawnArea ResolveSpawnArea(SpawnArea objectArea, float mapWidthPixels, float mapHeightPixels)
|
||||
{
|
||||
if (objectArea.x < 0.0f || objectArea.y < 0.0f) {
|
||||
return EffectiveSpawnArea({0.0f, 0.0f, mapWidthPixels, mapHeightPixels});
|
||||
}
|
||||
return EffectiveSpawnArea(objectArea);
|
||||
}
|
||||
|
||||
bool SpawnAreaContains(SpawnArea area, SpawnPoint point)
|
||||
{
|
||||
const SpawnArea effective = EffectiveSpawnArea(area);
|
||||
return point.x >= effective.x
|
||||
&& point.x <= effective.x + effective.width
|
||||
&& point.y >= effective.y
|
||||
&& point.y <= effective.y + effective.height;
|
||||
}
|
||||
|
||||
SpawnPoint ClampToSpawnArea(SpawnArea area, SpawnPoint point)
|
||||
{
|
||||
const SpawnArea effective = EffectiveSpawnArea(area);
|
||||
return {
|
||||
std::clamp(point.x, effective.x, effective.x + effective.width),
|
||||
std::clamp(point.y, effective.y, effective.y + effective.height),
|
||||
};
|
||||
}
|
||||
|
||||
SpawnPoint ApplySpawnDrift(SpawnArea area, SpawnPoint position, SpawnPoint drift)
|
||||
{
|
||||
return ClampToSpawnArea(area, {position.x + drift.x, position.y + drift.y});
|
||||
}
|
||||
|
||||
int MonsterSpawnCount(std::optional<int> count, std::optional<int> maxBeings)
|
||||
{
|
||||
if (count.has_value()) {
|
||||
return std::clamp(*count, 1, kMaximumSpawnCount);
|
||||
}
|
||||
if (maxBeings.has_value()) {
|
||||
return std::clamp(*maxBeings, 1, kMaximumSpawnCount);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
float MonsterRespawnDelay(std::optional<float> configuredDelay)
|
||||
{
|
||||
if (configuredDelay.has_value() && *configuredDelay >= 0.0f) {
|
||||
return *configuredDelay >= 1000.0f ? *configuredDelay / 1000.0f : *configuredDelay;
|
||||
}
|
||||
return kDefaultRespawnDelaySeconds;
|
||||
}
|
||||
|
||||
float TickRespawnTimer(float currentTimer, float deltaSeconds)
|
||||
{
|
||||
return std::max(0.0f, currentTimer - std::max(0.0f, deltaSeconds));
|
||||
}
|
||||
|
||||
bool RespawnTimerReady(float currentTimer)
|
||||
{
|
||||
return currentTimer <= 0.0f;
|
||||
}
|
||||
|
||||
double MonsterRespawnUntil(double nowSeconds, float respawnDelaySeconds)
|
||||
{
|
||||
return nowSeconds + static_cast<double>(std::max(0.0f, respawnDelaySeconds));
|
||||
}
|
||||
|
||||
float RemainingRespawnTime(double nowSeconds, double respawnUntilSeconds)
|
||||
{
|
||||
return static_cast<float>(std::max(0.0, respawnUntilSeconds - nowSeconds));
|
||||
}
|
||||
|
||||
bool RespawnDue(double nowSeconds, double respawnUntilSeconds)
|
||||
{
|
||||
return nowSeconds >= respawnUntilSeconds;
|
||||
}
|
||||
|
||||
std::string FallbackMonsterNameForMap(const std::string& mapName)
|
||||
{
|
||||
if (!AllowsFallbackZone(mapName)) {
|
||||
return {};
|
||||
}
|
||||
const MapWildZone& zone = WildZoneForMap(mapName);
|
||||
if (!zone.species.empty()) {
|
||||
return zone.species.front().speciesName;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int FallbackMonsterSpawnCount(const std::string& mapName)
|
||||
{
|
||||
if (!AllowsFallbackZone(mapName)) {
|
||||
return 0;
|
||||
}
|
||||
return FallbackCountForWildZone(WildZoneForMap(mapName));
|
||||
}
|
||||
|
||||
bool ShouldAddFallbackMonsters(const std::string& mapName, bool sawMonsterSpawn)
|
||||
{
|
||||
(void)sawMonsterSpawn;
|
||||
return FallbackMonsterSpawnCount(mapName) > 0;
|
||||
}
|
||||
|
||||
unsigned int StableSpawnSeed(const std::string& mapName, const std::string& objectName, int objectId)
|
||||
{
|
||||
std::uint32_t hash = 2166136261u;
|
||||
HashText(hash, mapName);
|
||||
HashText(hash, objectName);
|
||||
hash ^= static_cast<std::uint32_t>(objectId);
|
||||
hash *= 16777619u;
|
||||
return hash == 0 ? 1u : hash;
|
||||
}
|
||||
|
||||
SpawnPoint RandomPointInSpawnArea(SpawnArea area, std::mt19937& rng)
|
||||
{
|
||||
if (area.width <= 0.0f && area.height <= 0.0f) {
|
||||
return {area.x, area.y};
|
||||
}
|
||||
const SpawnArea effective = EffectiveSpawnArea(area);
|
||||
std::uniform_real_distribution<float> xDist(effective.x, effective.x + effective.width);
|
||||
std::uniform_real_distribution<float> yDist(effective.y, effective.y + effective.height);
|
||||
return {xDist(rng), yDist(rng)};
|
||||
}
|
||||
|
||||
std::optional<SpawnPoint> FindWalkableSpawnPoint(
|
||||
SpawnArea area,
|
||||
std::mt19937& rng,
|
||||
const std::function<bool(SpawnPoint)>& isWalkable,
|
||||
int randomAttempts,
|
||||
float fallbackStep)
|
||||
{
|
||||
for (int attempt = 0; attempt < randomAttempts; ++attempt) {
|
||||
const SpawnPoint point = RandomPointInSpawnArea(area, rng);
|
||||
if (isWalkable(point)) {
|
||||
return point;
|
||||
}
|
||||
}
|
||||
|
||||
const SpawnArea effective = EffectiveSpawnArea(area);
|
||||
const SpawnPoint center{
|
||||
effective.x + effective.width * 0.5f,
|
||||
effective.y + effective.height * 0.5f,
|
||||
};
|
||||
if (isWalkable(center)) {
|
||||
return center;
|
||||
}
|
||||
|
||||
const float step = std::max(kMinimumSpawnExtent, fallbackStep);
|
||||
const float maxRadius = std::max(effective.width, effective.height);
|
||||
for (float radius = step; radius <= maxRadius + step * 0.5f; radius += step) {
|
||||
for (float dx = -radius; dx <= radius; dx += step) {
|
||||
for (float dy = -radius; dy <= radius; dy += step) {
|
||||
if (std::abs(dx) < radius && std::abs(dy) < radius) {
|
||||
continue;
|
||||
}
|
||||
const SpawnPoint candidate = ClampToSpawnArea(effective, {center.x + dx, center.y + dy});
|
||||
if (isWalkable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
struct SpawnPoint {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
};
|
||||
|
||||
struct SpawnArea {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
};
|
||||
|
||||
SpawnArea EffectiveSpawnArea(SpawnArea area);
|
||||
SpawnArea ResolveSpawnArea(SpawnArea objectArea, float mapWidthPixels, float mapHeightPixels);
|
||||
bool SpawnAreaContains(SpawnArea area, SpawnPoint point);
|
||||
SpawnPoint ClampToSpawnArea(SpawnArea area, SpawnPoint point);
|
||||
SpawnPoint ApplySpawnDrift(SpawnArea area, SpawnPoint position, SpawnPoint drift);
|
||||
int MonsterSpawnCount(std::optional<int> count, std::optional<int> maxBeings);
|
||||
float MonsterRespawnDelay(std::optional<float> configuredDelay);
|
||||
float TickRespawnTimer(float currentTimer, float deltaSeconds);
|
||||
bool RespawnTimerReady(float currentTimer);
|
||||
double MonsterRespawnUntil(double nowSeconds, float respawnDelaySeconds);
|
||||
float RemainingRespawnTime(double nowSeconds, double respawnUntilSeconds);
|
||||
bool RespawnDue(double nowSeconds, double respawnUntilSeconds);
|
||||
std::string FallbackMonsterNameForMap(const std::string& mapName);
|
||||
int FallbackMonsterSpawnCount(const std::string& mapName);
|
||||
bool ShouldAddFallbackMonsters(const std::string& mapName, bool sawMonsterSpawn);
|
||||
unsigned int StableSpawnSeed(const std::string& mapName, const std::string& objectName, int objectId);
|
||||
SpawnPoint RandomPointInSpawnArea(SpawnArea area, std::mt19937& rng);
|
||||
std::optional<SpawnPoint> FindWalkableSpawnPoint(
|
||||
SpawnArea area,
|
||||
std::mt19937& rng,
|
||||
const std::function<bool(SpawnPoint)>& isWalkable,
|
||||
int randomAttempts = 24,
|
||||
float fallbackStep = 32.0f);
|
||||
Reference in New Issue
Block a user