最终整理版

This commit is contained in:
2026-06-03 17:04:06 +08:00
commit 959055ce90
1240 changed files with 80570 additions and 0 deletions
+119
View File
@@ -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;
}
+30
View File
@@ -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);
+473
View File
@@ -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;
}
+97
View File
@@ -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);
+105
View File
@@ -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;
}
+27
View File
@@ -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);
+442
View File
@@ -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;
}
+88
View File
@@ -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);
+187
View File
@@ -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};
}
+44
View File
@@ -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);
+83
View File
@@ -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;
}
+27
View File
@@ -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);
+86
View File
@@ -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());
}
+18
View File
@@ -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);
+196
View File
@@ -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;
}
+34
View File
@@ -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);
+219
View File
@@ -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;
}
+42
View File
@@ -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);