Add a location picker for selecting a location in a town or outdoor sector.

It's currently used in special node editing and in advanced town details.
This commit is contained in:
2025-02-28 01:26:29 -05:00
committed by Celtic Minstrel
parent 63bbb38374
commit 5861268e8c
24 changed files with 484 additions and 59 deletions

View File

@@ -23,7 +23,7 @@ cStringChoice::cStringChoice(cDialog* parent, bool editable)
, dlg(*ResMgr::dialogs.get(editable ? "choose-edit-string" : "choose-string"), parent)
{}
cStringChoice::cStringChoice(std::vector<std::string>& strs, std::string title, cDialog* parent, bool editable)
cStringChoice::cStringChoice(const std::vector<std::string>& strs, std::string title, cDialog* parent, bool editable)
: cStringChoice(parent, editable)
{
setTitle(title);

View File

@@ -41,7 +41,7 @@ public:
/// @param strs A list of all strings in the dialog.
/// @param title The title to show in the dialog.
/// @param parent Optionally, a parent dialog.
explicit cStringChoice(std::vector<std::string>& strs, std::string title, cDialog* parent = nullptr, bool editable = false);
explicit cStringChoice(const std::vector<std::string>& strs, std::string title, cDialog* parent = nullptr, bool editable = false);
/// Initializes a dialog from an iterator pair.
/// @param begin An iterator to the first string in the dialog.
/// @param end An iterator to one past the last string in the dialog.

View File

@@ -16,8 +16,8 @@ typedef std::map<std::string,cControl*>::iterator ctrlIter;
/// A superclass to represent a control that contains other controls.
class cContainer : public cControl {
void callHandler(event_fcn<EVT_CLICK>::type onClick, cDialog& me, std::string id, eKeyMod mods) override;
std::string clicking;
protected:
std::string clicking;
/// Parses a child control.
/// @param elem The element defining the control.
/// @param controls The map into which the control will be inserted.

View File

@@ -32,6 +32,18 @@ location cTilemap::getCell(const std::string& id) const {
return location(std::stoi(x_str) * cellWidth, std::stoi(y_str) * cellHeight);
}
location cTilemap::getCellPos(size_t x, size_t y) const {
rectangle bounds = getBounds();
bounds.offset(x * cellWidth, y * cellHeight);
return bounds.topLeft();
}
location cTilemap::getCellPos(cControl& child) const {
// If it's not ours, return origin.
if(!hasChild(child.getName())) return location();
return getCell(child.getName());
}
std::string cTilemap::buildId(const std::string& base, size_t x, size_t y) {
std::ostringstream sout;
if(!base.empty()) {
@@ -92,14 +104,37 @@ bool cTilemap::isScrollable() const {
return false;
}
bool cTilemap::handleClick(location where, cFramerateLimiter& fps_limiter) {
bool success = false;
isHandlingClick = true;
forEach([&](std::string id, cControl& ctrl) {
rectangle bounds = ctrl.getBounds();
bounds.offset(getBounds().topLeft());
bounds.offset(getCell(id));
if(!success && ctrl.isClickable() && bounds.contains(where)) {
rectangle localBounds = ctrl.getBounds();
ctrl.setBounds(bounds);
clicking = id;
if(ctrl.handleClick(where, fps_limiter)){
success = true;
} else clicking.clear();
ctrl.setBounds(localBounds);
}
});
isHandlingClick = false;
return success;
}
void cTilemap::draw() {
if(!isVisible()) return;
for(auto& ctrl : controls) {
rectangle localBounds = ctrl.second->getBounds();
rectangle globalBounds = localBounds;
globalBounds.offset(getBounds().topLeft());
globalBounds.offset(getCell(ctrl.first));
ctrl.second->setBounds(globalBounds);
if(isHandlingClick ? ctrl.first != clicking : true) {
globalBounds.offset(getBounds().topLeft());
globalBounds.offset(getCell(ctrl.first));
ctrl.second->setBounds(globalBounds);
}
ctrl.second->draw();
ctrl.second->setBounds(localBounds);
}
@@ -134,6 +169,25 @@ void cTilemap::recalcRect() {
frame.bottom -= spacing;
}
void cTilemap::attachClickHandlers(std::function<bool(cDialog&,std::string,eKeyMod)> handler, std::string prefix) {
for(auto& ctrl : controls) {
bool matches = false;
if(prefix.empty()) {
size_t first_x = ctrl.first.find_first_of("x"), last_x = ctrl.first.find_last_of("x"), first_y = ctrl.first.find_first_of("y"), last_y = ctrl.first.find_last_of("y");
if(first_x == 0 && last_x == 0 && first_y != std::string::npos && first_y == last_y) {
matches = true;
}
} else {
if(ctrl.first.compare(0, prefix.size(), prefix)) {
matches = true;
}
}
if(matches) {
ctrl.second->attachClickHandler(handler);
}
}
}
void cTilemap::fillTabOrder(std::vector<int>& specificTabs, std::vector<int>& reverseTabs) {
for(auto p : controls) {
cControl& ctrl = *p.second;

View File

@@ -19,6 +19,7 @@ class cTilemap : public cContainer {
static std::string buildId(const std::string& base, size_t x, size_t y);
std::string current_cell;
mutable int id_tries = 0;
bool isHandlingClick = false;
public:
std::string generateId(const std::string& baseId) const override;
bool parseAttribute(ticpp::Attribute& attr, std::string tagName, std::string fname) override;
@@ -34,6 +35,17 @@ public:
cControl& getChild(std::string id, size_t x, size_t y);
bool hasChild(size_t x, size_t y) const;
cControl& getChild(size_t x, size_t y);
location getCellPos(size_t x, size_t y) const;
location getCellPos(cControl& child) const;
/// Attach the same click handler to every instance of a child control.
/// @param handler The handler to attach.
/// @param prefix The unique ID of the control template.
/// @throw xHandlerNotSupported if any of the controls do not support click handlers.
/// @throw std::invalid_argument if any of the controls do not exist.
/// @see cControl::attachClickHandler()
/// @deprecated in favour of @ref attachEventHandlers
void attachClickHandlers(std::function<bool(cDialog&,std::string,eKeyMod)> handler, std::string prefix);
bool handleClick(location where, cFramerateLimiter& fps_limiter) override;
/// Recalculate the tilemap's bounding rect based on its contained controls.
void recalcRect() override;
/// Adds any fields in this tilemap to the tab order building arrays.

View File

@@ -28,7 +28,7 @@ namespace {
.sdf(eSpecField::EX1A, eSpecField::EX1B)
.ex2b(eSpecPicker::NODE);
node_properties_t S_TERRAIN = node_builder_t(eSpecType::IF_TER_TYPE)
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_AUTO)
.ex2a(STRT_TER)
.ex2b(eSpecPicker::NODE);
node_properties_t S_ALIVE = node_builder_t(eSpecType::IF_ALIVE)
@@ -41,7 +41,7 @@ namespace {
.ex1b(eSpecPicker::NODE)
.ex2a(eSpecPicker::TOGGLE);
node_properties_t S_ITEM_THERE = node_builder_t(eSpecType::IF_ITEM_CLASS_ON_SPACE)
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(eSpecPicker::ITEM_CLASS)
.ex2b(eSpecPicker::NODE)
.ex2c(eSpecPicker::TOGGLE);
@@ -72,7 +72,7 @@ namespace {
node_properties_t S_DAY = node_builder_t(eSpecType::IF_DAY_REACHED)
.ex1b(eSpecPicker::NODE);
node_properties_t S_FIELDS = node_builder_t(eSpecType::IF_FIELDS)
.rect()
.rect(eLocType::ACTIVE_TOWN)
.msg1(eSpecPicker::FIELD)
.msg2(eSpecPicker::NODE);
node_properties_t S_PARTY_SIZE = node_builder_t(eSpecType::IF_PARTY_SIZE)

View File

@@ -117,16 +117,16 @@ namespace{
.msg();
node_properties_t S_TERCHANGE = node_builder_t(eSpecType::CHANGE_TER)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_AUTO)
.ex2a(STRT_TER);
node_properties_t S_TERSWAP = node_builder_t(eSpecType::SWAP_TER)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_AUTO)
.ex2a(STRT_TER)
.ex2b(STRT_TER);
node_properties_t S_TERTRANS = node_builder_t(eSpecType::TRANS_TER)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B);
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_AUTO);
node_properties_t S_BUF_CLEAR = node_builder_t(eSpecType::CLEAR_BUF);
node_properties_t S_BUF_ADDSTR = node_builder_t(eSpecType::APPEND_STRING)
.ex1a(eSpecPicker::MSG_SINGLE)

View File

@@ -14,11 +14,12 @@ namespace {
node_properties_t S_WANDER = node_builder_t(eSpecType::OUT_MAKE_WANDER);
node_properties_t S_TOWN = node_builder_t(eSpecType::OUT_FORCE_TOWN)
.msg()
.loc(eSpecField::EX2A, eSpecField::EX2B, eLocType::SPECIFIED_TOWN, eSpecField::EX1A)
.ex1a(STRT_TOWN)
.ex1b(STRT_DIR);
node_properties_t S_ENCOUNTER = node_builder_t(eSpecType::OUT_PLACE_ENCOUNTER)
.msg();
node_properties_t S_TELEPORT = node_builder_t(eSpecType::OUT_MOVE_PARTY)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B);
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_OUT);
}

View File

@@ -13,36 +13,36 @@ node_category_info_t CAT_RECT{eSpecType::RECT_PLACE_FIELD, eSpecType::RECT_UNLOC
namespace {
node_properties_t S_FIELDS = node_builder_t(eSpecType::RECT_PLACE_FIELD)
.msg()
.rect()
.rect(eLocType::ACTIVE_TOWN)
.sdf2(+eSpecPicker::FIELD);
node_properties_t S_EXPLORE = node_builder_t(eSpecType::RECT_SET_EXPLORED)
.msg()
.rect()
.rect(eLocType::ACTIVE_AUTO)
.sdf1(eSpecPicker::TOGGLE);
node_properties_t S_MOVE = node_builder_t(eSpecType::RECT_MOVE_ITEMS)
.msg()
.rect()
.rect(eLocType::ACTIVE_TOWN)
.msg3(eSpecPicker::TOGGLE)
.loc(eSpecField::SDF1, eSpecField::SDF2);
.loc(eSpecField::SDF1, eSpecField::SDF2, eLocType::ACTIVE_TOWN);
node_properties_t S_DESTROY = node_builder_t(eSpecType::RECT_DESTROY_ITEMS)
.msg()
.rect();
.rect(eLocType::ACTIVE_TOWN);
node_properties_t S_CHANGE = node_builder_t(eSpecType::RECT_CHANGE_TER)
.msg()
.rect()
.rect(eLocType::ACTIVE_AUTO)
.sdf1(STRT_TER);
node_properties_t S_SWAP = node_builder_t(eSpecType::RECT_SWAP_TER)
.msg()
.rect()
.rect(eLocType::ACTIVE_AUTO)
.sdf1(STRT_TER)
.sdf2(STRT_TER);
node_properties_t S_TRANS = node_builder_t(eSpecType::RECT_TRANS_TER)
.msg()
.rect();
.rect(eLocType::ACTIVE_AUTO);
node_properties_t S_LOCK = node_builder_t(eSpecType::RECT_LOCK)
.msg()
.rect();
.rect(eLocType::ACTIVE_TOWN);
node_properties_t S_UNLOCK = node_builder_t(eSpecType::RECT_UNLOCK)
.msg()
.rect();
.rect(eLocType::ACTIVE_TOWN);
}

View File

@@ -17,38 +17,39 @@ namespace {
node_properties_t S_MISSILE = node_builder_t(eSpecType::TOWN_RUN_MISSILE)
.msg()
.pict(PIC_MISSILE)
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX2A, eSpecField::EX2B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.loc(eSpecField::EX2A, eSpecField::EX2B, eLocType::ACTIVE_TOWN)
.ex1c(STRT_PATH)
.ex2c(eSpecPicker::SOUND);
node_properties_t S_ATTACK = node_builder_t(eSpecType::TOWN_MONST_ATTACK)
.msg();
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN);
node_properties_t S_BOOM = node_builder_t(eSpecType::TOWN_BOOM_SPACE)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(PIC_BOOM)
.ex2c(eSpecPicker::SOUND);
node_properties_t S_TELEPORT = node_builder_t(eSpecType::TOWN_MOVE_PARTY)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(eSpecPicker::TOGGLE);
node_properties_t S_HIT = node_builder_t(eSpecType::TOWN_HIT_SPACE)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2b(eSpecPicker::DAMAGE_TYPE);
node_properties_t S_EXPLODE = node_builder_t(eSpecType::TOWN_EXPLODE_SPACE)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2b(eSpecPicker::DAMAGE_TYPE);
node_properties_t S_LOCK = node_builder_t(eSpecType::TOWN_LOCK_SPACE)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B);
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN);
node_properties_t S_UNLOCK = node_builder_t(eSpecType::TOWN_UNLOCK_SPACE)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B);
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN);
node_properties_t S_BURST = node_builder_t(eSpecType::TOWN_SFX_BURST)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(eSpecPicker::EXPLOSION)
.ex2b(eSpecPicker::TOGGLE)
.ex2c(eSpecPicker::SOUND);
@@ -56,23 +57,23 @@ namespace {
.msg();
node_properties_t S_SPAWN = node_builder_t(eSpecType::TOWN_PLACE_MONST)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(STRT_MONST)
.ex2b(eSpecPicker::TOGGLE);
node_properties_t S_KILL = node_builder_t(eSpecType::TOWN_DESTROY_MONST)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B);
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN);
node_properties_t S_NUKE = node_builder_t(eSpecType::TOWN_NUKE_MONSTS)
.msg()
.ex1a(STRT_MONST);
node_properties_t S_LEVER_G = node_builder_t(eSpecType::TOWN_GENERIC_LEVER)
.ex1b(eSpecPicker::NODE);
node_properties_t S_PORTAL_G = node_builder_t(eSpecType::TOWN_GENERIC_PORTAL)
.loc(eSpecField::EX1A, eSpecField::EX1B);
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN);
node_properties_t S_BUTTON_G = node_builder_t(eSpecType::TOWN_GENERIC_BUTTON)
.ex1b(eSpecPicker::NODE);
node_properties_t S_STAIR_G = node_builder_t(eSpecType::TOWN_GENERIC_STAIR)
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::SPECIFIED_TOWN, eSpecField::EX2A)
.ex2a(STRT_TOWN)
.ex2b(STRT_STAIR)
.ex2c(STRT_STAIR_MODE)
@@ -84,11 +85,11 @@ namespace {
node_properties_t S_PORTAL = node_builder_t(eSpecType::TOWN_PORTAL)
.msg1(eSpecPicker::MSG_SEQUENCE)
.pic()
.loc(eSpecField::EX1A, eSpecField::EX1B);
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN);
node_properties_t S_STAIR = node_builder_t(eSpecType::TOWN_STAIR)
.msg1(eSpecPicker::MSG_SEQUENCE)
.pic()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::SPECIFIED_TOWN, eSpecField::EX2A)
.ex2a(STRT_TOWN)
.ex2b(eSpecPicker::TOGGLE)
.ex2c(STRT_STAIR_MODE)
@@ -96,15 +97,15 @@ namespace {
node_properties_t S_OUTDOOR = node_builder_t(eSpecType::TOWN_RELOCATE)
.msg()
.field_pair(eSpecField::EX1A, eSpecField::EX1B, STRT_SECTOR)
.loc(eSpecField::EX2A, eSpecField::EX2B);
.loc(eSpecField::EX2A, eSpecField::EX2B, eLocType::SPECIFIED_OUT, eSpecField::EX1A);
node_properties_t S_ITEM = node_builder_t(eSpecType::TOWN_PLACE_ITEM)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(STRT_ITEM)
.ex2b(eSpecPicker::TOGGLE);
node_properties_t S_SPLIT = node_builder_t(eSpecType::TOWN_SPLIT_PARTY)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(eSpecPicker::SOUND);
node_properties_t S_REUNITE = node_builder_t(eSpecType::TOWN_REUNITE_PARTY)
.msg()
@@ -133,21 +134,21 @@ namespace {
.ex2c(eSpecPicker::TOGGLE);
node_properties_t S_FIELDS = node_builder_t(eSpecType::TOWN_SPELL_PAT_FIELD)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex1c(+STRT_SPELL_PAT)
.ex2a(+eSpecPicker::FIELD);
node_properties_t S_PATTERN = node_builder_t(eSpecType::TOWN_SPELL_PAT_BOOM)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex1c(+STRT_SPELL_PAT)
.ex2a(eSpecPicker::DAMAGE_TYPE)
.ex2c(STRT_SPELL_PAT_MODE);
node_properties_t S_WARP = node_builder_t(eSpecType::TOWN_RELOCATE_CREATURE)
.msg()
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2b(STRT_POS_MODE);
node_properties_t S_LABEL = node_builder_t(eSpecType::TOWN_PLACE_LABEL)
.msg1(eSpecPicker::MSG_SINGLE)
.loc(eSpecField::EX1A, eSpecField::EX1B)
.loc(eSpecField::EX1A, eSpecField::EX1B, eLocType::ACTIVE_TOWN)
.ex2a(STRT_LABEL_ALIGN);
}

View File

@@ -652,6 +652,11 @@ node_function_t::node_function_t(ePicType pic)
, pic_type(pic)
{}
node_function_t::node_function_t(eLocType loc)
: button(eSpecPicker::LOCATION)
, loc_type(loc)
{}
node_function_t operator+(eSpecPicker picker) {
node_function_t n(picker);
if(picker == eSpecPicker::NODE || picker == eSpecPicker::MSG_PAIR || picker == eSpecPicker::MSG_SINGLE || picker == eSpecPicker::MSG_SEQUENCE) {
@@ -765,10 +770,8 @@ node_builder_t& node_builder_t::msg() {
return field_pair(eSpecField::MSG1, eSpecField::MSG2, eSpecPicker::MSG_PAIR);
};
node_builder_t& node_builder_t::rect() {
// The intent is to specify that ex1a,ex1b, and ex2a,ex2b are locations.
// But specifying that two fields are a location isn't implemented yet.
return loc(eSpecField::EX1A, eSpecField::EX1B).loc(eSpecField::EX2A, eSpecField::EX2B).pict(eSpecPicker::TOGGLE);
node_builder_t& node_builder_t::rect(eLocType type) {
return loc(eSpecField::EX1A, eSpecField::EX1B, type).loc(eSpecField::EX2A, eSpecField::EX2B, type).pict(eSpecPicker::TOGGLE);
};
node_builder_t& node_builder_t::pic() {
@@ -847,8 +850,14 @@ node_builder_t& node_builder_t::sdf(eSpecField a, eSpecField b) {
return field_pair(a, b, eSpecPicker::SDF);
}
node_builder_t& node_builder_t::loc(eSpecField a, eSpecField b) {
return field_pair(a, b, eSpecPicker::LOCATION);
node_builder_t& node_builder_t::loc(eSpecField a, eSpecField b, eLocType type) {
return field_pair(a, b, type);
}
node_builder_t& node_builder_t::loc(eSpecField a, eSpecField b, eLocType type, eSpecField where) {
loc(a, b, type);
(node.*fields().map[b]).continuation = where;
return *this;
}
node_builder_t::operator node_properties_t() {

View File

@@ -160,6 +160,11 @@ enum class eSpecPicker {
POINTER,
};
enum class eLocType {
ACTIVE_AUTO, ACTIVE_TOWN, ACTIVE_OUT,
SPECIFIED_TOWN, SPECIFIED_OUT
};
enum class eSpecField { NONE, SDF1, SDF2, MSG1, MSG2, MSG3, PICT, PTYP, EX1A, EX1B, EX1C, EX2A, EX2B, EX2C, JUMP };
struct node_function_t {
@@ -167,6 +172,7 @@ struct node_function_t {
union {
eStrType str_type; // for eSpecPicker::STRING only
ePicType pic_type; // for eSpecPicker::PICTURE only; PIC_NONE = use pictype field from node
eLocType loc_type; // for eSpecPicker::LOCATION only
bool force_global; // for eSpecPicker::NODE and eSpecPicker::MSG_*
// other pickers don't put anything in here
};
@@ -179,6 +185,7 @@ struct node_function_t {
node_function_t(eSpecPicker button);
node_function_t(eStrType str);
node_function_t(ePicType pic);
node_function_t(eLocType loc);
private:
eSpecType self = eSpecType::NONE;
std::string lbl;
@@ -246,11 +253,13 @@ struct node_builder_t {
// Specifies that ex1a,ex1b and ex2a,ex2b define opposing corners of a rectangle.
// DO NOT use for other cases where there are two points, for example defining a path!
// Also DO NOT use for cases where an outdoor sector is identified by its coordinates.
node_builder_t& rect();
node_builder_t& rect(eLocType type);
// Specifies that the indicated two fields combine to define an SDF.
node_builder_t& sdf(eSpecField a, eSpecField b);
// Specifies that the indicated two fields combine to define a location.
node_builder_t& loc(eSpecField a, eSpecField b);
node_builder_t& loc(eSpecField a, eSpecField b, eLocType type);
// As above, but also notes that the area the location is in will be specified by the indicated field.
node_builder_t& loc(eSpecField a, eSpecField b, eLocType type, eSpecField where);
operator node_properties_t();
private:
node_properties_t node;

View File

@@ -8,6 +8,7 @@ scened_sources = Split("""
scen.fileio.cpp
scen.graphics.cpp
scen.keydlgs.cpp
scen.locpicker.cpp
scen.main.cpp
scen.townout.cpp
../view_dialogs.cpp
@@ -49,4 +50,4 @@ else:
env.Package(scened, install_dir, scened_info)
if debug_symbols is not None:
env.Install(install_dir, debug_symbols)
env.Install(install_dir, debug_symbols)

View File

@@ -11,6 +11,7 @@
#include "utility.hpp"
#include "scen.graphics.hpp"
#include "scen.keydlgs.hpp"
#include "scen.locpicker.hpp"
#include "scen.core.hpp"
#include "dialogxml/dialogs/dialog.hpp"
#include "dialogxml/widgets/control.hpp"
@@ -1035,6 +1036,88 @@ static bool edit_spec_enc_value(cDialog& me, std::string item_hit, node_stack_t&
store = choose_graphic(val, type, &me);
if(store < 0) store = val;
} break;
case eSpecPicker::LOCATION: {
auto otherField = get_control_for_field(fcn.continuation);
location loc;
loc.x = val;
loc.y = me[otherField].getTextAsNum();
static const std::string townStr = "Select a location in the current town:";
static const std::string outStr = "Select a location in the current outdoors:";
static const std::string specStr = "Select a location in ";
switch(fcn.loc_type) {
case eLocType::ACTIVE_OUT: loc = cLocationPicker(loc, *current_terrain, outStr, &me).run(); break;
case eLocType::ACTIVE_TOWN: loc = cLocationPicker(loc, *town, townStr, &me).run(); break;
case eLocType::ACTIVE_AUTO: {
switch(edit_stack.top().mode) {
case 1: loc = cLocationPicker(loc, *current_terrain, outStr, &me).run(); break;
case 2: loc = cLocationPicker(loc, *town, townStr, &me).run(); break;
case 0: {
bool is_town = true;
cLocationPicker picker(loc, *town, townStr, &me);
auto& switchBtn = picker->getControl("switch");
switchBtn.show();
switchBtn.attachClickHandler([&picker, &is_town](cDialog&, std::string, eKeyMod) {
if(is_town) {
is_town = false;
picker.setArea(*current_terrain);
picker.setTitle(outStr);
} else {
is_town = true;
picker.setArea(*town);
picker.setTitle(townStr);
}
return true;
});
loc = picker.run();
} break;
}
} break;
case eLocType::SPECIFIED_TOWN: {
node_function_t y_fcn = get_field_function(spec, otherField);
auto townField = get_control_for_field(y_fcn.continuation);
int townNum = me[townField].getTextAsNum();
townNum = minmax(0, scenario.towns.size() - 1, townNum);
cLocationPicker picker(loc, *scenario.towns[townNum], specStr + scenario.towns[townNum]->name + ":", &me);
auto& switchBtn = picker->getControl("switch");
switchBtn.show();
switchBtn.attachClickHandler([&picker, &townNum](cDialog& me, std::string, eKeyMod) {
townNum = choose_text(STRT_TOWN, townNum, &me, "Which town?");
picker.setArea(*scenario.towns[townNum]);
picker.setTitle(specStr + scenario.towns[townNum]->name + ":");
return true;
});
loc = picker.run();
me[townField].setTextToNum(townNum);
} break;
case eLocType::SPECIFIED_OUT: {
node_function_t y_fcn = get_field_function(spec, otherField);
auto sectorXField = get_control_for_field(y_fcn.continuation);
int sectorX = me[sectorXField].getTextAsNum();
sectorX = minmax(0, scenario.outdoors.width() - 1, sectorX);
node_function_t sec_fcn = get_field_function(spec, sectorXField);
auto sectorYField = get_control_for_field(sec_fcn.continuation);
int sectorY = me[sectorYField].getTextAsNum();
sectorY = minmax(0, scenario.outdoors.height() - 1, sectorY);
cLocationPicker picker(loc, *scenario.outdoors[sectorX][sectorY], specStr + scenario.outdoors[sectorX][sectorY]->name + ":", &me);
auto& switchBtn = picker->getControl("switch");
switchBtn.show();
switchBtn.attachClickHandler([&picker, &sectorX, &sectorY](cDialog& me, std::string, eKeyMod) {
int val = sectorX * scenario.outdoors.height() + sectorY;
val = choose_text(STRT_SECTOR, val, &me, "Which sector?");
sectorY = val % scenario.outdoors.height();
sectorX = val / scenario.outdoors.height();
picker.setArea(*scenario.outdoors[sectorX][sectorY]);
picker.setTitle(specStr + scenario.outdoors[sectorX][sectorY]->name + ":");
return true;
});
loc = picker.run();
me[sectorXField].setTextToNum(sectorX);
me[sectorYField].setTextToNum(sectorY);
} break;
}
store = loc.x;
me[otherField].setTextToNum(loc.y);
} break;
case eSpecPicker::FIELD: store = choose_field_type(val, &me, fcn.augmented); break;
case eSpecPicker::DAMAGE_TYPE: store = choose_damage_type(val, &me, true); break;
case eSpecPicker::EXPLOSION: store = choose_boom_type(val, &me); break;

View File

@@ -0,0 +1,130 @@
//
// scen.locpicker.cpp
// BoE
//
// Created by Celtic Minstrel on 2025-02-28.
//
#include "scen.locpicker.hpp"
#include "dialogxml/widgets/pict.hpp"
#include "dialogxml/widgets/tilemap.hpp"
#include "scenario/area.hpp"
#include "scenario/scenario.hpp"
#include "fileio/resmgr/res_dialog.hpp"
extern cScenario scenario;
cLocationPicker::cLocationPicker(location loc, cArea& area, const std::string& title, cDialog* parent)
: initial_loc(loc)
, chosen_loc(loc)
, area(&area)
, dlog(*ResMgr::dialogs.get("choose-location"), parent)
{
clamp_loc();
map = dynamic_cast<cTilemap*>(&dlog["map"]);
fill_terrain();
handle_scroll(""); // Hide unusable scroll buttons
dlog["switch"].hide();
setTitle(title);
using namespace std::placeholders;
dlog.attachClickHandlers(std::bind(&cLocationPicker::handle_close, this, _2), {"done", "cancel"});
dlog.attachClickHandlers(std::bind(&cLocationPicker::handle_scroll, this, _2), {"up", "down", "left", "right"});
map->attachClickHandlers(std::bind(&cLocationPicker::handle_select, this, _2), "");
}
void cLocationPicker::clamp_loc() {
chosen_loc.x = minmax(0, area->max_dim - 1, chosen_loc.x);
chosen_loc.y = minmax(0, area->max_dim - 1, chosen_loc.y);
viewport.x = 18 * floor(chosen_loc.x / 18.0);
viewport.y = 18 * floor(chosen_loc.y / 18.0);
}
void cLocationPicker::setArea(cArea& newArea) {
area = &newArea;
clamp_loc();
fill_terrain();
}
void cLocationPicker::setTitle(const std::string& title) {
dlog["prompt"].setText(title);
}
void cLocationPicker::fill_terrain() {
for(int x = 0; x < 18; x++) {
for(int y = 0; y < 18; y++) {
auto& pict = dynamic_cast<cPict&>(map->getChild(x, y));
location ter_loc = viewport;
ter_loc.x += x;
ter_loc.y += y;
if(ter_loc.x >= area->max_dim || ter_loc.y >= area->max_dim) pict.setPict(74);
else {
ter_num_t ter = area->terrain[ter_loc.x][ter_loc.y];
pict.setPict(scenario.ter_types[ter].map_pic);
}
}
}
place_pointer();
}
void cLocationPicker::place_pointer() {
if(chosen_loc.x >= viewport.x && chosen_loc.x < viewport.x + 18 && chosen_loc.y >= viewport.y && chosen_loc.y < viewport.y + 18) {
location relative_loc = chosen_loc;
relative_loc.x -= viewport.x;
relative_loc.y -= viewport.y;
location ctrlPos = map->getCellPos(relative_loc.x, relative_loc.y);
ctrlPos.x += 3;
ctrlPos.y += 3;
dlog["zpointer"].relocate(ctrlPos);
dlog["zpointer"].show();
} else {
dlog["zpointer"].hide();
}
}
bool cLocationPicker::handle_close(std::string item_hit) {
dlog.toast(item_hit == "done");
return true;
}
bool cLocationPicker::handle_scroll(std::string item_hit) {
if(item_hit == "up") {
if(viewport.y > 0) viewport.y -= 9;
} else if(item_hit == "down") {
if(viewport.y < area->max_dim - 18) viewport.y += 9;
} else if(item_hit == "left") {
if(viewport.x > 0) viewport.x -= 9;
} else if(item_hit == "right") {
if(viewport.x < area->max_dim - 18) viewport.x += 9;
}
if(viewport.x == 0) dlog["left"].hide();
else dlog["left"].show();
if(viewport.y == 0) dlog["up"].hide();
else dlog["up"].show();
if(viewport.x >= area->max_dim - 18) dlog["right"].hide();
else dlog["right"].show();
if(viewport.y >= area->max_dim - 18) dlog["down"].hide();
else dlog["down"].show();
fill_terrain();
return true;
}
bool cLocationPicker::handle_select(std::string item_hit) {
location clickedLoc = map->getCellPos(dlog[item_hit]);
location check{viewport.x + clickedLoc.x / 24, viewport.y + clickedLoc.y / 24};
if(check.x < area->max_dim && check.y < area->max_dim) {
chosen_loc = check;
place_pointer();
}
return true;
}
location cLocationPicker::run() {
dlog.run();
if(dlog.accepted()) return chosen_loc;
return initial_loc;
}
cDialog* cLocationPicker::operator->() {
return &dlog;
}

View File

@@ -0,0 +1,37 @@
//
// scen.locpicker.hpp
// BoE
//
// Created by Celtic Minstrel on 2025-02-28.
//
#ifndef BoE_scen_locpicker_h
#define BoE_scen_locpicker_h
#include <string>
#include "location.hpp"
#include "dialogxml/dialogs/dialog.hpp"
class cArea;
class cTilemap;
class cLocationPicker {
location initial_loc, chosen_loc, viewport;
cArea* area;
cDialog dlog;
cTilemap* map;
void clamp_loc();
void fill_terrain();
void place_pointer();
bool handle_close(std::string item_hit);
bool handle_scroll(std::string item_hit);
bool handle_select(std::string item_hit);
public:
cLocationPicker(location loc, cArea& area, const std::string& title, cDialog* parent);
void setArea(cArea& newArea) ;
void setTitle(const std::string& title);
location run();
cDialog* operator->();
};
#endif

View File

@@ -13,11 +13,13 @@
#include "scen.graphics.hpp"
#include "scen.townout.hpp"
#include "scen.keydlgs.hpp"
#include "scen.locpicker.hpp"
#include "scen.fileio.hpp"
#include "scen.core.hpp"
#include "mathutil.hpp"
#include "dialogxml/widgets/button.hpp"
#include "dialogxml/widgets/field.hpp"
#include "dialogxml/dialogs/strchoice.hpp"
#include "dialogxml/dialogs/strdlog.hpp"
#include "dialogxml/dialogs/choicedlog.hpp"
#include "tools/winutil.hpp"
@@ -815,6 +817,49 @@ void edit_advanced_town() {
town_dlg.attachFocusHandlers(loc_check, {"exit1-x", "exit2-x", "exit3-x", "exit4-x"});
town_dlg.attachFocusHandlers(loc_check, {"exit1-y", "exit2-y", "exit3-y", "exit4-y"});
town_dlg.attachClickHandlers(edit_advanced_town_special, {"edit-onexit1", "edit-onexit2", "edit-onexit3", "edit-onexit4", "edit-onenter", "edit-onenterdead", "edit-onhostile"});
town_dlg.attachClickHandlers([](cDialog& me, std::string which, eKeyMod) {
auto entrances = scenario.find_town_entrances(cur_town);
std::vector<location> sectors;
for(const auto& entrance : entrances) {
if(std::find(sectors.begin(), sectors.end(), entrance.out_sec) == sectors.end()) {
sectors.push_back(entrance.out_sec);
}
}
int which_sector = 0;
if(sectors.size() == 0) {
int sec = -1;
sec = choose_text(STRT_SECTOR, 0, &me, "Exit to which sector?");
if(sec == -1) return true; // cancelled
sectors.emplace_back(sec / scenario.outdoors.height(), sec % scenario.outdoors.height());
}
location loc(me[which + "-x"].getTextAsNum(), me[which + "-y"].getTextAsNum());
if((loc.x < 0 || loc.y < 0 || loc.x >= 48 || loc.y >= 48) && !entrances.empty()) {
loc = entrances[0].loc;
}
static const std::string directions[4]{"top", "left", "bottom", "right"};
std::string title = "Select outdoor location when exiting from the " + directions[which.back() - '1'] + ":";
cLocationPicker picker(loc, *scenario.outdoors[sectors[which_sector].x][sectors[which_sector].y], title, &me);
if(sectors.size() > 1) {
std::vector<std::string> out_names;
for(const auto& sec : sectors) {
out_names.push_back(scenario.outdoors[sec.x][sec.y]->name);
}
cControl& switchBtn = picker->getControl("switch");
switchBtn.show();
switchBtn.attachClickHandler([out_names, &sectors, &which_sector, &picker](cDialog& me, std::string, eKeyMod) {
cStringChoice pickstr(out_names, "Exit to which sector?", &me);
which_sector = pickstr.show(which_sector);
picker.setArea(*scenario.outdoors[sectors[which_sector].x][sectors[which_sector].y]);
return true;
});
}
loc = picker.run();
if(picker->accepted()) {
me[which + "-x"].setTextToNum(loc.x);
me[which + "-y"].setTextToNum(loc.y);
}
return true;
}, {"exit1", "exit2", "exit3", "exit4"});
town_dlg.attachClickHandlers([](cDialog& me, std::string which, eKeyMod) -> bool {
std::string fld = which.replace(0, 4, "bg");
int bg_i = me[fld].getTextAsNum();