diff --git a/proj/vs2013/Common/Common.vcxproj b/proj/vs2013/Common/Common.vcxproj
index 115b9c136..1522c3ec1 100644
--- a/proj/vs2013/Common/Common.vcxproj
+++ b/proj/vs2013/Common/Common.vcxproj
@@ -247,6 +247,13 @@
+
+
+
+
+
+
+
diff --git a/proj/vs2013/Common/Common.vcxproj.filters b/proj/vs2013/Common/Common.vcxproj.filters
index 1dbe77c9e..fd3da8467 100644
--- a/proj/vs2013/Common/Common.vcxproj.filters
+++ b/proj/vs2013/Common/Common.vcxproj.filters
@@ -647,6 +647,27 @@
Scenario
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
Scenario
diff --git a/proj/vs2017/Common/Common.vcxproj b/proj/vs2017/Common/Common.vcxproj
index 94d80a6d3..f20202f28 100644
--- a/proj/vs2017/Common/Common.vcxproj
+++ b/proj/vs2017/Common/Common.vcxproj
@@ -403,6 +403,13 @@
+
+
+
+
+
+
+
diff --git a/proj/vs2017/Common/Common.vcxproj.filters b/proj/vs2017/Common/Common.vcxproj.filters
index 5e00b2c58..a19a04d26 100644
--- a/proj/vs2017/Common/Common.vcxproj.filters
+++ b/proj/vs2017/Common/Common.vcxproj.filters
@@ -662,6 +662,27 @@
Scenario
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
+
+ Scenario
+
Scenario
diff --git a/proj/xc12/BoE.xcodeproj/project.pbxproj b/proj/xc12/BoE.xcodeproj/project.pbxproj
index dc5ae8c0f..f42563210 100755
--- a/proj/xc12/BoE.xcodeproj/project.pbxproj
+++ b/proj/xc12/BoE.xcodeproj/project.pbxproj
@@ -157,6 +157,13 @@
919B13A21BBCDF14009905A4 /* monst_legacy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919B13A11BBCDE18009905A4 /* monst_legacy.cpp */; };
919B13A41BBD8854009905A4 /* item_legacy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919B13A31BBD8849009905A4 /* item_legacy.cpp */; };
919B13A61BBDE986009905A4 /* spec_legacy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919B13A51BBDE985009905A4 /* spec_legacy.cpp */; };
+ 919BE86B2D658BC6000C64C6 /* special-general.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919BE86A2D658BBC000C64C6 /* special-general.cpp */; };
+ 919BE8722D66CD1C000C64C6 /* special-oneshot.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919BE8712D66CD1C000C64C6 /* special-oneshot.cpp */; };
+ 919BE88D2D66F6E3000C64C6 /* special-affect.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919BE88C2D66F6E2000C64C6 /* special-affect.cpp */; };
+ 919BE8942D676567000C64C6 /* special-condition.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919BE8932D676567000C64C6 /* special-condition.cpp */; };
+ 919BE8A02D676DDE000C64C6 /* special-town.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919BE89F2D676DDE000C64C6 /* special-town.cpp */; };
+ 919BE8AC2D677699000C64C6 /* special-rect.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919BE8AB2D677699000C64C6 /* special-rect.cpp */; };
+ 919BE8B32D6776A8000C64C6 /* special-outdoor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 919BE8B22D6776A8000C64C6 /* special-outdoor.cpp */; };
919CC2481B3772F300273FDA /* population.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91AC620A0FA2853700EEAE67 /* population.cpp */; };
919CC2491B3772FB00273FDA /* creature.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 914698FE1A747C4500F20F5E /* creature.cpp */; };
919CC24B1B37730300273FDA /* item.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91279D3D0F9D1D6A007B0D52 /* item.cpp */; };
@@ -766,6 +773,13 @@
919B13A51BBDE985009905A4 /* spec_legacy.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = spec_legacy.cpp; sourceTree = ""; };
919B13A71BBE297B009905A4 /* scrollpane.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = scrollpane.hpp; sourceTree = ""; };
919B13A81BBE2B54009905A4 /* scrollpane.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = scrollpane.cpp; sourceTree = ""; };
+ 919BE86A2D658BBC000C64C6 /* special-general.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "special-general.cpp"; sourceTree = ""; };
+ 919BE8712D66CD1C000C64C6 /* special-oneshot.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "special-oneshot.cpp"; sourceTree = ""; };
+ 919BE88C2D66F6E2000C64C6 /* special-affect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "special-affect.cpp"; sourceTree = ""; wrapsLines = 1; };
+ 919BE8932D676567000C64C6 /* special-condition.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "special-condition.cpp"; sourceTree = ""; };
+ 919BE89F2D676DDE000C64C6 /* special-town.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "special-town.cpp"; sourceTree = ""; };
+ 919BE8AB2D677699000C64C6 /* special-rect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "special-rect.cpp"; sourceTree = ""; };
+ 919BE8B22D6776A8000C64C6 /* special-outdoor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "special-outdoor.cpp"; sourceTree = ""; };
919DDBFA19006CC9003E7FED /* libboost_filesystem-mt.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libboost_filesystem-mt.dylib"; path = "/opt/local/libexec/boost/1.76/lib/libboost_filesystem-mt.dylib"; sourceTree = ""; };
919DDBFB19006CC9003E7FED /* libboost_system-mt.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libboost_system-mt.dylib"; path = "/opt/local/libexec/boost/1.76/lib/libboost_system-mt.dylib"; sourceTree = ""; };
919DDC091900750D003E7FED /* freetype.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = freetype.framework; path = /Library/Frameworks/freetype.framework; sourceTree = ""; };
@@ -1248,6 +1262,13 @@
91279CC10F9D19DA007B0D52 /* monster.cpp */,
91E5C79D0F9F60FA00C21460 /* outdoors.cpp */,
91279C580F9D1253007B0D52 /* scenario.cpp */,
+ 919BE88C2D66F6E2000C64C6 /* special-affect.cpp */,
+ 919BE8932D676567000C64C6 /* special-condition.cpp */,
+ 919BE86A2D658BBC000C64C6 /* special-general.cpp */,
+ 919BE8712D66CD1C000C64C6 /* special-oneshot.cpp */,
+ 919BE8B22D6776A8000C64C6 /* special-outdoor.cpp */,
+ 919BE8AB2D677699000C64C6 /* special-rect.cpp */,
+ 919BE89F2D676DDE000C64C6 /* special-town.cpp */,
91FDB5791A4E774E00DE5983 /* shop.cpp */,
91279CC60F9D1A02007B0D52 /* special.cpp */,
91E5C7B70F9F619D00C21460 /* talking.cpp */,
@@ -2068,6 +2089,7 @@
files = (
919CC24B1B37730300273FDA /* item.cpp in Sources */,
919CC24D1B37730E00273FDA /* location.cpp in Sources */,
+ 919BE8722D66CD1C000C64C6 /* special-oneshot.cpp in Sources */,
919CC24E1B37731400273FDA /* monster.cpp in Sources */,
919CC24F1B37731800273FDA /* outdoors.cpp in Sources */,
919CC2531B37732C00273FDA /* scenario.cpp in Sources */,
@@ -2079,6 +2101,7 @@
919CC25A1B37735100273FDA /* terrain.cpp in Sources */,
919CC25C1B37735C00273FDA /* town.cpp in Sources */,
919CC25F1B37736E00273FDA /* vehicle.cpp in Sources */,
+ 919BE88D2D66F6E3000C64C6 /* special-affect.cpp in Sources */,
919CC2601B37737200273FDA /* estreams.cpp in Sources */,
919CC2611B37738100273FDA /* gzstream.cpp in Sources */,
919CC2621B37738A00273FDA /* ticpp.cpp in Sources */,
@@ -2099,6 +2122,7 @@
415EEEB02D5534A500B47408 /* prefs.cpp in Sources */,
919CC2701B3773EC00273FDA /* scrollbar.cpp in Sources */,
919CC2711B3773F300273FDA /* cursors.mac.mm in Sources */,
+ 919BE86B2D658BC6000C64C6 /* special-general.cpp in Sources */,
919CC2721B3773F800273FDA /* fileio.cpp in Sources */,
919CC2741B37740200273FDA /* fileio_scen.cpp in Sources */,
919CC2751B37740A00273FDA /* render_image.cpp in Sources */,
@@ -2108,6 +2132,7 @@
919CC2791B37742200273FDA /* prefs.mac.mm in Sources */,
919CC27A1B37742800273FDA /* qdpict.mac.cpp in Sources */,
919CC27B1B37742D00273FDA /* sounds.cpp in Sources */,
+ 919BE8AC2D677699000C64C6 /* special-rect.cpp in Sources */,
919CC27C1B37743200273FDA /* special_parse.cpp in Sources */,
919CC27D1B37743700273FDA /* tarball.cpp in Sources */,
919CC27E1B37743B00273FDA /* undo.cpp in Sources */,
@@ -2116,9 +2141,11 @@
915473CF2C800AB000EB1C94 /* enchant.cpp in Sources */,
915AF9E81BBF8B5C008AEF49 /* scrollpane.cpp in Sources */,
91E128E71BC1E6DD00C8BE1D /* basicbtns.cpp in Sources */,
+ 919BE8942D676567000C64C6 /* special-condition.cpp in Sources */,
91E128ED1BC2076B00C8BE1D /* 3choice.cpp in Sources */,
91E128EE1BC2076B00C8BE1D /* choicedlog.cpp in Sources */,
9143044A2970EDC1003A3967 /* keymods.cpp in Sources */,
+ 919BE8B32D6776A8000C64C6 /* special-outdoor.cpp in Sources */,
91E128EF1BC2076B00C8BE1D /* pictchoice.cpp in Sources */,
91E128F01BC2076B00C8BE1D /* strchoice.cpp in Sources */,
91E128F11BC2076B00C8BE1D /* strdlog.cpp in Sources */,
@@ -2128,6 +2155,7 @@
91A2480E2969CFD200B8D90F /* res_dialog.cpp in Sources */,
91CE24921EA12ABD005BDCE4 /* gfxsheets.cpp in Sources */,
91CE24931EA12AC9005BDCE4 /* tiling.cpp in Sources */,
+ 919BE8A02D676DDE000C64C6 /* special-town.cpp in Sources */,
91EC1F0423DDFF9D00271891 /* res_cursor.cpp in Sources */,
91EC1F0523DDFF9D00271891 /* res_font.cpp in Sources */,
91EC1F0623DDFF9D00271891 /* res_image.cpp in Sources */,
diff --git a/src/scenario/special-affect.cpp b/src/scenario/special-affect.cpp
new file mode 100644
index 000000000..6b257638b
--- /dev/null
+++ b/src/scenario/special-affect.cpp
@@ -0,0 +1,93 @@
+//
+// special-affect.cpp
+// Common
+//
+// Created by Celtic Minstrel on 2025-02-20.
+//
+
+#include "special.hpp"
+
+// Note: If adding a new node type below, be sure to adjust the end point here too.
+node_category_info_t CAT_AFFECT{eSpecType::SELECT_TARGET, eSpecType::UNSTORE_PC};
+
+namespace {
+ node_properties_t S_SELECT = node_builder_t(eSpecType::SELECT_TARGET)
+ .msg();
+ node_properties_t S_DAMAGE = node_builder_t(eSpecType::DAMAGE)
+ .msg()
+ .ex2b(eSpecPicker::DAMAGE_TYPE)
+ .ex2c(eSpecPicker::SOUND);
+ node_properties_t S_HEALTH = node_builder_t(eSpecType::AFFECT_HP)
+ .msg();
+ node_properties_t S_MANA = node_builder_t(eSpecType::AFFECT_SP)
+ .msg();
+ node_properties_t S_EXP = node_builder_t(eSpecType::AFFECT_XP)
+ .msg();
+ node_properties_t S_SKILLPT = node_builder_t(eSpecType::AFFECT_SKILL_PTS)
+ .msg();
+ node_properties_t S_KILL = node_builder_t(eSpecType::AFFECT_DEADNESS)
+ .msg()
+ .ex1a(STRT_STATUS);
+ node_properties_t S_STATUS = node_builder_t(eSpecType::AFFECT_STATUS)
+ .msg()
+ .ex1c(eSpecPicker::STATUS);
+ node_properties_t S_TRAIT = node_builder_t(eSpecType::AFFECT_TRAITS)
+ .msg()
+ .ex1a(STRT_TRAIT);
+ node_properties_t S_ACTIONS = node_builder_t(eSpecType::AFFECT_AP)
+ .msg();
+ node_properties_t S_NAME = node_builder_t(eSpecType::AFFECT_NAME)
+ .msg()
+ .msg3(eSpecPicker::MSG_SINGLE);
+ node_properties_t S_LEVEL = node_builder_t(eSpecType::AFFECT_LEVEL)
+ .msg();
+ node_properties_t S_MORALE = node_builder_t(eSpecType::AFFECT_MORALE)
+ .msg();
+ node_properties_t S_CRYSTAL = node_builder_t(eSpecType::AFFECT_SOUL_CRYSTAL)
+ .msg();
+ node_properties_t S_EQUIP = node_builder_t(eSpecType::GIVE_ITEM)
+ .msg()
+ .pict(eSpecPicker::NODE)
+ .ex1a(STRT_ITEM)
+ .ex1b(STRT_ENCHANT);
+ node_properties_t S_TARGET = node_builder_t(eSpecType::AFFECT_MONST_TARG)
+ .msg();
+ node_properties_t S_ATTACK = node_builder_t(eSpecType::AFFECT_MONST_ATT)
+ .msg();
+ node_properties_t S_STAT_M = node_builder_t(eSpecType::AFFECT_MONST_STAT)
+ .msg()
+ .ex2a(STRT_MONST_STAT);
+ node_properties_t S_STAT_P = node_builder_t(eSpecType::AFFECT_STAT)
+ .msg()
+ .ex2a(STRT_SKILL);
+ node_properties_t S_MAGE = node_builder_t(eSpecType::AFFECT_MAGE_SPELL)
+ .msg()
+ .ex1a(STRT_MAGE);
+ node_properties_t S_PRIEST = node_builder_t(eSpecType::AFFECT_PRIEST_SPELL)
+ .msg()
+ .ex1a(STRT_PRIEST);
+ node_properties_t S_GOLD = node_builder_t(eSpecType::AFFECT_GOLD)
+ .msg();
+ node_properties_t S_FOOD = node_builder_t(eSpecType::AFFECT_FOOD)
+ .msg();
+ node_properties_t S_ALCHEMY = node_builder_t(eSpecType::AFFECT_ALCHEMY)
+ .msg()
+ .ex1a(STRT_ALCHEMY);
+ node_properties_t S_STATUS_PARTY = node_builder_t(eSpecType::AFFECT_PARTY_STATUS)
+ .msg()
+ .ex1c(eSpecPicker::STATUS_PARTY);
+ node_properties_t S_NEWPC = node_builder_t(eSpecType::CREATE_NEW_PC)
+ .sdf()
+ .msg()
+ .msg3(eSpecPicker::MSG_SINGLE)
+ .pict(PIC_PC)
+ .ptyp(eSpecPicker::NODE)
+ .ex1c(STRT_RACE);
+ node_properties_t S_STOREPC = node_builder_t(eSpecType::STORE_PC)
+ .sdf()
+ .msg();
+ node_properties_t S_UNSTOREPC = node_builder_t(eSpecType::UNSTORE_PC)
+ .sdf()
+ .msg()
+ .ex1b(eSpecPicker::NODE);
+}
diff --git a/src/scenario/special-condition.cpp b/src/scenario/special-condition.cpp
new file mode 100644
index 000000000..303636a42
--- /dev/null
+++ b/src/scenario/special-condition.cpp
@@ -0,0 +1,112 @@
+//
+// special-condition.cpp
+// Common
+//
+// Created by Celtic Minstrel on 2025-02-20.
+//
+
+#include "special.hpp"
+
+// Note: If adding a new node type below, be sure to adjust the end point here too.
+node_category_info_t CAT_COND{eSpecType::IF_SDF, eSpecType::IF_QUEST};
+
+namespace {
+ node_properties_t S_SDF = node_builder_t(eSpecType::IF_SDF)
+ .sdf()
+ .ex1b(eSpecPicker::NODE)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_TOWN = node_builder_t(eSpecType::IF_TOWN_NUM)
+ .ex1a(STRT_TOWN)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_RANDOM = node_builder_t(eSpecType::IF_RANDOM)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_SPECITEM = node_builder_t(eSpecType::IF_HAVE_SPECIAL_ITEM)
+ .ex1a(STRT_SPEC_ITEM)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_SDFCMP = node_builder_t(eSpecType::IF_SDF_COMPARE)
+ .sdf()
+ .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)
+ .ex2a(STRT_TER)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_ALIVE = node_builder_t(eSpecType::IF_ALIVE)
+ .ex1a(STRT_STATUS)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_GOLD = node_builder_t(eSpecType::IF_HAS_GOLD)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_FOOD = node_builder_t(eSpecType::IF_HAS_FOOD)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_ITEM_THERE = node_builder_t(eSpecType::IF_ITEM_CLASS_ON_SPACE)
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_ITEM_OWNED = node_builder_t(eSpecType::IF_HAVE_ITEM_CLASS)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_ITEM_EQUIP = node_builder_t(eSpecType::IF_EQUIP_ITEM_CLASS)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_MAGE = node_builder_t(eSpecType::IF_MAGE_SPELL)
+ .ex1a(STRT_MAGE)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_PRIEST = node_builder_t(eSpecType::IF_PRIEST_SPELL)
+ .ex1a(STRT_PRIEST)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_ALCHEMY = node_builder_t(eSpecType::IF_RECIPE)
+ .ex1a(STRT_ALCHEMY)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_STATUS = node_builder_t(eSpecType::IF_STATUS)
+ .ex1a(eSpecPicker::STATUS)
+ .ex1b(eSpecPicker::NODE)
+ .ex2b(STRT_ACCUM)
+ .ex2c(STRT_CMP);
+ node_properties_t S_LOOK = node_builder_t(eSpecType::IF_LOOKING)
+ .ex1c(eSpecPicker::NODE);
+ 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()
+ .msg1(eSpecPicker::FIELD)
+ .msg2(eSpecPicker::NODE);
+ node_properties_t S_PARTY_SIZE = node_builder_t(eSpecType::IF_PARTY_SIZE)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_EVENT = node_builder_t(eSpecType::IF_EVENT_OCCURRED)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_RACE = node_builder_t(eSpecType::IF_SPECIES)
+ .ex1a(STRT_RACE)
+ .ex1b(eSpecPicker::NODE)
+ .ex2b(STRT_CMP);
+ node_properties_t S_TRAIT = node_builder_t(eSpecType::IF_TRAIT)
+ .ex1a(STRT_TRAIT)
+ .ex1b(eSpecPicker::NODE)
+ .ex2b(STRT_CMP);
+ node_properties_t S_STAT = node_builder_t(eSpecType::IF_STATISTIC)
+ .ex1b(eSpecPicker::NODE)
+ .ex2a(+STRT_SKILL)
+ .ex2b(STRT_ACCUM);
+ node_properties_t S_TEXT = node_builder_t(eSpecType::IF_TEXT_RESPONSE)
+ .msg1(+eSpecPicker::MSG_SINGLE)
+ .ex1a(+eSpecPicker::MSG_SINGLE)
+ .ex1b(eSpecPicker::NODE)
+ .ex2a(+eSpecPicker::MSG_SINGLE)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_SDFEQ = node_builder_t(eSpecType::IF_SDF_EQ)
+ .sdf()
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_CONTEXT = node_builder_t(eSpecType::IF_CONTEXT)
+ .ex1a(STRT_CONTEXT)
+ .ex1c(eSpecPicker::NODE);
+ node_properties_t S_NUM = node_builder_t(eSpecType::IF_NUM_RESPONSE)
+ .msg1(+eSpecPicker::MSG_SINGLE)
+ .ex1b(STRT_CMP)
+ .ex1c(eSpecPicker::NODE)
+ .ex2b(STRT_CMP)
+ .ex2c(eSpecPicker::NODE);
+ node_properties_t S_BOAT = node_builder_t(eSpecType::IF_IN_BOAT)
+ .ex1c(eSpecPicker::NODE);
+ node_properties_t S_HORSE = node_builder_t(eSpecType::IF_ON_HORSE)
+ .ex1c(eSpecPicker::NODE);
+ node_properties_t S_QUEST = node_builder_t(eSpecType::IF_QUEST)
+ .ex1a(STRT_QUEST)
+ .ex1b(STRT_QUEST_STATUS)
+ .ex1c(eSpecPicker::NODE);
+}
diff --git a/src/scenario/special-general.cpp b/src/scenario/special-general.cpp
new file mode 100644
index 000000000..ae94a0bbe
--- /dev/null
+++ b/src/scenario/special-general.cpp
@@ -0,0 +1,139 @@
+//
+// special-general.cpp
+// BoE
+//
+// Created by Celtic Minstrel on 2025-02-18.
+//
+
+#include "special.hpp"
+
+// Note: If adding a new node type below, be sure to adjust the end point here too.
+node_category_info_t CAT_GENERAL{eSpecType::NONE, eSpecType::STR_BUF_TO_SIGN};
+
+namespace{
+ node_properties_t S_NONE = node_builder_t(eSpecType::NONE);
+ node_properties_t S_SETFLAG = node_builder_t(eSpecType::SET_SDF)
+ .sdf()
+ .msg();
+ node_properties_t S_INCFLAG = node_builder_t(eSpecType::INC_SDF)
+ .sdf()
+ .msg();
+ node_properties_t S_MSG = node_builder_t(eSpecType::DISPLAY_MSG)
+ .msg();
+ node_properties_t S_SHOP = node_builder_t(eSpecType::ENTER_SHOP)
+ .msg1(eSpecPicker::MSG_SINGLE)
+ .ex1a(STRT_SHOP)
+ .ex1b(STRT_COST_ADJ)
+ .jump(eSpecPicker::NONE);
+ node_properties_t S_MSG_SM = node_builder_t(eSpecType::DISPLAY_SM_MSG)
+ .msg();
+ node_properties_t S_FLIPFLAG = node_builder_t(eSpecType::FLIP_SDF)
+ .sdf()
+ .msg();
+ node_properties_t S_RANDFLAG = node_builder_t(eSpecType::SDF_RANDOM)
+ .sdf()
+ .msg();
+ node_properties_t S_ADDFLAG = node_builder_t(eSpecType::SDF_ADD)
+ .sdf()
+ .msg();
+ node_properties_t S_SUBFLAG = node_builder_t(eSpecType::SDF_DIFF)
+ .sdf()
+ .msg();
+ node_properties_t S_STORY = node_builder_t(eSpecType::STORY_DIALOG)
+ .msg1(eSpecPicker::MSG_SINGLE)
+ .pic();
+ node_properties_t S_PREVENT = node_builder_t(eSpecType::CANT_ENTER)
+ .msg();
+ node_properties_t S_TIME = node_builder_t(eSpecType::CHANGE_TIME)
+ .msg();
+ node_properties_t S_TIMER = node_builder_t(eSpecType::SCEN_TIMER_START)
+ .msg()
+ .ex1b(+eSpecPicker::NODE);
+ node_properties_t S_SND = node_builder_t(eSpecType::PLAY_SOUND)
+ .ex1a(eSpecPicker::SOUND);
+ node_properties_t S_HORSE_OWN = node_builder_t(eSpecType::CHANGE_HORSE_OWNER)
+ .msg();
+ node_properties_t S_BOAT_OWN = node_builder_t(eSpecType::CHANGE_BOAT_OWNER)
+ .msg();
+ node_properties_t S_TOWN_VIS = node_builder_t(eSpecType::SET_TOWN_VISIBILITY)
+ .msg()
+ .ex1a(STRT_TOWN);
+ node_properties_t S_EVENT = node_builder_t(eSpecType::MAJOR_EVENT_OCCURRED)
+ .msg();
+ node_properties_t S_FORCEGIVE = node_builder_t(eSpecType::FORCED_GIVE)
+ .msg()
+ .ex1a(STRT_ITEM)
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_BUYTYPE = node_builder_t(eSpecType::BUY_ITEMS_OF_TYPE)
+ .msg()
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_GLOBAL = node_builder_t(eSpecType::CALL_GLOBAL)
+ .jump(+eSpecPicker::NODE);
+ node_properties_t S_SETROW = node_builder_t(eSpecType::SET_SDF_ROW);
+ node_properties_t S_COPYFLAG = node_builder_t(eSpecType::COPY_SDF)
+ .sdf()
+ .sdf(eSpecField::EX1A, eSpecField::EX1B);
+ node_properties_t S_PICTURE = node_builder_t(eSpecType::DISPLAY_PICTURE)
+ .msg1(eSpecPicker::MSG_SINGLE)
+ .ex1a(PIC_FULL);
+ node_properties_t S_REST = node_builder_t(eSpecType::REST)
+ .msg();
+ node_properties_t S_MSG_TITLE = node_builder_t(eSpecType::TITLED_MSG)
+ .msg()
+ .msg3(eSpecPicker::MSG_SINGLE)
+ .pic();
+ node_properties_t S_END_SCEN = node_builder_t(eSpecType::END_SCENARIO);
+ node_properties_t S_SETPTR = node_builder_t(eSpecType::SET_POINTER)
+ .sdf();
+ node_properties_t S_CAMPFLAG = node_builder_t(eSpecType::SET_CAMP_FLAG)
+ .sdf()
+ .msg1(+eSpecPicker::MSG_SINGLE);
+ node_properties_t S_DEBUG = node_builder_t(eSpecType::PRINT_NUMS)
+ .sdf();
+ node_properties_t S_MULFLAG = node_builder_t(eSpecType::SDF_TIMES)
+ .sdf()
+ .msg();
+ node_properties_t S_DIVFLAG = node_builder_t(eSpecType::SDF_DIVIDE)
+ .sdf()
+ .sdf(eSpecField::EX1C, eSpecField::EX2C)
+ .msg();
+ node_properties_t S_EXPFLAG = node_builder_t(eSpecType::SDF_POWER)
+ .sdf()
+ .msg();
+ node_properties_t S_TERCHANGE = node_builder_t(eSpecType::CHANGE_TER)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(STRT_TER);
+ node_properties_t S_TERSWAP = node_builder_t(eSpecType::SWAP_TER)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(STRT_TER)
+ .ex2b(STRT_TER);
+ node_properties_t S_TERTRANS = node_builder_t(eSpecType::TRANS_TER)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(STRT_TER);
+ 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);
+ node_properties_t S_BUF_ADDNUM = node_builder_t(eSpecType::APPEND_NUM);
+ node_properties_t S_BUF_ADDMONST = node_builder_t(eSpecType::APPEND_MONST)
+ .ex1a(STRT_MONST);
+ node_properties_t S_BUF_ADDITEM = node_builder_t(eSpecType::APPEND_ITEM)
+ .ex1a(STRT_ITEM);
+ node_properties_t S_BUF_ADDTER = node_builder_t(eSpecType::APPEND_TER)
+ .ex1a(STRT_TER);
+ node_properties_t S_PAUSE = node_builder_t(eSpecType::PAUSE);
+ node_properties_t S_TALK = node_builder_t(eSpecType::START_TALK)
+ .pict(PIC_TALK)
+ .ex1b(STRT_MONST)
+ .jump(eSpecPicker::NONE);
+ node_properties_t S_QUEST = node_builder_t(eSpecType::UPDATE_QUEST)
+ .msg()
+ .ex1a(STRT_QUEST)
+ .ex1b(STRT_QUEST_STATUS);
+ node_properties_t S_BUF_SWAP = node_builder_t(eSpecType::SWAP_STR_BUF)
+ .msg();
+ node_properties_t S_ALTER_SIGN = node_builder_t(eSpecType::STR_BUF_TO_SIGN)
+ .msg();
+}
diff --git a/src/scenario/special-oneshot.cpp b/src/scenario/special-oneshot.cpp
new file mode 100644
index 000000000..273029f0e
--- /dev/null
+++ b/src/scenario/special-oneshot.cpp
@@ -0,0 +1,57 @@
+//
+// special-oneshot.cpp
+// Common
+//
+// Created by Celtic Minstrel on 2025-02-19.
+//
+
+#include "special.hpp"
+
+// Note: If adding a new node type below, be sure to adjust the end point here too.
+node_category_info_t CAT_ONCE{eSpecType::ONCE_GIVE_ITEM, eSpecType::ONCE_TRAP};
+
+namespace {
+ node_properties_t S_GIVE_ITEM = node_builder_t(eSpecType::ONCE_GIVE_ITEM)
+ .sdf()
+ .msg()
+ .ex1a(STRT_ITEM)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_GIVE_SPECITEM = node_builder_t(eSpecType::ONCE_GIVE_SPEC_ITEM)
+ .sdf()
+ .msg()
+ .ex1a(STRT_SPEC_ITEM);
+ node_properties_t S_NONE = node_builder_t(eSpecType::ONCE_NULL)
+ .sdf();
+ node_properties_t S_SETSDF = node_builder_t(eSpecType::ONCE_SET_SDF)
+ .sdf();
+ node_properties_t S_MSG = node_builder_t(eSpecType::ONCE_DISPLAY_MSG)
+ .sdf()
+ .msg();
+ node_properties_t S_DIALOG = node_builder_t(eSpecType::ONCE_DIALOG)
+ .sdf()
+ .msg1(eSpecPicker::MSG_SEQUENCE)
+ .pic()
+ .ex1a(STRT_BUTTON)
+ .ex2a(STRT_BUTTON)
+ .ex1b(eSpecPicker::NODE)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_ITEM_DIALOG = node_builder_t(eSpecType::ONCE_GIVE_ITEM_DIALOG)
+ .sdf()
+ .msg()
+ .msg3(STRT_SPEC_ITEM)
+ .pic()
+ .ex1a(STRT_ITEM)
+ .ex2b(eSpecPicker::NODE);
+ node_properties_t S_OUTENC = node_builder_t(eSpecType::ONCE_OUT_ENCOUNTER)
+ .sdf()
+ .msg();
+ node_properties_t S_TOWNENV = node_builder_t(eSpecType::ONCE_TOWN_ENCOUNTER)
+ .sdf()
+ .msg();
+ node_properties_t S_TRAP = node_builder_t(eSpecType::ONCE_TRAP)
+ .sdf()
+ .msg()
+ .pic()
+ .ex1a(STRT_TRAP)
+ .ex2b(+eSpecPicker::NODE);
+}
diff --git a/src/scenario/special-outdoor.cpp b/src/scenario/special-outdoor.cpp
new file mode 100644
index 000000000..97e487308
--- /dev/null
+++ b/src/scenario/special-outdoor.cpp
@@ -0,0 +1,24 @@
+//
+// special-outdoor.cpp
+// Common
+//
+// Created by Celtic Minstrel on 2025-02-20.
+//
+
+#include "special.hpp"
+
+// Note: If adding a new node type below, be sure to adjust the end point here too.
+node_category_info_t CAT_OUTD{eSpecType::OUT_MAKE_WANDER, eSpecType::OUT_MOVE_PARTY};
+
+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()
+ .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);
+}
diff --git a/src/scenario/special-rect.cpp b/src/scenario/special-rect.cpp
new file mode 100644
index 000000000..627117685
--- /dev/null
+++ b/src/scenario/special-rect.cpp
@@ -0,0 +1,46 @@
+//
+// special-rect.cpp
+// Common
+//
+// Created by Celtic Minstrel on 2025-02-20.
+//
+
+#include "special.hpp"
+
+// Note: If adding a new node type below, be sure to adjust the end point here too.
+node_category_info_t CAT_RECT{eSpecType::RECT_PLACE_FIELD, eSpecType::RECT_UNLOCK};
+
+namespace {
+ node_properties_t S_FIELDS = node_builder_t(eSpecType::RECT_PLACE_FIELD)
+ .msg()
+ .rect()
+ .sdf2(+eSpecPicker::FIELD);
+ node_properties_t S_EXPLORE = node_builder_t(eSpecType::RECT_SET_EXPLORED)
+ .msg()
+ .rect();
+ node_properties_t S_MOVE = node_builder_t(eSpecType::RECT_MOVE_ITEMS)
+ .msg()
+ .rect()
+ .loc(eSpecField::SDF1, eSpecField::SDF2);
+ node_properties_t S_DESTROY = node_builder_t(eSpecType::RECT_DESTROY_ITEMS)
+ .msg()
+ .rect();
+ node_properties_t S_CHANGE = node_builder_t(eSpecType::RECT_CHANGE_TER)
+ .msg()
+ .rect()
+ .sdf1(STRT_TER);
+ node_properties_t S_SWAP = node_builder_t(eSpecType::RECT_SWAP_TER)
+ .msg()
+ .rect()
+ .sdf1(STRT_TER)
+ .sdf2(STRT_TER);
+ node_properties_t S_TRANS = node_builder_t(eSpecType::RECT_TRANS_TER)
+ .msg()
+ .rect();
+ node_properties_t S_LOCK = node_builder_t(eSpecType::RECT_LOCK)
+ .msg()
+ .rect();
+ node_properties_t S_UNLOCK = node_builder_t(eSpecType::RECT_UNLOCK)
+ .msg()
+ .rect();
+}
diff --git a/src/scenario/special-town.cpp b/src/scenario/special-town.cpp
new file mode 100644
index 000000000..51f17cec1
--- /dev/null
+++ b/src/scenario/special-town.cpp
@@ -0,0 +1,136 @@
+//
+// special-town.cpp
+// Common
+//
+// Created by Celtic Minstrel on 2025-02-20.
+//
+
+#include "special.hpp"
+
+// Note: If adding a new node type below, be sure to adjust the end point here too.
+node_category_info_t CAT_TOWN{eSpecType::MAKE_TOWN_HOSTILE, eSpecType::TOWN_PLACE_LABEL};
+
+namespace {
+ node_properties_t S_ATTITUDE = node_builder_t(eSpecType::MAKE_TOWN_HOSTILE)
+ .msg()
+ .ex2a(STRT_ATTITUDE);
+ 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)
+ .ex2c(eSpecPicker::SOUND);
+ node_properties_t S_ATTACK = node_builder_t(eSpecType::TOWN_MONST_ATTACK)
+ .msg();
+ node_properties_t S_BOOM = node_builder_t(eSpecType::TOWN_BOOM_SPACE)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(PIC_BOOM)
+ .ex2c(eSpecPicker::SOUND);
+ node_properties_t S_TELEPORT = node_builder_t(eSpecType::TOWN_MOVE_PARTY)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B);
+ node_properties_t S_HIT = node_builder_t(eSpecType::TOWN_HIT_SPACE)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2b(eSpecPicker::DAMAGE_TYPE);
+ node_properties_t S_EXPLODE = node_builder_t(eSpecType::TOWN_EXPLODE_SPACE)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2b(eSpecPicker::DAMAGE_TYPE);
+ node_properties_t S_LOCK = node_builder_t(eSpecType::TOWN_LOCK_SPACE)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B);
+ node_properties_t S_UNLOCK = node_builder_t(eSpecType::TOWN_UNLOCK_SPACE)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B);
+ node_properties_t S_BURST = node_builder_t(eSpecType::TOWN_SFX_BURST)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(eSpecPicker::EXPLOSION)
+ .ex2c(eSpecPicker::SOUND);
+ node_properties_t S_WANDER = node_builder_t(eSpecType::TOWN_CREATE_WANDERING)
+ .msg();
+ node_properties_t S_SPAWN = node_builder_t(eSpecType::TOWN_PLACE_MONST)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(STRT_MONST);
+ node_properties_t S_KILL = node_builder_t(eSpecType::TOWN_DESTROY_MONST)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B);
+ 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);
+ 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)
+ .ex2a(STRT_TOWN)
+ .ex2b(STRT_STAIR)
+ .ex2c(STRT_STAIR_MODE)
+ .jump(eSpecPicker::NONE);
+ node_properties_t S_LEVER = node_builder_t(eSpecType::TOWN_LEVER)
+ .msg1(eSpecPicker::MSG_SEQUENCE)
+ .pic()
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_PORTAL = node_builder_t(eSpecType::TOWN_PORTAL)
+ .msg1(eSpecPicker::MSG_SEQUENCE)
+ .pic()
+ .loc(eSpecField::EX1A, eSpecField::EX1B);
+ node_properties_t S_STAIR = node_builder_t(eSpecType::TOWN_STAIR)
+ .msg1(eSpecPicker::MSG_SEQUENCE)
+ .pic()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(STRT_TOWN)
+ .ex2c(STRT_STAIR_MODE)
+ .jump(eSpecPicker::NONE);
+ node_properties_t S_OUTDOOR = node_builder_t(eSpecType::TOWN_RELOCATE)
+ .msg()
+ .loc(eSpecField::EX2A, eSpecField::EX2B);
+ node_properties_t S_ITEM = node_builder_t(eSpecType::TOWN_PLACE_ITEM)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .ex2a(STRT_ITEM);
+ node_properties_t S_SPLIT = node_builder_t(eSpecType::TOWN_SPLIT_PARTY)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B);
+ node_properties_t S_REUNITE = node_builder_t(eSpecType::TOWN_REUNITE_PARTY)
+ .msg();
+ node_properties_t S_TIMER = node_builder_t(eSpecType::TOWN_TIMER_START)
+ .msg()
+ .ex1b(eSpecPicker::NODE);
+ node_properties_t S_LIGHT = node_builder_t(eSpecType::TOWN_CHANGE_LIGHTING)
+ .msg()
+ .ex1a(STRT_LIGHT);
+ node_properties_t S_CHARM = node_builder_t(eSpecType::TOWN_SET_ATTITUDE)
+ .msg()
+ .ex1b(STRT_ATTITUDE);
+ node_properties_t S_CAMERA = node_builder_t(eSpecType::TOWN_SET_CENTER)
+ .msg();
+ node_properties_t S_FOG = node_builder_t(eSpecType::TOWN_LIFT_FOG)
+ .msg();
+ node_properties_t S_TARGET = node_builder_t(eSpecType::TOWN_START_TARGETING)
+ .msg()
+ .ex1a(STRT_SPELL_PAT);
+ node_properties_t S_FIELDS = node_builder_t(eSpecType::TOWN_SPELL_PAT_FIELD)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .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)
+ .ex1c(+STRT_SPELL_PAT)
+ .ex2a(eSpecPicker::DAMAGE_TYPE);
+ node_properties_t S_WARP = node_builder_t(eSpecType::TOWN_RELOCATE_CREATURE)
+ .msg()
+ .loc(eSpecField::EX1A, eSpecField::EX1B)
+ .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);
+}
diff --git a/src/scenario/special.cpp b/src/scenario/special.cpp
index 2b8b9cbd5..63efc8ea0 100644
--- a/src/scenario/special.cpp
+++ b/src/scenario/special.cpp
@@ -528,172 +528,11 @@ void cSpecial::import_legacy(legacy::special_node_type& old){
}
}
-// Key:
-// space - no button
-// m - Create/Edit button to edit message pair (covers msg1 and msg2 together)
-// M - Create/Edit button to edit single message
-// $ - As above, but always a scenario message
-// d - Create/Edit button to edit dialog message sequence (covers msg1 and msg2 together)
-// b - Choose button to select a button label
-// p - Choose button to select a picture (uses pictype for type)
-// ? - Choose button to select a picture type
-// s - Create/Edit button to edit special node
-// S - As above, but always a scenario node
-// x - Choose button to select a sound
-// X - Choose button to select a trap type
-// T - Choose button to select a town
-// i - Choose button to select an item
-// I - Choose button to select a special item
-// t - Choose button to select a terrain type
-// c - Choose button to select a monster type
-// C - Choose button to select a monster statistic
-// a - Choose button to select an alchemy recipe
-// A - Choose button to select a mage spell
-// P - Choose button to select a priest spell
-// k - Choose button to select a skill
-// K - As above, but add the special pseudo-skills for the if-statistic node
-// f - Choose button to select a field type
-// F - As above, but also include Dispel and Smash
-// q - Choose button to select a trait
-// Q - Choose button to select a species
-// = - Choose button to select a comparison method (<=, <, =, >, >=)
-// + - Choose button to select stat cumulation mode
-// * - Choose button to select a special node context
-// @ - Choose button to select a monster attitude
-// D - Choose button to select a damage type
-// ! - Choose button to select an explosion animation type
-// / - Choose button to select generic stairway text
-// : - Choose stairway trigger conditions
-// L - Choose button to select a town lighting type
-// & - Choose button to select a shop
-// % - Choose button to select shop cost adjustment
-// { - Choose button to select a spell pattern
-// } - As above, but allows you to select which version of the rotateable field
-// ^ - Choose button to select a positioning mode
-// e - Choose button to select a status effect
-// E - Choose button to select a party status effect
-// w - Choose button to select main party status effect
-// j - Choose button to select a quest
-// J - Choose button to select a quest status
-// < - Choose button to select a cardinal direction
-// ~ - Choose button to select a weapon enchantment
-// _ - Choose button to select a full sheet
-// 0..9 - Choose button to select a specific type of picture
-// (terrain, monster, dialog, talk, item, pc, field, boom, missile, status)
-static const char*const button_dict[7][11] = {
- { // general nodes
- " mmmMmmmmmMmmm mmmmmm Mmm $ mmmmmm mmm", // msg1
- " ", // msg2
- " M ", // msg3
- " p p 3 ", // pic
- " ? ? ", // pictype
- " & x T i _ M cit j ", // ex1a
- " % S ss cJ ", // ex1b
- " ", // ex1c
- " tt ", // ex2a
- " t ", // ex2b
- " ", // ex2c
- }, { // one-shot nodes
- "mm md d mmm", // msg1
- " ", // msg2
- " III ", // msg3
- " p p p", // pic
- " ? ? ?", // pictype
- "iI b i X", // ex1a
- " s ", // ex1b
- " ", // ex1c
- " b ", // ex2a
- "s s s S", // ex2b
- " ", // ex2c
- }, { // affect pc nodes
- "mmmmmmmmmmmmmmmmmmmmmmmmmmmm", // msg1
- " ", // msg2
- " M M ", // msg3
- " s 5 ", // pic
- " s ", // pictype
- " w q i AP a ", // ex1a
- " ~ s", // ex1b
- " e Q ", // ex1c
- " CK E ", // ex2a
- " D ", // ex2b
- " x ", // ex2c
- }, { // if-then nodes
- " f $ $ ", // msg1
- " s ", // msg2
- " ", // msg3
- " ", // pic
- " ", // pictype
- " T I w APae Qq $ * j", // ex1a
- "ssss sss ssssss s s sssss = J", // ex1b
- " s sssss", // ex1c
- " t K$ ", // ex2a
- "s ss s + s==+s = ", // ex2b
- " = s ", // ex2c
- }, { // town nodes
- "mmmmmmmmmmmmmm dddmmmmmmmmmmmmmM", // msg1
- " ", // msg2
- " ", // msg3
- " 8 ppp ", // pic
- " ??? ", // pictype
- " c L { ", // ex1a
- " s s s s @ ", // ex1b
- " }} ", // ex1c
- "@ 7 ! c T T i FD ", // ex2a
- " DD / ^ ", // ex2b
- " x x x : : ", // ex2c
- }, { // rectangle nodes
- "mmmmmmmmm", // msg1
- " ", // msg2
- " ", // msg3
- " ", // pic
- " ", // pictype
- " tt ", // sdf1
- " ", // unused
- " ", // ex1c
- "F t ", // sdf2
- " ", // unused
- " ", // ex2c
- }, { // outdoors nodes
- " mmm", // msg1
- " ", // msg2
- " ", // msg3
- " ", // pic
- " ", // pictype
- " T ", // ex1a
- " < ", // ex1b
- " ", // ex1c
- " ", // ex2a
- " ", // ex2b
- " ", // ex2c
- }
-};
-
-static int offsets[] = {
- int(eSpecType::NONE),
- int(eSpecType::ONCE_GIVE_ITEM),
- int(eSpecType::SELECT_TARGET),
- int(eSpecType::IF_SDF),
- int(eSpecType::MAKE_TOWN_HOSTILE),
- int(eSpecType::RECT_PLACE_FIELD),
- int(eSpecType::OUT_MAKE_WANDER),
-};
-
static eSpecCat getNodeCategory(eSpecType node) {
- int code = (int) node;
- if(code >= 0 && code <= 47)
- return eSpecCat::GENERAL;
- if(code >= 50 && code <= 63)
- return eSpecCat::ONCE;
- if(code >= 80 && code <= 107)
- return eSpecCat::AFFECT;
- if(code >= 130 && code <= 160)
- return eSpecCat::IF_THEN;
- if(code >= 170 && code <= 204)
- return eSpecCat::TOWN;
- if(code >= 210 && code <= 218)
- return eSpecCat::RECT;
- if(code >= 225 && code <= 228)
- return eSpecCat::OUTDOOR;
+ for(int i = 0; i <= int(eSpecCat::OUTDOOR); i++) {
+ eSpecCat cat = eSpecCat(i);
+ if((*cat).contains(node)) return cat;
+ }
return eSpecCat::INVALID;
}
@@ -702,68 +541,43 @@ static std::map& nodeProps() {
return props;
}
-void node_properties_t::load() {
- std::map& allNodeProps = nodeProps();
- // These are the node types that should not have a Create/Edit button on the JumpTo
- static std::set dead_ends = {eSpecType::ENTER_SHOP, eSpecType::START_TALK, eSpecType::TOWN_GENERIC_STAIR, eSpecType::TOWN_STAIR};
- // There's really no need to check all the way to the max of the underlying type.
- // It's unlikely we'd go above 255, so unsigned char would be fine, but just in case,
- // let's use unsigned short.
- // Could change the actual enum's underlying type instead though?
- using underlying = unsigned short;//std::underlying_type::type;
- for(underlying i = 0; i < std::numeric_limits::max(); i++) {
- eSpecType check = (eSpecType) i;
- eSpecCat category = getNodeCategory(check);
- if(category == eSpecCat::INVALID) continue;
- node_properties_t props;
- props.self = check;
- props.cat = category;
- int j = int(category), k = i - offsets[j];
- props.f_m1 = button_dict[j][0][k];
- props.f_m2 = button_dict[j][1][k];
- props.f_m3 = button_dict[j][2][k];
- props.f_pic = button_dict[j][3][k];
- props.f_pt = button_dict[j][4][k];
- if(category != eSpecCat::RECT) {
- props.f_sd1 = ' ';
- props.f_x1a = button_dict[j][5][k];
- props.f_x1b = button_dict[j][6][k];
- } else props.f_sd1 = button_dict[j][5][k];
- props.f_x1c = button_dict[j][7][k];
- if(category != eSpecCat::RECT) {
- props.f_sd2 = ' ';
- props.f_x2a = button_dict[j][8][k];
- props.f_x2b = button_dict[j][9][k];
- } else props.f_sd2 = button_dict[j][8][k];
- props.f_x2c = button_dict[j][10][k];
- if(category == eSpecCat::RECT) {
- props.f_x1a = props.f_x2a = ' ';
- props.f_x1b = props.f_x2b = ' ';
- }
- if(dead_ends.count(check)) {
- props.f_jmp = ' ';
- } else {
- props.f_jmp = check == eSpecType::CALL_GLOBAL ? 'S' : 's';
- }
- props.f_sd1.lbl_idx = 1; props.f_sd2.lbl_idx = 2;
- props.f_m1.lbl_idx = 3; props.f_m2.lbl_idx = 4; props.f_m3.lbl_idx = 5;
- props.f_pic.lbl_idx = 6; props.f_pt.lbl_idx = 7;
- props.f_x1a.lbl_idx = 8; props.f_x1b.lbl_idx = 9; props.f_x1c.lbl_idx = 10;
- props.f_x2a.lbl_idx = 11; props.f_x2b.lbl_idx = 12; props.f_x2c.lbl_idx = 13;
- props.f_jmp.lbl_idx = 14;
- props.f_sd1.self = props.f_sd2.self = props.f_m1.self = props.f_m2.self = props.f_m3.self = props.f_pic.self = props.f_pt.self = props.f_x1a.self = props.f_x1b.self = props.f_x1c.self = props.f_x2a.self = props.f_x2b.self = props.f_x2c.self = props.f_jmp.self = props.self;
- allNodeProps[check] = props;
- }
-}
-
const node_properties_t& operator* (eSpecType t) {
static node_properties_t invalid;
std::map& allNodeProps = nodeProps();
- if(allNodeProps.empty()) node_properties_t::load();
auto iter = allNodeProps.find(t);
return iter == allNodeProps.end() ? invalid : iter->second;
}
+// Note: While it might seem like it would be simpler to define these in a map,
+// having each one as a separate extern variable serves an important purpose:
+// it prevents the special node definitions from being optimized away by the linker.
+const node_category_info_t& operator* (eSpecCat t) {
+ extern node_category_info_t CAT_GENERAL, CAT_ONCE, CAT_AFFECT, CAT_COND, CAT_TOWN, CAT_RECT, CAT_OUTD;
+ static node_category_info_t CAT_INVALID;
+ switch(t) {
+ case eSpecCat::GENERAL: return CAT_GENERAL;
+ case eSpecCat::ONCE: return CAT_ONCE;
+ case eSpecCat::AFFECT: return CAT_AFFECT;
+ case eSpecCat::IF_THEN: return CAT_COND;
+ case eSpecCat::TOWN: return CAT_TOWN;
+ case eSpecCat::RECT: return CAT_RECT;
+ case eSpecCat::OUTDOOR: return CAT_OUTD;
+ case eSpecCat::INVALID: return CAT_INVALID;
+ }
+ return CAT_INVALID;
+}
+
+bool node_category_info_t::contains(eSpecType spec) const {
+ int code = (int) spec;
+ return code >= int(first) && code <= int(last);
+}
+
+node_properties_t::node_properties_t(eSpecType type)
+ : self(type)
+ , cat(getNodeCategory(type))
+ , f_jmp(eSpecPicker::NODE)
+{}
+
std::string node_properties_t::opcode() const {
if(self == eSpecType::NONE) return "nop";
return get_str("specials-opcodes", int(self));
@@ -771,8 +585,8 @@ std::string node_properties_t::opcode() const {
static std::string get_node_string(std::string base, eSpecType type, int which) {
eSpecCat cat = getNodeCategory(type);
- int i = int(cat), j = int(type);
- int strnum = (j - offsets[i]) * 17 + which + 1;
+ int offset = int((*cat).first), i = int(type);
+ int strnum = (i - offset) * 17 + which + 1;
switch(cat) {
case eSpecCat::GENERAL:
return get_str(base + "-general", strnum);
@@ -804,70 +618,56 @@ std::string node_properties_t::descr() const {
node_function_t::node_function_t() {}
-node_function_t::node_function_t(char c) {
- switch(c) {
- case ' ': button = eSpecPicker::NONE; break;
- case 'm': button = eSpecPicker::MSG_PAIR; force_global = false; break;
- case 'M': button = eSpecPicker::MSG_SINGLE; force_global = false; break;
- case '$': button = eSpecPicker::MSG_SINGLE; force_global = true; break;
- case 'd': button = eSpecPicker::MSG_SEQUENCE; force_global = false; break;
- case 'b': button = eSpecPicker::STRING; str_type = STRT_BUTTON; break;
- case 'p': button = eSpecPicker::PICTURE; pic_type = PIC_NONE; break;
- case '?': button = eSpecPicker::STRING; str_type = STRT_PICT; adjust = -1; break;
- case 's': button = eSpecPicker::NODE; force_global = false; break;
- case 'S': button = eSpecPicker::NODE; force_global = true; break;
- case 'x': button = eSpecPicker::SOUND; break;
- case 'X': button = eSpecPicker::STRING; str_type = STRT_TRAP; break;
- case 't': button = eSpecPicker::STRING; str_type = STRT_TER; break;
- case 'T': button = eSpecPicker::STRING; str_type = STRT_TOWN; break;
- case 'i': button = eSpecPicker::STRING; str_type = STRT_ITEM; break;
- case 'I': button = eSpecPicker::STRING; str_type = STRT_SPEC_ITEM; break;
- case 'c': button = eSpecPicker::STRING; str_type = STRT_MONST; adjust = -1; break;
- case 'C': button = eSpecPicker::STRING; str_type = STRT_MONST_STAT; break;
- case 'a': button = eSpecPicker::STRING; str_type = STRT_ALCHEMY; break;
- case 'A': button = eSpecPicker::STRING; str_type = STRT_MAGE; break;
- case 'P': button = eSpecPicker::STRING; str_type = STRT_PRIEST; break;
- case 'k': button = eSpecPicker::STRING; str_type = STRT_SKILL; augmented = false; break;
- case 'K': button = eSpecPicker::STRING; str_type = STRT_SKILL; augmented = true; break;
- case 'f': button = eSpecPicker::FIELD; augmented = false; break;
- case 'F': button = eSpecPicker::FIELD; augmented = true; break;
- case 'q': button = eSpecPicker::STRING; str_type = STRT_TRAIT; break;
- case 'Q': button = eSpecPicker::STRING; str_type = STRT_RACE; break;
- case '=': button = eSpecPicker::STRING; str_type = STRT_CMP; adjust = 2; break;
- case '+': button = eSpecPicker::STRING; str_type = STRT_ACCUM; adjust = 1; break;
- case '*': button = eSpecPicker::STRING; str_type = STRT_CONTEXT; break;
- case '@': button = eSpecPicker::STRING; str_type = STRT_ATTITUDE; break;
- case 'D': button = eSpecPicker::DAMAGE_TYPE; break;
- case '!': button = eSpecPicker::EXPLOSION; break;
- case '/': button = eSpecPicker::STRING; str_type = STRT_STAIR; break;
- case ':': button = eSpecPicker::STRING; str_type = STRT_STAIR_MODE; break;
- case 'L': button = eSpecPicker::STRING; str_type = STRT_LIGHT; break;
- case '&': button = eSpecPicker::STRING; str_type = STRT_SHOP; break;
- case '%': button = eSpecPicker::STRING; str_type = STRT_COST_ADJ; break;
- case '{': button = eSpecPicker::STRING; str_type = STRT_SPELL_PAT; augmented = false; break;
- case '}': button = eSpecPicker::STRING; str_type = STRT_SPELL_PAT; augmented = true; break;
- case '^': button = eSpecPicker::STRING; str_type = STRT_POS_MODE; break;
- case 'e': button = eSpecPicker::STATUS; break;
- case 'E': button = eSpecPicker::STATUS_PARTY; break;
- case 'w': button = eSpecPicker::STRING; str_type = STRT_STATUS; adjust = 1; break;
- case 'j': button = eSpecPicker::STRING; str_type = STRT_QUEST; break;
- case 'J': button = eSpecPicker::STRING; str_type = STRT_QUEST_STATUS; break;
- case '<': button = eSpecPicker::STRING; str_type = STRT_DIR; break;
- case '~': button = eSpecPicker::STRING; str_type = STRT_ENCHANT; break;
- case '_': button = eSpecPicker::PICTURE; pic_type = PIC_FULL; break;
- case '0': button = eSpecPicker::PICTURE; pic_type = PIC_TER; break;
- case '1': button = eSpecPicker::PICTURE; pic_type = PIC_MONST; break;
- case '2': button = eSpecPicker::PICTURE; pic_type = PIC_DLOG; break;
- case '3': button = eSpecPicker::PICTURE; pic_type = PIC_TALK; break;
- case '4': button = eSpecPicker::PICTURE; pic_type = PIC_ITEM; break;
- case '5': button = eSpecPicker::PICTURE; pic_type = PIC_PC; break;
- case '6': button = eSpecPicker::PICTURE; pic_type = PIC_FIELD; break;
- case '7': button = eSpecPicker::PICTURE; pic_type = PIC_BOOM; break;
- case '8': button = eSpecPicker::PICTURE; pic_type = PIC_MISSILE; break;
- case '9': button = eSpecPicker::PICTURE; pic_type = PIC_STATUS; break;
+node_function_t::node_function_t(eSpecPicker button)
+ : button(button)
+{
+ if(button == eSpecPicker::PICTURE) {
+ pic_type = PIC_NONE;
+ }
+ if(button == eSpecPicker::NODE || button == eSpecPicker::MSG_PAIR || button == eSpecPicker::MSG_SINGLE || button == eSpecPicker::MSG_SEQUENCE) {
+ force_global = false;
+ }
+}
+
+node_function_t::node_function_t(eStrType str)
+ : button(eSpecPicker::STRING)
+ , str_type(str)
+{
+ // Some string types require an adjustment to the index.
+ switch(str) {
+ case STRT_PICT: adjust = -1; break;
+ case STRT_MONST: adjust = -1; break;
+ case STRT_CMP: adjust = 2; break;
+ case STRT_ACCUM: adjust = 1; break;
+ case STRT_STATUS: adjust = 1; break;
+ default: break;
}
}
+node_function_t::node_function_t(ePicType pic)
+ : button(eSpecPicker::PICTURE)
+ , pic_type(pic)
+{}
+
+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) {
+ n.force_global = true;
+ }
+ if(picker == eSpecPicker::FIELD) {
+ n.augmented = true;
+ }
+ return n;
+}
+
+node_function_t operator+(eStrType str) {
+ node_function_t n(str);
+ if(str == STRT_SPELL_PAT || str == STRT_SKILL) {
+ n.augmented = true;
+ }
+ return n;
+}
+
std::string node_function_t::label() const {
return get_node_string("specials-text", self, lbl_idx);
}
@@ -927,3 +727,144 @@ node_function_t node_properties_t::ex2c(const cSpecial&) const {
node_function_t node_properties_t::jump(const cSpecial&) const {
return f_jmp;
}
+
+struct field_map {
+ std::map map = {
+ {eSpecField::SDF1, &node_properties_t::f_sd1},
+ {eSpecField::SDF2, &node_properties_t::f_sd2},
+ {eSpecField::MSG1, &node_properties_t::f_m1},
+ {eSpecField::MSG2, &node_properties_t::f_m2},
+ {eSpecField::MSG3, &node_properties_t::f_m3},
+ {eSpecField::PICT, &node_properties_t::f_pic},
+ {eSpecField::PTYP, &node_properties_t::f_pt},
+ {eSpecField::EX1A, &node_properties_t::f_x1a},
+ {eSpecField::EX1B, &node_properties_t::f_x1b},
+ {eSpecField::EX1C, &node_properties_t::f_x1c},
+ {eSpecField::EX2A, &node_properties_t::f_x2a},
+ {eSpecField::EX2B, &node_properties_t::f_x2b},
+ {eSpecField::EX2C, &node_properties_t::f_x2c},
+ {eSpecField::JUMP, &node_properties_t::f_jmp},
+ };
+};
+
+static field_map& fields() {
+ static field_map map;
+ return map;
+}
+
+node_builder_t& node_builder_t::sdf() {
+ // The intent is to specify that sdf1,sdf2 is a stuff done flag.
+ // But specifying that two fields are a stuff done flag isn't implemented yet.
+ return sdf(eSpecField::SDF1, eSpecField::SDF2);
+}
+
+node_builder_t& node_builder_t::msg() {
+ return msg1(eSpecPicker::MSG_PAIR).msg2(eSpecPicker::NONE);
+};
+
+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);
+};
+
+node_builder_t& node_builder_t::pic() {
+ return pict(eSpecPicker::PICTURE).ptyp(STRT_PICT);
+}
+
+node_builder_t& node_builder_t::sdf1(node_function_t picker) {
+ return field(eSpecField::SDF1, picker);
+}
+
+node_builder_t& node_builder_t::sdf2(node_function_t picker) {
+ return field(eSpecField::SDF2, picker);
+}
+
+node_builder_t& node_builder_t::jump(node_function_t picker) {
+ return field(eSpecField::JUMP, picker);
+}
+
+node_builder_t& node_builder_t::msg1(node_function_t picker) {
+ return field(eSpecField::MSG1, picker);
+}
+
+node_builder_t& node_builder_t::msg2(node_function_t picker) {
+ return field(eSpecField::MSG2, picker);
+}
+
+node_builder_t& node_builder_t::msg3(node_function_t picker) {
+ return field(eSpecField::MSG3, picker);
+}
+
+node_builder_t& node_builder_t::pict(node_function_t picker) {
+ return field(eSpecField::PICT, picker);
+}
+
+node_builder_t& node_builder_t::ptyp(node_function_t picker) {
+ return field(eSpecField::PTYP, picker);
+}
+
+node_builder_t& node_builder_t::ex1a(node_function_t picker) {
+ return field(eSpecField::EX1A, picker);
+}
+
+node_builder_t& node_builder_t::ex1b(node_function_t picker) {
+ return field(eSpecField::EX1B, picker);
+}
+
+node_builder_t& node_builder_t::ex1c(node_function_t picker) {
+ return field(eSpecField::EX1C, picker);
+}
+
+node_builder_t& node_builder_t::ex2a(node_function_t picker) {
+ return field(eSpecField::EX2A, picker);
+}
+
+node_builder_t& node_builder_t::ex2b(node_function_t picker) {
+ return field(eSpecField::EX2B, picker);
+}
+
+node_builder_t& node_builder_t::ex2c(node_function_t picker) {
+ return field(eSpecField::EX2C, picker);
+}
+
+node_builder_t& node_builder_t::field(eSpecField field, node_function_t picker) {
+ picker.self = node.self;
+ node.*fields().map[field] = picker;
+ return *this;
+}
+
+node_builder_t& node_builder_t::sdf(eSpecField a, eSpecField b) {
+ // A stuff done flag picker isn't implemented yet.
+ return *this;
+}
+
+node_builder_t& node_builder_t::loc(eSpecField a, eSpecField b) {
+ // A location picker isn't implemented yet
+ return *this;
+}
+
+node_builder_t::operator node_properties_t() {
+ node.set_label_indices();
+ std::map& allNodeProps = nodeProps();
+ allNodeProps.emplace(node.self, node);
+ return node;
+}
+
+void node_properties_t::set_label_indices() {
+ f_sd1.lbl_idx = 1;
+ f_sd2.lbl_idx = 2;
+ f_m1.lbl_idx = 3;
+ f_m2.lbl_idx = 4;
+ f_m3.lbl_idx = 5;
+ f_pic.lbl_idx = 6;
+ f_pt.lbl_idx = 7;
+ f_x1a.lbl_idx = 8;
+ f_x1b.lbl_idx = 9;
+ f_x1c.lbl_idx = 10;
+ f_x2a.lbl_idx = 11;
+ f_x2b.lbl_idx = 12;
+ f_x2c.lbl_idx = 13;
+ f_jmp.lbl_idx = 14;
+ f_sd1.self = f_sd2.self = f_jmp.self = f_m1.self = f_m2.self = f_m3.self = f_pic.self = f_pt.self = f_x1a.self = f_x1b.self = f_x1c.self = f_x2a.self = f_x2b.self = f_x2c.self = self;
+}
diff --git a/src/scenario/special.hpp b/src/scenario/special.hpp
index 8cd12a398..8cc5a327b 100644
--- a/src/scenario/special.hpp
+++ b/src/scenario/special.hpp
@@ -126,6 +126,13 @@ enum class eSpecCat {
GENERAL, ONCE, AFFECT, IF_THEN, TOWN, RECT, OUTDOOR
};
+struct node_category_info_t {
+ eSpecType first = eSpecType::NONE, last = eSpecType::NONE;
+ node_category_info_t() = default;
+ node_category_info_t(eSpecType a, eSpecType b) : first(a), last(b) {}
+ bool contains(eSpecType spec) const;
+};
+
enum eStrType {
STRT_MONST, STRT_ITEM, STRT_TER, STRT_BUTTON,
STRT_SPEC_ITEM, STRT_MAGE, STRT_PRIEST, STRT_ALCHEMY,
@@ -146,8 +153,10 @@ enum class eSpecPicker {
STATUS, STATUS_PARTY,
};
+enum class eSpecField { SDF1, SDF2, MSG1, MSG2, MSG3, PICT, PTYP, EX1A, EX1B, EX1C, EX2A, EX2B, EX2C, JUMP };
+
struct node_function_t {
- eSpecPicker button;
+ eSpecPicker button = eSpecPicker::NONE;
union {
eStrType str_type; // for eSpecPicker::STRING only
ePicType pic_type; // for eSpecPicker::PICTURE only; PIC_NONE = use pictype field from node
@@ -159,15 +168,21 @@ struct node_function_t {
std::string label() const;
std::string help() const; // maybe we don't need this though? I guess it would be for a hypothetical help button next to each field to give addition info on how that one field works.
node_function_t();
- node_function_t(char c);
+ node_function_t(eSpecPicker button);
+ node_function_t(eStrType str);
+ node_function_t(ePicType pic);
private:
eSpecType self = eSpecType::NONE;
std::string lbl;
int lbl_idx = 0, sub_idx = 0;
bool needs_split = false;
+ friend struct node_builder_t;
friend struct node_properties_t;
};
+node_function_t operator+(eSpecPicker);
+node_function_t operator+(eStrType);
+
struct node_properties_t {
eSpecType self;
eSpecCat cat;
@@ -178,13 +193,75 @@ struct node_properties_t {
node_function_t pic(const cSpecial&) const, pictype(const cSpecial&) const;
node_function_t ex1a(const cSpecial&) const, ex1b(const cSpecial&) const, ex1c(const cSpecial&) const;
node_function_t ex2a(const cSpecial&) const, ex2b(const cSpecial&) const, ex2c(const cSpecial&) const;
- node_properties_t() : self(eSpecType::INVALID), cat(eSpecCat::INVALID) {}
- node_properties_t(std::initializer_list>);
- static void load();
+ node_properties_t() : self(eSpecType::INVALID), cat(eSpecCat::INVALID), f_jmp(eSpecPicker::NODE) {
+ set_label_indices();
+ }
private:
+ node_properties_t(eSpecType type);
node_function_t f_sd1, f_sd2, f_jmp, f_m1, f_m2, f_m3, f_pic, f_pt, f_x1a, f_x1b, f_x1c, f_x2a, f_x2b, f_x2c;
+ void set_label_indices();
+ friend struct node_builder_t;
+ friend struct field_map;
};
const node_properties_t& operator* (eSpecType t);
+const node_category_info_t& operator* (eSpecCat t);
+
+// Builds the information needed to display the correct buttons when editing a special node.
+struct node_builder_t {
+ node_builder_t(eSpecType type) : node(type) {}
+ // Specifies that a particular field should use a specified picker.
+ node_builder_t& field(eSpecField field, node_function_t picker);
+ // Quick overloads for each possible field.
+ node_builder_t& sdf1(node_function_t picker);
+ node_builder_t& sdf2(node_function_t picker);
+ node_builder_t& jump(node_function_t picker);
+ node_builder_t& msg1(node_function_t picker);
+ node_builder_t& msg2(node_function_t picker);
+ node_builder_t& msg3(node_function_t picker);
+ node_builder_t& pict(node_function_t picker);
+ node_builder_t& ptyp(node_function_t picker);
+ node_builder_t& ex1a(node_function_t picker);
+ node_builder_t& ex1b(node_function_t picker);
+ node_builder_t& ex1c(node_function_t picker);
+ node_builder_t& ex2a(node_function_t picker);
+ node_builder_t& ex2b(node_function_t picker);
+ node_builder_t& ex2c(node_function_t picker);
+ // Specifies that sdf1 and sdf2 combine to define an SDF
+ node_builder_t& sdf();
+ // Specifies that msg1 and msg2 specify a pair of local strings
+ node_builder_t& msg();
+ // Specifies that pict and pictype have their typical meaning of defining a picture
+ node_builder_t& pic();
+ // 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();
+ // 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);
+ operator node_properties_t();
+private:
+ node_properties_t node;
+};
+
+// An overview of how the builder works.
+// Most of the time, the node_function_t will be constructed implicitly using
+// either one of its implicit constructors or one of the overloaded unary + operators.
+
+// An eStrType implies a string picker, ie eSpecPicker::STRING.
+// However, passing eSpecPicker::STRING directly doesn't do anything useful.
+
+// An ePicType implies a graphic picker, ie eSpecPicker::PICTURE.
+// Passing eSpecPicker::PICTURE directly means that another field (usually pictype) will determine the type.
+
+// The prefix + has a useful meaning only in a few special cases:
+// +eSpecPicker::NODE means always edit a scenario node, even if the caller is a town or outdoor node.
+// +eSpecPicker::MSG_* is similar – always edit a scenario string, never a town or outdoor string.
+// +eSpecPicker::FIELD adds the pseudo-fields Move Mountains and Dispel to the list.
+// +STRT_SKILL adds read-only pseudo-skills such as "current HP" to the list
+// +STRT_SPELL_PAT expands the "rotateable wall" into all its possible orientations
+// More cases may be added.
#endif
diff --git a/src/scenedit/scen.keydlgs.cpp b/src/scenedit/scen.keydlgs.cpp
index 57a1be27c..ea31dfcac 100644
--- a/src/scenedit/scen.keydlgs.cpp
+++ b/src/scenedit/scen.keydlgs.cpp
@@ -661,15 +661,8 @@ static bool edit_spec_enc_type(cDialog& me, std::string item_hit, node_stack_t&
else if(item_hit == "town") category = eSpecCat::TOWN;
else if(item_hit == "out") category = eSpecCat::OUTDOOR;
else if(item_hit == "rect") category = eSpecCat::RECT;
- int start = -1, finish = -1, current = int(edit_stack.top().node.type);
- for(int i = 0; i < std::numeric_limits::max(); i++) {
- eSpecCat check = (*eSpecType(i)).cat;
- if(start >= 0 && check == eSpecCat::INVALID) {
- finish = i - 1;
- break;
- } else if(check == category && start < 0)
- start = i;
- }
+ auto bounds = *category;
+ int start = int(bounds.first), finish = int(bounds.last), current = int(edit_stack.top().node.type);
if(start < 0 || finish < 0) return true;
std::vector choices;
for(int i = start; i <= finish; i++) {