Files
EVEngine/src/modules/map/DualGrid.cpp
T
西风逍遥游andCursor Agent 1dbea1ebe7 feat(map): Tilemap dual-grid autotile resolve (#15)
* feat(map): add dual-grid autotile resolve for TileLayer

Implement Oskar Stålberg-style dual-grid: logic layer paints fill/empty,
display layer gets a half-tile offset 15-tile resolve. Document that Tiled
has no native dual-grid (Terrain/Wang Corner Set is same-grid only).

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* fix(map): use ComponentRef for DualGrid config access

TileLayer::config() returns ecs::ComponentRef, not a raw pointer.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* test(map): fix dual-grid filledGid expectations and clean layers

Correct corner sampling for a single logic cell (BR vs TL) and clear
layers after resolve tests to avoid ECS draw pollution.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* test(map): fix ComponentRef usage in dual-grid orientation tests

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-10 14:03:37 +08:00

152 lines
5.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "map/DualGrid.h"
#include "map/TileOrientation.h"
namespace eve::map {
namespace {
// SpriteCook / common dual-grid 4×4 sheet: index by 4-bit corner mask.
// mask 0 → empty; other values map to atlas local ids 0..15.
constexpr std::array<int, 16> kDefaultFrameByMask = {
-1, 15, 8, 9, 0, 11, 14, 7, 13, 4, 1, 10, 3, 2, 5, 6,
};
int resolveFirstGid(TileLayer *display, int firstDisplayGid) {
if (firstDisplayGid > 0) return firstDisplayGid;
const int ts = display->getTilesetFirstGid();
return ts > 0 ? ts : 1;
}
float staggerPitchY(const TileLayer::Config &cfg) {
const bool hex = cfg.orientation == MapOrientation::Hexagonal && cfg.hexSideLength > 0.f;
return hex ? (cfg.tileH + cfg.hexSideLength) * 0.5f : cfg.tileH * 0.5f;
}
float staggerPitchX(const TileLayer::Config &cfg) {
const bool hex = cfg.orientation == MapOrientation::Hexagonal && cfg.hexSideLength > 0.f;
return hex ? (cfg.tileW + cfg.hexSideLength) * 0.5f : cfg.tileW * 0.5f;
}
} // namespace
const std::array<int, 16> &dualGridDefaultFrameTable() { return kDefaultFrameByMask; }
int dualGridDefaultFrame(int mask) {
if (mask < 0 || mask > 15) return -1;
return kDefaultFrameByMask[size_t(mask)];
}
void dualGridHalfOffset(const TileLayer::Config &cfg, float &offX, float &offY) {
switch (cfg.orientation) {
case MapOrientation::Isometric:
// tileToWorld(tx-0.5, ty-0.5) with same iso formula ⇒ origin (ox, oy - th/2).
offX = 0.f;
offY = -cfg.tileH * 0.5f;
break;
case MapOrientation::Staggered:
case MapOrientation::Hexagonal:
if (cfg.staggerAxis == StaggerAxis::Y) {
offX = -cfg.tileW * 0.5f;
offY = -staggerPitchY(cfg) * 0.5f;
} else {
offX = -staggerPitchX(cfg) * 0.5f;
offY = -cfg.tileH * 0.5f;
}
break;
case MapOrientation::Orthogonal:
default:
offX = -cfg.tileW * 0.5f;
offY = -cfg.tileH * 0.5f;
break;
}
}
bool dualGridLogicFilled(TileLayer &logic, int tx, int ty, int filledGid) {
auto cfg = logic.config();
if (tx < 0 || ty < 0 || tx >= cfg->mapW || ty >= cfg->mapH) return false;
const int gid = int(tileGid(uint32_t(logic.getTile(tx, ty))));
if (gid == 0) return false;
if (filledGid == 0) return true;
return gid == filledGid;
}
int dualGridMaskAt(TileLayer &logic, int dx, int dy, int filledGid) {
const bool tl = dualGridLogicFilled(logic, dx - 1, dy - 1, filledGid);
const bool tr = dualGridLogicFilled(logic, dx, dy - 1, filledGid);
const bool bl = dualGridLogicFilled(logic, dx - 1, dy, filledGid);
const bool br = dualGridLogicFilled(logic, dx, dy, filledGid);
return dualGridMaskFromCorners(tl, tr, bl, br);
}
bool resolveDualGrid(TileLayer *logic, TileLayer *display, const DualGridOptions &opts,
std::string *error) {
if (!logic || !display) {
if (error) *error = "resolveDualGrid: logic and display layers required";
return false;
}
if (logic == display) {
if (error) *error = "resolveDualGrid: logic and display must be different layers";
return false;
}
const int logicW = logic->getMapWidth();
const int logicH = logic->getMapHeight();
if (logicW <= 0 || logicH <= 0) {
if (error) *error = "resolveDualGrid: logic layer has empty size";
return false;
}
const float tileW = logic->getTileWidth();
const float tileH = logic->getTileHeight();
display->setTileSize(tileW, tileH);
display->resize(logicW + 1, logicH + 1);
auto lc = logic->config();
auto dc = display->config();
dc->orientation = lc->orientation;
dc->staggerAxis = lc->staggerAxis;
dc->staggerIndex = lc->staggerIndex;
dc->hexSideLength = lc->hexSideLength;
if (opts.applyHalfOffset) {
float offX = 0.f, offY = 0.f;
dualGridHalfOffset(*lc, offX, offY);
display->setOrigin(logic->getX() + offX, logic->getY() + offY);
} else {
display->setOrigin(logic->getX(), logic->getY());
}
// Keep draw bookkeeping in sync when display is freshly created.
display->setLayer(logic->getLayer());
display->setCamera(logic->draw()->camera);
display->setCanvas(logic->draw()->canvas);
const int firstGid = resolveFirstGid(display, opts.firstDisplayGid);
const int filledGid = opts.filledGid;
for (int dy = 0; dy < logicH + 1; ++dy) {
for (int dx = 0; dx < logicW + 1; ++dx) {
const int mask = dualGridMaskAt(*logic, dx, dy, filledGid);
int frame = -1;
if (opts.useDefaultFrameTable) {
frame = dualGridDefaultFrame(mask);
} else if (mask != 0) {
frame = mask;
}
const int gid = (frame < 0) ? 0 : firstGid + frame;
display->setTile(dx, dy, gid);
}
}
if (opts.hideLogic) logic->setVisible(false);
display->setVisible(true);
if (error) error->clear();
return true;
}
bool resolveDualGrid(TileLayer *logic, TileLayer *display, std::string *error) {
return resolveDualGrid(logic, display, DualGridOptions{}, error);
}
} // namespace eve::map