,
-// // minEra: "lbr" / "aos" / "ml" / "sa" / "hs" / "tol",
-// // maxEra: ...
-// };
-
-const MasonryMap = {};
-
-(function initMasonryMap()
+const craftMapRegistryID = 4038;
+var MasonryMap = {};
+var craftItems = [];
+
+/** @type { () => boolean } */
+function LoadMasonryMap()
{
- // graniteIndex: 0 = iron, 1 = dull copper, ... 8 = valorite
- for( var graniteIndex = 0; graniteIndex < craftItems.length; graniteIndex++ )
+ MasonryMap = {};
+ craftItems = [];
+
+ var masonryEntries = TriggerEvent( craftMapRegistryID, "CraftMapRegistry", "masonry" );
+
+ if( !masonryEntries || !IsMasonryArrayValue( masonryEntries ) )
{
- var graniteRows = craftItems[graniteIndex];
+ Console.Warning( "Masonry: Unable to load masonry craft map data." );
+ return false;
+ }
- // pageIdx: 0..6 => pages 1..9
- for( var pageIdx = 0; pageIdx < myPage.length; pageIdx++ )
- {
- var dictList = myPage[pageIdx];
- var makeList = graniteRows[pageIdx];
+ for( var i = 0; i < masonryEntries.length; i++ )
+ {
+ var entry = masonryEntries[i];
- for( var i = 0; i < dictList.length && i < makeList.length; i++ )
- {
- // Old script uses:
- // page 1 => 100..112
- // page 2 => 200..204
- // page 3 => 300..305
- // etc.
- var buttonID = ( ( pageIdx + 1 ) * 100 ) + i;
- var dictID = dictList[i];
- var makeID = makeList[i];
-
- if( !MasonryMap[buttonID] )
- {
- MasonryMap[buttonID] = {
- dictID: dictID,
- page: pageIdx + 1,
- timerID: pageIdx + 1,
- graniteMake: [],
- // recipeID: undefined,
- // minEra: undefined,
- // maxEra: undefined
- skill: 11, // carpentry / masonry skill ID
- harvest: [14011], // granite dict
- harvest2: [], // optional second resource
- harvest3: [], // optional second resource
- harvest4: [] // optional second resource
- };
- }
+ if( !entry || typeof entry.buttonID == "undefined" )
+ continue;
- MasonryMap[buttonID].graniteMake[graniteIndex] = makeID;
- }
- }
+ if( entry.skill === undefined )
+ entry.skill = 11;
+
+ MasonryMap[entry.buttonID] = entry;
}
-})();
-
-// 3) AFTER initMasonryMap, you can override entries:
-// Page 1 Starts buttonID 100 - 107 for map example
-//MasonryMap[100].customName = "Elven Broadsword";
-//MasonryMap[100].recipeID = 5101; // if you want it recipe-locked
-//MasonryMap[100].minEra = "ml"; // if you want it ML and later only
-
-//Page 1 starts at 100
-MasonryMap[102].minEra = "se"; // small urn
-MasonryMap[103].minEra = "se"; // Tower Sculpture
-MasonryMap[104].minEra = "sa"; // gargoyle painting
-MasonryMap[105].minEra = "sa"; // gargoyle sculpture
-MasonryMap[106].minEra = "sa"; // gargoyle vase
-MasonryMap[107].minEra = "tol"; // Tall 18th Anniversary Vase
-MasonryMap[107].recipeID = 3500;
-MasonryMap[108].minEra = "tol"; // Short 18th Anniversary Vase
-MasonryMap[108].recipeID = 3501;
-//Page 2 starts at 200
-MasonryMap[205].minEra = "sa"; // ritual table
-//Page 3 starts at 300
-MasonryMap[304].minEra = "sa"; // gargoyle statue
-MasonryMap[305].minEra = "sa"; // gryphon statue
-//Page 4 starts at 400
-MasonryMap[400].minEra = "ml"; // stone anvil (east)
-MasonryMap[400].recipeID = 3520;
-MasonryMap[401].minEra = "ml"; // stone anvil (south)
-MasonryMap[401].recipeID = 3521;
-MasonryMap[402].minEra = "sa"; // large gargish bed (east)
-MasonryMap[402].harvest2 = [10016]; // Cloth
-MasonryMap[403].minEra = "sa"; // large gargish bed (south)
-MasonryMap[403].harvest2 = [10016]; // Cloth
-MasonryMap[404].minEra = "sa"; // gargish cot (east)
-MasonryMap[404].harvest2 = [10016]; // Cloth
-MasonryMap[405].minEra = "sa"; // gargish cot (south)
-MasonryMap[405].harvest2 = [10016]; // Cloth
-//Page 5 starts at 500
-MasonryMap[500].minEra = "sa"; // gargish stone arms
-MasonryMap[501].minEra = "sa"; // gargish stone chest
-MasonryMap[502].minEra = "sa"; // gargish stone leggings
-MasonryMap[503].minEra = "sa"; // gargish stone kilt
-MasonryMap[504].minEra = "sa"; // gargish stone arms
-MasonryMap[505].minEra = "sa"; // gargish stone chest
-MasonryMap[506].minEra = "sa"; // gargish stone leggings
-MasonryMap[507].minEra = "sa"; // gargish stone kilt
-MasonryMap[508].minEra = "sa"; // large stone shield
-MasonryMap[509].minEra = "sa"; // gargish stone amulet
-//Page 6 starts at 600
-MasonryMap[600].minEra = "sa"; // stone war sword
-//Page 7 starts at 700
-MasonryMap[700].minEra = "tol"; // Rough Windowless
-MasonryMap[701].minEra = "tol"; // Rough Window
-MasonryMap[702].minEra = "tol"; // Rough Arch
-MasonryMap[703].minEra = "tol"; // Rough Pillar
-MasonryMap[704].minEra = "tol"; // Rough Rounded Arch
-MasonryMap[705].minEra = "tol"; // Rough Small Arch
-MasonryMap[706].minEra = "tol"; // Rough Angled Pillar
-MasonryMap[707].minEra = "tol"; // Short Rough
-MasonryMap[708].minEra = "tol"; // Stone Door (S In)
-MasonryMap[709].minEra = "tol"; // Stone Door (E Out)
-MasonryMap[710].minEra = "tol"; // Left Metal Door (S In)
-MasonryMap[711].minEra = "tol"; // Right Metal Door (S In)
-//Page 8 starts at 800
-MasonryMap[800].minEra = "tol"; // short rough
-MasonryMap[801].minEra = "tol"; // rough steps
-MasonryMap[802].minEra = "tol"; // rough corner steps
-MasonryMap[803].minEra = "tol"; // rough rounded corner step
-MasonryMap[804].minEra = "tol"; // rough inset steps
-MasonryMap[805].minEra = "tol"; // rough rounded inset steps
-//Page 9 starts at 900
-MasonryMap[900].minEra = "tol"; // light paver
-MasonryMap[901].minEra = "tol"; // medium paver
-MasonryMap[902].minEra = "tol"; // dark paver
+
+ Console.Print( "Masonry: Loaded " + masonryEntries.length + " craft map entries.\n" );
+ return true;
+}
+
+/** @type { ( value: any ) => boolean } */
+function IsMasonryArrayValue( value )
+{
+ return Object.prototype.toString.call( value ) == "[object Array]";
+}
function PageX( socket, pUser, pageNum )
{
if( !ValidateObject( pUser ))
return;
+ if( !MasonryMap || Object.keys( MasonryMap ).length == 0 )
+ {
+ if( !LoadMasonryMap() )
+ {
+ socket.SysMessage( "Masonry craft map failed to load." );
+ return;
+ }
+ }
+
// Pages 1 - 9: normal crafting pages
// Page 999: optional "Last Ten Blacksmith" (if you decide to use it later)
@@ -1267,7 +847,7 @@ function onGumpPress( pSock, pButton, gumpData )
var resourceHue = pUser.GetTempTag( "resourceHue" );
// Ensure graniteID within range
- if( graniteID < 0 || graniteID >= craftItems.length )
+ if( graniteID < 0 || graniteID >= entry2.graniteMake.length )
graniteID = 0;
// Era / recipe gating
diff --git a/data/js/skill/craft/tailoring.js b/data/js/skill/craft/tailoring.js
index f07b50e95..f29dcf09f 100644
--- a/data/js/skill/craft/tailoring.js
+++ b/data/js/skill/craft/tailoring.js
@@ -11,101 +11,43 @@ const displayUnlearnedRecipes = true; // Show recipes player ha
const coreShardEra = EraStringToNum( GetServerSetting( "CoreShardEra" ));
const tailoringSkillID = 34; // Tailoring skill index
-//////////////////////////////////////////////////////////////////////////////////////////
-// Tailoring CraftingMap
-//////////////////////////////////////////////////////////////////////////////////////////
-
-// You can later add:
-// skill, minEra, maxEra, recipeID, harvest, harvestNames, customName
-// per entry, like in Alchemy / Tinkering.
-
-const CraftingMap = {
- // Hats (Page 1)
- 130: { dictID: 11415, page: 1, timerID: 1, harvest: [ 10016 ] },
- 131: { dictID: 11416, page: 1, timerID: 1, harvest: [ 10016 ] },
- 132: { dictID: 11417, page: 1, timerID: 1, harvest: [ 10016 ] },
- 134: { dictID: 11418, page: 1, timerID: 1, harvest: [ 10016 ] },
- 133: { dictID: 11419, page: 1, timerID: 1, harvest: [ 10016 ] },
- 136: { dictID: 11420, page: 1, timerID: 1, harvest: [ 10016 ] },
- 137: { dictID: 11421, page: 1, timerID: 1, harvest: [ 10016 ] },
- 138: { dictID: 11422, page: 1, timerID: 1, harvest: [ 10016 ] },
- 139: { dictID: 11423, page: 1, timerID: 1, harvest: [ 10016 ] },
- 140: { dictID: 11424, page: 1, timerID: 1, harvest: [ 10016 ] },
- 141: { dictID: 11425, page: 1, timerID: 1, harvest: [ 10016 ] },
- 135: { dictID: 11470, page: 1, timerID: 1, harvest: [ 10016 ] },
-
- // Shirts & Pants (Page 2)
- 142: { dictID: 11426, page: 2, timerID: 2, harvest: [ 10016 ] },
- 143: { dictID: 11427, page: 2, timerID: 2, harvest: [ 10016 ] },
- 144: { dictID: 11428, page: 2, timerID: 2, harvest: [ 10016 ] },
- 145: { dictID: 11429, page: 2, timerID: 2, harvest: [ 10016 ] },
- 146: { dictID: 11430, page: 2, timerID: 2, harvest: [ 10016 ] },
- 147: { dictID: 11431, page: 2, timerID: 2, harvest: [ 10016 ] },
- 148: { dictID: 11432, page: 2, timerID: 2, harvest: [ 10016 ] },
- 149: { dictID: 11433, page: 2, timerID: 2, harvest: [ 10016 ] },
- 150: { dictID: 11434, page: 2, timerID: 2, harvest: [ 10016 ] },
- 151: { dictID: 11435, page: 2, timerID: 2, harvest: [ 10016 ] },
- 180: { dictID: 11436, page: 2, timerID: 2, harvest: [ 10016 ] },
- 152: { dictID: 11437, page: 2, timerID: 2, harvest: [ 10016 ] },
- 153: { dictID: 11438, page: 2, timerID: 2, harvest: [ 10016 ] },
- 154: { dictID: 11439, page: 2, timerID: 2, harvest: [ 10016 ] },
-
- // Misc (Page 3)
- 155: { dictID: 11440, page: 3, timerID: 3, harvest: [ 10016 ] },
- 156: { dictID: 11441, page: 3, timerID: 3, harvest: [ 10016 ] },
- 157: { dictID: 11442, page: 3, timerID: 3, harvest: [ 10016 ] },
- 158: { dictID: 11443, page: 3, timerID: 3, harvest: [ 10016 ] },
-
- // Footwear (Page 4)
- 159: { dictID: 11444, page: 4, timerID: 4, harvest: [ 10007 ] },
- 160: { dictID: 11445, page: 4, timerID: 4, harvest: [ 10007 ] },
- 161: { dictID: 11446, page: 4, timerID: 4, harvest: [ 10007 ] },
- 162: { dictID: 11447, page: 4, timerID: 4, harvest: [ 10007 ] },
-
- // Leather Armor (Page 5)
- 163: { dictID: 11448, page: 5, timerID: 5, harvest: [ 10007 ] },
- 164: { dictID: 11449, page: 5, timerID: 5, harvest: [ 10007 ] },
- 165: { dictID: 11450, page: 5, timerID: 5, harvest: [ 10007 ] },
- 166: { dictID: 11451, page: 5, timerID: 5, harvest: [ 10007 ] },
- 167: { dictID: 11452, page: 5, timerID: 5, harvest: [ 10007 ] },
- 168: { dictID: 11453, page: 5, timerID: 5, harvest: [ 10007 ] },
-
- // Studded Armor (Page 6)
- 169: { dictID: 11454, page: 6, timerID: 6, harvest: [ 10007 ] },
- 170: { dictID: 11455, page: 6, timerID: 6, harvest: [ 10007 ] },
- 171: { dictID: 11456, page: 6, timerID: 6, harvest: [ 10007 ] },
- 172: { dictID: 11457, page: 6, timerID: 6, harvest: [ 10007 ] },
- 173: { dictID: 11458, page: 6, timerID: 6, harvest: [ 10007 ] },
-
- // Female Armor (Page 7)
- 174: { dictID: 11459, page: 7, timerID: 7, harvest: [ 10007 ] },
- 175: { dictID: 11460, page: 7, timerID: 7, harvest: [ 10007 ] },
- 176: { dictID: 11461, page: 7, timerID: 7, harvest: [ 10007 ] },
- 177: { dictID: 11462, page: 7, timerID: 7, harvest: [ 10007 ] },
- 178: { dictID: 11463, page: 7, timerID: 7, harvest: [ 10007 ] },
- 179: { dictID: 11464, page: 7, timerID: 7, harvest: [ 10007 ] },
-
- // Bone Armor (Page 8)
- 181: { dictID: 11465, page: 8, timerID: 8, harvest: [ 10007, 10008 ] },
- 182: { dictID: 11466, page: 8, timerID: 8, harvest: [ 10007, 10008 ] },
- 183: { dictID: 11467, page: 8, timerID: 8, harvest: [ 10007, 10008 ] },
- 184: { dictID: 11468, page: 8, timerID: 8, harvest: [ 10007, 10008 ] },
- 185: { dictID: 11469, page: 8, timerID: 8, harvest: [ 10007, 10008 ] } // can add recipeID/minEra here later
-};
-
-// Fill defaults (skill, etc.)
-(function initTailoringMap()
+const craftMapRegistryID = 4038;
+var CraftingMap = {};
+
+/** @type { () => boolean } */
+function LoadTailoringMap()
{
- for( var key in CraftingMap )
+ CraftingMap = {};
+
+ var tailoringEntries = TriggerEvent( craftMapRegistryID, "CraftMapRegistry", "tailoring" );
+
+ if( !tailoringEntries || !IsTailoringArrayValue( tailoringEntries ) )
{
- if( !CraftingMap.hasOwnProperty( key ))
+ Console.Warning( "Tailoring: Unable to load tailoring craft map data." );
+ return false;
+ }
+
+ for( var i = 0; i < tailoringEntries.length; i++ )
+ {
+ var entry = tailoringEntries[i];
+
+ if( !entry || typeof entry.makeID == "undefined" )
continue;
- var entry = CraftingMap[key];
if( entry.skill === undefined )
entry.skill = tailoringSkillID;
+
+ CraftingMap[entry.makeID] = entry;
}
-})();
+
+ return true;
+}
+
+/** @type { ( value: any ) => boolean } */
+function IsTailoringArrayValue( value )
+{
+ return Object.prototype.toString.call( value ) == "[object Array]";
+}
/** @type { ( socket: Socket, pUser: Character, pageNum: number ) => void } */
function PageX( socket, pUser, pageNum )
@@ -113,6 +55,15 @@ function PageX( socket, pUser, pageNum )
if( !socket || !ValidateObject( pUser ))
return;
+ if( !CraftingMap || Object.keys( CraftingMap ).length == 0 )
+ {
+ if( !LoadTailoringMap() )
+ {
+ socket.SysMessage( "Tailoring craft map failed to load." );
+ return;
+ }
+ }
+
var subPage = pUser.GetTempTag( "subPage" ) || 1;
var pageItems;
diff --git a/data/js/skill/craft/tinkering.js b/data/js/skill/craft/tinkering.js
index c5eb1e409..1af465688 100644
--- a/data/js/skill/craft/tinkering.js
+++ b/data/js/skill/craft/tinkering.js
@@ -10,141 +10,6 @@ const displayUnlearnedRecipes = true; // For future recipe use
const coreShardEra = EraStringToNum( GetServerSetting( "CoreShardEra" ));
const tinkeringSkillID = 37; // Index of "tinkering" in ItemDetailGump.skillNames[]
-// o--------------------------------------------------------------------------o
-// | TinkeringMap |
-// o--------------------------------------------------------------------------o
-// | Keyed by makeID (create entry ID). |
-// | Each entry: |
-// | dictID - dictionary entry for row text (11801..11982) |
-// | page - main category page (1..9) |
-// | timerID - which page timer should reopen |
-// | skill - skill used (default: tinkeringSkillID) |
-// | recipeID? - optional recipe ID |
-// | minEra?, maxEra? - optional era gating |
-// | harvest?[] - optional dictIDs for MATERIALS list |
-// | harvestNames?[] - optional custom material names |
-// | requiresGemTarget? - jewelry requiring manual gem selection |
-// o--------------------------------------------------------------------------o
-
-/*const TinkeringMap = {
- // Page 1 - Wooden Items
- 274: { dictID: 11801, page: 1, timerID: 1, harvest: [ 10014 ] }, // Axle
- 273: { dictID: 11802, page: 1, timerID: 1, harvest: [ 10014 ] }, // Clock Frame
- 270: { dictID: 11803, page: 1, timerID: 1, harvest: [ 10014 ] }, // Jointing Plane
- 271: { dictID: 11804, page: 1, timerID: 1, harvest: [ 10014 ] }, // Moulding Plane
- 272: { dictID: 11805, page: 1, timerID: 1, harvest: [ 10014 ] }, // Smoothing Plane
-
- // Page 2 - Tools
- 218: { dictID: 11820, page: 2, timerID: 2, harvest: [ 10015 ] }, // Dovetail Saw
- 215: { dictID: 11821, page: 2, timerID: 2, harvest: [ 10015 ] }, // Draw Knife
- 252: { dictID: 11822, page: 2, timerID: 2, harvest: [ 10015 ] }, // Froe
- 255: { dictID: 11823, page: 2, timerID: 2, harvest: [ 10015 ] }, // Hammer
- 214: { dictID: 11824, page: 2, timerID: 2, harvest: [ 10015 ] }, // Hatchet
- 258: { dictID: 11825, page: 2, timerID: 2, harvest: [ 10015 ] }, // Inshave
- 260: { dictID: 11826, page: 2, timerID: 2, harvest: [ 10015 ] }, // Lockpick
- 211: { dictID: 11827, page: 2, timerID: 2, harvest: [ 10015 ] }, // Mortar and Pestle
- 259: { dictID: 11828, page: 2, timerID: 2, harvest: [ 10015 ] }, // Pick Axe
- 217: { dictID: 11829, page: 2, timerID: 2, harvest: [ 10015 ] }, // Saw
- 210: { dictID: 11830, page: 2, timerID: 2, harvest: [ 10015 ] }, // Scissors
- 212: { dictID: 11831, page: 2, timerID: 2, harvest: [ 10015 ] }, // Scorp
- 216: { dictID: 11832, page: 2, timerID: 2, harvest: [ 10015 ] }, // Sewing Kit
- 254: { dictID: 11833, page: 2, timerID: 2, harvest: [ 10015 ] }, // Shovel
- 257: { dictID: 11834, page: 2, timerID: 2, harvest: [ 10015 ] }, // Sledge Hammer
- 256: { dictID: 11835, page: 2, timerID: 2, harvest: [ 10015 ] }, // Smith's Hammer
- 253: { dictID: 11836, page: 2, timerID: 2, harvest: [ 10015 ] }, // Tongs
- 213: { dictID: 11837, page: 2, timerID: 2, harvest: [ 10015 ] }, // Tool Kit (Tinker's tools)
- 284: { dictID: 11838, page: 2, timerID: 2, harvest: [ 10015 ] }, // Fletcher's Tools
-
- // Page 3 - Parts
- 224: { dictID: 11860, page: 3, timerID: 3, harvest: [ 10015 ] }, // Barrel Hoops
- 221: { dictID: 11861, page: 3, timerID: 3, harvest: [ 10015 ] }, // Barrel Tap
- 220: { dictID: 11862, page: 3, timerID: 3, harvest: [ 10015 ] }, // Clock parts
- 219: { dictID: 11863, page: 3, timerID: 3, harvest: [ 10015 ] }, // Gears
- 225: { dictID: 11864, page: 3, timerID: 3, harvest: [ 10015 ] }, // Hinge
- 223: { dictID: 11865, page: 3, timerID: 3, harvest: [ 10015 ] }, // Sextant parts
- 222: { dictID: 11866, page: 3, timerID: 3, harvest: [ 10015 ] }, // Springs
-
- // Page 4 - Utensils
- 226: { dictID: 11880, page: 4, timerID: 4, harvest: [ 10015 ] }, // Butcher Knife
- 232: { dictID: 11881, page: 4, timerID: 4, harvest: [ 10015 ] }, // Cleaver
- 230: { dictID: 11882, page: 4, timerID: 4, harvest: [ 10015 ] }, // Fork
- 231: { dictID: 11883, page: 4, timerID: 4, harvest: [ 10015 ] }, // Fork
- 235: { dictID: 11884, page: 4, timerID: 4, harvest: [ 10015 ] }, // Goblet
- 233: { dictID: 11885, page: 4, timerID: 4, harvest: [ 10015 ] }, // Knife
- 234: { dictID: 11886, page: 4, timerID: 4, harvest: [ 10015 ] }, // Knife
- 236: { dictID: 11887, page: 4, timerID: 4, harvest: [ 10015 ] }, // Pewter Mug
- 229: { dictID: 11888, page: 4, timerID: 4, harvest: [ 10015 ] }, // Plate
- 237: { dictID: 11889, page: 4, timerID: 4, harvest: [ 10015 ] }, // Skinning Knife
- 227: { dictID: 11890, page: 4, timerID: 4, harvest: [ 10015 ] }, // Spoon
- 228: { dictID: 11891, page: 4, timerID: 4, harvest: [ 10015 ] }, // Spoon
-
- // Page 5 - Jewelry (gem-targeted)
- 243: { dictID: 11900, page: 5, timerID: 5, requiresGemTarget: true, harvest: [ 10015, 12005 ] }, // Bracelet
- 241: { dictID: 11901, page: 5, timerID: 5, requiresGemTarget: true, harvest: [ 10015, 12005 ] }, // Earrings
- 239: { dictID: 11902, page: 5, timerID: 5, requiresGemTarget: true, harvest: [ 10015, 12005 ] }, // Necklace (Golden beads)
- 240: { dictID: 11903, page: 5, timerID: 5, requiresGemTarget: true, harvest: [ 10015, 12005 ] }, // Necklace (Silver beads)
- 242: { dictID: 11904, page: 5, timerID: 5, requiresGemTarget: true, harvest: [ 10015, 12005 ] }, // Necklace (Round)
- 238: { dictID: 11905, page: 5, timerID: 5, requiresGemTarget: true, harvest: [ 10015, 12006 ] }, // Weddingband (newbiefied)
-
- // Page 6 - Miscellaneous
- 245: { dictID: 11920, page: 6, timerID: 6, harvest: [ 10015, 12000 ] }, // Candelabra (also appears in Candles page in old script)
- 248: { dictID: 11921, page: 6, timerID: 6, harvest: [ 10015 ] }, // Globe
- 251: { dictID: 11922, page: 6, timerID: 6, harvest: [ 10015 ] }, // Heating stand
- 247: { dictID: 11923, page: 6, timerID: 6, harvest: [ 10015 ] }, // Iron Key
- 244: { dictID: 11924, page: 6, timerID: 6, harvest: [ 10015 ] }, // Keyring
- 250: { dictID: 11925, page: 6, timerID: 6, harvest: [ 10015 ] }, // Lantern
- 246: { dictID: 11926, page: 6, timerID: 6, harvest: [ 10015 ] }, // Scales
- 249: { dictID: 11927, page: 6, timerID: 6, harvest: [ 10015 ] }, // Spy glass
-
- // Page 7 - Multi-Component Items
- 275: { dictID: 11940, page: 7, timerID: 7, harvest: [ 11801, 11863 ] }, // Axle and Gears
- 276: { dictID: 11941, page: 7, timerID: 7, harvest: [ 11802, 11862 ] }, // Clock
- 277: { dictID: 11942, page: 7, timerID: 7, harvest: [ 11802, 11862 ] }, // Clock
- 278: { dictID: 11943, page: 7, timerID: 7, harvest: [ 11801, 11863 ] }, // Clock Parts
- 279: { dictID: 11944, page: 7, timerID: 7, harvest: [ 10634 ] }, // Locked Box
- 280: { dictID: 11945, page: 7, timerID: 7, harvest: [ 10638 ] }, // Locked Chest
- 281: { dictID: 11946, page: 7, timerID: 7, harvest: [ 10642, 11861, 10612, 10928 ] }, // Potion Keg
- 282: { dictID: 11947, page: 7, timerID: 7, harvest: [ 11948 ] }, // Sextant
- 283: { dictID: 11948, page: 7, timerID: 7, harvest: [ 11801, 11863 ] }, // Sextant Parts
-
- // Page 8 - Candles
- // NOTE: in the original script, Candelabra (245) appears here too.
- // To avoid conflicts (map is keyed by makeID), this page uses the
- // actual candle-specific items only.
- 310: { dictID: 11961, page: 8, timerID: 8, harvest: [ 10015, 12000 ] }, // Standing Candelabra
- 315: { dictID: 11962, page: 8, timerID: 8, harvest: [ 10015, 12000 ] }, // Regular Candle
- 312: { dictID: 11963, page: 8, timerID: 8, harvest: [ 12000 ] }, // Round Candle
- 316: { dictID: 11964, page: 8, timerID: 8, harvest: [ 12000, 12004 ] }, // Skull with Candle
- 314: { dictID: 11965, page: 8, timerID: 8, harvest: [ 12000 ] }, // Small Candle
- 311: { dictID: 11966, page: 8, timerID: 8, harvest: [ 10015, 12000 ] }, // Tall Candle
- 313: { dictID: 11967, page: 8, timerID: 8, harvest: [ 12000 ] }, // Thick Candle
-
- // Page 9 - Traps
- 261: { dictID: 11980, page: 9, timerID: 9, harvest: [ 10015, 12001 ] }, // Dart Trap
- 263: { dictID: 11981, page: 9, timerID: 9, harvest: [ 10015, 12003 ] }, // Explosion Trap
- 262: { dictID: 11982, page: 9, timerID: 9, harvest: [ 10015, 12002 ] } // Poison Trap
-};
-
-
-// Fill in defaults (skill, etc)
-(function initTinkeringMap()
-{
- for( var key in TinkeringMap )
- {
- if( !TinkeringMap.hasOwnProperty( key ))
- continue;
-
- var entry = TinkeringMap[key];
-
- if( entry.skill === undefined )
- entry.skill = tinkeringSkillID;
-
- // In future you can do e.g.:
- // entry.harvest = [ woodDictID, ingotDictID ];
- // entry.harvestNames = [ "Wood", "Ingots" ];
- }
-})();
-*/
const craftMapRegistryID = 4038;
var TinkeringMap = {};
From c4b5a1b0205ef597696dab80b24565effbb75d2e Mon Sep 17 00:00:00 2001
From: Dragon Slayer <85514184+DragonSlayer62@users.noreply.github.com>
Date: Wed, 13 May 2026 16:20:34 -0500
Subject: [PATCH 42/43] Update index.html
---
docs/index.html | 450 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 450 insertions(+)
diff --git a/docs/index.html b/docs/index.html
index 287904d4d..343406068 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -6040,6 +6040,456 @@ Rank System
Calculation of rank takes place as part of the crafting process. First, a range of potential ranks is determined based on the values of the MINRANK and MAXRANK tags for an item in the create DFNs. Then, for each skill required to craft the item, the system calculates a temporary rank based on the player's skill, random chance, and a UOX.INI setting (SKILLLEVEL=7 ) which acts as a global modifier to the system to make it easier (low SKILLLEVEL value) to get high rank items, or harder (high SKILLLEVEL value) to get high rank items. A value of 10 makes the difficulty roughly similar to the difficulty of the official UO shards around the LBR era.
When a temporary rank has been calculated for all skills required to craft the item, the average of all these is chosen as the item's final rank, and the modifiers are permanently applied to the item's properties.
+ JSON Crafting Data
+In addition to the older DFN-based create system, UOX3 can use JSON files to control the visible crafting menu data. The JSON files do not replace the actual CreateEntry definitions that decide what item is made, what resources are consumed, and what skills are checked. Instead, they make the crafting menus easier to organize, customize and extend without editing the crafting scripts directly.
+
+The JSON crafting files are stored in:
+js/jsdata/crafting/
+
+
+Crafting JSON Files
+
+
+ alchemy.json - Alchemy menu entries
+ blacksmithing.json - Blacksmithing menu entries
+ carpentry.json - Carpentry menu entries
+ cartography.json - Cartography menu entries
+ cooking.json - Cooking menu entries
+ fletching.json - Bowcraft/Fletching menu entries
+ glassblowing.json - Glassblowing menu entries
+ masonry.json - Masonry menu entries
+ tailoring.json - Tailoring menu entries
+ tinkering.json - Tinkering menu entries
+ resourcemap.json - Shared resource definitions used by crafting menus
+
+
+
+Why Use JSON Crafting Data?
+The JSON crafting system makes crafting easier to maintain. Shard admins can move items between pages, rename menu entries, add custom labels, add era limits, add recipe requirements, define item detail resources and configure some special post-crafting behavior without digging through the crafting scripts.
+
+
+ Move an item to a different crafting page by changing its page value.
+ Change the order of items by changing their position or button mapping in the JSON file.
+ Use dictID for dictionary-based names, or customName for a custom display name.
+ Add recipe locks with recipeID .
+ Add expansion or era limits with minEra and maxEra .
+ Show custom resource names in the item detail gump with harvest and harvestNames .
+ Define special post-craft behavior with craftComplete .
+ Update resource lists, hues, dictionary labels and skill gates through resourcemap.json .
+
+
+
+Basic Craft Entry Example
+
+
{
+ "makeID": 274,
+ "label": "Axle",
+ "dictID": 11801,
+ "page": 1,
+ "timerID": 1,
+ "harvest": [10014]
+}
+
+
This entry adds a craftable item to the menu. The makeID points to the CreateEntry that actually creates the item. The dictID is used for the displayed name, page decides which menu page the item appears on and timerID decides which page reopens after crafting.
+
+
+Craft Entry Fields
+Each crafting JSON file is an array of craft entries. Most entries use only a few fields, but more can be added when needed.
+
+
+Craft Entry Fields
+
+
+
makeID : number
+ The CreateEntry ID used when the player crafts the item. This must match a valid entry from the create system.
+
+
+
+
buttonID : number
+ Optional menu button ID used by crafting skills that need one visible row to map to different makeIDs based on the selected resource. Blacksmithing and Masonry commonly use this style.
+
+
+
+
dictID : number
+ Dictionary entry used for the menu text. This allows the crafting menu to use UOX3 dictionary files for display names.
+
+
+
+
label : text
+ A readable label for admins editing the JSON file. This is useful so the item can be identified without looking up the dictionary ID. It does not have to be the final in-game item name.
+
+
+
+
customName : text
+ Overrides the dictionary text shown in the crafting menu. Use this for custom items that do not have a dictionary entry.
+
+
+
+
page : number
+ The crafting page the entry appears on. Changing this value moves the item to another page in the crafting menu.
+
+
+
+
timerID : number
+ The page/timer value used when the crafting menu reopens after crafting or after a failed attempt.
+
+
+
+
skill : number
+ Optional skill ID override used by the item detail gump and menu logic. If omitted, the crafting script uses the default skill for that craft.
+
+
+
+
harvest : number array
+ Dictionary IDs used to show resource names in the item detail gump. This does not consume resources by itself; the actual resource requirements still come from the CreateEntry.
+
+
+
+
harvestNames : text array
+ Optional custom resource labels for the item detail gump. Use this when a resource name should be clearer than the dictionary entry.
+
+
+
+
recipeID : number
+ Optional recipe requirement. If set, the player must know the recipe before crafting the item.
+
+
+
+
minEra : text
+ Optional minimum shard era required before the entry appears or can be crafted.
+
+
+
+
maxEra : text
+ Optional maximum shard era allowed before the entry is hidden or blocked.
+
+
+
+
requiresGemTarget : true/false
+ Used by Tinkering jewelry entries that require the player to target a gem before crafting.
+
+
+
+
oreMake : number array
+ Used by Blacksmithing entries where one visible menu row can craft different CreateEntries based on the selected ore type.
+
+
+
+
graniteMake : number array
+ Used by Masonry entries where one visible menu row can craft different CreateEntries based on the selected granite type.
+
+
+
+
craftComplete : object
+ Optional post-crafting behavior. This lets the JSON entry request special handling after the item is successfully crafted.
+
+
+
+Era Restrictions
+Craft entries can be limited by shard era. This is useful for keeping newer content hidden on older-era shards.
+
+
+Era Restricted Entry
+
+
{
+ "makeID": 3018,
+ "label": "Glass Sword",
+ "customName": "glass sword",
+ "page": 2,
+ "timerID": 2,
+ "minEra": "sa",
+ "harvest": [13504]
+}
+
+
This entry only appears when the shard era is Stygian Abyss or later.
+
+
+Common era values include:
+t2a , uor , lbr , aos , se , ml , sa , hs , tol
+
+Recipe Locked Crafting
+An entry with a recipeID requires the player to know that recipe before the item can be crafted. This allows custom shards to add rare recipe scrolls or progression-based unlocks without changing the menu script.
+
+
+Recipe Locked Entry
+
+
{
+ "makeID": 3500,
+ "label": "Tall 18th Anniversary Vase",
+ "dictID": 14057,
+ "page": 1,
+ "timerID": 1,
+ "recipeID": 3500,
+ "minEra": "tol",
+ "harvest": [14011]
+}
+
+
This item requires recipe 3500 and only appears in Time of Legends or later.
+
+
+Custom Names
+If an item does not have a dictionary entry, or if a shard wants a different name shown in the menu, use customName .
+
+
+Custom Name Example
+
+
{
+ "makeID": 3012,
+ "label": "Hollow Prism",
+ "customName": "hollow prism",
+ "page": 1,
+ "timerID": 1,
+ "minEra": "ml",
+ "harvest": [13504]
+}
+
+
The menu will display hollow prism instead of looking up a dictionary entry.
+
+
+Item Detail Resources
+The harvest field controls what resource names are shown in the item detail gump. This is for display only. The real resource amounts and item IDs still come from the CreateEntry in the create system.
+
+
+Multiple Resource Display Example
+
+
{
+ "makeID": 281,
+ "label": "Potion Keg",
+ "dictID": 11946,
+ "page": 7,
+ "timerID": 7,
+ "harvest": [10642, 11861, 10612, 10928]
+}
+
+
This lets the item detail gump show multiple needed material labels, such as barrel parts and potion-related materials.
+
+
+
+Custom Resource Name Example
+
+
{
+ "makeID": 308,
+ "label": "Open Keg",
+ "dictID": 10642,
+ "page": 3,
+ "timerID": 3,
+ "harvest": [10611, 10612, 11860],
+ "harvestNames": ["Barrel Staves", "Barrel Lid", "Barrel Hoops"]
+}
+
+
Use harvestNames when the default dictionary label is not clear enough for players or admins.
+
+
+Post-Crafting Behavior
+Some crafted items need special behavior after they are made. The JSON system supports this through the craftComplete object.
+
+
+craftComplete Types
+
+
+
gemJewelry
+ Used for gem-based jewelry. The crafted jewelry is named after the gem used.
+
+
+
+
lockableContainer
+ Used for crafted containers that can receive a lock and key if the crafter has enough Tinkering skill.
+
+
+
+
autoIdentifyPotion
+ Used for alchemy potions. Crafted potions are automatically identified for the crafter.
+
+
+
+
craftedMap
+ Used for crafted maps. The map receives drawn map data based on the crafter's Cartography skill and position.
+
+
+
+
+craftComplete Example
+
+
{
+ "makeID": 243,
+ "label": "Bracelet",
+ "dictID": 11900,
+ "page": 5,
+ "timerID": 5,
+ "requiresGemTarget": true,
+ "harvest": [10015, 12005],
+ "craftComplete": { "type": "gemJewelry" }
+}
+
+
This entry crafts a bracelet, requires a gem target and uses the selected gem name as part of the crafted item's name.
+
+
+Blacksmithing and Masonry Resource Variants
+Some crafting menus have one visible menu row, but the final CreateEntry changes depending on the selected resource. Blacksmithing uses oreMake , while Masonry uses graniteMake .
+
+
+Resource Variant Example
+
+
{
+ "buttonID": 100,
+ "label": "Ringmail Gloves",
+ "dictID": 10289,
+ "page": 1,
+ "timerID": 1,
+ "oreMake": [1, 101, 201, 301, 401, 501, 601, 701, 801],
+ "harvest": [10015]
+}
+
+
The first value is used for iron, the second for dull copper, the third for shadow iron and so on. The resource order comes from resourcemap.json .
+
+
+Resource Map
+The file resourcemap.json defines resource sets used by the crafting menus. It controls the resource display name, item ID, hue, selected resource index and optional skill requirement for resource selection menus.
+
+
+Resource Map Example
+
+
{
+ "resourceSet": "ore",
+ "label": "Ore",
+ "tempTag": "ORE",
+ "defaultIndex": 0,
+ "items": [
+ { "index": 0, "label": "Iron", "dictID": 10291, "itemID": 7154, "hue": 0, "minSkill": 0 },
+ { "index": 1, "label": "Dull Copper", "dictID": 10203, "itemID": 7154, "hue": 2419, "minSkill": 650 },
+ { "index": 2, "label": "Shadow Iron", "dictID": 10204, "itemID": 7154, "hue": 2406, "minSkill": 700 }
+ ]
+}
+
+
This defines selectable ore types for crafting menus. The menu can count the player's matching resources by using the item ID and hue from the selected entry.
+
+
+
+Resource Map Fields
+
+
+
resourceSet : text
+ Unique name for this resource group. Examples: ore , granite , dragonScales , wood , tailoring .
+
+
+
+
label : text
+ Readable label for admins editing the resource map.
+
+
+
+
dictID : number
+ Optional default dictionary entry for the resource set.
+
+
+
+
tempTag : text
+ Temp tag used to remember the player's selected resource. Examples: ORE , Granite , Scale .
+
+
+
+
defaultIndex : number
+ Which resource index is used if the player has not selected anything yet.
+
+
+
+
items : array
+ List of resources in the set. Each item can define index , label , dictID , itemID , hue and minSkill .
+
+
+
+
itemID : number
+ Item ID used when counting the player's resources.
+
+
+
+
hue : number
+ Item hue used when counting the player's resources. Use 0 for normal uncolored resources.
+
+
+
+
minSkill : number
+ Optional skill requirement for selecting this resource. For ores this is usually Mining skill. For scales this can be Blacksmithing skill or left at 0.
+
+
+
+Grouped Resources
+Some resource sets count more than one item ID. For example, wood can count both logs and boards, and tailoring can count leather and hides. These grouped resources do not need an index for every item because the menu adds them together.
+
+
+Grouped Resource Example
+
+
{
+ "resourceSet": "wood",
+ "label": "Wood",
+ "dictID": 10687,
+ "items": [
+ { "label": "Logs", "itemID": 7136, "hue": 0 },
+ { "label": "Boards", "itemID": 7127, "hue": 0 }
+ ]
+}
+
+
This lets the crafting menu show one wood count made from both logs and boards.
+
+
+Adding a New Crafting Entry
+Most new craftables can be added with the following steps:
+
+ Add or confirm the item exists in the CreateEntry system.
+ Add a dictionary entry for the menu label, or decide on a customName .
+ Add a new entry to the correct crafting JSON file.
+ Set makeID , page and timerID .
+ Add harvest entries so the item detail gump shows useful material names.
+ Add optional fields such as recipeID , minEra , maxEra or craftComplete .
+ Reload scripts or restart UOX3, then test the crafting menu and item detail gump.
+
+
+
+Full Custom Item Example
+
+
{
+ "makeID": 5000,
+ "label": "Mythril Gearbox",
+ "customName": "mythril gearbox",
+ "page": 7,
+ "timerID": 7,
+ "recipeID": 5000,
+ "minEra": "ml",
+ "harvest": [10015, 11863],
+ "harvestNames": ["Mythril Ingots", "Gears"]
+}
+
+
This entry adds a custom item to a crafting page, gives it a custom menu name, requires a recipe, limits it to Mondain's Legacy or later and gives clearer resource names in the detail gump.
+
+
+Adding a New Resource
+To add a new selectable resource such as a new ore, granite or scale type, add a new item entry to the correct resource set in resourcemap.json . The new entry should have a unique index, display label, dictionary ID, item ID, hue and skill requirement.
+
+
+New Resource Example
+
+
{
+ "index": 9,
+ "label": "Mythril",
+ "dictID": 25000,
+ "itemID": 7154,
+ "hue": 2200,
+ "minSkill": 1000
+}
+
+
After adding this to the ore resource set, the ore selection menu can show Mythril and count Mythril ingots by item ID and hue.
+
+
+Important Notes
+
+ The JSON files control menu data, not the low-level item creation rules.
+ The actual crafted item, resource consumption, skill checks, sounds and delays still come from the CreateEntry system.
+ makeID , oreMake and graniteMake values must point to valid CreateEntries.
+ label is for admin readability. Use dictID or customName for menu display text.
+ craftComplete only supports behavior types that the crafting complete script knows how to handle.
+ If an entry does not appear, check its page , minEra , maxEra and recipe requirement.
+ If resource counts look wrong, check the itemID and hue in resourcemap.json .
+
From 40876a824c5f8f6b32666d7d531a180d45c8fc95 Mon Sep 17 00:00:00 2001
From: Dragon Slayer <85514184+DragonSlayer62@users.noreply.github.com>
Date: Thu, 14 May 2026 14:43:11 -0500
Subject: [PATCH 43/43] custom crafting
---
data/js/jsdata/crafting/artcraft.json | 18 +
data/js/jsdata/crafting/crafttools.json | 103 +++
data/js/jsdata/crafting/customcrafts.json | 44 ++
data/js/jse_fileassociations.scp | 1 +
data/js/skill/craft/crafttool.js | 358 ++++-----
data/js/skill/craft/customcraft.js | 846 ++++++++++++++++++++++
data/js/skill/craft/itemdetailgump.js | 25 +-
7 files changed, 1227 insertions(+), 168 deletions(-)
create mode 100644 data/js/jsdata/crafting/artcraft.json
create mode 100644 data/js/jsdata/crafting/crafttools.json
create mode 100644 data/js/jsdata/crafting/customcrafts.json
create mode 100644 data/js/skill/craft/customcraft.js
diff --git a/data/js/jsdata/crafting/artcraft.json b/data/js/jsdata/crafting/artcraft.json
new file mode 100644
index 000000000..7de867713
--- /dev/null
+++ b/data/js/jsdata/crafting/artcraft.json
@@ -0,0 +1,18 @@
+[
+ {
+ "makeID": 6000,
+ "label": "Charcoal Sketch",
+ "customName": "charcoal sketch",
+ "page": 1,
+ "timerID": 1,
+ "harvest": [ 10016 ]
+ },
+ {
+ "makeID": 6001,
+ "label": "Canvas Painting",
+ "customName": "canvas painting",
+ "page": 2,
+ "timerID": 2,
+ "harvest": [ 10016, 10015 ]
+ }
+]
diff --git a/data/js/jsdata/crafting/crafttools.json b/data/js/jsdata/crafting/crafttools.json
new file mode 100644
index 000000000..1b15fa2b1
--- /dev/null
+++ b/data/js/jsdata/crafting/crafttools.json
@@ -0,0 +1,103 @@
+[
+ {
+ "craft": "carpentry",
+ "craftIndex": 1,
+ "scriptID": 4025,
+ "legacyScriptID": 4006,
+ "maxPage": 10,
+ "toolIDs": [ 4134, 4135, 4136, 4137, 4140, 4141, 4142, 4143, 4144, 4145, 4146, 4147, 4324, 4325, 4326 ]
+ },
+ {
+ "craft": "alchemy",
+ "craftIndex": 2,
+ "scriptID": 4028,
+ "legacyScriptID": 4007,
+ "maxPage": 4,
+ "toolIDs": [ 3739 ]
+ },
+ {
+ "craft": "fletching",
+ "craftIndex": 3,
+ "scriptID": 4029,
+ "legacyScriptID": 4005,
+ "maxPage": 3,
+ "toolIDs": [ 4130, 7121, 7124 ]
+ },
+ {
+ "craft": "tailoring",
+ "craftIndex": 4,
+ "scriptID": 4030,
+ "legacyScriptID": 4004,
+ "maxPage": 8,
+ "toolIDs": [ 3997 ]
+ },
+ {
+ "craft": "blacksmithing",
+ "craftIndex": 5,
+ "scriptID": 4023,
+ "legacyReturnTrue": true,
+ "maxPage": 7,
+ "specialPages": [
+ {
+ "page": 8,
+ "function": "Page8"
+ }
+ ],
+ "toolIDs": [ 4027, 4028, 5091, 5092 ]
+ },
+ {
+ "craft": "cooking",
+ "craftIndex": 6,
+ "scriptID": 4034,
+ "legacyScriptID": 104,
+ "maxPage": 4,
+ "toolIDs": [ 4163, 2431, 2530, 4158 ]
+ },
+ {
+ "craft": "tinkering",
+ "craftIndex": 7,
+ "scriptID": 4032,
+ "legacyScriptID": 4003,
+ "maxPage": 9,
+ "sectionIDs": [ "tinkerstools" ],
+ "toolIDs": [ 7864, 7865, 7866, 7867, 7868 ]
+ },
+ {
+ "craft": "cartography",
+ "craftIndex": 8,
+ "scriptID": 4035,
+ "maxPage": 1,
+ "sectionIDs": [ "mapmakerspen" ]
+ },
+ {
+ "craft": "glassblowing",
+ "craftIndex": 9,
+ "scriptID": 4036,
+ "maxPage": 1,
+ "sectionIDs": [ "blowpipe" ],
+ "requiredTag": "GlassBlowing",
+ "requiredTagMessage": 6300
+ },
+ {
+ "craft": "masonry",
+ "craftIndex": 10,
+ "scriptID": 4037,
+ "maxPage": 9,
+ "sectionIDs": [ "malletandchisel" ],
+ "requiredTag": "StoneCrafting",
+ "requiredTagMessage": 6297,
+ "specialPages": [
+ {
+ "page": 20,
+ "function": "Page20"
+ }
+ ]
+ },
+ {
+ "craft": "artcraft",
+ "craftIndex": 100,
+ "scriptID": 4040,
+ "maxPage": 2,
+ "sectionIDs": [ "paintbrush" ]
+ }
+]
\ No newline at end of file
diff --git a/data/js/jsdata/crafting/customcrafts.json b/data/js/jsdata/crafting/customcrafts.json
new file mode 100644
index 000000000..7b1a6ed15
--- /dev/null
+++ b/data/js/jsdata/crafting/customcrafts.json
@@ -0,0 +1,44 @@
+[
+ {
+ "craft": "artcraft",
+ "mapFile": "artcraft",
+ "skillID": 0,
+ "maxPage": 2,
+ "menuName": "Art Craft",
+ "showLastTen": true,
+
+ "allowRepair": true,
+ "repairText": "Restore Artwork",
+ "repairTargetText": "Select the artwork you want to restore.",
+ "repairSuccessText": "You carefully restore the artwork.",
+ "repairFailedText": "You fail to restore the artwork.",
+ "repairFullText": "That artwork does not need restoration.",
+ "repairWrongMaterialText": "That is not artwork you can restore.",
+ "repairSkillName": "itemid",
+ "repairMinSkill": 250,
+ "repairMaterialType": "cloth",
+ "repairSound": 42,
+
+ "allowRecycle": true,
+ "recycleText": "Recover Materials",
+ "recycleTargetText": "Select the artwork you want to recover materials from.",
+ "recycleSuccessText": "You recover usable materials from the artwork.",
+ "recycleWrongMaterialText": "You cannot recover materials from that.",
+ "recycleMaterialType": "cloth",
+ "recycleResourceID": "0x0EED",
+ "recycleResourceHue": 0,
+ "recycleResourceAmount": 1,
+ "recycleResourceName": "reclaimed art supplies",
+
+ "categories": [
+ {
+ "page": 1,
+ "label": "Drawings"
+ },
+ {
+ "page": 2,
+ "label": "Paintings"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/data/js/jse_fileassociations.scp b/data/js/jse_fileassociations.scp
index 74fe04152..6a4116b84 100644
--- a/data/js/jse_fileassociations.scp
+++ b/data/js/jse_fileassociations.scp
@@ -245,6 +245,7 @@
4036=skill/craft/glassblowing.js
4037=skill/craft/masonry.js
4038=skill/craft/craftmap_registry.js
+4040=skill/craft/customcraft.js
4050=skill/mining.js
4055=skill/snooping.js
diff --git a/data/js/skill/craft/crafttool.js b/data/js/skill/craft/crafttool.js
index 34dc95c0f..a2f01ae33 100644
--- a/data/js/skill/craft/crafttool.js
+++ b/data/js/skill/craft/crafttool.js
@@ -11,6 +11,178 @@ const CookingID = 4034;
const CartographyID = 4035;
const GlassblowingID = 4036;
const MasonryID = 4037;
+var craftToolMap = null;
+var craftToolMapLoaded = false;
+var craftToolMapLoadError = false;
+
+/** @type { () => object|null } */
+function LoadCraftToolMap()
+{
+ if( craftToolMapLoaded )
+ return craftToolMap;
+
+ craftToolMap = null;
+ craftToolMapLoaded = false;
+ craftToolMapLoadError = false;
+
+ var craftToolFile = new UOXCFile();
+ craftToolFile.Open( "crafttools.json", "r", "crafting", true );
+
+ if( craftToolFile == null || craftToolFile.Length() < 0 )
+ {
+ Console.Error( "CraftTool system: Unable to open js/jsdata/crafting/crafttools.json" );
+ craftToolMapLoadError = true;
+ return null;
+ }
+
+ var fileText = "";
+ while( !craftToolFile.EOF() )
+ {
+ var rawLine = craftToolFile.ReadUntil( "\n" );
+ if( rawLine != null && typeof( rawLine ) != "undefined" )
+ {
+ fileText += rawLine;
+ }
+ }
+
+ craftToolFile.Close();
+ craftToolFile.Free();
+
+ fileText = SanitizeCraftToolJsonText( fileText );
+
+ try
+ {
+ craftToolMap = JSON.parse( fileText );
+ }
+ catch( error )
+ {
+ Console.Error( "CraftTool system: Failed to parse crafttools.json: " + error );
+ Console.Error( "CraftTool system: crafttools.json length after sanitize: " + fileText.length );
+ Console.Error( "CraftTool system: last char code: " + fileText.charCodeAt( fileText.length - 1 ));
+ craftToolMapLoadError = true;
+ return null;
+ }
+
+ if( !IsCraftToolArrayValue( craftToolMap ))
+ {
+ Console.Error( "CraftTool system: crafttools.json must contain a JSON array." );
+ craftToolMapLoadError = true;
+ return null;
+ }
+
+ craftToolMapLoaded = true;
+ Console.Print( "CraftTool system: Loaded " + craftToolMap.length + " craft tool entries.\n" );
+
+ return craftToolMap;
+}
+
+/** @type { ( pUser: Character, iUsed: Item ) => object|null } */
+function GetCraftToolEntry( pUser, iUsed )
+{
+ var toolMap = LoadCraftToolMap();
+
+ if( !toolMap )
+ return null;
+
+ for( var i = 0; i < toolMap.length; i++ )
+ {
+ var entry = toolMap[i];
+
+ if( CraftToolMatchesEntry( iUsed, entry ))
+ return entry;
+ }
+
+ return null;
+}
+
+/** @type { ( iUsed: Item, entry: object ) => boolean } */
+function CraftToolMatchesEntry( iUsed, entry )
+{
+ if( !entry )
+ return false;
+
+ if( entry.sectionIDs && IsCraftToolArrayValue( entry.sectionIDs ))
+ {
+ for( var i = 0; i < entry.sectionIDs.length; i++ )
+ {
+ if( iUsed.sectionID == entry.sectionIDs[i] )
+ return true;
+ }
+ }
+
+ if( entry.toolIDs && IsCraftToolArrayValue( entry.toolIDs ))
+ {
+ for( var j = 0; j < entry.toolIDs.length; j++ )
+ {
+ if( iUsed.id == entry.toolIDs[j] )
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/** @type { ( value: any ) => boolean } */
+function IsCraftToolArrayValue( value )
+{
+ return Object.prototype.toString.call( value ) == "[object Array]";
+}
+
+/** @type { ( text: string ) => string } */
+function SanitizeCraftToolJsonText( text )
+{
+ if( text == null || typeof( text ) == "undefined" )
+ return "";
+
+ text = String( text );
+
+ if( text.length > 0 && text.charCodeAt( 0 ) == 65279 )
+ text = text.substring( 1 );
+
+ text = text.split( "\r\n" ).join( "\n" );
+ text = text.split( "\r" ).join( "\n" );
+ text = text.split( String.fromCharCode( 160 ) ).join( " " );
+ text = text.split( String.fromCharCode( 255 ) ).join( "" );
+ text = text.split( "\t" ).join( " " );
+
+ text = TrimCraftToolString( text );
+
+ var lastBracket = text.lastIndexOf( "]" );
+ if( lastBracket >= 0 )
+ text = text.substring( 0, lastBracket + 1 );
+
+ return TrimCraftToolString( text );
+}
+
+/** @type { ( text: string ) => string } */
+function TrimCraftToolString( text )
+{
+ if( text == null || typeof( text ) == "undefined" )
+ return "";
+
+ return text.replace( /^\s+|\s+$/g, "" );
+}
+
+/** @type { ( specialPages: object ) => object|null } */
+function BuildSpecialPageMap( specialPages )
+{
+ if( !specialPages || !IsCraftToolArrayValue( specialPages ))
+ return null;
+
+ var pageMap = {};
+
+ for( var i = 0; i < specialPages.length; i++ )
+ {
+ var entry = specialPages[i];
+
+ if( entry && typeof entry.page != "undefined" && entry["function"] )
+ {
+ pageMap[entry.page] = entry["function"];
+ }
+ }
+
+ return pageMap;
+}
/**
* Ensure the tool is usable: charges > 0, in range, not locked down,
@@ -104,30 +276,6 @@ function openCraftMenu(pUser, socket, scriptID, craftIndex, maxPage, specialPage
}
}
-// Tool ID helpers for readability
-function isCarpentryTool( id )
-{
- return (( id >= 0x1026 && id <= 0x1029 ) ||
- ( id >= 0x102C && id <= 0x102F ) ||
- ( id >= 0x1030 && id <= 0x1035 ) ||
- ( id >= 0x10E4 && id <= 0x10E6 ));
-}
-
-function isFletchingTool( id )
-{
- return ( id == 0x1022 || id == 0x1BD1 || id == 0x1BD4 );
-}
-
-function isBlacksmithTool( id )
-{
- return ( id == 0x0FBB || id == 0x0FBC || id == 0x13E3 || id == 0x13E4 );
-}
-
-function isCookingTool( id )
-{
- return ( id == 0x1043 || id == 0x097F || id == 0x09E2 || id == 0x103E );
-}
-
// ---------------------------------------------------------------------------
// Main entry
// ---------------------------------------------------------------------------
@@ -144,165 +292,43 @@ function onUseChecked( pUser, iUsed )
// Save tool on socket so skill gumps can reference it
socket.tempObj = iUsed;
- var id = iUsed.id;
- // -------------------------------------------------------------------
- // Carpentry
- // -------------------------------------------------------------------
- if( isCarpentryTool( id ))
- {
- if( enableUOX3Craft == 1 )
- {
- // Old UOX3 carpentry gump
- TriggerEvent( 4006, "onUseChecked", pUser, iUsed );
- return false;
- }
+ var craftToolEntry = GetCraftToolEntry( pUser, iUsed );
- // New carpentry menu – Pages 1–10
- openCraftMenu( pUser, socket, CarpentryID, 1, 10 );
+ if( !craftToolEntry )
return false;
- }
-
- // -------------------------------------------------------------------
- // Alchemy (mortar and pestle)
- // -------------------------------------------------------------------
- if( id == 0x0E9B )
- {
- if( enableUOX3Craft == 1 )
- {
- TriggerEvent( 4007, "onUseChecked", pUser, iUsed );
- return false;
- }
-
- // New alchemy menu – Pages 1–4
- openCraftMenu( pUser, socket, AlchemyID, 2, 4 );
- return false;
- }
- // -------------------------------------------------------------------
- // Bowcraft / Fletching
- // -------------------------------------------------------------------
- if( isFletchingTool( id ))
+ if( craftToolEntry.requiredTag && pUser.GetTag( craftToolEntry.requiredTag ) == 0 )
{
- if( enableUOX3Craft == 1 )
- {
- TriggerEvent( 4005, "onUseChecked", pUser, iUsed );
- return false;
- }
-
- // New fletching menu – Pages 1–3
- openCraftMenu( pUser, socket, FletchingID, 3, 3 );
- return false;
- }
-
- // -------------------------------------------------------------------
- // Tailoring (sewing kit)
- // -------------------------------------------------------------------
- if( id == 0x0F9D )
- {
- if( enableUOX3Craft == 1 )
- {
- TriggerEvent( 4004, "onUseChecked", pUser, iUsed );
- return false;
- }
+ if( craftToolEntry.requiredTagMessage )
+ socket.SysMessage( GetDictionaryEntry( craftToolEntry.requiredTagMessage, socket.language ));
- // New tailoring menu – Pages 1–8
- openCraftMenu( pUser, socket, TailoringID, 4, 8 );
return false;
}
- // -------------------------------------------------------------------
- // Blacksmithing (tongs, smith hammers)
- // -------------------------------------------------------------------
- if( isBlacksmithTool( id ))
+ if( enableUOX3Craft == 1 )
{
- if( enableUOX3Craft == 1 )
- {
- // Fall back to original blacksmith behavior when enabled
+ if( craftToolEntry.legacyReturnTrue )
return true;
- }
-
- // New blacksmithing menu – Pages 1–7 = PageX, page 8 = Page8
- openCraftMenu( pUser, socket, BlacksmithingID, 5, 7, { 8: "Page8" } );
- return false;
- }
- // -------------------------------------------------------------------
- // Cooking (skillet, flour sifter, rolling pin, etc.)
- // -------------------------------------------------------------------
- if( isCookingTool( id ))
- {
- if( enableUOX3Craft == 1 )
+ if( craftToolEntry.legacyScriptID )
{
- // Old-school cooking: use raw food with heat sources, or legacy script
- TriggerEvent( 104, "onUseChecked", pUser, iUsed );
+ TriggerEvent( craftToolEntry.legacyScriptID, "onUseChecked", pUser, iUsed );
return false;
}
-
- // New cooking menu – Pages 1–4
- openCraftMenu( pUser, socket, CookingID, 6, 4 );
- return false;
}
- // -------------------------------------------------------------------
- // Tinkering (tinker's tools)
- // -------------------------------------------------------------------
- if( iUsed.sectionID == "tinkerstools" || // optional if you use sectionID
- id == 0x1EB8 || id == 0x1EB9 || id == 0x1EBA || id == 0x1EBB || id == 0x1EBC )
- {
- if( enableUOX3Craft == 1 )
- {
- TriggerEvent( 4003, "onUseChecked", pUser, iUsed );
- return false;
- }
-
- // New tinkering menu – Pages 1–9
- openCraftMenu( pUser, socket, TinkeringID, 7, 9 );
- return false;
- }
-
- // -------------------------------------------------------------------
- // Cartography (mapmaker's pen)
- // -------------------------------------------------------------------
- if( iUsed.sectionID == "mapmakerspen" )
- {
- // Only one page currently – PageX with page 1
- openCraftMenu( pUser, socket, CartographyID, 8, 1 );
- return false;
- }
-
- // -------------------------------------------------------------------
- // Glassblowing (blow pipe)
- // -------------------------------------------------------------------
- if( iUsed.sectionID == "blowpipe" )
- {
- if( pUser.GetTag( "GlassBlowing" ) == 0 )
- {
- // NOTE: fixed .Language -> .language here
- socket.SysMessage( GetDictionaryEntry( 6300, socket.language )); // You haven't learned glassblowing.
- return false;
- }
-
- // New glassblowing menu – Page 1 only for now
- openCraftMenu( pUser, socket, GlassblowingID, 9, 1 );
- return false;
- }
-
- // -------------------------------------------------------------------
- // Masonry (mallet and chisel)
- // -------------------------------------------------------------------
- if( iUsed.sectionID == "malletandchisel" )
- {
- if( pUser.GetTag( "StoneCrafting" ) == 0 )
- {
- socket.SysMessage( GetDictionaryEntry( 6297, socket.language )); // You haven't learned masonry.
- return false;
- }
-
- // Masonry: Pages 1–9 = PageX, Page 20 = Page20
- openCraftMenu( pUser, socket, MasonryID, 10, 9, { 20: "Page20" } );
- return false;
- }
+ if( craftToolEntry.craft )
+ pUser.SetTempTag( "CUSTOMCRAFT", craftToolEntry.craft );
+
+ openCraftMenu(
+ pUser,
+ socket,
+ craftToolEntry.scriptID,
+ craftToolEntry.craftIndex,
+ craftToolEntry.maxPage,
+ BuildSpecialPageMap( craftToolEntry.specialPages )
+ );
return false;
}
\ No newline at end of file
diff --git a/data/js/skill/craft/customcraft.js b/data/js/skill/craft/customcraft.js
new file mode 100644
index 000000000..8ddb3ac58
--- /dev/null
+++ b/data/js/skill/craft/customcraft.js
@@ -0,0 +1,846 @@
+///
+// @ts-check
+
+var textHue = 0x480;
+var customCraftID = 4040;
+var craftMapRegistryID = 4038;
+var itemDetailsScriptID = 4026;
+var itemsPerPage = 10;
+var displayUnlearnedRecipes = true;
+var coreShardEra = EraStringToNum( GetServerSetting( "CoreShardEra" ) );
+
+var customCraftDefinitions = null;
+var customCraftDefinitionsLoaded = false;
+var customCraftDefinitionsLoadError = false;
+var CustomCraftMap = {};
+
+/** @type { () => object|null } */
+function LoadCustomCraftDefinitions()
+{
+ if( customCraftDefinitionsLoaded )
+ return customCraftDefinitions;
+
+ customCraftDefinitions = null;
+ customCraftDefinitionsLoaded = false;
+ customCraftDefinitionsLoadError = false;
+
+ var customCraftFile = new UOXCFile();
+ customCraftFile.Open( "customcrafts.json", "r", "crafting", true );
+
+ if( customCraftFile == null || customCraftFile.Length() < 0 )
+ {
+ Console.Error( "CustomCraft system: Unable to open js/jsdata/crafting/customcrafts.json" );
+ customCraftDefinitionsLoadError = true;
+ return null;
+ }
+
+ var fileText = "";
+ while( !customCraftFile.EOF() )
+ {
+ var rawLine = customCraftFile.ReadUntil( "\n" );
+ if( rawLine != null && typeof( rawLine ) != "undefined" )
+ {
+ fileText += rawLine;
+ }
+ }
+
+ customCraftFile.Close();
+ customCraftFile.Free();
+
+ fileText = SanitizeCustomCraftJsonText( fileText );
+
+ try
+ {
+ customCraftDefinitions = JSON.parse( fileText );
+ }
+ catch( error )
+ {
+ Console.Error( "CustomCraft system: Failed to parse customcrafts.json: " + error );
+ customCraftDefinitionsLoadError = true;
+ return null;
+ }
+
+ if( !IsCustomCraftArrayValue( customCraftDefinitions ))
+ {
+ Console.Error( "CustomCraft system: customcrafts.json must contain a JSON array." );
+ customCraftDefinitionsLoadError = true;
+ return null;
+ }
+
+ customCraftDefinitionsLoaded = true;
+ Console.Print( "CustomCraft system: Loaded " + customCraftDefinitions.length + " custom craft definitions.\n" );
+
+ return customCraftDefinitions;
+}
+
+/** @type { ( craftName: string ) => object|null } */
+function GetCustomCraftDefinition( craftName )
+{
+ var definitions = LoadCustomCraftDefinitions();
+
+ if( !definitions )
+ return null;
+
+ for( var i = 0; i < definitions.length; i++ )
+ {
+ if( definitions[i] && definitions[i].craft == craftName )
+ return definitions[i];
+ }
+
+ return null;
+}
+
+/** @type { ( pUser: Character ) => object|null } */
+function GetActiveCustomCraftDefinition( pUser )
+{
+ var craftName = pUser.GetTempTag( "CUSTOMCRAFT" );
+
+ if( !craftName )
+ return null;
+
+ return GetCustomCraftDefinition( craftName );
+}
+
+/** @type { ( craftDefinition: object ) => boolean } */
+function LoadCustomCraftMap( craftDefinition )
+{
+ CustomCraftMap = {};
+
+ if( !craftDefinition )
+ return false;
+
+ var mapFile = craftDefinition.mapFile || craftDefinition.craft;
+ var craftEntries = TriggerEvent( craftMapRegistryID, "CraftMapRegistry", mapFile );
+
+ if( !craftEntries || !IsCustomCraftArrayValue( craftEntries ))
+ {
+ Console.Warning( "CustomCraft system: Unable to load custom craft map data for " + mapFile + "." );
+ return false;
+ }
+
+ for( var i = 0; i < craftEntries.length; i++ )
+ {
+ var entry = craftEntries[i];
+
+ if( !entry || typeof entry.makeID == "undefined" )
+ continue;
+
+ if( entry.skill === undefined && typeof craftDefinition.skillID != "undefined" )
+ entry.skill = craftDefinition.skillID;
+
+ CustomCraftMap[entry.makeID] = entry;
+ }
+
+ Console.Print( "CustomCraft system: Loaded " + craftEntries.length + " entries for " + mapFile + ".\n" );
+ return true;
+}
+
+/** @type { ( socket: Socket, pUser: Character, pageNum: number ) => void } */
+function PageX( socket, pUser, pageNum )
+{
+ if( !socket || !ValidateObject( pUser ))
+ return;
+
+ var craftDefinition = GetActiveCustomCraftDefinition( pUser );
+
+ if( !craftDefinition )
+ {
+ socket.SysMessage( "Custom craft definition was not found." );
+ return;
+ }
+
+ if( !CustomCraftMap || Object.keys( CustomCraftMap ).length == 0 )
+ {
+ if( !LoadCustomCraftMap( craftDefinition ))
+ {
+ socket.SysMessage( "Custom craft map failed to load." );
+ return;
+ }
+ }
+
+ var pageItems = [];
+
+ if( pageNum == 999 )
+ {
+ var lastTenRaw = pUser.GetTempTag( "LastTenCustomCraft_" + craftDefinition.craft ) || "";
+ var split = lastTenRaw.split( "," );
+
+ for( var i = 0; i < split.length; i++ )
+ {
+ var value = parseInt( split[i], 10 );
+ if( !isNaN( value ) && CustomCraftMap[value] )
+ pageItems.push( value );
+ }
+ }
+ else
+ {
+ for( var makeIDStr in CustomCraftMap )
+ {
+ if( !CustomCraftMap.hasOwnProperty( makeIDStr ))
+ continue;
+
+ var makeID = parseInt( makeIDStr, 10 );
+ var data = CustomCraftMap[makeID];
+
+ if( data.page == pageNum && EraOK( data ))
+ {
+ var needsRecipe = data.recipeID;
+ var showAll = displayUnlearnedRecipes;
+
+ if( !needsRecipe || showAll || HasLearnedRecipe( pUser, needsRecipe ))
+ pageItems.push( makeID );
+ }
+ }
+
+ pageItems.sort( function( a, b )
+ {
+ var entryA = CustomCraftMap[a];
+ var entryB = CustomCraftMap[b];
+
+ if( entryA && entryB )
+ {
+ if( entryA.sortOrder != undefined && entryB.sortOrder != undefined )
+ return entryA.sortOrder - entryB.sortOrder;
+
+ if( entryA.dictID && entryB.dictID )
+ return entryA.dictID - entryB.dictID;
+ }
+
+ return a - b;
+ });
+ }
+
+ var subPage = pUser.GetTempTag( "subPage" ) || 1;
+ var totalSubPages = Math.ceil( pageItems.length / itemsPerPage );
+
+ if( totalSubPages < 1 )
+ totalSubPages = 1;
+
+ if( subPage < 1 )
+ subPage = 1;
+
+ if( subPage > totalSubPages )
+ subPage = totalSubPages;
+
+ pUser.SetTempTag( "page", pageNum );
+ pUser.SetTempTag( "subPage", subPage );
+
+ var startIndex = ( subPage - 1 ) * itemsPerPage;
+ var endIndex = Math.min( startIndex + itemsPerPage, pageItems.length );
+
+ var customGump = new Gump();
+ BuildCustomCraftFrame( customGump, socket, pUser, craftDefinition, pageNum );
+
+ for( var j = startIndex; j < endIndex; j++ )
+ {
+ var index = j - startIndex;
+ var makeEntryID = pageItems[j];
+ var entry = CustomCraftMap[makeEntryID];
+ var entryText = GetCustomCraftEntryText( entry, socket );
+
+ customGump.AddButton( 220, 60 + ( index * 20 ), 4005, 4007, 1, 0, makeEntryID );
+ customGump.AddText( 255, 60 + ( index * 20 ), textHue, entryText );
+ customGump.AddButton( 480, 60 + ( index * 20 ), 4011, 4012, 1, 0, 20000 + makeEntryID );
+ }
+
+ if( pageItems.length == 0 )
+ customGump.AddText( 220, 60, textHue, "No items available on this page." );
+
+ if( subPage > 1 )
+ {
+ customGump.AddButton( 220, 260, 4014, 4015, 1, 0, 8000 + ( subPage - 1 ));
+ customGump.AddHTMLGump( 255, 263, 100, 18, false, false, " " + GetDictionaryEntry( 10101, socket.language ) + "" );
+ }
+
+ if( subPage < totalSubPages )
+ {
+ customGump.AddButton( 370, 260, 4005, 4007, 1, 0, 9000 + ( subPage + 1 ));
+ customGump.AddHTMLGump( 405, 263, 100, 18, false, false, " " + GetDictionaryEntry( 10100, socket.language ) + "" );
+ }
+
+ customGump.Send( socket );
+ customGump.Free();
+}
+
+/** @type { ( customGump: Gump, socket: Socket, pUser: Character, craftDefinition: object, pageNum: number ) => void } */
+function BuildCustomCraftFrame( customGump, socket, pUser, craftDefinition, pageNum )
+{
+ customGump.AddPage( 0 );
+ customGump.AddBackground( 0, 0, 530, 437, 5054 );
+ customGump.AddTiledGump( 10, 10, 510, 22, 2624 );
+ customGump.AddTiledGump( 10, 292, 150, 45, 2624 );
+ customGump.AddTiledGump( 165, 292, 355, 45, 2624 );
+ customGump.AddTiledGump( 10, 342, 510, 85, 2624 );
+ customGump.AddTiledGump( 10, 37, 200, 250, 2624 );
+ customGump.AddTiledGump( 215, 37, 305, 250, 2624 );
+ customGump.AddCheckerTrans( 10, 10, 510, 417 );
+
+ var menuName = craftDefinition.menuName || craftDefinition.craft || "Custom Craft";
+
+ customGump.AddHTMLGump( 0, 12, 530, 20, 0, 0, " " + menuName + " " );
+ customGump.AddHTMLGump( 0, 37, 220, 22, 0, 0, " " + GetDictionaryEntry( 10286, socket.language ) + " " );
+ customGump.AddHTMLGump( 230, 37, 280, 22, 0, 0, " " + GetDictionaryEntry( 10287, socket.language ) + " " );
+ customGump.AddHTMLGump( 10, 302, 150, 25, 0, 0, " " + GetDictionaryEntry( 10288, socket.language ) + " " );
+
+ var categories = craftDefinition.categories || [];
+
+ for( var i = 0; i < categories.length; i++ )
+ {
+ var category = categories[i];
+ var categoryPage = category.page || ( i + 1 );
+ var categoryText = category.label || ( "Page " + categoryPage );
+
+ customGump.AddButton( 20, 60 + ( i * 20 ), 4005, 4007, 1, 0, 10000 + categoryPage );
+ customGump.AddText( 55, 60 + ( i * 20 ), textHue, categoryText );
+ }
+
+ if( craftDefinition.showLastTen )
+ {
+ customGump.AddButton( 20, 250, 4005, 4007, 1, 0, 10999 );
+ customGump.AddText( 55, 250, textHue, "Last Ten" );
+ }
+
+ if( craftDefinition.allowRepair )
+ {
+ customGump.AddButton( 20, 345, 4005, 4007, 1, 0, 90001 );
+ customGump.AddText( 55, 345, textHue, GetCustomCraftText( craftDefinition, "repairText", "Repair Item" ) );
+ }
+
+ if( craftDefinition.allowRecycle )
+ {
+ customGump.AddButton( 20, 365, 4005, 4007, 1, 0, 90002 );
+ customGump.AddText( 55, 365, textHue, GetCustomCraftText( craftDefinition, "recycleText", "Recycle Item" ) );
+ }
+}
+
+/** @type { ( entry: object, socket: Socket ) => string } */
+function GetCustomCraftEntryText( entry, socket )
+{
+ if( !entry )
+ return "[Missing Entry]";
+
+ if( entry.customName )
+ return entry.customName;
+
+ if( entry.dictID )
+ {
+ var dictText = GetDictionaryEntry( entry.dictID, socket.language );
+ if( dictText && dictText != "" )
+ return dictText;
+ }
+
+ if( entry.label )
+ return entry.label;
+
+ return "[Unnamed Item: " + entry.makeID + "]";
+}
+
+/** @type { ( pSock: Socket, pButton: number, gumpData: any ) => void } */
+function onGumpPress( pSock, pButton, gumpData )
+{
+ if( !pSock )
+ return;
+
+ var pUser = pSock.currentChar;
+
+ if( !ValidateObject( pUser ))
+ return;
+
+ var craftDefinition = GetActiveCustomCraftDefinition( pUser );
+
+ if( !craftDefinition )
+ return;
+
+ if( !CustomCraftMap || Object.keys( CustomCraftMap ).length == 0 )
+ {
+ if( !LoadCustomCraftMap( craftDefinition ))
+ return;
+ }
+
+ if( pButton == 0 )
+ {
+ pSock.CloseGump( customCraftID + 0xffff, 0 );
+ return;
+ }
+
+ if( craftDefinition.allowRepair && pButton == 90001 )
+ {
+ pSock.CustomTarget( 0, GetCustomCraftText( craftDefinition, "repairTargetText", "Select the item you want to repair." ) );
+ return;
+ }
+
+ if( craftDefinition.allowRecycle && pButton == 90002 )
+ {
+ pSock.CustomTarget( 1, GetCustomCraftText( craftDefinition, "recycleTargetText", "Select the item you want to recycle." ) );
+ return;
+ }
+
+ if( pButton >= 10000 && pButton < 11000 )
+ {
+ if( pButton == 10999 )
+ PageX( pSock, pUser, 999 );
+ else
+ PageX( pSock, pUser, pButton - 10000 );
+
+ return;
+ }
+
+ if( pButton >= 8000 && pButton < 9000 )
+ {
+ pUser.SetTempTag( "subPage", pButton - 8000 );
+ PageX( pSock, pUser, pUser.GetTempTag( "page" ) || 1 );
+ return;
+ }
+
+ if( pButton >= 9000 && pButton < 10000 )
+ {
+ pUser.SetTempTag( "subPage", pButton - 9000 );
+ PageX( pSock, pUser, pUser.GetTempTag( "page" ) || 1 );
+ return;
+ }
+
+ if( pButton >= 20000 )
+ {
+ ShowCustomCraftItemDetails( pSock, pUser, pButton - 20000 );
+ return;
+ }
+
+ var entry = CustomCraftMap[pButton];
+
+ if( !entry )
+ return;
+
+ if( !EraOK( entry ))
+ {
+ pSock.SysMessage( "That item is not available in this era." );
+ return;
+ }
+
+ if( entry.recipeID && !HasLearnedRecipe( pUser, entry.recipeID ))
+ {
+ TriggerEvent( 4022, "NeedRecipe", pSock, entry.recipeID );
+ return;
+ }
+
+ pUser.SetTempTag( "MAKELAST", pButton );
+ StoreLastTenCustomCraft( pUser, craftDefinition, pButton );
+
+ pSock.CloseGump( customCraftID + 0xffff, 0 );
+ MakeItem( pSock, pUser, pButton );
+ pUser.StartTimer( 2000, entry.timerID || entry.page || 1, true );
+}
+
+/** @type { ( pSock: Socket, pUser: Character, makeID: number ) => void } */
+function ShowCustomCraftItemDetails( pSock, pUser, makeID )
+{
+ var entry = CustomCraftMap[makeID];
+
+ if( !entry )
+ return;
+
+ pUser.SetTempTag( "ITEMDETAILS", makeID );
+ pUser.SetTempTag( "Skill", entry.skill || 0 );
+ pUser.SetTempTag( "needRecipeID", entry.recipeID || 0 );
+
+ ClearCustomCraftHarvestTags( pUser );
+
+ if( entry.harvest && IsCustomCraftArrayValue( entry.harvest ))
+ {
+ for( var i = 0; i < entry.harvest.length && i < 4; i++ )
+ {
+ var tagName = ( i == 0 ? "Harvest" : "Harvest" + ( i + 1 ));
+ pUser.SetTempTag( tagName, entry.harvest[i] );
+ }
+ }
+
+ if( entry.harvestNames && IsCustomCraftArrayValue( entry.harvestNames ))
+ {
+ for( var j = 0; j < entry.harvestNames.length && j < 4; j++ )
+ {
+ var nameTag = ( j == 0 ? "HarvestName" : "Harvest" + ( j + 1 ) + "Name" );
+ pUser.SetTempTag( nameTag, entry.harvestNames[j] );
+ }
+ }
+
+ TriggerEvent( itemDetailsScriptID, "ItemDetailGump", pUser );
+}
+
+/** @type { ( pUser: Character, craftDefinition: object, makeID: number ) => void } */
+function StoreLastTenCustomCraft( pUser, craftDefinition, makeID )
+{
+ if( !craftDefinition || !craftDefinition.showLastTen )
+ return;
+
+ var tagName = "LastTenCustomCraft_" + craftDefinition.craft;
+ var rawValue = pUser.GetTempTag( tagName ) || "";
+ var split = rawValue.split( "," );
+ var newList = [ makeID.toString() ];
+
+ for( var i = 0; i < split.length; i++ )
+ {
+ var value = parseInt( split[i], 10 );
+
+ if( !isNaN( value ) && value != makeID && newList.length < 10 )
+ newList.push( value.toString() );
+ }
+
+ pUser.SetTempTag( tagName, newList.join( "," ) );
+}
+
+/** @type { ( timerObj: Character, timerID: number ) => void } */
+function onTimer( timerObj, timerID )
+{
+ if( !ValidateObject( timerObj ))
+ return;
+
+ var socket = timerObj.socket;
+
+ if( !socket )
+ return;
+
+ PageX( socket, timerObj, timerID );
+}
+
+/** @type { ( entry: object ) => boolean } */
+function EraOK( entry )
+{
+ if( !entry )
+ return false;
+
+ if( entry.minEra && coreShardEra < EraStringToNum( entry.minEra ))
+ return false;
+
+ if( entry.maxEra && coreShardEra > EraStringToNum( entry.maxEra ))
+ return false;
+
+ return true;
+}
+
+/** @type { ( pUser: Character, recipeID: number ) => boolean } */
+function HasLearnedRecipe( pUser, recipeID )
+{
+ if( !recipeID || recipeID <= 0 )
+ return true;
+
+ return TriggerEvent( 4022, "NeedRecipe", pUser.socket, recipeID );
+}
+
+/** @type { ( pUser: Character ) => void } */
+function ClearCustomCraftHarvestTags( pUser )
+{
+ pUser.SetTempTag( "Harvest", null );
+ pUser.SetTempTag( "Harvest2", null );
+ pUser.SetTempTag( "Harvest3", null );
+ pUser.SetTempTag( "Harvest4", null );
+ pUser.SetTempTag( "HarvestName", null );
+ pUser.SetTempTag( "Harvest2Name", null );
+ pUser.SetTempTag( "Harvest3Name", null );
+ pUser.SetTempTag( "Harvest4Name", null );
+}
+
+/** @type { ( value: any ) => boolean } */
+function IsCustomCraftArrayValue( value )
+{
+ return Object.prototype.toString.call( value ) == "[object Array]";
+}
+
+/** @type { ( text: string ) => string } */
+function SanitizeCustomCraftJsonText( text )
+{
+ if( text == null || typeof( text ) == "undefined" )
+ return "";
+
+ text = String( text );
+
+ if( text.length > 0 && text.charCodeAt( 0 ) == 65279 )
+ text = text.substring( 1 );
+
+ text = text.split( "\r\n" ).join( "\n" );
+ text = text.split( "\r" ).join( "\n" );
+ text = text.split( String.fromCharCode( 160 ) ).join( " " );
+ text = text.split( String.fromCharCode( 255 ) ).join( "" );
+ text = text.split( "\t" ).join( " " );
+
+ text = TrimCustomCraftString( text );
+
+ var lastBracket = text.lastIndexOf( "]" );
+ if( lastBracket >= 0 )
+ text = text.substring( 0, lastBracket + 1 );
+
+ return TrimCustomCraftString( text );
+}
+
+/** @type { ( text: string ) => string } */
+function TrimCustomCraftString( text )
+{
+ if( text == null || typeof( text ) == "undefined" )
+ return "";
+
+ return text.replace( /^\s+|\s+$/g, "" );
+}
+
+/** @type { () => void } */
+function ReloadCustomCraftDefinitions()
+{
+ customCraftDefinitions = null;
+ customCraftDefinitionsLoaded = false;
+ customCraftDefinitionsLoadError = false;
+
+ LoadCustomCraftDefinitions();
+}
+
+/** @type { ( craftDefinition: object, keyName: string, fallbackText: string ) => string } */
+function GetCustomCraftText( craftDefinition, keyName, fallbackText )
+{
+ if( craftDefinition && craftDefinition[keyName] )
+ return craftDefinition[keyName];
+
+ return fallbackText;
+}
+
+/** @type { ( pSock: Socket, pUser: Character, targetObj: Item, craftDefinition: object ) => boolean } */
+function CheckCustomCraftTargetItem( pSock, pUser, targetObj, craftDefinition )
+{
+ if( !ValidateObject( targetObj ) || !targetObj.isItem )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "invalidTargetText", "That is not a valid item." ));
+ return false;
+ }
+
+ var ownerObj = GetPackOwner( targetObj, 0 );
+ if( !ValidateObject( ownerObj ) || ownerObj.serial != pUser.serial )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "mustBeInPackText", "That item must be in your backpack." ));
+ return false;
+ }
+
+ return true;
+}
+
+/** @type { ( pUser: Character, stationIDs: any ) => boolean } */
+function HasNearbyCustomCraftStation( pUser, stationIDs )
+{
+ if( !stationIDs || !IsCustomCraftArrayValue( stationIDs ))
+ return true;
+
+ var nearbyStation = AreaItemFunction( "FindNearbyCustomCraftStation", pUser, 3, stationIDs );
+ return nearbyStation != 0;
+}
+
+/** @type { ( pUser: Character, trgItem: Item, stationIDs: any ) => boolean } */
+function FindNearbyCustomCraftStation( pUser, trgItem, stationIDs )
+{
+ if( !ValidateObject( trgItem ) || !trgItem.isItem )
+ return false;
+
+ for( var i = 0; i < stationIDs.length; i++ )
+ {
+ if( trgItem.id == stationIDs[i] )
+ return true;
+ }
+
+ return false;
+}
+
+/** @type { ( pSock: Socket, pUser: Character, targetObj: Item, craftDefinition: object ) => void } */
+function RepairCustomCraftItem( pSock, pUser, targetObj, craftDefinition )
+{
+ if( !CheckCustomCraftTargetItem( pSock, pUser, targetObj, craftDefinition ))
+ return;
+
+ if( craftDefinition.repairStationIDs && !HasNearbyCustomCraftStation( pUser, craftDefinition.repairStationIDs ))
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairNoStationText", "You need to be near the proper crafting station to repair that." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+
+ if( craftDefinition.repairMaterialType )
+ {
+ var materialType = TriggerEvent( 2506, "GetItemMaterialType", targetObj );
+ if( materialType != craftDefinition.repairMaterialType )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairWrongMaterialText", "That item cannot be repaired with this craft." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+ }
+
+ if( targetObj.maxhp <= 0 )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairInvalidDurabilityText", "That item cannot be repaired." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+
+ if( targetObj.health >= targetObj.maxhp )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairFullText", "That item is already fully repaired." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+
+ var repairSkillID = craftDefinition.repairSkillID;
+ var repairSkillName = craftDefinition.repairSkillName;
+ var repairSkill = 0;
+ var repairSkillCap = 1000;
+
+ if( typeof repairSkillID != "undefined" )
+ {
+ repairSkill = pUser.skills[repairSkillID];
+ repairSkillCap = pUser.skillCaps[repairSkillID];
+ }
+ else if( repairSkillName )
+ {
+ repairSkill = pUser.skills[repairSkillName];
+ repairSkillCap = pUser.skillCaps[repairSkillName];
+ }
+
+ var minSkill = craftDefinition.repairMinSkill || 0;
+ if( repairSkill < minSkill )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairTooDifficultText", "You are not skilled enough to repair that." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+
+ var missingHP = targetObj.maxhp - targetObj.health;
+ var repairDifficulty = Math.floor(( missingHP * 1000 ) / targetObj.maxhp );
+ var minDifficulty = repairDifficulty - 250;
+
+ if( minDifficulty < 0 )
+ minDifficulty = 0;
+
+ var maxDifficulty = repairDifficulty + 250;
+ if( maxDifficulty > repairSkillCap )
+ maxDifficulty = repairSkillCap;
+
+ if( RandomNumber( minDifficulty, 1000 ) < repairSkill )
+ {
+ if( typeof repairSkillID != "undefined" )
+ pUser.CheckSkill( repairSkillID, minDifficulty, maxDifficulty );
+
+ if( GetServerSetting( "ItemRepairDurabilityLoss" ))
+ targetObj.maxhp -= 1;
+
+ targetObj.health = targetObj.maxhp;
+
+ if( craftDefinition.repairSound )
+ pSock.SoundEffect( craftDefinition.repairSound, true );
+
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairSuccessText", "You repair the item." ));
+ }
+ else
+ {
+ var damageLoss = craftDefinition.repairFailureDamage || 1;
+ targetObj.health -= damageLoss;
+
+ if( targetObj.health <= 0 )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairDestroyedText", "The item has been destroyed." ));
+ targetObj.Delete();
+ }
+ else
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "repairFailedText", "You fail to repair the item." ));
+ }
+ }
+
+ RestartCustomCraftGump( pUser, craftDefinition );
+}
+
+/** @type { ( pSock: Socket, pUser: Character, targetObj: Item, craftDefinition: object ) => void } */
+function RecycleCustomCraftItem( pSock, pUser, targetObj, craftDefinition )
+{
+ if( !CheckCustomCraftTargetItem( pSock, pUser, targetObj, craftDefinition ))
+ return;
+
+ if( craftDefinition.recycleStationIDs && !HasNearbyCustomCraftStation( pUser, craftDefinition.recycleStationIDs ))
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "recycleNoStationText", "You need to be near the proper crafting station to recycle that." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+
+ if( craftDefinition.recycleMaterialType )
+ {
+ var materialType = TriggerEvent( 2506, "GetItemMaterialType", targetObj );
+ if( materialType != craftDefinition.recycleMaterialType )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "recycleWrongMaterialText", "That item cannot be recycled with this craft." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+ }
+
+ var recycleResourceID = craftDefinition.recycleResourceID || 0;
+ if( recycleResourceID == 0 )
+ {
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "recycleNoResourceText", "This craft does not define a recycled resource." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+ return;
+ }
+
+ var resourceAmount = craftDefinition.recycleResourceAmount || 1;
+ var resourceHue = craftDefinition.recycleResourceHue || 0;
+
+ if( craftDefinition.recycleUseTargetHue )
+ resourceHue = targetObj.colour;
+
+ if( craftDefinition.recycleFromCreateEntry )
+ {
+ var createEntry = null;
+
+ if( targetObj.entryMadeFrom != null && targetObj.entryMadeFrom != 0 )
+ createEntry = CreateEntries[targetObj.entryMadeFrom];
+
+ if( createEntry && createEntry.resources && createEntry.resources.length > 0 )
+ {
+ resourceAmount = Math.max( 1, Math.floor( createEntry.resources[0][0] / 2 ));
+ }
+ }
+
+ targetObj.Delete();
+
+ var newResource = CreateDFNItem( pSock, pUser, recycleResourceID.toString(), resourceAmount, "ITEM", true, resourceHue );
+
+ if( ValidateObject( newResource ) && craftDefinition.recycleResourceName )
+ newResource.name = craftDefinition.recycleResourceName;
+
+ pSock.SysMessage( GetCustomCraftText( craftDefinition, "recycleSuccessText", "You recycle the item." ));
+ RestartCustomCraftGump( pUser, craftDefinition );
+}
+
+/** @type { ( pUser: Character, craftDefinition: object ) => void } */
+function RestartCustomCraftGump( pUser, craftDefinition )
+{
+ if( !ValidateObject( pUser ))
+ return;
+
+ var timerID = pUser.GetTempTag( "page" ) || 1;
+ var delay = craftDefinition.actionDelay || 200;
+
+ pUser.StartTimer( delay, timerID, true );
+}
+
+/** @type { ( pSock: Socket, pUser: Character, targetObj: any, targetID: number ) => void } */
+function onCallback0( pSock, pUser, targetObj, targetID )
+{
+ var craftDefinition = GetActiveCustomCraftDefinition( pUser );
+
+ if( !craftDefinition || !craftDefinition.allowRepair )
+ return;
+
+ RepairCustomCraftItem( pSock, pUser, targetObj, craftDefinition );
+}
+
+/** @type { ( pSock: Socket, pUser: Character, targetObj: any, targetID: number ) => void } */
+function onCallback1( pSock, pUser, targetObj, targetID )
+{
+ var craftDefinition = GetActiveCustomCraftDefinition( pUser );
+
+ if( !craftDefinition || !craftDefinition.allowRecycle )
+ return;
+
+ RecycleCustomCraftItem( pSock, pUser, targetObj, craftDefinition );
+}
\ No newline at end of file
diff --git a/data/js/skill/craft/itemdetailgump.js b/data/js/skill/craft/itemdetailgump.js
index 845376fd9..04a804705 100644
--- a/data/js/skill/craft/itemdetailgump.js
+++ b/data/js/skill/craft/itemdetailgump.js
@@ -12,6 +12,7 @@ const Cooking = 4034;
const Cartography = 4035;
const Glassblowing = 4036;
const Masonry = 4037;
+const CustomCraft = 4040;
const exceptionalWearablesOnly = true;
@@ -101,9 +102,16 @@ function ItemDetailGump( pUser )
var recipeID = pUser.GetTempTag( "needRecipeID" );
- if( detailTag !== null )
+ if (detailTag !== null)
{
- createEntry = CreateEntries[ detailTag ];
+ try
+ {
+ createEntry = CreateEntries[detailTag];
+ }
+ catch (error)
+ {
+ createEntry = null;
+ }
}
if( skillTag >= 0 && skillTag < skillNames.length )
@@ -158,6 +166,14 @@ function ItemDetailGump( pUser )
if( createEntry == null )
{
+ ItemDetailsGump( itemGump, pUser );
+
+ itemGump.AddHTMLGump( 170, 130, 320, 20, false, false, " Missing CreateEntry" );
+ itemGump.AddHTMLGump( 170, 150, 320, 20, false, false, " CreateEntry ID " + detailTag + " was not found." );
+ itemGump.AddHTMLGump( 170, 170, 320, 40, false, false, " Check the makeID in the crafting JSON file. and make sure you add it to create dfn files" );
+
+ itemGump.Send( socket );
+ itemGump.Free();
return;
}
@@ -585,6 +601,11 @@ function onGumpPress( pSock, pButton, gumpData )
break;
}
break;
+ case 100: // Custom Craft
+ pUser.SetTempTag( "ITEMDETAILS", null );
+ pSock.CloseGump( gumpID, 0 );
+ TriggerEvent( CustomCraft, "PageX", pSock, pUser, pUser.GetTempTag( "page" ) || 1 );
+ break;
}
}
}