1 /*
2  *
3  *  Iter Vehemens ad Necem (IVAN)
4  *  Copyright (C) Timo Kiviluoto
5  *  Released under the GNU General
6  *  Public License
7  *
8  *  See LICENSING which should be included
9  *  along with this file for more details
10  *
11  */
12 
13 /* compiled thru command.cpp */
14 
15 #include <typeinfo>
16 
17 #include "hiteffect.h"
18 #include "traps.h"
19 #include "dbgmsgproj.h"
20 
21 #include "cmdcraftfilters.cpp"
22 
23 std::vector<recipedata> vSuspended; //TODO suspendable action should be more global to be reused by other actions than crafting!
24 
25 //recipedata* craftcore::prpdSuspended=NULL;
26 
AddSuspended(const recipedata & rpd)27 void craftcore::AddSuspended(const recipedata& rpd)
28 {
29   rpd.rc.integrityCheck();
30 
31   if(!rpd.rc.IsCanBeSuspended())
32     ABORT("action can't be suspended %s",rpd.dbgInfo().CStr());
33 
34   for(int i=0;i<vSuspended.size();i++)
35     if(vSuspended[i].id()==rpd.id())
36       ABORT("this crafting was already suspended '%s'",vSuspended[i].id().CStr());
37 
38 //  rpd.rc.ClearRefs();
39 
40   vSuspended.push_back(rpd);
41 }
42 
RemoveIfSuspended(const recipedata & rpd)43 void craftcore::RemoveIfSuspended(const recipedata&rpd)
44 {
45   for(int i=0;i<vSuspended.size();i++)
46     if(vSuspended[i].id()==rpd.id()){
47       vSuspended.erase(vSuspended.begin()+i);
48       break;
49     }
50 }
51 
FindRecipedata(festring fsRpdId)52 recipedata craftcore::FindRecipedata(festring fsRpdId)
53 {
54   for(int i=0;i<vSuspended.size();i++)
55     if(vSuspended[i].id()==fsRpdId)
56       return vSuspended[i];
57 
58   ABORT("unable to find recipedata id='%s'",fsRpdId.CStr());
59   return recipedata(); //dummy... just to let it be compiled...
60 }
61 
ClearSuspendedList()62 void craftcore::ClearSuspendedList()
63 {
64   vSuspended.clear();
65 }
66 
EmptyContentsIfPossible(recipedata & rpd,item * itContainer,bool bMoveToInventory)67 bool craftcore::EmptyContentsIfPossible(recipedata& rpd,item* itContainer, bool bMoveToInventory)
68 {
69   materialcontainer* mc = dynamic_cast<materialcontainer*>(itContainer); //potions, mines... also bananas xD
70   itemcontainer* ic = dynamic_cast<itemcontainer*>(itContainer); //chests
71 
72   bool bEmptied = false;
73 
74   if(mc!=NULL){
75     if(mc->GetSecondaryMaterial()!=NULL){
76       if(bMoveToInventory)
77         PrepareRemains(rpd,mc->GetSecondaryMaterial());
78       else
79         delete mc->RemoveSecondaryMaterial(); //prevents keeping: ex. random liquids like antidote
80     }
81     bEmptied = true;
82   }
83 
84   if(ic!=NULL){ //empty chests etc
85     stack* stkC = ic->GetContained();
86     itemvector ivC;
87     stkC->FillItemVector(ivC);
88     for(int i=0;i<ivC.size();i++){
89       if(bMoveToInventory)
90         ivC[i]->MoveTo(rpd.rc.H()->GetStack());
91       else
92         SendToHellSafely(ivC[i]);
93     }
94 
95     bEmptied = true;
96   }
97 
98   return bEmptied;
99 }
100 
101 /**
102  * Why this is important?
103  *
104  * Because if the item remain on slot, and 1 turn has not passed,
105  * what may happpen on crafting code in case user gives up on crafting action,
106  * it will provide inconsistent inventory contents (something that was sent to hell
107  * but is still on inventory as the turn has not passed to properly send it to hell).
108  *
109  * If all the above is correctly understood as "it is how thing work",
110  * then crafting should be fixed to always pass one turn even if user giveup?
111  * But that doesnt looks good, giveup = cancel, should not pass a turn at all.
112  *
113  * This shall be used for anything related to crafting until something better is coded.
114  */
SendToHellSafely(item * it)115 void craftcore::SendToHellSafely(item* it)
116 {
117   it->RemoveFromSlot(); //just in case to prevent problems later... like crashing elsewhere!!!
118   it->SendToHell(); //DBG3("SentToHell",it,it->GetID());//,lumpAtInv,lumpAtInv->GetID());
119   DBGITEM(it,"SentToHell:Safely");
120 //  **rit=NULL;
121 }
122 
CraftSkill(character * Char)123 float craftcore::CraftSkill(character* Char){ //is the current capability of successfully crafting
124   float fBonus = 0; // influence/weights of each stat will be the FINAL divider!
125   float fWeight = 0;
126   float fDivFinal = 0;
127 
128   #define CALCSK(weig,attr) fWeight = weig; fBonus += Char->GetAttribute(attr)*weig; fDivFinal+=weig;
129   CALCSK(15.0,DEXTERITY); //by importance order
130   CALCSK(7.5,WISDOM);
131   CALCSK(3.0,PERCEPTION); //TODO could counter fumbles directly
132   CALCSK(3.0,ARM_STRENGTH); //TODO half if only one arm?
133   CALCSK(2.5,AGILITY);
134   CALCSK(2.5,INTELLIGENCE);
135   CALCSK(2.0,ENDURANCE); //TODO could lower 0.1 per turn crafting, til 0.0
136   CALCSK(1.5,WILL_POWER); //TODO could lower 0.1 per turn crafting, til 0.0
137   CALCSK(0.25,LEG_STRENGTH);
138   //TODO CHARISMA //if one day there is item quality, well finished, attribute that could increase sell price
139   //TODO MANA //if one day anything magical is allowed to be crafted
140 
141   float fSkill = 0;
142   float fCharSkill = Char->GetCWeaponSkill(CRAFTING)->GetLevel(); // base/learned
143   // if polymorphed, goes as override (tho we cant poly into uniques right? we would need generic tailors and smiths...)
144   if(dynamic_cast<guard*>(Char))
145     fCharSkill=25;
146   else
147   if(dynamic_cast<kamikazedwarf*>(Char))
148     fCharSkill=50;
149   else
150   if(dynamic_cast<tailor*>(Char))
151     fCharSkill=75;
152   else
153   if(dynamic_cast<smith*>(Char))
154     fCharSkill=100;
155   fSkill += fCharSkill;
156   fSkill += fBonus/fDivFinal; // in short, if all stats are 10, craft skill would be 10
157   fSkill -= 10.0; // to make advancing important on the beggining
158   if(fSkill<1.0)fSkill=1.0; //safety
159   return fSkill;
160 }
161 
canBeCrafted(item * it)162 bool craftcore::canBeCrafted(item* it){
163   if(dynamic_cast<lump*>(it)!=NULL)
164     return true;
165   if(dynamic_cast<stick*>(it)!=NULL) //checked when splitting
166     return true;
167   if(dynamic_cast<itemcontainer*>(it)!=NULL) //to allow crafting chests again
168     return true;
169 
170   const itemdatabase* itdb = it->GetDataBase();
171 
172   if(it->GetCategory()==POTION)
173   {
174     item* Bottle = dynamic_cast<potion*>(it);
175     if(Bottle && (Bottle->GetNameSingular() == "bottle" || Bottle->GetNameSingular() == "vial")) //TODO really necessary the singular name check? excessive?
176     {
177       if(!Bottle->GetSecondaryMaterial())
178         return true;
179       if( // extracting from corpses
180           // TODO may be the "kind of action" (as a parameter for this func) could allow some specific materials only,  so a new action EXTRACT_FROM_CORPSE would allow only the ones below
181          Bottle->GetSecondaryMaterial()->GetConfig()==SULPHURIC_ACID ||
182          Bottle->GetSecondaryMaterial()->GetConfig()==POISON_LIQUID ||
183          Bottle->GetSecondaryMaterial()->GetConfig()==MAGIC_LIQUID
184         )
185         return true;
186     }
187 
188     DBG3(Bottle->GetSecondaryMaterial()->GetConfig(),SULPHURIC_ACID,POISON_LIQUID);
189     return false;
190   }
191 
192   // TODO allow near oven, but the problem is the ingredients... also may be blocked below too
193   if(it->GetCategory()==FOOD)
194   {
195     item* Can = dynamic_cast<can*>(it);
196     if(Can && !Can->GetSecondaryMaterial())
197       return true;
198 
199     DBGLN;
200     return false;
201   }
202 
203   //TODO all these things should have some easier kind of property to be checked
204   if(!craftcore::MoreCraftDeniedFilters(it)){
205     DBGLN;
206     return false;
207   }
208 
209   if( // more complex/generic filter, better keep as last check?
210     game::IsQuestItem(it) ||
211     it->GetEnchantment()!=0 ||
212     it->GetCategory()==BOOK ||
213     it->GetCategory()==MISC ||
214     it->GetCategory()==SCROLL ||
215     !itdb->CanBeWished ||
216     itdb->Possibility <= 0 ||
217     !itdb->PostFix.IsEmpty() ||
218     false // just to make it easier to re-organize and add checks above
219   ){
220     DBGLN;
221     return false;
222   }
223 
224   return true;
225 }
226 
HasSuspended()227 bool craftcore::HasSuspended() {
228   return vSuspended.size()>0;
229 }
ResumeSuspendedTo(character * Char,recipedata & rpd)230 bool craftcore::ResumeSuspendedTo(character* Char,recipedata& rpd)
231 {
232   if(!HasSuspended())
233     ABORT("no suspended craft action to set to %s!",Char->GetName(DEFINITE).CStr());
234 
235   if(Char->GetAction()!=NULL){DBG1("AlreadySomethingElse,How?");DBGSTK; //may never happen tho... TODO ABORT() ?
236     ADD_MESSAGE("You are already doing something else.");
237     return false;
238   }
239 
240   rpd.rc.ClearRefs(); //good to cleanup, will be set again also
241 
242   rpd.rc.integrityCheck();
243 
244   bool bReqSamePos = false;
245 //  if(rpd.bMeltable)bReqSamePos=true;
246   if(!rpd.v2AnvilLocation.Is0())bReqSamePos=true; DBGSV2(rpd.v2AnvilLocation);
247   if(!rpd.v2ForgeLocation.Is0())bReqSamePos=true; DBGSV2(rpd.v2ForgeLocation);
248   if(!rpd.v2WorkbenchLocation.Is0())bReqSamePos=true; DBGSV2(rpd.v2WorkbenchLocation);
249   if(!rpd.v2TailoringWorkbenchLocation.Is0())bReqSamePos=true;
250   if(rpd.otSpawnType!=CTT_NONE && !rpd.v2PlaceAt.Is0())bReqSamePos=true; DBG1(rpd.otSpawnType);
251   if(bReqSamePos){
252     if(rpd.rc.GetDungeonLevelID() != craftcore::CurrentDungeonLevelID()){
253       //TODO better message: place? location? dungeon level sounds a bit non-immersive, or not?
254       ADD_MESSAGE("You need to be in the same dungeon as before to continue crafting.");
255       return false;
256     }
257 
258     if(rpd.v2PlayerCraftingAt != Char->GetPos()){
259       festring fsDist;
260       int iDist = (rpd.v2PlayerCraftingAt - Char->GetPos()).GetLengthSquare();
261       if(iDist<=2)fsDist<<"and you are almost there!";
262       else
263       if(iDist<=10)fsDist<<"and you are nearby.";
264       else
265       if(iDist<=20)fsDist<<"but you are still a bit far away from it...";
266       else
267         fsDist<<"but it seems to be quite far from here.";
268       ADD_MESSAGE("You need to be where you were crafting before, %s",fsDist.CStr());
269       game::SetDrawMapOverlay(true); //TODO make this optional?
270       game::PositionQuestion(CONST_S("It was here!"), rpd.v2PlayerCraftingAt, 0, 0, false);
271       game::SetDrawMapOverlay(false);
272       return false;
273     }
274   }else{
275     rpd.v2PlayerCraftingAt = Char->GetPos();
276   }
277 
278   Char->SwitchToCraft(rpd);
279 
280   return true;
281 }
282 
Save(outputfile & SaveFile) const283 void recipecore::Save(outputfile& SaveFile) const
284 {
285   integrityCheck();
286 
287   SaveFile //commented ones are just to keep the clarity/organization
288     << bCanBeSuspended
289     << iDungeonLevelID
290     ;
291 }
292 
Save(outputfile & SaveFile) const293 void recipedata::Save(outputfile& SaveFile) const
294 {
295   rc.Save(SaveFile);
296 
297   SaveFile //commented ones are just to keep the clarity/organization
298     << ingredientsIDs
299     << iAddDexterity
300     << iBaseTurnsToFinish
301     << itSpawnTot
302     << v2ForgeLocation
303 
304     << v2PlaceAt
305     << bSuccesfullyCompleted
306     << v2AnvilLocation
307     << v2PlayerCraftingAt
308     << fsCraftInfo
309 
310     << itToolID
311     << itTool2ID
312     << itSpawnType
313     << fsItemSpawnSearchPrototype
314 
315     << itSpawnCfg
316     << itSpawnMatMainCfg
317     << itSpawnMatMainVol
318     << itSpawnMatSecCfg
319     << itSpawnMatSecVol
320 
321     << otSpawnCfg
322     << otSpawnMatMainCfg
323     << otSpawnMatMainVol
324     << otSpawnMatSecCfg
325     << otSpawnMatSecVol
326 
327     << otSpawnType
328     << bSpawnBroken
329     << fDifficulty
330     << bCanBeBroken
331     << bMeltable
332 
333     << v2WorkbenchLocation
334     << iRemainingTurnsToFinish
335     << bGradativeCraftOverride
336     << bTailoringMode
337     << v2TailoringWorkbenchLocation
338 
339     << lDamageFinalItem
340 
341     ;
342 }
343 
Load(inputfile & SaveFile)344 void recipecore::Load(inputfile& SaveFile)
345 {
346   SaveFile //commented ones are just to keep the clarity/organization
347     >> bCanBeSuspended
348     >> iDungeonLevelID
349     ;
350 }
351 
Load(inputfile & SaveFile)352 void recipedata::Load(inputfile& SaveFile)
353 {
354   rc.Load(SaveFile);
355 
356   SaveFile //commented ones are just to keep the clarity/organization
357     >> ingredientsIDs
358     >> iAddDexterity
359     >> iBaseTurnsToFinish
360     >> itSpawnTot
361     >> v2ForgeLocation
362 
363     >> v2PlaceAt
364     >> bSuccesfullyCompleted
365     >> v2AnvilLocation
366     >> v2PlayerCraftingAt
367     >> fsCraftInfo
368 
369     >> itToolID
370     >> itTool2ID
371     >> itSpawnType
372     >> fsItemSpawnSearchPrototype
373 
374     >> itSpawnCfg
375     >> itSpawnMatMainCfg
376     >> itSpawnMatMainVol
377     >> itSpawnMatSecCfg
378     >> itSpawnMatSecVol
379 
380     >> otSpawnCfg
381     >> otSpawnMatMainCfg
382     >> otSpawnMatMainVol
383     >> otSpawnMatSecCfg
384     >> otSpawnMatSecVol
385 
386     >> otSpawnType
387     >> bSpawnBroken
388     >> fDifficulty
389     >> bCanBeBroken
390     >> bMeltable
391 
392     >> v2WorkbenchLocation
393     >> iRemainingTurnsToFinish
394     >> bGradativeCraftOverride
395     >> bTailoringMode
396     >> v2TailoringWorkbenchLocation
397 
398     >> lDamageFinalItem
399 
400     ;
401 
402   if(game::GetCurrentSavefileVersion() >= 135){
403     SaveFile >> bTailoringMode;
404     SaveFile >> v2TailoringWorkbenchLocation;
405   }
406 
407 //  if(otSpawnType!=CTT_NONE)
408 //    SaveFile >> otSpawn;
409   rc.integrityCheck();
410 }
id() const411 cfestring recipedata::id() const
412 {
413   /**
414    * this is a simple way to detect if the recipedata is the same,
415    * here shall only contain things that would not change thru the
416    * whole crafting process/time
417    * TODO just use an ulong ID like chars and items do? but keep this as debug info at least!!!
418    */
419 
420   festring fs;
421 
422   #define RPDINFO(o) fs<<(#o)<<"="<<(o)<<"; ";
423   RPDINFO(rc.IsCanBeSuspended());
424 
425   RPDINFO(itToolID);
426   RPDINFO(itTool2ID);
427 
428   RPDINFO(itSpawnCfg);
429   RPDINFO(itSpawnMatMainCfg);
430   RPDINFO(itSpawnMatMainVol);
431   RPDINFO(itSpawnMatSecCfg);
432   RPDINFO(itSpawnMatSecVol);
433 
434   RPDINFO(otSpawnCfg);
435   RPDINFO(otSpawnMatMainCfg);
436   RPDINFO(otSpawnMatMainVol);
437   RPDINFO(otSpawnMatSecCfg);
438   RPDINFO(otSpawnMatSecVol);
439 
440   RPDINFO(fsItemSpawnSearchPrototype);
441   RPDINFO(fsCraftInfo);
442 
443   #define RPDINFOV2(o) fs<<(#o)<<"="<<(o.X)<<","<<(o.Y)<<"; ";
444   RPDINFOV2(v2AnvilLocation);
445   RPDINFOV2(v2ForgeLocation);
446   RPDINFOV2(v2WorkbenchLocation);
447   RPDINFOV2(v2TailoringWorkbenchLocation);
448   RPDINFOV2(v2PlaceAt);
449   RPDINFOV2(v2PlayerCraftingAt);
450 
451   return fs;
452 }
453 
dbgInfo() const454 cfestring recipedata::dbgInfo() const
455 {
456   festring fs;
457   fs << id();
458   return fs;
459 }
460 
461 clock_t RPDInitKey = clock();
462 
SetHumanoid(character * C)463 void recipecore::SetHumanoid(character* C){
464   integrityCheck(C);
465 
466   if(C==NULL)
467     ABORT("actor can't be null, btw h='%s'",h==NULL?"NULL":h->GetName(DEFINITE).CStr());
468 
469   if(C==h)
470     return;
471 
472   humanoid* hNew = dynamic_cast<humanoid*>(C);
473   if(hNew==NULL)
474     ABORT("Only humanoids can craft C='%s' h='%s'",C->GetName(DEFINITE).CStr(),h==NULL?"NULL":h->GetName(DEFINITE).CStr());
475 
476   h = hNew;
477 }
478 
integrityCheck(character * Actor) const479 void recipecore::integrityCheck(character* Actor) const
480 {
481   if(initKey!=RPDInitKey){ //FIRST TEST!!!!!!!!!!!!!!!!!!!!!
482     //TODO bools get crazy values too if not initialized
483     ABORT("recipedata corrupted, not initialized or invalid");// it will not be possible to show info, would crash on it... , dbgInfo().CStr());
484   }
485 }
486 
recipecore(humanoid * H,uint sel)487 recipecore::recipecore(humanoid* H,uint sel){
488   initKey = RPDInitKey;
489 
490   h=H;
491 
492   bCanBeSuspended=true;
493   iDungeonLevelID=craftcore::CurrentDungeonLevelID();
494 }
495 
recipedata(humanoid * H,uint sel)496 recipedata::recipedata(humanoid* H,uint sel) : rc(H,sel)
497 {
498   itTool=NULL;
499   itTool2=NULL;
500   lsqrPlaceAt = NULL;
501   lsqrCharPos = rc.H()==NULL ? NULL : game::GetCurrentLevel()->GetLSquare(rc.H()->GetPos());
502   itWeakestIngredient = NULL;
503   lsqrActor=NULL;
504 
505   // no need to save
506   SelectedRecipe=sel;
507   bSpendCurrentTurn=false; //TODO review everywhere to let it work as expected instead of always spending a turn what ignores this
508   bAlreadyExplained=false;
509   bHasAllIngredients=false;
510   bCanStart=false;
511 
512   bCanBePlaced=false;
513   xplodStr=0;
514   iStrongerXplod=0;
515   v2XplodAt=v2(0,0);
516   bOnlyXplodIfCriticalFumble=false;
517 
518   bSpecialExtendedTurns=false;
519   iMinTurns=0;
520   bFailedTerminateCancel=false;
521   bFailedSuspend=false;
522 
523 
524   ////////////////////////////////////////////////////////////////////////////////////
525   /// saveables
526   //////////////////////////////////
527 
528   ingredientsIDs.clear(); //just to init
529   iAddDexterity=0;
530   iBaseTurnsToFinish=1; //TODO should be based on attributes
531   itSpawnTot=1;
532   v2ForgeLocation=v2(0,0);
533 
534   v2PlaceAt=v2(0,0);
535   bSuccesfullyCompleted=false;
536   v2AnvilLocation=v2(0,0);
537   v2PlayerCraftingAt=v2(0,0);
538   fsCraftInfo="";
539 
540   itToolID=0;
541   itTool2ID=0;
542   itSpawnType=CIT_NONE;
543   fsItemSpawnSearchPrototype="";
544 
545   itSpawnCfg=0;
546   itSpawnMatMainCfg=0;
547   itSpawnMatMainVol=0;
548   itSpawnMatSecCfg=0;
549   itSpawnMatSecVol=0;
550 
551   otSpawnCfg=0;
552   otSpawnMatMainCfg=0;
553   otSpawnMatMainVol=0;
554   otSpawnMatSecCfg=0;
555   otSpawnMatSecVol=0;
556 
557   otSpawnType=CTT_NONE;
558   bSpawnBroken=false;
559   fDifficulty=1.0;
560   bCanBeBroken=true; //most can, anyway b4 breaking should be revalidated
561   bMeltable=false;
562 
563   v2WorkbenchLocation=v2(0,0);
564   iRemainingTurnsToFinish=iBaseTurnsToFinish;
565   bGradativeCraftOverride=false;
566   bTailoringMode=false;
567   v2TailoringWorkbenchLocation=v2(0,0);
568 
569   lDamageFinalItem=0;
570 }
571 
CurrentDungeonLevelID()572 int craftcore::CurrentDungeonLevelID(){
573   return game::GetCurrentDungeonIndex()*100+game::GetCurrentLevelIndex();
574 }
575 
CopySpawnItemCfgFrom(item * itCfg)576 void recipedata::CopySpawnItemCfgFrom(item* itCfg)
577 {
578   rc.integrityCheck();
579   if(itCfg==NULL)
580     ABORT("NULL itCfg");
581 
582   itSpawnCfg = itCfg->GetConfig();
583   material* matM = itCfg->GetMainMaterial();
584   itSpawnMatMainCfg = matM->GetConfig();
585   itSpawnMatMainVol = matM->GetVolume();
586 //  itSpawnMatMainSpoilLevel = matM->GetSpoilLevel();
587   if(itCfg->GetSecondaryMaterial()!=NULL){
588     material* matS = itCfg->GetSecondaryMaterial();
589     itSpawnMatSecCfg = matS->GetConfig();
590     itSpawnMatSecVol = matS->GetVolume();
591 //    itSpawnMatSecSpoilLevel = matS->GetSpoilLevel();
592   }
593 }
594 
CopySpawnTerrainCfgFrom(olterrain * otCfg)595 void recipedata::CopySpawnTerrainCfgFrom(olterrain* otCfg){
596   rc.integrityCheck();
597   if(otCfg==NULL)
598     ABORT("NULL otCfg");
599 
600   otSpawnCfg = otCfg->GetConfig();
601   otSpawnMatMainCfg = otCfg->GetMainMaterial()->GetConfig();
602   otSpawnMatMainVol = otCfg->GetMainMaterial()->GetVolume();
603   if(otCfg->GetSecondaryMaterial()!=NULL){
604     otSpawnMatSecCfg = otCfg->GetSecondaryMaterial()->GetConfig();
605     otSpawnMatSecVol = otCfg->GetSecondaryMaterial()->GetVolume();
606   }
607 }
608 
SpawnTerrain(recipedata & rpd,festring & fsCreated)609 olterrain* crafthandle::SpawnTerrain(recipedata& rpd, festring& fsCreated){
610   rpd.rc.integrityCheck();
611 
612   olterrain* otSpawn = NULL;
613 
614   switch(rpd.otSpawnType){
615     case CTT_FURNITURE:
616       otSpawn=decoration::Spawn(rpd.otSpawnCfg);
617       game::CheckAddAutoMapNote(otSpawn->GetLSquareUnder());
618       break;
619     case CTT_DOOR:
620       otSpawn=door::Spawn(rpd.otSpawnCfg);
621       ((door*)otSpawn)->SetIsLocked(false);
622       ((door*)otSpawn)->SetIsOpened(true);
623       //TODO configure lock type based randomly in one of the keys available on player's inventory
624       break;
625     case CTT_WALL:
626       otSpawn=wall::Spawn(rpd.otSpawnCfg); //earth::Spawn();
627       break;
628   }
629 
630   if(otSpawn==NULL)
631     ABORT("craft spawned no terrain.");
632 
633   delete otSpawn->SetMainMaterial(material::MakeMaterial(rpd.otSpawnMatMainCfg,rpd.otSpawnMatMainVol));
634   if(rpd.otSpawnMatSecCfg>0)
635     delete otSpawn->SetSecondaryMaterial(material::MakeMaterial(rpd.otSpawnMatSecCfg,rpd.otSpawnMatSecVol));
636 
637   fsCreated << "You built ";
638   rpd.lsqrPlaceAt->ChangeOLTerrainAndUpdateLights(otSpawn); //TODO a forge seems to not be emitting light...
639 
640   fsCreated << otSpawn->GetName(INDEFINITE);
641 
642   return otSpawn;
643 }
644 
ClearRefs()645 void recipecore::ClearRefs()
646 {
647   h = NULL;
648 }
649 
ClearRefs()650 void recipedata::ClearRefs(){
651   /**
652    * This is to help on granting consistency.
653    * As in case of save/load all pointers will change.
654    * This is important to revalidate all pointers from IDs.
655    */
656 
657   itTool = NULL;
658   itTool2 = NULL;
659   lsqrPlaceAt = NULL;
660   lsqrCharPos = NULL;
661   itWeakestIngredient = NULL;
662   lsqrActor = NULL;
663 
664   rc.ClearRefs();
665 }
666 
667 struct ci{ //create item info/helper/data/config/param
668   bool bMultSelect = true;
669 
670   int iReqCfg=0;
671   bool bMainMaterRemainsBecomeLump=false;
672   bool bOverridesQuestion=false;
673   bool bMsgInsuficientMat=true;
674   bool bInstaAddIngredients=false;
675 
676   bool bFirstMainMaterIsFilter=true;
677   int iReqMatCfgMain=0; //setting this overrides bFirstMainMaterIsFilter
678   int iReqMatCfgSec=0;
679   bool bJustAcceptFirstChosenAndReturn=false;
680   bool bAllowDegradation = false;
681 
682   bool bFirstItemMustHaveFullVolumeRequired=false;
683   bool bAllowMeltables=true;
684   bool bAllowWood=true;
685   bool bAllowBones=true;
686   int iMinMainMaterStr=0;
687 
688   float fUsablePercVol=1.0;
689   bool bMustBeTailorable=false;
690   bool bMixRemainingLump=true;
691   bool bAddEquippedItemsToChoiceList=false;
692   bool bIsMainIngredient=true; //false if it is secondary ingredient
693 };
694 struct recipe{
695   festring action;
696   festring name;
697   int iListIndex;
698 
699   festring desc;
700 
initrecipe701   void init(cchar* act,cchar* nm){
702     action=(act);
703     name=(nm);
704     iListIndex=(-1);//,desc(""){};
705   }
706 
fillInforecipe707   virtual void fillInfo(){ ABORT("missing recipe info implementation"); }
708 
failPlacementMsgrecipe709   void failPlacementMsg(recipedata& rpd){
710     ADD_MESSAGE("%s can't be placed here.",name.CStr());
711     rpd.SetAlreadyExplained();
712   }
failIngredientsMsgrecipe713   void failIngredientsMsg(recipedata& rpd){
714     festring fsMsg;
715     fsMsg<<"Required ingredients to "<<action<<" "<<name<<" are not met.";
716     ADD_MESSAGE(fsMsg.CStr());
717     rpd.SetAlreadyExplained();
718   }
failToolMsgrecipe719   void failToolMsg(recipedata& rpd,festring tool){
720     ADD_MESSAGE("You don't have a strong enough %s to work on the requested materials.",tool.CStr());
721     // "strong enough" means that a too weak material wont be able to work on a much stronger material
722     rpd.SetAlreadyExplained();
723   }
724 
IsTheSelectedOnerecipe725   bool IsTheSelectedOne(recipedata& rpd){
726     if(desc.IsEmpty())
727       fillInfo();
728     if(desc.IsEmpty())
729       ABORT("recipe info is still empty");
730 
731     return rpd.SelectedRecipe == iListIndex;
732   }
733 
workrecipe734   virtual bool work(recipedata& rpd){ return false; }
735 
~reciperecipe736   virtual ~recipe(){}
737 
whereRawrecipe738   bool whereRaw(recipedata& rpd,festring fsMsg,bool acceptSelfLocation=false){
739     int Dir = game::DirectionQuestion(fsMsg, false, acceptSelfLocation);
740     if(Dir != DIR_ERROR) {
741       v2 v2W = rpd.rc.H()->GetPos() + game::GetMoveVector(Dir);
742       if(rpd.rc.H()->GetArea()->IsValidPos(v2W)){
743         rpd.lsqrPlaceAt = rpd.rc.H()->GetNearLSquare(v2W);
744         return true;
745       }
746     }
747     return false;
748   }
749 
whererecipe750   bool where(recipedata& rpd,bool acceptSelfLocation=false)
751   {
752     if(whereRaw(rpd,"Build it where?",acceptSelfLocation))
753       if(rpd.lsqrPlaceAt != NULL && rpd.lsqrPlaceAt->GetOLTerrain() == NULL &&
754          rpd.lsqrPlaceAt->GetCharacter() == NULL &&
755          !(rpd.lsqrPlaceAt->GetRoom() && rpd.lsqrPlaceAt->GetRoom()->GetMaster() &&
756          rpd.lsqrPlaceAt->GetRoom()->GetMaster() != PLAYER)) // Prevents building in owned rooms.
757       {
758         rpd.v2PlaceAt = rpd.lsqrPlaceAt->GetPos();
759         rpd.bCanBePlaced=true;
760         return true;
761       }
762 
763     failPlacementMsg(rpd);
764     return false;
765   }
766 
calcMeltableTurnsrecipe767   static int calcMeltableTurns(material* mat,float fMult=1.0){
768     /**
769      * min is gold: str 55 and spends 5 turns each 1000cm3.
770      * TODO quite arbritrary but gameplay wise enough?
771      * TODO should use density? or something else than str? fireresistance is not defined for most and is not melting point either...
772      */
773     DBG2(mat->GetStrengthValue(),mat->GetVolume());
774     static const int iBaseTurns = 5;
775     static const float fMinStr = 55.0; //float for precision
776     static const float fBaseVol = 1000.0; //float for precision
777     float f = iBaseTurns * fMult * (mat->GetStrengthValue()/fMinStr) * (mat->GetVolume()/fBaseVol); //vol is in cm3, so per 1L or 1Kg(water)
778     if(f>0 && f<1)f=1;
779     return f;
780   }
781 
chkWeaponrecipe782   static bool chkWeapon(item* it, int iCfg, int iWeaponCategory, int iMainMatterMinStrengh){
783     return
784       dynamic_cast<meleeweapon*>(it)!=NULL
785       &&
786       it->GetMainMaterial()->GetStrengthValue()>=iMainMatterMinStrengh
787       &&
788       (
789         (iCfg!=0 && it->GetConfig()==iCfg) ||
790         (iWeaponCategory!=0 && it->GetWeaponCategory()==iWeaponCategory)
791       );
792   }
793 
setToolsrecipe794   static bool setTools(recipedata& rpd,item* itTool,item* itTool2){
795     addTool(rpd,itTool);
796     addTool(rpd,itTool2);
797     return rpd.itTool2!=NULL;
798   }
addToolrecipe799   static void addTool(recipedata& rpd,item* itTool){
800     if(rpd.itTool==itTool)return;
801     if(rpd.itTool2==itTool)return;
802 
803     if(rpd.itTool==NULL){
804       rpd.itTool=itTool;
805       return;
806     }
807 
808     if(rpd.itTool2==NULL){
809       rpd.itTool2=itTool;
810       return;
811     }
812 
813     ABORT("at most 2 tools '%s' '%s' '%s'",rpd.itTool->GetName(INDEFINITE).CStr(),rpd.itTool2->GetName(INDEFINITE).CStr(),itTool->GetName(INDEFINITE).CStr());
814   }
815 
findCarvingToolSpecificrecipe816   static item* findCarvingToolSpecific(recipedata& rpd, item* itTool,int iMinCarvingStr, int iType, int& riMult, int iIncMult){ //this one is to avoid using #define mess :>, but instead of a simple if() we get several method calls... tho more readable...
817     if(itTool==NULL){
818       itTool = FindTool(rpd, iType, 0, iMinCarvingStr);
819       if(itTool!=NULL)riMult+=iIncMult;
820     }
821     return itTool;
822   }
findTailoringToolrecipe823   static item* findTailoringTool(recipedata& rpd,item* itToWorkOn){
824     int iCarvingStr=0;
825 
826     material* matM = itToWorkOn->GetMainMaterial();
827     material* matS = itToWorkOn->GetSecondaryMaterial();
828     if(!craftcore::IsMeltable(matM))
829       iCarvingStr=matM->GetStrengthValue();
830     if(matS!=NULL && !craftcore::IsMeltable(matS))
831       iCarvingStr=Max(iCarvingStr,matS->GetStrengthValue());
832 
833     int iMinCarvingStr = iCarvingStr/2;
834 
835     // any blanded thing bug preferably a dagger
836     int iMult=1;
837     item* itTool = FindTool(rpd, DAGGER, 0, iMinCarvingStr); //carving: tool cant be too much weaker
838     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,DAGGER,iMult,0); //TODO should be SCISSORS
839     if(itTool!=NULL){
840       if(iCarvingStr>1){
841         int itStr=itTool->GetMainMaterial()->GetStrengthValue();
842         if(itStr<iCarvingStr)
843           iMult++;
844         if(itStr==iMinCarvingStr)
845           iMult++;
846       }
847       calcToolTurns(rpd,iMult);
848 
849       if(!recipe::findOLT(rpd,TAILORING_BENCH)){
850         ADD_MESSAGE("As you lack a workbench, it will take a while."); //it is good to measure, hold tight, has a good height etc...
851         rpd.iBaseTurnsToFinish *= 3;
852       }
853     }else{
854       //ADD_MESSAGE("You have no carving tool good enough to work on the requested material.");
855       // Already covered by failToolMsg()
856       rpd.SetAlreadyExplained();
857     }
858 
859     return itTool;
860   }
findCarvingToolrecipe861   static item* findCarvingTool(recipedata& rpd,item* itToWorkOn){
862     int iCarvingStr=0;
863 
864     material* matM = itToWorkOn->GetMainMaterial();
865     material* matS = itToWorkOn->GetSecondaryMaterial();
866     if(!craftcore::IsMeltable(matM))
867       iCarvingStr=matM->GetStrengthValue();
868     if(matS!=NULL && !craftcore::IsMeltable(matS))
869       iCarvingStr=Max(iCarvingStr,matS->GetStrengthValue());
870 
871     int iMinCarvingStr = iCarvingStr/2;
872 
873     // any blanded thing bug preferably a dagger
874     int iMult=1;
875     item* itTool = FindTool(rpd, DAGGER, 0, iMinCarvingStr); //carving: tool cant be too much weaker
876     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,DAGGER,iMult,0);
877     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,SICKLE,iMult,1);
878     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,SHORT_SWORD,iMult,1);
879     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,AXE,iMult,2);
880     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,MEAT_CLEAVER,iMult,2);
881     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,LONG_SWORD,iMult,2);
882     itTool = findCarvingToolSpecific(rpd,itTool,iMinCarvingStr,SPEAR,iMult,3); // mmm... spear is easy enough to be found already TODO all other more difficult to be used weapons with blades may be added below here :), basically create a balsa dagger using a balsa spear and you are good to go xD
883     if(itTool!=NULL){
884       if(iCarvingStr>1){
885         int itStr=itTool->GetMainMaterial()->GetStrengthValue();
886         if(itStr<iCarvingStr)
887           iMult++;
888         if(itStr==iMinCarvingStr)
889           iMult++;
890       }
891       calcToolTurns(rpd,iMult);
892 
893       if(!recipe::findOLT(rpd,WORK_BENCH)){
894         ADD_MESSAGE("As you lack a workbench, it will take a while."); //it is good to measure, hold tight, has a good height etc...
895         rpd.iBaseTurnsToFinish *= 3;
896       }
897     }else{
898       //ADD_MESSAGE("You have no carving tool good enough to work on the requested material.");
899       // Already covered by failToolMsg()
900       rpd.SetAlreadyExplained();
901     }
902 
903     return itTool;
904   }
905 
FindToolrecipe906   static item* FindTool(recipedata& rpd, int iCfg, int iWeaponCategory=0, int iMainMatterMinStrengh=0){
907     itemvector vit = vitInv(rpd);
908     for(int i=0;i<vit.size();i++)
909       if(chkWeapon(vit[i],iCfg,iWeaponCategory,iMainMatterMinStrengh))
910         return vit[i];
911     for(int i=0;i<vit.size();i++)
912       if(chkWeapon(vit[i],iCfg|BROKEN,iWeaponCategory,iMainMatterMinStrengh))
913         return vit[i];
914     return NULL;
915   }
916 
FindByWeaponCategoryrecipe917   static item* FindByWeaponCategory(recipedata& rpd,int iWCategory){
918     return FindTool(rpd,0,iWCategory);
919   }
920 
calcToolTurnsrecipe921   static void calcToolTurns(recipedata& rpd,int iPrecalculatedMultBasedOnTool){
922     if(iPrecalculatedMultBasedOnTool==0)
923       return;
924 
925     int iBaseTurns = rpd.iBaseTurnsToFinish;
926     float fIncTurnsStep=0.25;
927     int iAddTurns=iBaseTurns*(iPrecalculatedMultBasedOnTool*fIncTurnsStep);
928     rpd.iBaseTurnsToFinish = iBaseTurns + iAddTurns;
929   }
930 
FindBluntToolrecipe931   static item* FindBluntTool(recipedata& rpd){
932     //TODO could be based on volume and weight vs strengh and dexterity to determine how hard is to use the tool, unbalanced considering the required precision
933     static const int iTotToolTypes=3;
934     static const int aiTypes[iTotToolTypes]={HAMMER, FRYING_PAN, WAR_HAMMER};
935 
936     item* it = NULL;
937     int iMult = 0;
938     for(int j=0;j<iTotToolTypes;j++){DBG2(j,aiTypes[j]);
939       it = FindTool(rpd, aiTypes[j]);
940       if(it){DBG2(it->GetConfig(),it->GetName(DEFINITE).CStr());
941         iMult=j;
942         break;
943       }
944     }
945 
946     if(it==NULL){
947       it = FindByWeaponCategory(rpd,BLUNT_WEAPONS);
948       iMult=iTotToolTypes; //max
949     }
950 
951     if(it!=NULL)
952       calcToolTurns(rpd,iMult);
953     else{
954       ADD_MESSAGE("You have no blunt tool.");
955       rpd.SetAlreadyExplained();
956     }
957 
958     return it;
959   }
960 
FindCuttingToolrecipe961   static item* FindCuttingTool(recipedata& rpd){//,bool bReversedTimeMult=false){
962     int iBaseTurns = rpd.iBaseTurnsToFinish;
963 
964     item* it = NULL;
965     int iMult = 0;
966     int iMaxMult = 0;
967 
968     #define FBWC(WC) \
969       if(it==NULL){ \
970         it = FindByWeaponCategory(rpd,WC); \
971         if(it==NULL)iMult++; \
972       } \
973       iMaxMult++;
974     FBWC(SMALL_SWORDS); //DAGGER is here
975     FBWC(AXES); //has more cutting power but less precision and takes more time TODO is this good?
976     FBWC(LARGE_SWORDS); //slower
977     FBWC(POLE_ARMS); //even slower, hard to use
978 
979     if(it!=NULL){
980 //      if(bReversedTimeMult)
981 //        iMult = iMaxMult-iMult;
982 
983       calcToolTurns(rpd,iMult);
984     }else{
985       ADD_MESSAGE("You have no cutting tool.");
986       rpd.SetAlreadyExplained();
987     }
988 
989     return it;
990   }
991 
992   /**
993    * prepare the filter for ALL items also resetting them first!
994    */
prepareFilterrecipe995   template <typename T> static void prepareFilter(
996     recipedata& rpd,
997     const itemvector& vi,
998     long reqVol,
999     ci CI=ci(),
1000     std::vector<ulong>* ptmpIngredientsIDs = NULL
1001   ){
1002     for(int i=0;i<vi.size();i++){
1003       vi[i]->SetValidRecipeIngredient(false);
1004       if(dynamic_cast<T*>(vi[i])!=NULL){
1005         if(vi[i]->IsBurning())continue;
1006 
1007         /**
1008          * w/o this, things can be indirectly repaired to their original state,
1009          * unless spawining/cloning/copying changes to have the same
1010          * (or average in case of many to one) degradation data
1011          *
1012          * degradation should only be "fixable" thru magic (scroll of repair)
1013          */
1014         if(!CI.bAllowDegradation && craftcore::IsDegraded(vi[i])) //TODO lower the volume of "remaining perfect materials" based on the degradation levels ?
1015           continue;
1016 
1017         if(CI.iReqCfg>0 && vi[i]->GetConfig()!=CI.iReqCfg)
1018           continue;
1019 
1020         if(CI.bFirstItemMustHaveFullVolumeRequired && vi[i]->GetVolume()<reqVol)
1021           continue;
1022 
1023         if(!CI.bAllowMeltables && craftcore::IsMeltable(vi[i]))
1024           continue;
1025 
1026         if(CI.bMustBeTailorable && !(vi[i]->GetMainMaterial()->GetCategoryFlags() & CAN_BE_TAILORED))
1027           continue;
1028 
1029         if(vi[i]->GetStrengthValue() < CI.iMinMainMaterStr)
1030           continue;
1031 
1032         bool bAlreadyUsed=false;
1033         for(int j=0;j<rpd.ingredientsIDs.size();j++){
1034           if(vi[i]->GetID()==rpd.ingredientsIDs[j]){
1035             bAlreadyUsed=true;
1036             break;
1037           }
1038         }
1039         if(ptmpIngredientsIDs!=NULL)
1040           for(int j=0;j < ptmpIngredientsIDs->size();j++){
1041             if(vi[i]->GetID() == (*ptmpIngredientsIDs)[j]){
1042               bAlreadyUsed=true;
1043               break;
1044             }
1045           }
1046         if(bAlreadyUsed)
1047           continue;
1048 
1049         if(CI.iReqMatCfgMain > 0 && vi[i]->GetMainMaterial()->GetConfig() != CI.iReqMatCfgMain)
1050           continue;
1051 
1052         if(CI.iReqMatCfgSec > 0 && vi[i]->GetMainMaterial()->GetConfig() != CI.iReqMatCfgSec)
1053           continue;
1054 
1055         vi[i]->SetValidRecipeIngredient(true);
1056       }
1057     }
1058   }
1059 
joinLumpsEqualTorecipe1060   void joinLumpsEqualTo(recipedata& rpd,material* matM){
1061     // multiple (compatible with 1st) lumps will be mixed in a big one again
1062     for(int i=1;i<rpd.ingredientsIDs.size();i++){
1063       item* LumpToAdd = game::SearchItem(rpd.ingredientsIDs[i]);DBGLN;
1064       if(dynamic_cast<lump*>(LumpToAdd)==NULL)continue;
1065 
1066       material* LumpToAddM = LumpToAdd->GetMainMaterial();DBGLN;
1067       if(LumpToAddM->GetConfig()!=matM->GetConfig())continue;
1068 
1069       // join
1070       matM->SetVolume(matM->GetVolume()+LumpToAddM->GetVolume());DBGLN;
1071 
1072       craftcore::SendToHellSafely(LumpToAdd);
1073     }
1074   }
1075 
joinLumpsEqualToFirstrecipe1076   void joinLumpsEqualToFirst(recipedata& rpd){
1077     item* Lump = game::SearchItem(rpd.ingredientsIDs[0]);
1078     material* matM=Lump->GetMainMaterial();
1079     joinLumpsEqualTo(rpd,matM);
1080   }
1081 
askForEqualLumpsrecipe1082   void askForEqualLumps(recipedata& rpd){
1083     ci CI;
1084     CI.bOverridesQuestion=true;
1085     CI.bMsgInsuficientMat=false;
1086     CI.bInstaAddIngredients=true;
1087     int iWeakestCfgDummy;
1088     bool bDummy = choseIngredients<lump>(
1089       festring("First chosen lump's material will be mixed with further ones of same material only, hit ESC to accept."),
1090       1000000, //just any "impossible" huge volume as "limit"
1091       rpd, iWeakestCfgDummy, CI); // true, 0, false, true, false, true);
1092   }
1093 
choseIngredientsrecipe1094   template <typename T> static truth choseIngredients(
1095       cfestring fsQ,
1096       long reqVolPrecise,
1097       recipedata& rpd,
1098       int& iWeakestCfg,
1099       ci CI=ci()
1100   ){DBGLN;
1101     if(CI.fUsablePercVol>1.0 || CI.fUsablePercVol<=0)
1102       ABORT("usable vol is max 100%% %f",CI.fUsablePercVol);
1103 
1104     /**
1105      * ex.: vol=100; usable=0.5; req=200=100/0.5;
1106      * remaining lump: after shapping the stone, should become lump, ex.: dagger 25cm3 req 33cm3,
1107      * prepare a stone with 25cm3 and a lump with 8cm3
1108      */
1109     long reqVolTotal = reqVolPrecise/CI.fUsablePercVol;
1110 
1111     long reqVol = reqVolTotal;
1112 
1113     if(reqVol==0)
1114       ABORT("ingredient required 0 volume?");
1115 
1116     const itemvector vi = vitInv(rpd,true,CI.bAddEquippedItemsToChoiceList);
1117     prepareFilter<T>(rpd,vi,reqVol,CI);
1118 
1119     int iWeakest=-1;
1120     game::RegionListItemEnable(true);
1121     game::RegionSilhouetteEnable(true);
1122     std::vector<ulong> tmpIngredientsIDs;
1123     for(;;)
1124     {
1125       itemvector ToUse;
1126       game::DrawEverythingNoBlit();
1127       int flags = CI.bMultSelect ? REMEMBER_SELECTED : REMEMBER_SELECTED|NO_MULTI_SELECT;
1128 
1129       festring fsFullQ;
1130       festring specialDesc;
1131       if(CI.bOverridesQuestion)
1132         fsFullQ=fsQ;
1133       else{
1134         fsFullQ = festring("What ingredient(s) will you use? (hit ESC for more options if available)");
1135         specialDesc = fsQ+" ["+reqVol+"cm3]";
1136       }
1137 
1138 //      rpd.rc.H()->GetStack()->DrawContents(ToUse, rpd.rc.H(),
1139 //        fsFullQ, flags, &item::IsValidRecipeIngredient);
1140       col16 col = specialDesc.Find("MAIN")!=festring::NPos ? YELLOW : LIGHT_GRAY; //hightlight MAIN material as the question may happen more than once for each material to avoid confusion
1141       rpd.rc.H()->GetStack()->DrawContents(ToUse, 0, rpd.rc.H(), fsFullQ, CONST_S(""),
1142         CONST_S(""), specialDesc, col, flags, &item::IsValidRecipeIngredient);
1143 
1144       if(ToUse.empty())
1145         break;
1146 
1147       if(CI.bJustAcceptFirstChosenAndReturn){
1148         tmpIngredientsIDs.push_back(ToUse[0]->GetID());
1149       }else for(int i=0;i<ToUse.size();i++){
1150         material* matM = ToUse[i]->GetMainMaterial();
1151         if(CI.bFirstMainMaterIsFilter && CI.iReqMatCfgMain==0)
1152           CI.iReqMatCfgMain=matM->GetConfig();
1153 
1154         if(iWeakest==-1 || iWeakest > matM->GetStrengthValue()){
1155           iWeakest = matM->GetStrengthValue();
1156           iWeakestCfg = matM->GetConfig();
1157           rpd.itWeakestIngredient = ToUse[i];
1158         }
1159 
1160         tmpIngredientsIDs.push_back(ToUse[i]->GetID()); DBG1(ToUse[i]->GetID());
1161         reqVol -= ToUse[i]->GetVolume(); DBG2(reqVol,ToUse[i]->GetVolume());
1162         ToUse[i]->SetValidRecipeIngredient(false); //just to not be shown again on the list
1163 
1164         if(reqVol<=0){
1165           long lRemainingVol = reqVol * -1; //beyond required
1166 
1167           if(CI.fUsablePercVol<1.0) //while shapping this is what is "lost"
1168             lRemainingVol += reqVolTotal * (1.0 - CI.fUsablePercVol); //ex.: a round rock being shaped into a spear tip
1169 
1170           if(lRemainingVol>0 && CI.bMainMaterRemainsBecomeLump){
1171             long lVolM = matM->GetVolume();
1172             lVolM -= lRemainingVol; //to sub
1173             if(lVolM<=0)
1174               ABORT("ingredient volume reduced to negative or zero %ld %ld %s %s",lVolM,lRemainingVol,matM->GetName(DEFINITE).CStr(),ToUse[i]->GetNameSingular().CStr());
1175             if(!CI.bMultSelect && lVolM!=reqVolPrecise) //TODO use error margin because of float VS integer calc? ex.: if diff is +1 or -1, just allow it.
1176               ABORT("remaining vol calc needs fixing %ld != %ld, %f, %ld",lVolM,reqVolPrecise,CI.fUsablePercVol,lRemainingVol);
1177 
1178             matM->SetVolume(lVolM);
1179             item* lumpR = craftcore::PrepareRemains(rpd,matM,CIT_NONE,lRemainingVol);
1180             item* lumpMixed=NULL;
1181             if(CI.bMixRemainingLump)
1182               lumpMixed=lumpMix(vi,lumpR,rpd.bSpendCurrentTurn);
1183             undoremains* pur = CI.bIsMainIngredient ? &rpd.urMain : &rpd.urSecond;
1184             if(pur->ulUndoRemainsIngredientID!=0)
1185               ABORT("craft remains undo for %s ingredient was already set!",(CI.bIsMainIngredient?"MAIN":"SECOND"));
1186             pur->ulUndoRemainsIngredientID = ToUse[i]->GetID();
1187             pur->ulUndoRemainsLumpID = lumpMixed!=NULL ? lumpMixed->GetID() : lumpR->GetID();
1188             pur->lUndoRemainsVolume = lRemainingVol;
1189             DBG6("UndoRemain:Set",CI.bIsMainIngredient,pur->ulUndoRemainsIngredientID,pur->ulUndoRemainsLumpID,pur->lUndoRemainsVolume,rpd.dbgInfo().CStr());
1190 
1191             material* matS = ToUse[i]->GetSecondaryMaterial();
1192             if(matS!=NULL && matS->GetVolume()>0)
1193               ABORT("ingredient secondary material should not have volume %ld %s %s",matS->GetVolume(),matS->GetName(DEFINITE).CStr(),ToUse[i]->GetNameSingular().CStr());
1194           }
1195 
1196           break; //SUCCESS!
1197         }
1198 
1199 //        if(CI.bFirstItemMustHaveFullVolumeRequired)
1200 //          break; //may fail
1201       }
1202 
1203       if(reqVol<=0 || CI.bInstaAddIngredients || CI.bJustAcceptFirstChosenAndReturn){
1204         for(int i=0;i<tmpIngredientsIDs.size();i++)
1205           rpd.ingredientsIDs.push_back(tmpIngredientsIDs[i]);
1206 
1207         if(CI.bJustAcceptFirstChosenAndReturn)
1208           break;
1209 
1210         if(CI.bInstaAddIngredients)
1211           tmpIngredientsIDs.clear(); //to avoid dups
1212         else
1213           break; //success
1214       }
1215 
1216       if(CI.bFirstMainMaterIsFilter) //prepare filter for next list display
1217         prepareFilter<T>(rpd,vi,reqVol,CI,&tmpIngredientsIDs);
1218 
1219 //      if(CI.bFirstItemMustHaveFullVolumeRequired)
1220 //        break;
1221     }
1222 
1223     game::RegionListItemEnable(false);
1224     game::RegionSilhouetteEnable(false);
1225 
1226     if(!CI.bJustAcceptFirstChosenAndReturn)
1227       if(CI.bMsgInsuficientMat && reqVol>0){ // && rpd.ingredientsIDs.size()>0)
1228         festring fsIngTpNm;
1229         if(CI.iReqCfg==INGOT)
1230           fsIngTpNm = "ingot";
1231         else
1232         {
1233           // TODO:
1234           //  demangling simple like that may give weird text one day if other types are added
1235           //  This is way too confusing, just use generic "material" and rework later.
1236           //fsIngTpNm = std::string(typeid(T).name()).substr(1).c_str();
1237           fsIngTpNm = "material";
1238         }
1239 
1240         ADD_MESSAGE("You don't have enough %ss.",fsIngTpNm.CStr());
1241       }
1242 
1243     if(reqVol<=0)
1244       return true;
1245 
1246     if(CI.bJustAcceptFirstChosenAndReturn && rpd.ingredientsIDs.size()>0)
1247       return true;
1248 
1249     return false;
1250   }
1251 
choseOneIngredientrecipe1252   template <typename T> static bool choseOneIngredient(recipedata& rpd, ci* pCI=NULL){
1253     int iWeakestCfgDummy;
1254     ci CIok;
1255     if(pCI)CIok=*pCI;
1256     CIok.bMultSelect=false;
1257     CIok.bJustAcceptFirstChosenAndReturn=true;
1258     return choseIngredients<T>(
1259       festring(""),
1260       1, //just to chose one of anything
1261       rpd,
1262       iWeakestCfgDummy,
1263       CIok);
1264   }
1265 
vitInvrecipe1266   static itemvector vitInv(recipedata& rpd,bool bAllowWielded=true,bool bAllowAllEquipped=false){
1267     itemvector vi;
1268 
1269     //prefer already equipped
1270     if(bAllowWielded){ //TODO not showing on list...
1271       if(rpd.rc.H()->GetRightWielded())vi.push_back(rpd.rc.H()->GetRightWielded());
1272       if(rpd.rc.H()->GetLeftWielded ())vi.push_back(rpd.rc.H()->GetLeftWielded ());
1273     }
1274 
1275     if(bAllowAllEquipped){ //TODO not showing on list...
1276       for(int c = 0; c < rpd.rc.H()->GetEquipments(); ++c){
1277         if(
1278           c!=RIGHT_WIELDED_INDEX &&
1279           c!=LEFT_WIELDED_INDEX &&
1280           rpd.rc.H()->GetEquipment(c)
1281         ){
1282           vi.push_back(rpd.rc.H()->GetEquipment(c));
1283         }
1284       }
1285     }
1286 
1287     rpd.rc.H()->GetStack()->FillItemVector(vi); //TODO once, the last item from here had an invalid pointer, HOW?
1288 
1289     return vi;
1290   }
1291 
1292   /**
1293    * @return the lump where it was mixed into (or the input lump)
1294    */
lumpMixrecipe1295   static item* lumpMix(itemvector vi,item* lumpToMix, bool& bSpendCurrentTurn){
1296     // to easily (as possible) create a big lump
1297     lump* lumpAtInv=NULL;
1298     for(int i=0;i<vi.size();i++){
1299       if(lumpToMix==vi[i])continue;
1300 
1301       if(dynamic_cast<lump*>(vi[i])!=NULL){
1302         lump* lumpAtInv = (lump*)vi[i];
1303         if(lumpAtInv->GetMainMaterial()->GetConfig() == lumpToMix->GetMainMaterial()->GetConfig()){
1304           lumpAtInv->GetMainMaterial()->SetVolume(
1305             lumpAtInv->GetMainMaterial()->GetVolume() + lumpToMix->GetMainMaterial()->GetVolume());
1306           lumpAtInv->CalculateAll();
1307 
1308           craftcore::SendToHellSafely(lumpToMix); DBG5("SentToHell",lumpToMix,lumpToMix->GetID(),lumpAtInv,lumpAtInv->GetID());
1309           bSpendCurrentTurn=true; //this is necessary or item wont be sent to hell...
1310           break;
1311         }
1312       }
1313     }
1314 
1315     return lumpAtInv!=NULL ? lumpAtInv : lumpToMix;
1316   }
1317 
SetOLTrecipe1318   static void SetOLT(recipedata& rpd,lsquare* lsqr,int iCfgOLT){
1319     switch(iCfgOLT){
1320     case FORGE:
1321       rpd.v2ForgeLocation = lsqr->GetPos();
1322       break;
1323     case ANVIL:
1324       rpd.v2AnvilLocation = lsqr->GetPos();
1325       break;
1326     case WORK_BENCH:
1327       rpd.v2WorkbenchLocation = lsqr->GetPos();
1328       break;
1329     case TAILORING_BENCH:
1330       rpd.v2TailoringWorkbenchLocation = lsqr->GetPos();
1331       break;
1332     }
1333   }
1334 
chkOLTrecipe1335   static bool chkOLT(recipedata& rpd,lsquare* lsqr,int iCfgOLT){DBGLN;
1336     olterrain* ot = lsqr->GetOLTerrain();
1337     if(ot!=NULL && ot->GetConfig() == iCfgOLT){
1338       return true;
1339     }
1340     return false;
1341   }
findOLTrecipe1342   static bool findOLT(recipedata& rpd,int iCfgOLT,bool bReqOnlyVisible=false){
1343     bool bFound=false;
1344     //obs.: lsqr->CanBeFeltByPlayer() will NOT work if player CAN MOVE on it!
1345     if(bReqOnlyVisible){DBGLN; //even if far away
1346       for(int iY=0;iY<game::GetCurrentLevel()->GetYSize();iY++){for(int iX=0;iX<game::GetCurrentLevel()->GetXSize();iX++){
1347         lsquare* lsqr = game::GetCurrentLevel()->GetLSquare(v2(iX,iY));DBG3(lsqr,iX,iY);
1348         if( chkOLT(rpd,lsqr,iCfgOLT) && (lsqr->GetPos().IsAdjacent(rpd.rc.H()->GetPos()) || lsqr->CanBeSeenBy(rpd.rc.H())) ){
1349           SetOLT(rpd,lsqr,iCfgOLT);
1350           bFound = true;
1351           break;
1352         }
1353       }}
1354     }else{DBGLN; //must be on adjacent square
1355       for(int iDir=0;iDir<8;iDir++){
1356         v2 v2Add = game::GetMoveVector(iDir);
1357         v2 v2Pos = rpd.rc.H()->GetPos() + v2Add; DBG3(DBGAV2(v2Add),DBGAV2(v2Pos),iDir);
1358         if(game::GetCurrentLevel()->IsValidPos(v2Pos)){
1359           lsquare* lsqr = rpd.rc.H()->GetNearLSquare(v2Pos);DBG1(lsqr);
1360 //          if(lsqr->CanBeFeltBy(rpd.rc.H()) && chkOLT(rpd,lsqr,iCfgOLT)){
1361           if(chkOLT(rpd,lsqr,iCfgOLT) && lsqr->GetPos().IsAdjacent(rpd.rc.H()->GetPos())){
1362             SetOLT(rpd,lsqr,iCfgOLT);
1363             bFound = true;
1364             break;
1365           }
1366         }
1367       }
1368     }
1369 
1370     if(!bFound){
1371       festring fsWhat;
1372       switch(iCfgOLT){
1373       case FORGE:fsWhat="forge";break;
1374       case ANVIL:fsWhat="anvil";break;
1375       case WORK_BENCH:fsWhat="workbench";break;
1376       case TAILORING_BENCH:fsWhat="tailoring bench";break;
1377       }
1378 
1379       festring fsMsg="No ";
1380       if(bReqOnlyVisible)
1381         fsMsg<<"visible ";
1382       fsMsg<<fsWhat<<" nearby.";
1383       ADD_MESSAGE(fsMsg.CStr());
1384 
1385       rpd.SetAlreadyExplained();
1386 
1387       return false;
1388     }
1389 
1390     return true;
1391   }
1392 };
1393 
1394 /**
1395  * As we can't (shouldn't) kick webs...
1396  *
1397  * This is a special kind of "recipe"
1398  * is a way to change the existing environment, like engrave does,
1399  * but this one was implemented as crafting code
1400  * so in short, this could be a command like engrave is, but was implemented thru crafting.
1401  *
1402  * This action is like a simple slash on the web, so will spend only one turn.
1403  *
1404  * TODO
1405  * may be, more functionality could be added, like collect web (spiker silk) to be able to
1406  * craft leather/clothing or other things.
1407  * may be even recreate a single square spider web with it's strength based on craft skill!
1408  */
1409 struct srpCutWeb : public recipe{
fillInfosrpCutWeb1410   virtual void fillInfo(){
1411     init("cut","a web");
1412     desc << "Cut a web down. Much safer than tearing it down, especially if you use a cutting tool. You might still get stuck, though.";
1413   }
1414 
worksrpCutWeb1415   virtual bool work(recipedata& rpd){
1416     if(!recipe::whereRaw(rpd,"Cut a web where?",true)){
1417       rpd.SetAlreadyExplained(); //no need to explain a cancelled action
1418       return false;
1419     }
1420 
1421     web* w=NULL;
1422     std::vector<trap*> TrapVector;
1423     rpd.lsqrPlaceAt->FillTrapVector(TrapVector);
1424     for(int i=0;i<TrapVector.size();i++){
1425       if(dynamic_cast<web*>(TrapVector[i])==NULL)
1426         continue;
1427       w = (web*)TrapVector[i];
1428       break;
1429     }
1430     if(w==NULL){
1431       ADD_MESSAGE("There is no web there.");
1432       rpd.SetAlreadyExplained();
1433       return false;
1434     }
1435 
1436     humanoid* h = rpd.rc.H();
1437 
1438     arm* ra = h->GetRightArm();
1439     arm* la = h->GetLeftArm();
1440     if(ra && !ra->IsUsable())ra=NULL;
1441     if(la && !la->IsUsable())la=NULL;
1442     if(!ra && !la){
1443       ADD_MESSAGE("You have no usable arm to do that.");
1444       rpd.SetAlreadyExplained();
1445       return false;
1446     }
1447 
1448     bool bSelfPos = rpd.lsqrPlaceAt->GetPos() == h->GetPos();
1449 
1450     rpd.itTool = FindCuttingTool(rpd); // no blunt, no non-cutting, imagine a web that can hold the weight of a whole body, only cutting tools
1451     if(rpd.itTool)
1452       ADD_MESSAGE("You will use your %s to cut the web.",rpd.itTool->GetName(UNARTICLED).CStr());
1453 //    rpd.bAlreadyExplained=false;
1454 //    item* wieldBkp=NULL;
1455 //    bool bIsWBkpRHand = true;
1456 //    if(rpd.itTool!=NULL){
1457 //      bool bWielded=false;
1458 //      #define RLWIELD(rl,br) \
1459 //        if(!bWielded && h->Get##rl##Arm() && !h->Get##rl##Arm()->IsStuck()){ \
1460 //          if(h->Get##rl##Wielded()){ \
1461 //            wieldBkp = h->Get##rl##Wielded(); \
1462 //            wieldBkp->MoveTo(h->GetStack()); \
1463 //          } \
1464 //          rpd.itTool->RemoveFromSlot(); \
1465 //          h->Set##rl##Wielded(rpd.itTool); \
1466 //          bWielded=true; \
1467 //          bIsWBkpRHand=br; \
1468 //        }
1469 //      RLWIELD(Right,true);
1470 //      RLWIELD(Left,false);
1471 //    }
1472 
1473     /**
1474      * IMPORTANT!
1475      * this repetition is about action quality and NOT time taken
1476      * because trying to tear down the web is a random check
1477      * based on a fixed modifier (vanilla one) per turn!!!
1478      */
1479     int tot=1;
1480     if(ra && la)
1481       tot+=2; //means something like using both arms to destroy a single web spot
1482     if(bSelfPos)
1483       tot *= 3; //to make it worthier than just trying to move, and to compensate for not moving too as player won't insta flee from attacks
1484     if(rpd.itTool!=NULL) //float multiplier last thing!
1485       tot *= 1 + craftcore::CraftSkill(h)/10.0;
1486     DBG1(tot);
1487     bool bSuccess = false;
1488     for(int i=0;i<tot;i++){
1489       if(w->TryToTearDown(h)){
1490         bSuccess=true;
1491         break;
1492       }
1493     }
1494 
1495     if(bSuccess){
1496       rpd.SetAlreadyExplained();
1497       material* matSSilk=material::MakeMaterial(SPIDER_SILK);
1498       craftcore::FinishSpawning(rpd, craftcore::PrepareRemains(rpd,matSSilk,CIT_LUMP,RAND()%6+3));
1499     }else{
1500       bool bGetStuckOnTheWeb=false;
1501       bool bLoseWeapon=false;
1502       bool bLoseGlove=false;
1503       bool bCriticalFumble=false;
1504       int iFumblePower=10;
1505       item* itGlove = ra?ra->GetGauntlet():NULL;
1506       if(!itGlove)itGlove = la?la->GetGauntlet():NULL;
1507       if(craftcore::CheckFumble(rpd, bCriticalFumble, iFumblePower)){DBGLN;
1508         if(bCriticalFumble){DBGLN;
1509           bGetStuckOnTheWeb=true;
1510           if(rpd.itTool){
1511             if(RAND()%100 < 50){DBGLN;
1512               bLoseWeapon=true;
1513             }
1514           }else if(itGlove){
1515             if(RAND()%100 < 50){DBGLN;
1516               bLoseGlove=true;
1517             }
1518           }
1519         }else{DBGLN;
1520           if(rpd.itTool){DBGLN;
1521             bLoseWeapon=true;
1522           }else if(itGlove){
1523             bLoseGlove=true;
1524           }else{DBGLN;
1525             bGetStuckOnTheWeb=true;
1526           }
1527         }
1528       }
1529 
1530       if(bGetStuckOnTheWeb){
1531         if(rpd.lsqrPlaceAt->GetCharacter()==NULL){
1532           h->Remove();
1533           h->PutTo(rpd.lsqrPlaceAt->GetPos()); //TODO check for walkability first!
1534           w->StepOnEffect(h);
1535           ADD_MESSAGE("You've got stuck on the web!");
1536           rpd.SetAlreadyExplained();
1537         }else{
1538           if(rpd.itTool){
1539             bLoseWeapon=true;
1540           }else if(itGlove){
1541             bLoseGlove=true;
1542           }
1543         }
1544       }
1545 
1546       if(bLoseWeapon){
1547         rpd.itTool->RemoveFromSlot();
1548         rpd.itTool->MoveTo(rpd.lsqrPlaceAt->GetStack()); //TODO check if is not a WALL!!!
1549         ADD_MESSAGE("You lost your %s!",rpd.itTool->GetName(UNARTICLED).CStr());
1550         rpd.SetAlreadyExplained();
1551       }
1552 
1553       if(bLoseGlove){
1554         itGlove->RemoveFromSlot();
1555         itGlove->MoveTo(rpd.lsqrPlaceAt->GetStack());
1556         ADD_MESSAGE("You lost your %s!",itGlove->GetName(UNARTICLED).CStr());
1557         rpd.SetAlreadyExplained();
1558       }
1559 
1560       if(bSelfPos && !bGetStuckOnTheWeb && !bLoseWeapon){
1561         w->StepOnEffect(h); //so every try will make it more difficult!! :)
1562       }
1563 
1564       int iSt = w->GetTrapBaseModifier();DBG1(iSt);
1565       iSt -= 1 + RAND()%5; // small spider = 10, big = 25, wand beam = 50
1566       if(iSt<=0)
1567         iSt=1;
1568       w->SetStrength(iSt);
1569 
1570       if(!rpd.bAlreadyExplained){
1571         ADD_MESSAGE("You fail to tear down the web.");
1572         rpd.SetAlreadyExplained();
1573       }
1574 
1575     }
1576 
1577 //    if(wieldBkp!=NULL){
1578 //      if(wieldBkp->GetSlot()->FindCarrier() == h){
1579 //        wieldBkp->RemoveFromSlot();;
1580 //        if(bIsWBkpRHand){
1581 //          if(h->GetRightWielded())
1582 //            h->GetRightWielded()->MoveTo(h->GetStack());
1583 //          h->SetRightWielded(wieldBkp);
1584 //        }else{
1585 //          if(h->GetLeftWielded())
1586 //            h->GetLeftWielded()->MoveTo(h->GetStack());
1587 //          h->SetLeftWielded(wieldBkp);
1588 //        }
1589 //      }
1590 //    }
1591 
1592     h->EditAP(-500); //to let time pass
1593     craftcore::CraftSkillAdvance(rpd);
1594 
1595     rpd.bSpendCurrentTurn=true;
1596 
1597     return true;
1598   }
1599 };srpCutWeb rpCutWeb;
1600 
1601 struct srpOltBASE : public recipe{
1602  protected:
1603   int iReqVol=0;
1604   int iReqVolSkulls=0;
1605   int iTurns=0;
1606   bool bRequiresWhere=false;
1607   bool bAllowSimpleStones=false;
1608   bool bReqForge=false;
1609   bool bReqToolBlunt=true;
1610   ci CIdefault;
1611   festring fsDescBASE = "You will need a hammer or other blunt tool.";
1612 
spawnCfgsrpOltBASE1613   virtual bool spawnCfg(recipedata& rpd){return false;}
1614 
1615  public:
worksrpOltBASE1616   virtual bool work(recipedata& rpd){DBGLN;
1617     if(iReqVol==0)ABORT("Recipe vol is 0 for OLT");
1618     if(iTurns==0)ABORT("Recipe turns is 0 for OLT");
1619 
1620     if(bReqForge)
1621       if(!recipe::findOLT(rpd,FORGE))
1622         return false;
1623 
1624     DBGLN;
1625     if(bRequiresWhere){DBGLN;
1626       if(!recipe::where(rpd))
1627         return false;
1628     }else{DBGLN;
1629       rpd.lsqrPlaceAt=rpd.lsqrCharPos;
1630     }
1631 
1632     rpd.iBaseTurnsToFinish=iTurns;
1633 
1634     if(bReqToolBlunt){
1635       rpd.itTool = FindBluntTool(rpd);
1636       if(rpd.itTool==NULL)
1637         return false;
1638     }
1639 
1640     if(rpd.lsqrPlaceAt->GetOLTerrain()!=NULL){
1641       ADD_MESSAGE("It can't be placed here.");
1642       rpd.SetAlreadyExplained();
1643       return false;
1644     }
1645 
1646     rpd.bCanBePlaced=true;
1647 
1648     festring fsQ("to build ");fsQ<<name;
1649     int iCfg=-1;
1650     bool bH=false;
1651     CIdefault.bFirstMainMaterIsFilter=false; //TODO move to constructor
1652     if(!bH){
1653       ci CI = CIdefault;
1654       CI.iReqCfg = bAllowSimpleStones?0:INGOT;
1655       CI.bMainMaterRemainsBecomeLump = true;
1656       bH=choseIngredients<stone>(fsQ, iReqVol, rpd, iCfg, CI); //true, bAllowSimpleStones?0:INGOT, true);
1657     }
1658     if(!bH && CIdefault.bAllowWood)
1659       bH=choseIngredients<stick>(fsQ, iReqVol, rpd, iCfg, CIdefault);
1660     if(CIdefault.bAllowBones){
1661       if(!bH && iReqVolSkulls>0)
1662         bH=choseIngredients<skull> (fsQ, iReqVolSkulls, rpd, iCfg, CIdefault);
1663       if(!bH)
1664         bH=choseIngredients<bone> (fsQ, iReqVol, rpd, iCfg, CIdefault);
1665     }
1666 
1667     if(bH){
1668       rpd.bHasAllIngredients=bH;
1669       rpd.v2PlaceAt = rpd.lsqrPlaceAt->GetPos();
1670       if(!spawnCfg(rpd))ABORT("Recipe spawn cfg not set %s",desc.CStr());
1671       rpd.otSpawnMatMainCfg=iCfg;
1672       rpd.otSpawnMatMainVol=iReqVol;
1673       rpd.bCanStart=true;
1674     }else
1675       failIngredientsMsg(rpd);
1676 
1677     return true;
1678   }
1679 };
1680 struct srpDoor : public srpOltBASE{
spawnCfgsrpDoor1681   virtual bool spawnCfg(recipedata& rpd){
1682     rpd.otSpawnType=CTT_DOOR;
1683     rpd.otSpawnCfg=SECRET_DOOR; //it currently just looks like BRICK_OLD
1684     return true;
1685   }
1686 
fillInfosrpDoor1687   virtual void fillInfo(){
1688     init("build","a secret door");
1689     desc << "Build a door. You will need a hammer or some other blunt tool, plus sticks, bones or ingots.";
1690   }
1691 
worksrpDoor1692   virtual bool work(recipedata& rpd){
1693     iReqVol=30000;  //TODO this volume should be on the .dat file TODO breaking a door will only drop a few lumps about 2500cm3 only... makes no sense for metal doors
1694     iTurns=30;
1695     return srpOltBASE::work(rpd);
1696   }
1697 };srpDoor rpDoor;
1698 struct srpChair : public srpOltBASE{
1699 //  virtual olterrain* spawn(recipedata& rpd){
spawnCfgsrpChair1700   virtual bool spawnCfg(recipedata& rpd){
1701     rpd.otSpawnType=CTT_FURNITURE;
1702     rpd.otSpawnCfg=CHAIR;
1703 //    return decoration::Spawn(CHAIR);
1704     return true;
1705   }
1706 
fillInfosrpChair1707   virtual void fillInfo(){
1708     init("build","a chair");
1709     desc << "Create a chair to rest your tired legs. You will need a hammer or some other blunt tool.";
1710   }
1711 
worksrpChair1712   virtual bool work(recipedata& rpd){
1713     iReqVol=15000;       //TODO this volume should be on the .dat file
1714     iTurns=15;
1715     return srpOltBASE::work(rpd);
1716   }
1717 };srpChair rpChair;
1718 struct srpAnvil : public srpOltBASE{
spawnCfgsrpAnvil1719   virtual bool spawnCfg(recipedata& rpd){
1720     rpd.otSpawnType=CTT_FURNITURE;
1721     rpd.otSpawnCfg=ANVIL;
1722     return true;
1723   }
1724 
fillInfosrpAnvil1725   virtual void fillInfo(){
1726     init("build","an anvil");
1727     desc << "Create an anvil for further crafting, using ingots. You must be near a forge.\n " << fsDescBASE;
1728   }
1729 
worksrpAnvil1730   virtual bool work(recipedata& rpd){
1731     iReqVol=750*3; //when destroyed provides 250 to 750 x3, so lets use the max to avoid spawning extra material volume
1732     iTurns=15;
1733     bReqForge=true;
1734     CIdefault.bAllowBones=false;
1735     CIdefault.bAllowWood=false;
1736     return srpOltBASE::work(rpd);
1737   }
1738 };srpAnvil rpAnvil;
1739 struct srpForge : public srpOltBASE{
spawnCfgsrpForge1740   virtual bool spawnCfg(recipedata& rpd){
1741     rpd.otSpawnType=CTT_FURNITURE;
1742     rpd.otSpawnCfg=FORGE;
1743     return true;
1744   }
1745 
fillInfosrpForge1746   virtual void fillInfo(){
1747     init("build","a forge");
1748     desc << "Build a forge for further crafting. " << fsDescBASE;
1749   }
1750 
worksrpForge1751   virtual bool work(recipedata& rpd){
1752     iReqVol=15000;
1753     iTurns=30;
1754     bRequiresWhere=true;
1755     bAllowSimpleStones=true;
1756     CIdefault.bAllowBones=false;
1757     CIdefault.bAllowWood=false;
1758     //TODO require fire source like fireball wand or 3 lanterns
1759     return srpOltBASE::work(rpd);
1760   }
1761 };srpForge rpForge;
1762 struct srpWorkBench : public srpOltBASE{
spawnCfgsrpWorkBench1763   virtual bool spawnCfg(recipedata& rpd){
1764     rpd.otSpawnType=CTT_FURNITURE;
1765     rpd.otSpawnCfg=WORK_BENCH;
1766     return true;
1767   }
1768 
fillInfosrpWorkBench1769   virtual void fillInfo(){
1770     init("build","a crafting workbench");
1771     desc << "Build a workbench for further crafting. " << fsDescBASE;
1772   }
1773 
worksrpWorkBench1774   virtual bool work(recipedata& rpd){
1775     iReqVol=3000;
1776     iTurns=10;
1777     bRequiresWhere=true;
1778     return srpOltBASE::work(rpd);
1779   }
1780 };srpWorkBench rpWorkBench;
1781 struct srpTWorkBench : public srpOltBASE{
spawnCfgsrpTWorkBench1782   virtual bool spawnCfg(recipedata& rpd){
1783     rpd.otSpawnType=CTT_FURNITURE;
1784     rpd.otSpawnCfg=TAILORING_BENCH;
1785     return true;
1786   }
1787 
fillInfosrpTWorkBench1788   virtual void fillInfo(){
1789     init("build","a tailoring workbench");
1790     desc << "Build a tailoring workbench for further crafting. " << fsDescBASE;
1791   }
1792 
worksrpTWorkBench1793   virtual bool work(recipedata& rpd){
1794     iReqVol=9000;
1795     iTurns=30;
1796     bRequiresWhere=true;
1797     return srpOltBASE::work(rpd);
1798   }
1799 };srpTWorkBench rpTWorkBench;
1800 struct srpWall2 : public srpOltBASE{
spawnCfgsrpWall21801   virtual bool spawnCfg(recipedata& rpd){
1802     rpd.otSpawnType=CTT_WALL;
1803 
1804     rpd.otSpawnCfg=BRICK_OLD;
1805 //    if(dynamic_cast<stone>(game::SearchItem(rpd.ingredientsIDs[0]))!=NULL)
1806     if(dynamic_cast<stone*>(rpd.itWeakestIngredient)!=NULL)
1807       rpd.otSpawnCfg=STONE_WALL;
1808 
1809     return true;
1810   }
1811 
fillInfosrpWall21812   virtual void fillInfo(){
1813     init("construct","a wall");
1814     desc << "Construct a wall by piling stones, sticks or bones.";
1815   }
1816 
worksrpWall21817   virtual bool work(recipedata& rpd){
1818     //TODO this doesnt look good. anyway this volume should be on the .dat file as wall/earthWall attribute...
1819     iReqVol=9000; //TODO is this too little? a broken wall drops 3 rocks that is about 1000 each, so 3 walls to build one is ok?
1820     iReqVolSkulls=12000; //TODO is this too little? necromancers can spawn skeletons making it easy to get skulls, but the broken bone wall will drop bones and not skulls...
1821     iTurns=20;
1822     bRequiresWhere=true;
1823     bAllowSimpleStones=true;
1824     bReqToolBlunt=false;
1825     return srpOltBASE::work(rpd);
1826   }
1827 };srpWall2 rpWall2;
1828 
1829 struct srpJoinLumps : public recipe{
fillInfosrpJoinLumps1830   virtual void fillInfo(){
1831     init("merge","lumps");
1832     desc << "Merge lumps of the same material into a single, bigger one.";
1833   }
1834 
worksrpJoinLumps1835   virtual bool work(recipedata& rpd){ // it is just like to put them all together, no effort, instant.
1836     rpd.fDifficulty=0.1;
1837 
1838     askForEqualLumps(rpd);
1839 
1840     if(rpd.ingredientsIDs.empty()){
1841       ADD_MESSAGE("You have no lumps to work with.");
1842       rpd.SetAlreadyExplained();
1843       return false;
1844     }
1845 
1846     joinLumpsEqualToFirst(rpd);
1847     craftcore::CraftSkillAdvance(rpd);
1848 
1849     rpd.SetAlreadyExplained();
1850 
1851     return true;
1852   }
1853 };srpJoinLumps rpJoinLumps;
1854 
1855 struct srpMelt : public srpJoinLumps{
fillInfosrpMelt1856   virtual void fillInfo(){
1857     init("melt","an ingot");
1858     desc << "Melt lumps of metal into ingots, later usable in crafting. You must be near a forge.";
1859   }
1860 
worksrpMelt1861   virtual bool work(recipedata& rpd){
1862     // lumps are not usable until melt into an ingot.
1863 
1864     //TODO wands should xplode, other magical items should release something harmful beyond the very effect they are imbued with.
1865 
1866     if(!recipe::findOLT(rpd,FORGE))
1867       return false;
1868 
1869     askForEqualLumps(rpd);
1870 
1871     if(rpd.ingredientsIDs.empty()){
1872       failIngredientsMsg(rpd);
1873       return false;
1874     }
1875 
1876     item* Lump = game::SearchItem(rpd.ingredientsIDs[0]);
1877 
1878     item* LumpMeltable=NULL;
1879     if(craftcore::IsMeltable(Lump))
1880       LumpMeltable = Lump;
1881 
1882     if(LumpMeltable==NULL){
1883       ADD_MESSAGE("Can't melt %s.",Lump->GetName(INDEFINITE).CStr());
1884       rpd.SetAlreadyExplained();
1885       return false;
1886     }
1887 
1888     material* matM=LumpMeltable->GetMainMaterial();
1889 
1890     joinLumpsEqualTo(rpd,matM);
1891 
1892     rpd.iBaseTurnsToFinish = calcMeltableTurns(matM,5); DBG1(rpd.iBaseTurnsToFinish);
1893 
1894     rpd.bGradativeCraftOverride=true;
1895 
1896     rpd.bHasAllIngredients=true;
1897 
1898     rpd.itSpawnType = CIT_STONE;
1899     rpd.itSpawnCfg = INGOT;
1900 //    rpd.itSpawn = stone::Spawn(INGOT, NO_MATERIALS);
1901 
1902     /**
1903      * 25cm3 is each of the 2 parts of a dagger, but is too small?
1904      * Smaller ingots are easier to manage, less user interaction as they fit better.
1905      * BUT it may generate a HUGE LOT of tiny ingots and slow down the game when dropping/picking up :(
1906      */
1907     static long iIngotVol = 250; //TODO savegame this
1908     long iIngotVolTmp = game::NumberQuestion(festring("What volume shall the ingots have? [min 25cm3, ENTER/ESC accepts last (default) ")+iIngotVol+"cm3]",WHITE,true); //TODO how to let ESC cancel it?
1909     if(iIngotVolTmp>0){
1910       if(iIngotVolTmp<25)
1911         iIngotVolTmp=25;
1912       iIngotVol=iIngotVolTmp;
1913     }
1914 
1915     long lVolRemaining = 0;
1916     long lVolM = LumpMeltable->GetMainMaterial()->GetVolume();
1917     if(lVolM <= iIngotVol){
1918       rpd.itSpawnTot = 1;
1919       iIngotVol = lVolM;
1920     }else{
1921       rpd.itSpawnTot = lVolM / iIngotVol;
1922       lVolRemaining = lVolM % iIngotVol;
1923       if(lVolRemaining>0){ //split
1924         LumpMeltable->GetMainMaterial()->SetVolume(lVolM - lVolRemaining); // keep the exact remaining volume of what will be used
1925 
1926         /**
1927          * IMPORTANT!!!
1928          * the duplicator will vanish with the item ID that is being duplicated
1929          */
1930         item* itLumpR = LumpMeltable->DuplicateToStack(rpd.rc.H()->GetStack());
1931 
1932         itLumpR->GetMainMaterial()->SetVolume(lVolRemaining);
1933       }
1934     }
1935     DBG4(lVolRemaining,rpd.itSpawnTot,lVolM,iIngotVol);
1936 
1937 //    delete rpd.itSpawn->SetMainMaterial(material::MakeMaterial(
1938 //        LumpMeltable->GetMainMaterial()->GetConfig(), iIngotVol ));
1939     rpd.itSpawnMatMainCfg = LumpMeltable->GetMainMaterial()->GetConfig();
1940     rpd.itSpawnMatMainVol = iIngotVol;
1941 
1942     rpd.ingredientsIDs.clear(); //only the final meltable lump shall be sent to hell when it finishes
1943     rpd.ingredientsIDs.push_back(LumpMeltable->GetID()); //must be AFTER the duplicator
1944 
1945     rpd.bMeltable = true;
1946 
1947     rpd.bCanStart=true;
1948 
1949     return true;
1950   }
1951 };srpMelt rpMelt;
1952 
1953 struct srpDismantle : public recipe{ //TODO this is instantaneous, should take time?
fillInfosrpDismantle1954   virtual void fillInfo(){
1955     init("dismantle","item");
1956     desc << "Dismantle an item to recover its materials.\nMetal items require a forge to dismantle, all other need just a cutting tool.";
1957   }
1958 
worksrpDismantle1959   virtual bool work(recipedata& rpd){
1960     // lumps are not usable until melt into an ingot.
1961 
1962     //TODO wands should xplode, other magical items should release something harmful beyond the very effect they are imbued with.
1963 
1964     ///////////////////// chose item to melt/smash
1965     game::DrawEverythingNoBlit();
1966     ci CI;
1967     CI.bAllowDegradation=true; //to let user know what is happening w/o spamming it.
1968     if(!choseOneIngredient<item>(rpd,&CI)){
1969       rpd.SetAlreadyExplained(); //no need to explain if nothing chosen
1970       return false;
1971     }
1972 
1973     item* itToUse = game::SearchItem(rpd.ingredientsIDs[0]); DBG2(rpd.ingredientsIDs[0],itToUse);
1974     rpd.ingredientsIDs.clear();
1975 
1976     material* matM=NULL;
1977     material* matS=NULL;
1978     material* matIngot=NULL;
1979 
1980     matM = itToUse->GetMainMaterial();
1981     matS = itToUse->GetSecondaryMaterial();
1982 
1983     if(game::IsQuestItem(itToUse)){
1984       ADD_MESSAGE("You feel that would be a bad idea and carefully store it back in your inventory.");
1985       rpd.SetAlreadyExplained();
1986       return false;
1987     }
1988     // This is a quick fix, maybe allow dismantling mirrored items into mirrored lumps?
1989     if(itToUse->GetLifeExpectancy())
1990     {
1991       ADD_MESSAGE("%s is made of temporary magical force rather than physical matter and cannot be dismantled.",itToUse->GetName(DEFINITE).CStr());
1992       rpd.SetAlreadyExplained();
1993       return false;
1994     }
1995 
1996     if(dynamic_cast<corpse*>(itToUse)!=NULL || matM==NULL){ //TODO may be there are other things than corpses that also have no main material?
1997       ADD_MESSAGE("You should try to split %s instead.",itToUse->GetName(DEFINITE).CStr());
1998       rpd.SetAlreadyExplained();
1999       return false;
2000     }
2001 
2002     if(craftcore::IsDegraded(itToUse,true)){
2003       rpd.SetAlreadyExplained();
2004       return false;
2005     }
2006 
2007     rpd.bMeltable = craftcore::IsMeltable(itToUse);
2008     if(rpd.bMeltable){
2009       if(!recipe::findOLT(rpd,FORGE))
2010         return false;
2011     }else{
2012       rpd.itTool = FindCuttingTool(rpd);//,true);
2013       if(!rpd.itTool){
2014 //        failToolMsg(rpd,"a cutting weapon");
2015         return false;
2016       }
2017     }
2018 
2019     /////////////////////// dismantle into lumps
2020     if(dynamic_cast<lump*>(itToUse)!=NULL){
2021       ADD_MESSAGE("%s is already a lump.", itToUse->GetName(DEFINITE).CStr());
2022       rpd.SetAlreadyExplained();
2023       return false;
2024     }
2025 
2026     /**
2027      * Meltable sticks should be lumpable to easify the code.
2028      * Kept in case of wanting to re-enabling for some reason...
2029      *
2030     if(dynamic_cast<stick*>(itToUse)!=NULL){
2031       ADD_MESSAGE("%s is already a stick.", itToUse->GetName(DEFINITE).CStr());
2032       rpd.SetAlreadyExplained();
2033       return false;
2034     }
2035      */
2036 
2037     // for now, uses just one turn to smash anything into lumps but needs to be near a FORGE
2038     // TODO should actually require a stronger hammer than the material's hardness being smashed, and could be anywhere...
2039     bool bW = itToUse->IsWeapon(rpd.rc.H());
2040     item* RmnM = craftcore::PrepareRemains(rpd, matM, (bW && itToUse->GetConfig()==QUARTER_STAFF) ? CIT_STICK : CIT_NONE);
2041     item* RmnS = matS==NULL ? NULL : craftcore::PrepareRemains(rpd, matS, bW ? CIT_STICK : CIT_LUMP); //must always be created to not lose it
2042 
2043     craftcore::EmptyContentsIfPossible(rpd,itToUse,true);
2044 
2045     ADD_MESSAGE("%s was completely dismantled.", itToUse->GetName(DEFINITE).CStr());
2046     rpd.SetAlreadyExplained();
2047 
2048     craftcore::SendToHellSafely(itToUse); DBG3("SentToHell",itToUse->GetID(),itToUse); //TODO if has any magic should release it and also harm
2049 
2050     rpd.bSpendCurrentTurn=true; //this is necessary or item wont be sent to hell...
2051 
2052     if(dynamic_cast<lump*>(RmnM)!=NULL)
2053       lumpMix(vitInv(rpd),RmnM,rpd.bSpendCurrentTurn);
2054     if(dynamic_cast<lump*>(RmnS)!=NULL)
2055       lumpMix(vitInv(rpd),RmnS,rpd.bSpendCurrentTurn);
2056 
2057     craftcore::CraftSkillAdvance(rpd);
2058 
2059     return true;
2060   }
2061 };srpDismantle rpDismantle;
2062 
addMaterialInfo(character * C,item * it)2063 void addMaterialInfo(character* C,item* it){
2064   itemvector v;
2065   material* matM = it->GetMainMaterial();
2066   C->GetStack()->FillItemVector(v);
2067   if(!it->HasTag('m')){ //material info transfered to item from
2068     for(int i=0;i<v.size();i++){
2069       if(dynamic_cast<materialmanual*>(v[i])){
2070         it->SetTag('m');
2071         it->SetLabel(it->GetLabel()+"s"+matM->GetStrengthValue()+"f"+matM->GetFlexibility());
2072         ADD_MESSAGE("You consult %s about %s. It has a strength of %d and a flexibility of %i.",
2073           v[i]->GetName(DEFINITE).CStr(),matM->GetNameStem().CStr(),matM->GetStrengthValue(),matM->GetFlexibility());
2074         break;
2075       }
2076     }
2077   }
2078 }
2079 
2080 struct srpInspect : public recipe{
fillInfosrpInspect2081   virtual void fillInfo(){
2082     init("inspect","item materials");
2083     desc << "Carefully inspect what materials an item is made of.";
2084   }
2085 
worksrpInspect2086   virtual bool work(recipedata& rpd){
2087     ci CI;
2088     CI.bAddEquippedItemsToChoiceList=true;
2089     if(!choseOneIngredient<item>(rpd,&CI)){
2090       rpd.SetAlreadyExplained();
2091       return false;
2092     }
2093 
2094     //TODO add each material details if in possesion of the book of matarials!
2095     item* it0 = game::SearchItem(rpd.ingredientsIDs[0]);
2096     material* matM = it0->GetMainMaterial();
2097     material* matS = it0->GetSecondaryMaterial();
2098     festring fs;
2099     fs<<it0->GetName(DEFINITE)<<" is made of ";
2100     if(matM)fs<<matM->GetName(UNARTICLED);
2101     if(matS){
2102       if(matM)fs<<" and "; //actually, there is only 2nd material if there is main but anyway...
2103       fs<<matS->GetName(UNARTICLED);
2104     }
2105     fs<<".";
2106 
2107     if(matM||matS){
2108       ADD_MESSAGE("%s",fs.CStr());
2109       addMaterialInfo(rpd.rc.H(),it0);
2110       craftcore::CraftSkillAdvance(rpd);
2111     }else{
2112       ADD_MESSAGE("You can't inspect %s.",it0->GetName(INDEFINITE).CStr());
2113     }
2114     rpd.SetAlreadyExplained();
2115     return true;
2116   }
2117 };srpInspect rpInspect;
2118 
2119 struct srpResistanceVS : public recipe{
fillInfosrpResistanceVS2120   virtual void fillInfo(){
2121     init("check","material strength");
2122     desc << "Check the relative hardness of two items. The item made of softer material will receive a scratch.";
2123   }
2124 
worksrpResistanceVS2125   virtual bool work(recipedata& rpd){
2126     ci CI;
2127     CI.bAddEquippedItemsToChoiceList=true;
2128     CI.iMinMainMaterStr=1;
2129     if(!choseOneIngredient<item>(rpd,&CI)){
2130       rpd.SetAlreadyExplained();
2131       return false;
2132     }
2133 
2134     // yes, a 2nd time
2135     if(!choseOneIngredient<item>(rpd,&CI)){
2136       rpd.SetAlreadyExplained();
2137       return false;
2138     }
2139 
2140     if(rpd.ingredientsIDs.size()!=2){
2141       rpd.SetAlreadyExplained();
2142       return false;
2143     }
2144 
2145     item* it0 = game::SearchItem(rpd.ingredientsIDs[0]);
2146     item* it1 = game::SearchItem(rpd.ingredientsIDs[1]);
2147     item* itWeaker=NULL;
2148     item* itStronger=NULL;
2149     int iStr0=it0->GetMainMaterial()->GetStrengthValue(); //main materials only
2150     int iStr1=it1->GetMainMaterial()->GetStrengthValue();
2151     int iStrDiff=iStr0-iStr1;
2152     float fRatio=Min(iStr0,iStr1)/(float)Max(iStr0,iStr1);
2153 
2154     if(iStrDiff<0){
2155       ADD_MESSAGE("%s receives a scratch.",it0->GetName(DEFINITE).CStr());
2156       itWeaker=it0;
2157       itStronger=it1;
2158     }else
2159     if(iStrDiff>0){
2160       ADD_MESSAGE("%s receives a scratch.",it1->GetName(DEFINITE).CStr());
2161       itWeaker=it1;
2162       itStronger=it0;
2163     }else
2164     if(iStrDiff==0)
2165       ADD_MESSAGE("It seems both items are equally hard.");
2166 
2167     if(itWeaker!=NULL){ //the item may break
2168       float dmg =
2169         ( itStronger->GetWeight()*itStronger->GetMainMaterial()->GetStrengthValue() ) /
2170         (float)
2171         ( itWeaker  ->GetWeight()*itWeaker  ->GetMainMaterial()->GetStrengthValue() )   ;
2172       itWeaker->ReceiveDamage(rpd.rc.H(), (int)dmg, THROW|PHYSICAL_DAMAGE); //based on item::Fly() "breaks" but not that much
2173     }
2174 
2175     rpd.SetAlreadyExplained();
2176     addMaterialInfo(rpd.rc.H(),it0);
2177     addMaterialInfo(rpd.rc.H(),it1);
2178     craftcore::CraftSkillAdvance(rpd);
2179 
2180     return true;
2181   }
2182 };srpResistanceVS rpResistanceVS;
2183 
2184 struct srpSplitLump : public recipe{
explainsrpSplitLump2185   void explain(recipedata& rpd,festring fs){
2186     ADD_MESSAGE("You need a cutting tool to split %s.",fs.CStr());
2187     rpd.SetAlreadyExplained();
2188   }
2189 
reqCutsrpSplitLump2190   bool reqCut(recipedata& rpd,cfestring fs){
2191     rpd.itTool = FindCuttingTool(rpd);
2192     if(rpd.itTool==NULL){
2193       explain(rpd,fs);
2194       return false;
2195     }
2196     return true;
2197   }
2198 
fillInfosrpSplitLump2199   virtual void fillInfo(){
2200     init("split","raw materials");
2201     desc << "Split raw materials to make them easier to work with.\n"
2202             "To remove some volume, specify a negative value in cm3.\n"
2203             "It is good to cut precisely not to waste materials.\n"
2204             "Note that stones will require a hammer and a dagger.";
2205   }
2206 
worksrpSplitLump2207   virtual bool work(recipedata& rpd){
2208     item* ToSplit = NULL;
2209 
2210     rpd.bGradativeCraftOverride=true; //may be disabled below
2211     rpd.fDifficulty=0.1;
2212 
2213     if(ToSplit==NULL && choseOneIngredient<lump>(rpd)){ //can  be split just with hands
2214       ToSplit = game::SearchItem(rpd.ingredientsIDs[0]);
2215       rpd.itSpawnType = CIT_LUMP;
2216     }
2217 
2218     bool bHumanoidCorpse=false;
2219     if(ToSplit==NULL && choseOneIngredient<corpse>(rpd)){ //this is just a simplified conversion from corpse to a big lump...
2220       corpse* Corpse = (corpse*)game::SearchItem(rpd.ingredientsIDs[0]);
2221 
2222       if(!reqCut(rpd,Corpse->GetName(INDEFINITE)))
2223         return false;
2224 
2225       //TODO this should take time as an action
2226       character* D = Corpse->GetDeceased(); DBG2(Corpse->GetName(DEFINITE).CStr(),D->GetName(DEFINITE).CStr())
2227       static const materialdatabase* flesh;flesh = material::GetDataBase(D->GetFleshMaterial());
2228       ToSplit = craftcore::PrepareRemains(rpd,material::MakeMaterial(flesh->Config,Corpse->GetVolume()));
2229       if(dynamic_cast<humanoid*>(D)!=NULL){
2230         /**
2231          * this is to try to keep necromancers, raising zombies, challenging,
2232          * despite their action is random and not timed, so let it be random here too :)
2233          * TODO instead of just lump, also remove head and limbs (if available) so a friendly zombie could attach it?
2234          */
2235         rpd.iMinTurns = 3 + RAND()%3;
2236         rpd.bGradativeCraftOverride=false;
2237 
2238         // tmp flesh lump
2239         rpd.itSpawnCfg = ToSplit->GetConfig();
2240         rpd.itSpawnMatMainCfg = ToSplit->GetMainMaterial()->GetConfig();
2241         craftcore::SendToHellSafely(ToSplit);
2242 
2243         ToSplit = Corpse;
2244         bHumanoidCorpse=true;
2245       }else{
2246         craftcore::SendToHellSafely(Corpse);
2247       }
2248 
2249       rpd.ingredientsIDs.clear();
2250       rpd.ingredientsIDs.push_back(ToSplit->GetID());
2251 
2252       rpd.itSpawnType = CIT_LUMP;
2253 
2254       rpd.SetAlreadyExplained(); //no need to say anything
2255     }
2256 
2257     if(ToSplit==NULL && choseOneIngredient<stick>(rpd)){
2258       ToSplit = game::SearchItem(rpd.ingredientsIDs[0]);
2259       if(!reqCut(rpd,ToSplit->GetName(INDEFINITE)))
2260         return false;
2261       rpd.itSpawnType = CIT_STICK;
2262     }
2263 
2264     if(ToSplit==NULL){
2265       ci CI;
2266       CI.bAllowMeltables=false;
2267       if(choseOneIngredient<stone>(rpd,&CI)){
2268         ToSplit = game::SearchItem(rpd.ingredientsIDs[0]);
2269         if(!setTools(rpd,FindBluntTool(rpd),findCarvingTool(rpd,ToSplit))){ //the blunt is to hit the hilt of the cutting one
2270           explain(rpd,ToSplit->GetName(INDEFINITE));
2271           return false;
2272         }
2273         rpd.itSpawnType = CIT_STONE;
2274       }
2275     }
2276 
2277     if(ToSplit==NULL){
2278       rpd.SetAlreadyExplained(); //no need to say anything
2279       return false;
2280     }
2281 
2282     /*
2283     if(craftcore::IsDegraded(ToSplit)){
2284       rpd.SetAlreadyExplained();
2285       return false;
2286     }
2287     */
2288 
2289     if(ToSplit->GetSecondaryMaterial()!=NULL)
2290       ABORT("can only split items without secondary material");
2291 
2292     if(ToSplit->GetVolume()<2){
2293       ADD_MESSAGE("It must have 2 or more cm3 to be splitable."); //TODO just for now.. negative volumes could be split one day with extra code
2294       rpd.SetAlreadyExplained();
2295       return false;
2296     }
2297 
2298     // some items may not have main material like corpses
2299     long volTot=ToSplit->GetVolume();
2300 
2301     festring fsInfo;
2302     ToSplit->AddInventoryEntry(rpd.rc.H(),fsInfo,1,true);
2303     rpd.itSpawnTot = game::NumberQuestion(festring()+"Split "+fsInfo+" in how many parts? [2 or more]", WHITE, true);
2304     if(rpd.itSpawnTot==1 || rpd.itSpawnTot==0){
2305       rpd.SetAlreadyExplained(); //no need to say anything
2306       return false;
2307     }
2308 
2309     if(rpd.itSpawnTot<0){ //single cut mode
2310       if(bHumanoidCorpse){
2311         ADD_MESSAGE("This needs to be split first."); //see 'why' about necromancers above... TODO a better message?
2312         rpd.SetAlreadyExplained();
2313         return false;
2314       }
2315 
2316       int iCutVol = rpd.itSpawnTot * -1;
2317       material* matM = ToSplit->GetMainMaterial();
2318       if(matM==NULL)
2319         ABORT("main material is null for %s?",ToSplit->GetName(DEFINITE).CStr());
2320       if((matM->GetVolume() - iCutVol) < 1){
2321         rpd.SetAlreadyExplained(); //no need to say anything
2322         return false;
2323       }
2324 
2325       item* cut = craftcore::PrepareRemains(rpd,matM,craftcore::CitType(ToSplit),iCutVol);
2326       //cut->GetMainMaterial()->SetVolume(iCutVol);
2327       matM->SetVolume(matM->GetVolume() - iCutVol);
2328       ToSplit->SetMainMaterial(matM); //just to let it calculate too
2329       ToSplit->CalculateAll(); //calculate again to help?
2330       craftcore::FinishSpawning(rpd,ToSplit); //to help on calculating emitation
2331       rpd.SetAlreadyExplained(); //no need to say anything
2332       return true; //TODO should take more turns?
2333     }
2334 
2335     rpd.itSpawnMatMainVol = volTot/rpd.itSpawnTot;
2336     if(rpd.itSpawnMatMainVol < 1){
2337       ADD_MESSAGE("The split part must have some volume.");
2338       rpd.SetAlreadyExplained();
2339       return false;
2340     }
2341 
2342     /**
2343      * gradative work takes care of remaining vol
2344      * if not gradative, this little remainder will just be lost (probably only in case of corpses)
2345      * kept the code active as reference if someone thinks a good way to use that value :)
2346      */
2347     long volRest = volTot%rpd.itSpawnTot;
2348 
2349     if(rpd.itSpawnCfg==0)
2350       rpd.itSpawnCfg = ToSplit->GetConfig();
2351     if(rpd.itSpawnMatMainCfg==0)
2352       rpd.itSpawnMatMainCfg = ToSplit->GetMainMaterial()->GetConfig();
2353 
2354     float fTotTurns=0;
2355     for(int i=1;i<=rpd.itSpawnTot;i++){ //TODO should be based on cut area/length
2356       float fCurrentPartVol=volTot/(float)i;
2357       float fTurns=fCurrentPartVol/100000.0; //reference: bear is 150000cm3
2358       fTotTurns+=fTurns; //turn time is one minute but when fighting it also spends 1 min... w/e...
2359     }
2360     rpd.iBaseTurnsToFinish = fTotTurns;
2361     if(rpd.iBaseTurnsToFinish<1)
2362       rpd.iBaseTurnsToFinish=1;
2363 
2364     rpd.SetAlreadyExplained(); //no need to say anything
2365 
2366     rpd.bCanStart = true;
2367 
2368     return true;
2369   }
2370 };srpSplitLump rpSplitLump;
2371 
2372 struct srpForgeItem : public recipe{
fillInfosrpForgeItem2373   virtual void fillInfo(){
2374     init("create","an item");
2375     desc << "Try to craft an item.\n"
2376     "You will need an appropriate tool. Normally a hammer or a dagger are used, "
2377     "but other blunt and cutting weapons can serve as well. To work with metals, "
2378     "you need to have ingots prepared and to be standing near a forge and an anvil. "
2379     "Working on a workbench will be faster.\n"
2380     "Ingots and stick are always fully used up, while stones can be used up partially. "
2381     "Any remaining materials will become lumps.";
2382   }
2383 
2384   /**
2385    * IMPORTANT!!!
2386    *
2387    * dont use difficulty to calc/increase turns as it is already a per-turn check
2388    */
applyDifficultysrpForgeItem2389   void applyDifficulty(recipedata& rpd,item* itSpawn){
2390     //TODO are these difficulties below well balanced (not cheap neither excessive/notFun)?
2391     whistle* itWist = dynamic_cast<whistle*>(itSpawn);
2392     if(itWist){
2393       rpd.fDifficulty = 3.5;
2394       rpd.bSpecialExtendedTurns=true;
2395     }
2396 
2397     key* itKey = dynamic_cast<key*>(itSpawn);
2398     if(itKey!=NULL){ //TODO are these difficulties below good?
2399       rpd.fDifficulty = 3.0;
2400       if(itKey->CanOpenLockType(HEXAGONAL_LOCK))
2401         rpd.fDifficulty = 4.0;
2402       if(itKey->CanOpenLockType(OCTAGONAL_LOCK))
2403         rpd.fDifficulty = 4.0;
2404       if(itKey->CanOpenLockType(HEART_SHAPED_LOCK))
2405         rpd.fDifficulty = 5.0;
2406 
2407       rpd.bSpecialExtendedTurns=true;
2408     }
2409 
2410     stethoscope* itSte = dynamic_cast<stethoscope*>(itSpawn);
2411     if(itSte){
2412       rpd.fDifficulty = 3.5;
2413       rpd.bSpecialExtendedTurns=true;
2414     }
2415 
2416     meleeweapon* itMW = dynamic_cast<meleeweapon*>(itSpawn);
2417     if(itMW!=NULL){ //TODO are these difficulties below good?
2418       rpd.fDifficulty = 2.0;
2419       if(itMW->IsTwoHanded())
2420         rpd.fDifficulty = 3.5;
2421     }
2422 
2423     shield* itSH = dynamic_cast<shield*>(itSpawn);
2424     if(itSH!=NULL){
2425       rpd.fDifficulty = 2.5;
2426     }
2427 
2428     armor* itAR = dynamic_cast<armor*>(itSpawn);
2429     if(itAR!=NULL){
2430       rpd.fDifficulty = 1.5;
2431       bodyarmor* itBAR = dynamic_cast<bodyarmor*>(itSpawn);
2432       if(itBAR!=NULL)
2433         rpd.fDifficulty = 3.0;
2434     }
2435   }
2436 
worksrpForgeItem2437   virtual bool work(recipedata& rpd){DBGLN;
2438     // let user type the item name
2439     static festring Default; //static to help on reusing! like creating more of the same
2440     item* itSpawn;
2441 
2442     for(;;){
2443       festring Temp;
2444       Temp << Default;DBG4(Default.CStr(),Default.GetSize(),Temp.CStr(),Temp.GetSize()); // to let us fix previous instead of having to fully type it again
2445 
2446       if(game::DefaultQuestion(Temp, CONST_S("What do you want to try to create?"), Default, true) == ABORTED){DBGLN;
2447         /**
2448          * The wishing system will try to guess what item matches whatever we type,
2449          * so it may not be what we typed exactly...
2450          */
2451         break;
2452       }DBG1(Temp.CStr());
2453 
2454       itSpawn = protosystem::CreateItemToCraft(Temp);DBGLN;
2455 
2456       if(itSpawn){DBGLN;
2457         if(craftcore::canBeCrafted(itSpawn)){DBG4("SendingToHellRejectedCraftItem",itSpawn->GetID(),itSpawn->GetNameSingular().CStr(),itSpawn);
2458           rpd.itSpawnType = CIT_PROTOTYPE;
2459           rpd.fsItemSpawnSearchPrototype = Temp;
2460           break;
2461         }else{
2462           festring fsDoWhat = "You don't have the power required to enchant";
2463           if(itSpawn->GetCategory()==FOOD)fsDoWhat="You never learned how to cook"; //TODO sounds weird for kiwi/banana TODO could at least roast stuff TODO needs seeds to create flour and bake breads
2464           if(itSpawn->GetCategory()==POTION)fsDoWhat="You never learned how to make";
2465           if(itSpawn->GetCategory()==BOOK)fsDoWhat="You don't have time to write";
2466           if(itSpawn->GetCategory()==MISC)fsDoWhat="You are overwhelmed by the complexity of";
2467           ADD_MESSAGE("%s %s!",fsDoWhat.CStr(),itSpawn->GetName(INDEFINITE).CStr()); //itCreate->GetNameSingular());//
2468           craftcore::SendToHellSafely(itSpawn);
2469           itSpawn = NULL; //IMPORTANT!!! if user press ESC...
2470         }
2471       }else{
2472         ADD_MESSAGE("You don't know how to create %s.",Temp.CStr());
2473       }
2474 
2475       Default.Empty();DBG1(Default.CStr());
2476       Default << Temp;
2477     }
2478 
2479     if(itSpawn==NULL){
2480       rpd.SetAlreadyExplained(); //actually was just cancelled by user
2481       return false;
2482     }
2483 
2484     //////////////////////////////////////////////////////////////////////////////////////////////////
2485     /* This message ends up being rather confusing.
2486     if(Default == itSpawn->GetName(INDEFINITE)) //TODO compare for EQUAL ignoring case
2487       ADD_MESSAGE("Now you need the materials to create a %s.",itSpawn->GetName(INDEFINITE).CStr());
2488     else
2489       ADD_MESSAGE("Now you need the materials to create a %s as you would probably create %s.",Default.CStr(),itSpawn->GetName(INDEFINITE).CStr());
2490     */
2491 
2492     long lVolM = itSpawn->GetMainMaterial()->GetVolume();
2493     if(lVolM==0)
2494       ABORT("main material 0 volume??? %s",itSpawn->GetName(DEFINITE).CStr());
2495 
2496     long lVolS = 0;
2497     if(itSpawn->GetSecondaryMaterial()!=NULL){
2498       lVolS = itSpawn->GetSecondaryMaterial()->GetVolume();
2499       if(lVolS==0)
2500         ABORT("secondary material set with 0 volume??? %s",itSpawn->GetName(DEFINITE).CStr());
2501     }
2502 
2503     DBG2(lVolM,lVolS);
2504     int iCfgM=-1;
2505     int iCfgS=-1;
2506 
2507     ci CIM;
2508     CIM.bMainMaterRemainsBecomeLump=true;
2509 
2510     ci CIS;
2511     CIS.bMainMaterRemainsBecomeLump=true;
2512     CIS.bIsMainIngredient=false;
2513 
2514     // some material constraints/limitations
2515     if(dynamic_cast<potion*>(itSpawn)!=NULL){
2516       CIM.iReqMatCfgMain=GLASS; //TODO there are other materials that can hold sulphuric acid?
2517     }
2518 
2519     bool bIsItemContainer = dynamic_cast<itemcontainer*>(itSpawn) !=NULL;
2520     bool bIsWeapon = itSpawn->IsWeapon(rpd.rc.H());
2521 
2522     bool bMainMatOk = false;
2523     bool bMustTailor = dynamic_cast<whip*>(itSpawn) || dynamic_cast<cloak*>(itSpawn);
2524     bool bCanTailor = dynamic_cast<armor*>(itSpawn) &&
2525       !( dynamic_cast<helmet*>(itSpawn) ||
2526          dynamic_cast<shield*>(itSpawn)    );
2527     if(bMustTailor || bCanTailor){ // tailoring
2528       festring fsM("as MAIN material (cloth "); // only main can be cloth
2529       float fPerc = 0.85;
2530       fsM<<(int)(fPerc*100)<<"%)";
2531       /**
2532        * cloth will be cut and sewed
2533        */
2534       if(!bMainMatOk){
2535         askForEqualLumps(rpd);
2536         if(!rpd.ingredientsIDs.empty()){
2537           joinLumpsEqualToFirst(rpd);
2538           rpd.ingredientsIDs.clear();
2539         }
2540 
2541         ci CI = CIM;
2542         CI.fUsablePercVol=fPerc;
2543         CI.bMustBeTailorable = true;
2544         CI.bMixRemainingLump = false;
2545         bMainMatOk = choseIngredients<lump>(fsM,lVolM, rpd, iCfgM, CI); //TODO instead of <lump> should be a new item with new graphics called <cloth>
2546         if(bMainMatOk)
2547           rpd.bTailoringMode=true;
2548       }
2549     }
2550     if(!bMustTailor){
2551       // crafting with stones or ingots
2552       if(!bMainMatOk){
2553         ci CI = CIM;
2554         CI.iReqCfg=INGOT; //meltables are 100% usable
2555         festring fsM("as MAIN material (ingots 100%)");
2556         bMainMatOk = choseIngredients<stone>(fsM,lVolM, rpd, iCfgM, CI);
2557       }
2558 
2559       if(!bMainMatOk){
2560         ci CI = CIM;
2561         CI.bFirstItemMustHaveFullVolumeRequired=true; //carving: only one ingredient piece per material allowed, so it must have required volume
2562         CI.bMultSelect=false;
2563         CI.fUsablePercVol=0.75;
2564         festring fsM("as MAIN material (stones "); //roundy shape loses material
2565         fsM<<(int)(CI.fUsablePercVol*100)<<"%)";
2566         bMainMatOk = choseIngredients<stone>(fsM,lVolM, rpd, iCfgM, CI);
2567       }
2568 
2569       // crafting with sticks and bones
2570       if(!bMainMatOk){
2571         festring fsM("as MAIN material (sticks/bones ");
2572         float fPerc = bIsItemContainer ? 1.0 : 0.5;
2573         fsM<<(int)(fPerc*100)<<"%)";
2574         /**
2575          * stick shape can't provide enough to the required dimensions (this is a extremely wild simplification btw :))
2576          * so, this will require twice as much sticks if not a container, to be crafted ex.: 35/0.5=70
2577          */
2578         if(!bMainMatOk){
2579           ci CI = CIM;
2580           CI.fUsablePercVol=fPerc;
2581           CI.bFirstItemMustHaveFullVolumeRequired=true; //carving: only one ingredient piece per material allowed, so it must have required volume
2582           bMainMatOk = choseIngredients<bone>(fsM,lVolM, rpd, iCfgM, CI);
2583         }
2584         if(!bMainMatOk){
2585           ci CI = CIM;
2586           CI.fUsablePercVol=fPerc;
2587           if(!bIsItemContainer)
2588             CI.bFirstItemMustHaveFullVolumeRequired=true; //carving: only one ingredient piece per material allowed, so it must have required volume
2589           CI.bFirstMainMaterIsFilter=false; //wooden things are cheap (resistances, strength etc), so getting mixed into weakest will cause no trouble like losing good meltables (as they arent even), so let user chose any wood
2590           bMainMatOk = choseIngredients<stick>(fsM,lVolM, rpd, iCfgM, CI);
2591         }
2592       }
2593     }
2594 
2595     if(!bMainMatOk){
2596       ADD_MESSAGE("You don't have the materials to craft a %s.", Default.CStr());
2597       rpd.SetAlreadyExplained();
2598       craftcore::SendToHellSafely(itSpawn);
2599       return false;
2600     }
2601 
2602     //////////////////////////////////////////////////////////////////////////////////////////////////
2603     bool bContainerEmptied = craftcore::EmptyContentsIfPossible(rpd,itSpawn);
2604 
2605     /**
2606      * TODO problem: basically sulphuric acid can already be stored on a metal can ...
2607      * TODO every materialcontainer should rust depending on it's contents, if made of anything else than glass
2608      * Keeping allowing creating materialcontainer of non glass because the fix is already required for the existing metal can,
2609      * so preventing it would still not fix how 'metal can' works...
2610      */
2611 
2612     bool bReqS = bIsWeapon;
2613     bool bAllowS = true;
2614 //    if(mc)bAllowS=false;
2615     if(bContainerEmptied)bAllowS=false;
2616     if(lVolS==0)bAllowS=false;
2617     if(bAllowS){DBGLN;
2618       bool bSecondMatOk = false;
2619       festring fsS("as Secondary material");DBGLN;
2620       if(!bSecondMatOk){
2621         ci CI=CIS;
2622         CI.iReqCfg=INGOT;
2623         bSecondMatOk = choseIngredients<stone>(fsS,lVolS, rpd, iCfgS, CI);
2624       }
2625       if(!bSecondMatOk){
2626         ci CI=CIS; //carving: only one stone per material allowed, so it must have required volume
2627         CI.bFirstItemMustHaveFullVolumeRequired=true;
2628         CI.bMultSelect=false;
2629         bSecondMatOk = choseIngredients<stone>(fsS,lVolS, rpd, iCfgS, CI);
2630       }
2631       if(bIsWeapon){DBGLN; //this is mainly to prevent "material containers" being filled with non-sense materials like a bottle fille with wood... TODO powders one day would be ok
2632         if(!bSecondMatOk){
2633           ci CI=CIS;
2634           bSecondMatOk = choseIngredients<bone>(fsS,lVolS, rpd, iCfgS, CI);
2635         }
2636         if(!bSecondMatOk){
2637           ci CI=CIS;
2638           bSecondMatOk = choseIngredients<stick>(fsS,lVolS, rpd, iCfgS, CI);
2639         }
2640       }
2641 
2642       if(!bSecondMatOk){
2643         ADD_MESSAGE("You will craft it later...");
2644         rpd.SetAlreadyExplained();
2645         craftcore::SendToHellSafely(itSpawn);
2646         return false;
2647       }
2648     }
2649 
2650     if(bReqS && !bAllowS)
2651       ABORT("item reqs secondary mat but doesnt allow it??? %s",itSpawn->GetName(DEFINITE).CStr());
2652 
2653     if(rpd.bTailoringMode){
2654       long lVolSewing = lVolM/100;
2655       if(lVolSewing==0)lVolSewing=1;
2656       int iSCfg=-1;
2657       ci CISW;
2658       CISW.bMainMaterRemainsBecomeLump=true;
2659       CISW.bMixRemainingLump = false;
2660       CISW.iReqMatCfgMain=SPIDER_SILK;
2661       if(!choseIngredients<lump>(cfestring("as sewing material"),lVolSewing,rpd,iSCfg,CISW)){ //TODO instead of <lump> should be <sewingthread> with new graphics
2662         ADD_MESSAGE("You don't have enough sewing thread...");
2663         rpd.SetAlreadyExplained();
2664         craftcore::SendToHellSafely(itSpawn);
2665         return false;
2666       }
2667     }
2668 
2669     rpd.bHasAllIngredients=true;
2670 
2671     rpd.bCanBeBroken = itSpawn->CanBeBroken();
2672 
2673     delete itSpawn->SetMainMaterial(material::MakeMaterial(iCfgM,lVolM));
2674     if(bAllowS)
2675       delete itSpawn->SetSecondaryMaterial(material::MakeMaterial(iCfgS,lVolS));
2676 
2677     material* matM = itSpawn->GetMainMaterial();
2678     material* matS = itSpawn->GetSecondaryMaterial();
2679     bool bMeltableM = craftcore::IsMeltable(matM);
2680     bool bMeltableS = (matS!=NULL && craftcore::IsMeltable(matS));
2681     rpd.bMeltable = bMeltableM || bMeltableS;
2682 
2683     /**
2684      * ex.:
2685      * working with valpurium will take much longer,
2686      * and the longer it takes the more are the checks to determine if it will fumble and break.
2687      */
2688 
2689     float fMult=10;//hammering to form it takes time even if the volume is low.
2690     rpd.iBaseTurnsToFinish=calcMeltableTurns(matM,fMult); DBG4(rpd.iBaseTurnsToFinish,matM->GetName(DEFINITE).CStr(),matM->GetConfig(),matM->GetVolume());
2691     if(bAllowS && matS!=NULL){
2692       rpd.iBaseTurnsToFinish+=calcMeltableTurns(matS,fMult);
2693       DBG4(rpd.iBaseTurnsToFinish,matS->GetName(DEFINITE).CStr(),matS->GetConfig(),matS->GetVolume());
2694     }
2695 
2696     //////////////////////////////////////////////////////////////////////////////////////////////////
2697     if(rpd.bMeltable){
2698       if(!recipe::findOLT(rpd,FORGE,true)){
2699         craftcore::SendToHellSafely(itSpawn);
2700         return false;
2701       }
2702 
2703       if(!recipe::findOLT(rpd,ANVIL)){ //must be near the anvil to use it!!!
2704         craftcore::SendToHellSafely(itSpawn);
2705         return false;
2706       }
2707     }
2708 
2709     if(rpd.bTailoringMode){
2710       if(!recipe::findOLT(rpd,TAILORING_BENCH)){ //must be near it //TODO should be a new bench called TAILORING_BENCH with new graphics one day...
2711         craftcore::SendToHellSafely(itSpawn);
2712         return false;
2713       }
2714     }
2715 
2716     //////////////////////////////////////////////////////////////////////////////////////////////////
2717     applyDifficulty(rpd,itSpawn);
2718 
2719     if(!itSpawn->CanBeBroken() || !itSpawn->CanBeBurned())
2720       rpd.bSpecialExtendedTurns=true;
2721 
2722     if(rpd.bSpecialExtendedTurns){DBGLN;
2723       /**
2724        * special extra turns
2725        * for special items
2726        */
2727       int iBaseTTF = rpd.iBaseTurnsToFinish;
2728 
2729       rpd.iBaseTurnsToFinish += iBaseTTF*10;
2730 
2731       if(!rpd.bMeltable) //carving takes longer
2732         rpd.iBaseTurnsToFinish *= 1.25;
2733     }
2734 
2735     /************************************************************************************************
2736      * this sector OVERRIDES all turns calculations from above if necessary,
2737      * while keeping all other prepared data.
2738      * These things, mainly by their volume, wont fit well in the complex turns calcs above.
2739      * Consider it like ingots/sticks/planks/bones/etc are ready just need to be joined.
2740      ****************************************************************************************/
2741     itemcontainer* ic = dynamic_cast<itemcontainer*>(itSpawn);
2742     if(ic!=NULL){ DBGLN;
2743       /**
2744        * Reference is: (from current .dat values)
2745        * - large biggest stg. volume
2746             StorageVolume = 1000000;
2747             DamageDivider = 4;
2748             StrengthModifier = 200;
2749        * - strongbox biggest damage divider.
2750             StorageVolume = 5000;
2751             DamageDivider = 5;
2752             StrengthModifier = 150;
2753        * - small chest (weakest)
2754             StorageVolume = 10000;
2755             DamageDivider = 2;
2756             StrengthModifier = 100;
2757        */
2758 
2759       int iMaxTurns=60*3; //for large chest reference 3h
2760 
2761       /**
2762        * transform large chest field values into max turns.
2763        */
2764       float SVolRef=1000000;
2765       float DDivRef=4;
2766       float StrModRef=200;
2767 
2768       float SVolRatio = ic->GetStorageVolume()/SVolRef; //large is 10x normal chest, strongbox is 1/2 small chest
2769       float DDivRatio = ic->GetDamageDivider()/DDivRef; //strongbox has biggest value
2770       float StrModRatio = ic->GetStrengthModifier()/StrModRef; //strongbox equals to normal chest
2771 
2772       float SVolW=1.0;
2773       float DDivW=3.0;
2774       float StrMW=2.0;
2775       float W = SVolW+DDivW+StrMW;
2776 
2777       float Ratio = (SVolW*SVolRatio + DDivW*DDivRatio + StrMW*StrModRatio)/W; //div by total weight of summed ratios
2778 
2779       rpd.iBaseTurnsToFinish = iMaxTurns * Ratio; DBG3(rpd.iBaseTurnsToFinish,ic->GetNameSingular().CStr(),ic->GetAdjective().CStr());
2780     }
2781 
2782     /// TOOLS ///////////////////////////////////////////////////////////////////////////////////////////////
2783     // HAMMER like for meltables (still hot and easy to work, any hammer will do)
2784     //TODO glass should require proper tools (don't know what but sure not a hammer)
2785     bool bMissingTools=false;
2786     if(rpd.bTailoringMode){
2787       rpd.itTool = findTailoringTool(rpd,itSpawn); // only main material can be tailored
2788       if(rpd.itTool==NULL)
2789         bMissingTools=true;
2790 
2791       if(!bMissingTools){
2792         if(bMeltableS){
2793           rpd.itTool2 = FindBluntTool(rpd);
2794           if(rpd.itTool2==NULL)
2795             bMissingTools=true;
2796         }else{
2797           rpd.itTool2 = findCarvingTool(rpd,itSpawn);
2798           if(rpd.itTool2==NULL)
2799             bMissingTools=true;
2800         }
2801       }
2802     }else{
2803       if(bMeltableM){
2804         rpd.itTool = FindBluntTool(rpd);
2805         if(rpd.itTool==NULL)
2806           bMissingTools=true;
2807       }else{
2808         rpd.itTool = findCarvingTool(rpd,itSpawn);
2809         if(rpd.itTool==NULL)
2810           bMissingTools=true;
2811       }
2812 
2813       if(!bMissingTools){
2814         if(bMeltableS){
2815           rpd.itTool2 = FindBluntTool(rpd);
2816           if(rpd.itTool2==NULL)
2817             bMissingTools=true;
2818         }else{
2819           rpd.itTool2 = findCarvingTool(rpd,itSpawn);
2820           if(rpd.itTool2==NULL)
2821             bMissingTools=true;
2822         }
2823       }
2824     }
2825 
2826     if(!bMissingTools){
2827       if(rpd.itTool2==rpd.itTool)
2828         rpd.itTool2=NULL;
2829     }
2830 
2831     DBG1(rpd.iBaseTurnsToFinish);
2832 
2833     if(bMissingTools){
2834       rpd.itTool=rpd.itTool2=NULL; //to make it easy to check/inform, wont start if missing one anyway
2835       failToolMsg(rpd,"tool");
2836       craftcore::SendToHellSafely(itSpawn);
2837       return false;
2838     }
2839 
2840     /// finishing ///////////////////////////////////////////////////////////////////////////////////////////////
2841     rpd.CopySpawnItemCfgFrom(itSpawn);
2842     craftcore::SendToHellSafely(itSpawn);
2843 
2844     rpd.bCanStart=true;
2845 
2846     return true;
2847   }
2848 };srpForgeItem rpForgeItem;
2849 
2850 struct srpFluidsBASE : public recipe{
2851  protected:
2852   int iAddVolMin;
2853   int iAddVolExtra;
2854   festring fsTool;
2855   festring fsCorpse;
2856   festring fsBottle;
2857   int iLiqCfg;
2858   int iMatEff;
2859 
chkCorpsesrpFluidsBASE2860   virtual bool chkCorpse(const materialdatabase* blood,const materialdatabase* flesh){return false;}
2861 
2862  public:
srpFluidsBASEsrpFluidsBASE2863   srpFluidsBASE(){
2864     //TODO call super class constructor?
2865 
2866     iAddVolMin = 25;
2867     iAddVolExtra = 75;
2868     fsTool="dagger";
2869     fsCorpse="creature corpse";
2870     fsBottle="bottle";
2871     iLiqCfg = -1;
2872     iMatEff = -1;
2873   }
2874 
worksrpFluidsBASE2875   virtual bool work(recipedata& rpd){
2876     //extract fluids (not blood as it can be used as nutrition right? *eww* :P)
2877 
2878     itemvector vi;
2879 
2880     ///////////// tool ////////////////
2881     rpd.itTool = FindTool(rpd, DAGGER);
2882     if(rpd.itTool==NULL){
2883       failToolMsg(rpd,"dagger");
2884       return false;
2885     }
2886 
2887     item* itCorpse=NULL;
2888     vi = vitInv(rpd);
2889     for(int i=0;i<vi.size();i++){
2890       corpse* Corpse = dynamic_cast<corpse*>(vi[i]);
2891       if(Corpse!=NULL){
2892         character* D = Corpse->GetDeceased(); DBG2(Corpse->GetName(DEFINITE).CStr(),D->GetName(DEFINITE).CStr())
2893         static const materialdatabase* blood;blood = material::GetDataBase(D->GetBloodMaterial());
2894         static const materialdatabase* flesh;flesh = material::GetDataBase(D->GetFleshMaterial());
2895 
2896         if(chkCorpse(blood,flesh)){DBGLN;
2897           itCorpse=Corpse;
2898           break;
2899         }
2900       }
2901     }DBGLN;
2902 
2903     if(itCorpse==NULL){
2904       ADD_MESSAGE("No useful corpse to work with.");
2905       rpd.SetAlreadyExplained();
2906       return false;
2907     }
2908 
2909 
2910     //TODO extract poison glands as a new item (so a new recipe to create it) to be used here instead of the corpse?
2911     item* itBottle=NULL;
2912     material* mat = NULL;
2913     long currentVolume=0;
2914 
2915     // look for compatible bottle first
2916     vi = vitInv(rpd);
2917     for(int i=0;i<vi.size();i++){DBGLN;
2918       potion* pot = dynamic_cast<potion*>(vi[i]);
2919       if(pot!=NULL){DBGLN;
2920         mat = pot->GetSecondaryMaterial();
2921         if(mat==NULL)continue;
2922 
2923         bool bChkVol=false;DBG3(mat->GetEffect(),iMatEff,iLiqCfg);
2924         if(iMatEff>-1){
2925           if(mat->GetEffect()==iMatEff) //TODO should be more precise like the material actually be the same poison of that kind of spider...
2926             bChkVol=true;
2927         }else{
2928           if(mat->GetConfig()==iLiqCfg)
2929             bChkVol=true;
2930         }
2931 
2932         if(bChkVol){DBGLN;
2933           long vol = mat->GetVolume();
2934           if(vol < pot->GetDefaultSecondaryVolume()){ //less than max
2935             itBottle = pot;
2936             currentVolume = vol;
2937             break;
2938           }
2939         }
2940       }
2941     }DBGLN;
2942 
2943     // look for empty bottle
2944     if(itBottle==NULL){DBGLN;
2945       vi = vitInv(rpd);
2946       for(int i=0;i<vi.size();i++){
2947         potion* pot = dynamic_cast<potion*>(vi[i]);
2948         if(pot!=NULL){DBGLN;
2949           mat = pot->GetSecondaryMaterial();
2950           if(mat==NULL){ //empty
2951             itBottle = pot;
2952             break;
2953           }
2954         }
2955       }
2956     }DBGLN;
2957 
2958     if(itBottle==NULL){
2959       ADD_MESSAGE("No bottle available.");
2960       rpd.SetAlreadyExplained();
2961       return false;
2962     }
2963 
2964     // ready
2965     int iAddVolume = +iAddVolMin +(RAND()%iAddVolExtra);
2966     int volume = currentVolume + iAddVolume;
2967 
2968     if(volume > itBottle->GetDefaultSecondaryVolume())
2969       volume = itBottle->GetDefaultSecondaryVolume();
2970 
2971     /***
2972      * TODO
2973      * mmm seems to have no strengh diff?
2974      * only takes more time if "stronger" like "not from large spider"
2975     mat = itBottle->GetSecondaryMaterial();
2976     if(mat!=NULL)mat->GetEffectStrength(); //TODO could average current poison strengh in some way if ever one day
2977     */
2978 
2979     rpd.itSpawnType = CIT_POTION;
2980     rpd.itSpawnCfg = itBottle->GetConfig(); //may be a vial
2981     rpd.itSpawnMatMainCfg = itBottle->GetMainMaterial()->GetConfig();
2982     rpd.itSpawnMatMainVol = itBottle->GetMainMaterial()->GetVolume();
2983     rpd.itSpawnMatSecCfg = iLiqCfg;DBG1(iLiqCfg);
2984     rpd.itSpawnMatSecVol = volume;
2985 //    rpd.itSpawn = potion::Spawn(itBottle->GetConfig()); //may be a vial
2986 //    delete rpd.itSpawn->SetSecondaryMaterial(liquid::Spawn(iLiqCfg, volume));
2987 //    rpd.SetSpawnItemCfg(rpd.itSpawn);
2988 
2989     rpd.ingredientsIDs.push_back(itBottle->GetID()); //just to be destroyed too if crafting completes
2990     rpd.ingredientsIDs.push_back(itCorpse->GetID());
2991     rpd.bHasAllIngredients=true;
2992 
2993     rpd.iBaseTurnsToFinish=5;
2994 
2995     rpd.bCanStart=true;
2996 
2997     return true;
2998   }
2999 };
3000 
3001 struct srpPoison : public srpFluidsBASE{
3002 
chkCorpsesrpPoison3003   virtual bool chkCorpse(const materialdatabase* blood,const materialdatabase* flesh){
3004     return (blood->Effect==EFFECT_POISON || flesh->Effect==EFFECT_POISON);
3005   }
3006 
fillInfosrpPoison3007   virtual void fillInfo(){
3008     init("extract","poison");
3009     desc << "Use a " << fsTool << " to " << action << " " << name << " from a poisonous corpse into a " << fsBottle << ".";
3010   }
3011 
worksrpPoison3012   virtual bool work(recipedata& rpd){
3013     iLiqCfg=POISON_LIQUID;
3014     iMatEff=EFFECT_POISON;
3015 
3016     return srpFluidsBASE::work(rpd);
3017   }
3018 };srpPoison rpPoison;
3019 
3020 struct srpAcid : public srpFluidsBASE{
chkCorpsesrpAcid3021   virtual bool chkCorpse(const materialdatabase* blood,const materialdatabase* flesh){
3022     return (blood->Acidicity)>0 || (flesh->Acidicity)>0;
3023   }
3024 
fillInfosrpAcid3025   virtual void fillInfo(){
3026     init("extract","acid");
3027     desc << "Use a " << fsTool << " to " << action << " " << name << " from an acidic corpse into a " << fsBottle << ".";
3028   }
3029 
worksrpAcid3030   virtual bool work(recipedata& rpd){
3031     iLiqCfg=SULPHURIC_ACID;
3032 
3033     return srpFluidsBASE::work(rpd);
3034   }
3035 };srpAcid rpAcid;
3036 
3037 struct srpMagic : public srpFluidsBASE{
chkCorpsesrpMagic3038   virtual bool chkCorpse(const materialdatabase* blood, const materialdatabase* flesh){
3039     return (blood->Effect == EFFECT_MUSHROOM || flesh->Effect == EFFECT_MUSHROOM ||
3040             blood->Effect == EFFECT_MAGIC_MUSHROOM || flesh->Effect == EFFECT_MAGIC_MUSHROOM);
3041   }
3042 
fillInfosrpMagic3043   virtual void fillInfo(){
3044     init("extract","raw magic");
3045     desc << "Use a " << fsTool << " to " << action << " raw liquefied magic from a mushroom into a " << fsBottle <<  ".";
3046   }
3047 
worksrpMagic3048   virtual bool work(recipedata& rpd){
3049     iLiqCfg=MAGIC_LIQUID;
3050 
3051     return srpFluidsBASE::work(rpd);
3052   }
3053 };srpMagic rpMagic;
3054 
3055 felist craftRecipes(CONST_S("What do you want to craft?"));
3056 std::vector<recipe*> vrpAllRecipes;
3057 
updateCraftDesc()3058 void updateCraftDesc(){
3059   craftRecipes.EmptyDescription();
3060 
3061   float fSkill=craftcore::CraftSkill(PLAYER); //TODO should this dynamic value show too where stats are?
3062   festring fsSkill="Crafting Proficiency: ";  // It's actually different from skills, so don't call it a skill.
3063   static char cSkill[20];
3064   sprintf(cSkill, "%.1f",fSkill);
3065   fsSkill<<cSkill;
3066 
3067   festring fsDesc=fsSkill;
3068   if(vSuspended.size()>0)
3069     fsDesc<<" (Suspended Actions: "<<vSuspended.size()<<")";
3070 
3071   craftRecipes.AddDescription(fsDesc,fSkill<10?RED:WHITE);
3072 }
3073 
addRecipe(recipe * prp)3074 void addRecipe(recipe* prp){
3075   prp->iListIndex=vrpAllRecipes.size()+1; //+1 is about the 1st entry related to suspended actions
3076 
3077   if(prp->name.IsEmpty())
3078     ABORT("empty recipe name '%s' '%s' %d",prp->name.CStr(),prp->desc.CStr(),prp->iListIndex);
3079 
3080   craftRecipes.AddEntry(festring()+prp->action+" "+prp->name, LIGHT_GRAY, 20, prp->iListIndex, true); DBG2(prp->name.CStr(),prp->iListIndex);
3081   craftRecipes.SetLastEntryHelp(prp->desc);
3082 
3083   vrpAllRecipes.push_back(prp);
3084 }
addMissingMsg(festring & where,cfestring & what)3085 void addMissingMsg(festring& where, cfestring& what){
3086   if(where.GetSize()>0)
3087     where<<", ";
3088   where<<what;
3089 }
3090 /**
3091  * Dear developer, be sure to read the ABORT message before adding a recipe! :)
3092  */
Craft(character * Char)3093 truth commandsystem::Craft(character* Char) //TODO currently this is an over simplified crafting system... should be easy to add recipes and show their formulas...
3094 {
3095   return craftcore::Craft(Char);
3096 }
CheckArms(humanoid * h)3097 truth craftcore::CheckArms(humanoid* h)
3098 {
3099   bool bLOk,bROk; //dummy
3100   return CheckArms(h,bLOk,bROk);
3101 }
CheckArms(humanoid * h,bool & bLOk,bool & bROk)3102 truth craftcore::CheckArms(humanoid* h,bool& bLOk,bool& bROk)
3103 {
3104   bLOk = h->GetLeftArm() !=NULL && h->GetLeftArm()->IsUsable();
3105   bROk = h->GetRightArm()!=NULL && h->GetRightArm()->IsUsable();
3106   if(!bLOk && !bROk){
3107     ADD_MESSAGE("You need at least one usable arm to be able to craft.");
3108     return false;
3109   }
3110 
3111   /**
3112    * TODO
3113    * resuming a suspended,
3114    * if begun with 1 arm now have 2, should lower remaining turns to speed up
3115    * if begun with 2 arms now have 1, should increase remaining turns
3116    * also, changes on crafting skill (based on stats) should also dynamically affect remaining turns
3117    * !!!BEWARE!!! calculations depending on remaining turns may break!!!
3118    */
3119 
3120   return true;
3121 }
CheckHumanoid(character * Char)3122 humanoid* craftcore::CheckHumanoid(character* Char)
3123 {
3124   humanoid* h = dynamic_cast<humanoid*>(Char);
3125   if(h==NULL){
3126     ADD_MESSAGE("This monster cannot craft.");
3127     return NULL;
3128   }
3129   return h;
3130 }
3131 
CheckSelectedAndInitRecipeIfNeeded(bool bInitRecipes,recipedata & rpd,recipe * prp,recipe & rp)3132 recipe* CheckSelectedAndInitRecipeIfNeeded(bool bInitRecipes,recipedata& rpd,recipe* prp, recipe& rp){ // to avoid macro hard readability
3133   if(prp==NULL && rp.IsTheSelectedOne(rpd))prp=&rp; \
3134   DBG3(prp,&rp,rp.desc.CStr()); \
3135   if(bInitRecipes)addRecipe((recipe*)&rp);
3136   return prp;
3137 }
3138 
UndoRemainsIfNeeded(recipedata & rpd)3139 void craftcore::UndoRemainsIfNeeded(recipedata& rpd)
3140 {DBGLN;
3141   static undoremains* pur;
3142   static item *itIngredient, *itLump;
3143   static material* matM;
3144   for(int i=0;i<2;i++){
3145     switch(i){
3146       case 0: pur=&rpd.urMain;break;
3147       case 1: pur=&rpd.urSecond;break;
3148     }
3149     DBG6("UndoRemain:Work",i,pur->ulUndoRemainsIngredientID,pur->ulUndoRemainsLumpID,pur->lUndoRemainsVolume,rpd.dbgInfo().CStr());
3150 
3151     if(pur->lUndoRemainsVolume==0)
3152       continue;
3153 
3154     itIngredient = game::SearchItem(pur->ulUndoRemainsIngredientID);
3155     if(itIngredient){
3156       matM = itIngredient->GetMainMaterial();
3157       matM->SetVolume(matM->GetVolume()+pur->lUndoRemainsVolume);
3158       itIngredient->CalculateAll();
3159     }else{
3160       // should abort? or this would be just a minor issue?
3161       DBG2("UndoRemain:IngredientVanished",rpd.dbgInfo().CStr());
3162     }
3163 
3164     itLump = game::SearchItem(pur->ulUndoRemainsLumpID);
3165     if(itLump){
3166       matM = itLump->GetMainMaterial();
3167       long vol = matM->GetVolume() - pur->lUndoRemainsVolume;
3168       if(vol<=0){
3169         craftcore::SendToHellSafely(itLump);
3170         if(vol<0){
3171           // should abort? or this would be just a minor issue?
3172           DBG2("UndoRemain:LumpNegativeVol",rpd.dbgInfo().CStr());
3173         }
3174       }else{
3175         matM->SetVolume(vol);
3176         itLump->CalculateAll();
3177       }
3178     }
3179   }
3180 }
3181 
Craft(character * Char)3182 truth craftcore::Craft(character* Char) //TODO currently this is an over simplified crafting system... should be easy to add recipes and show their formulas...
3183 {
3184   humanoid* h = CheckHumanoid(Char);
3185   if(h==NULL)
3186     return false;
3187 
3188   bool bLOk,bROk;
3189   if(!CheckArms(h,bLOk,bROk))
3190     return false;
3191 
3192   uint sel = FELIST_ERROR_BIT;
3193   if(vrpAllRecipes.size()>0){
3194     game::SetStandardListAttributes(craftRecipes);
3195     craftRecipes.AddFlags(SELECTABLE);
3196     craftRecipes.ClearFilter();
3197     updateCraftDesc();
3198     sel = craftRecipes.Draw(); DBG1(sel);
3199 
3200     if(sel & FELIST_ERROR_BIT)
3201       return false;
3202 
3203     if(sel==0 && !craftcore::HasSuspended()){
3204       ADD_MESSAGE("You were doing nothing special.");
3205       return false;
3206     }
3207     if(sel==0 && craftcore::HasSuspended()){
3208       int key = game::KeyQuestion(CONST_S("There are suspended crafting actions: (r)esume/ENTER or (c)ancel?"),
3209         KEY_ESC, 3, 'r', 'c', KEY_ENTER);
3210       if(key==KEY_ESC)return false;
3211 
3212       if(key==KEY_ENTER)key='r';
3213 
3214       felist LSusp("Suspended crafting actions:",WHITE);
3215       game::SetStandardListAttributes(LSusp);
3216       LSusp.AddFlags(SELECTABLE);
3217       for(int i=0;i<vSuspended.size();i++)
3218         LSusp.AddEntry(vSuspended[i].fsCraftInfo,LIGHT_GRAY);
3219 
3220       festring fsDo;
3221       bool bResume=false;
3222       col16 col=WHITE;
3223       switch(key){
3224       case 'r':
3225         fsDo="Resume";
3226         bResume=true;
3227         break;
3228       case 'c':
3229         fsDo="Cancel (cannot be restored after this)"; //TODO could...
3230         col=RED;
3231         bResume=false;
3232         break;
3233       default:
3234         return false; //TODO ever reached?
3235       }
3236 
3237       fsDo<<" which crafting action?";
3238       LSusp.AddDescription(fsDo,col);
3239       uint Sel = LSusp.Draw();
3240       if(Sel<vSuspended.size()){ //TODO is necessary to check esc key, error bit etc?
3241         bool bErase=true;
3242         if(bResume)
3243           bErase = craftcore::ResumeSuspendedTo(Char,vSuspended[Sel]);
3244         if(bErase)
3245           vSuspended.erase(vSuspended.begin()+Sel);
3246         return bResume;
3247       }
3248       return false;
3249     }
3250 
3251   }
3252   recipedata rpd(h,sel);
3253 
3254   /******************************************************************************************
3255    * 1st call it just initializes the recipes list after all recipes have been configured!
3256    */
3257   bool bInitRecipes = vrpAllRecipes.size()==0;
3258 
3259   if(bInitRecipes){
3260     craftRecipes.AddEntry(festring()+"Suspended crafting actions", LIGHT_GRAY, 20, 0, true);
3261     craftRecipes.SetLastEntryHelp("Resume or remove already started crafting actions.");
3262   }
3263 
3264   // these are kind of grouped and not ordered like a-z
3265   recipe* prp=NULL;
3266   // just a simple macro to easify maintenance
3267   #define RP(MACRO_PARAM_rp) prp=CheckSelectedAndInitRecipeIfNeeded(bInitRecipes,rpd,prp,MACRO_PARAM_rp);
3268 
3269   // RECIPES!
3270 
3271   if(bInitRecipes)craftRecipes.AddEntry(festring()+"Construction:", DARK_GRAY, 0, NO_IMAGE, false);
3272   RP(rpAnvil);
3273   RP(rpChair);
3274   RP(rpDoor);
3275   RP(rpForge);
3276   RP(rpWall2);
3277   RP(rpWorkBench);
3278   RP(rpTWorkBench);
3279 
3280   if(bInitRecipes)craftRecipes.AddEntry(festring()+"Alchemy:", DARK_GRAY, 0, NO_IMAGE, false);
3281   RP(rpAcid);
3282   RP(rpPoison);
3283   RP(rpMagic);
3284 
3285   if(bInitRecipes)craftRecipes.AddEntry(festring()+"Material crafting:", DARK_GRAY, 0, NO_IMAGE, false);
3286   RP(rpDismantle);
3287   RP(rpSplitLump);
3288   RP(rpJoinLumps);
3289   RP(rpResistanceVS);
3290   RP(rpInspect);
3291 
3292   if(bInitRecipes)craftRecipes.AddEntry(festring()+"Item crafting:", DARK_GRAY, 0, NO_IMAGE, false);
3293   RP(rpForgeItem);
3294   RP(rpMelt);
3295 
3296   if(bInitRecipes)craftRecipes.AddEntry(festring()+"Tailoring:", DARK_GRAY, 0, NO_IMAGE, false);
3297   RP(rpCutWeb);
3298 
3299   if(bInitRecipes)
3300     return Craft(Char); //init recipes descriptions at least, one time recursion and returns here :>
3301 
3302   if(prp==NULL){
3303     // no crafting action was selected
3304     return false;
3305   }
3306 
3307   // craft!
3308   //ADD_MESSAGE("Your chosen crafting action is to %s %s.",prp->action.CStr(),prp->name.CStr());
3309   bool bDummy = prp->work(rpd); //bDummy(fied) as there is more detailed fail status from rpd bools
3310 
3311   if(!rpd.bCanStart){
3312     UndoRemainsIfNeeded(rpd);
3313     if(!rpd.bAlreadyExplained)
3314       ABORT("explain why crafting won't work.");
3315     return true;
3316   }
3317 
3318   if(rpd.ingredientsIDs.size()==0){
3319     ABORT("no ingredients chosen?");
3320     return true; // dummy
3321   }else{
3322     for(int i=0;i<rpd.ingredientsIDs.size();i++){
3323       item* it = game::SearchItem(rpd.ingredientsIDs[i]);
3324       it->SetTag('c'); //being used to craft
3325     }
3326   }
3327 
3328   if(rpd.otSpawnType==CTT_NONE && rpd.itSpawnType==CIT_NONE){
3329     ABORT("requested to craft nothing? %s",rpd.dbgInfo().CStr());
3330     return true; //dummy
3331   }
3332 
3333   if(rpd.iBaseTurnsToFinish<1){
3334     ABORT("invalid iBaseTurnsToFinish %d",rpd.iBaseTurnsToFinish);
3335     return true; //dummy
3336   }
3337 
3338   if(rpd.itTool!=NULL && rpd.itTool2!=NULL){
3339     if(rpd.itTool==rpd.itTool2){ //keep this check to fix any code bofore this.
3340       ABORT("both tools are the same item %lu:%s %lu:%s",rpd.itTool->GetID(),rpd.itTool->GetName(INDEFINITE).CStr(),rpd.itTool2->GetID(),rpd.itTool2->GetName(INDEFINITE).CStr());
3341       return true; //dummy
3342     }
3343   }
3344 
3345   ///////////////////////// finally all looks ok //////////////////////////////
3346 
3347   festring fsTools;
3348   if(rpd.itTool!=NULL)
3349     fsTools=rpd.itTool->GetName(INDEFINITE);
3350   if(rpd.itTool2!=NULL){
3351     if(!fsTools.IsEmpty())
3352       fsTools<<" and ";
3353     fsTools<<rpd.itTool2->GetName(INDEFINITE);
3354   }
3355   if(!fsTools.IsEmpty())
3356     ADD_MESSAGE("You will use %s as a tool.",fsTools.CStr());
3357 
3358   int iCraftTimeMult=1;
3359 
3360   if(!bLOk || !bROk){ //using only one hand will take more time even if only one tool is required as even if 2 were, only 1 would be handled per time
3361     ADD_MESSAGE("You only have one arm, this will take longer.");
3362     iCraftTimeMult++;
3363   }
3364 
3365   if(rpd.itTool !=NULL && rpd.itTool ->IsBroken()){
3366     ADD_MESSAGE("The first tool is broken, this will take longer.");
3367     iCraftTimeMult++;
3368   }
3369   if(rpd.itTool2!=NULL && rpd.itTool2->IsBroken()){
3370     ADD_MESSAGE("The second tool is broken, this will take longer.");
3371     iCraftTimeMult++;
3372   }
3373 
3374 //  DBGEXEC( //solved, the problem was the duplicate item code that modifies the duplicated ID ...
3375 //    for(int iDbg123=0;iDbg123<rpd.ingredientsIDs.size();iDbg123++){
3376 //      item* itDbg123=game::SearchItem(rpd.ingredientsIDs[iDbg123]);
3377 //      if(itDbg123==NULL)ABORT("ingredient id %d vanished?",rpd.ingredientsIDs[iDbg123]);
3378 //    }
3379 //  );DBG1(rpd.iBaseTurnsToFinish);
3380 
3381   rpd.iBaseTurnsToFinish*=iCraftTimeMult;
3382 
3383   /**********************************************************************
3384    * LAST turn calc thing!!!
3385    * ex.: initial dex=10 wis=10 is 1.0 means wont modify turns
3386    **********************************************************************/
3387   rpd.iBaseTurnsToFinish /= craftcore::CraftSkill(Char)/10.0;
3388   if(rpd.iBaseTurnsToFinish==0) //if div zeroed it
3389     rpd.iBaseTurnsToFinish=1;
3390   if(rpd.iBaseTurnsToFinish<rpd.iMinTurns)
3391     rpd.iBaseTurnsToFinish=rpd.iMinTurns;
3392   rpd.iRemainingTurnsToFinish = rpd.iBaseTurnsToFinish;
3393   /**
3394    * TODO
3395    * the problem is that this considers each turn as 1 minute but... it is not like that...
3396    * character::EditAP() determines how time will pass... and that may vary during
3397    * the craft proccess!! basically it is imprevisible...
3398    */
3399   // Warn if will take too long
3400   int iH = rpd.iBaseTurnsToFinish/60;
3401   int iD = iH/24;
3402   iH = iH%24;
3403   int iM = rpd.iBaseTurnsToFinish%60;
3404   if(iH>=1 || iD>1){
3405     festring fs;
3406     fs<<"You are not sure but you think it may take ";
3407     if(iD>1)
3408       fs<<iD<<" days "; //this may happen in case the stats/skill went too low, so user has a chance to recover from the debuff
3409     fs<<iH<<" hours and "<<iM<<" minutes to complete";
3410     fs<<", Continue? [y/N]";
3411     if(!game::TruthQuestion(fs)){
3412       UndoRemainsIfNeeded(rpd);
3413       return true; // see at the end why
3414     }
3415   }
3416 
3417   if(rpd.otSpawnType!=CTT_NONE && rpd.v2PlaceAt.Is0())
3418     rpd.v2PlaceAt = rpd.lsqrPlaceAt!=NULL ? rpd.lsqrPlaceAt->GetPos() : rpd.lsqrCharPos->GetPos(); //may be ignored anyway, is just a fallback
3419 
3420   rpd.iAddDexterity=5; //TODO crafting difficult things should give more dexterity (wisdom too?)
3421 
3422   rpd.v2PlayerCraftingAt = Char->GetPos();
3423 
3424   if(rpd.itTool !=NULL)rpd.itToolID =rpd.itTool ->GetID();
3425   if(rpd.itTool2!=NULL)rpd.itTool2ID=rpd.itTool2->GetID();
3426 
3427   rpd.fsCraftInfo =
3428     prp->action+" "+prp->name+
3429     (rpd.itSpawnCfg!=0 ? festring(" ("+rpd.fsItemSpawnSearchPrototype+")") : festring())+
3430     ", started at "+game::GetCurrentDungeon()->GetLevelDescription(game::GetCurrentLevelIndex(), true);
3431 
3432   rpd.ClearRefs(); //pointers must be revalidated on the action handler
3433   DBG1(rpd.dbgInfo().CStr());
3434   if(Char->SwitchToCraft(rpd)) // everything must be set before this!!!
3435     ADD_MESSAGE("You will work on %s now.",prp->name.CStr());
3436 
3437   /**
3438    * ATTENTION!!!
3439    * the (crafting code) complexity of granting one turn wont be lost isnt worth letting the game crash randomly somewhere
3440    * that is hard to track the source of the problem (mainly because of sendtohell() not being applied if turn is not spent).
3441    * so to ALWAYS spend current turn, when things sent to hell will properly be applied everywhere required
3442    * is the SAFEST thing! dont change this please even if you are sure all looks perfect! ;)
3443    if(rpd.bSpendCurrentTurn)return true;else return false; //old code
3444    */
3445   return true; //spends current turn
3446 }
3447 
GetTool()3448 item* recipedata::GetTool()
3449 {
3450   return !itTool  && itToolID  ? game::SearchItem(itToolID)  : NULL;
3451 }
3452 
GetTool2()3453 item* recipedata::GetTool2()
3454 {
3455   return !itTool2 && itTool2ID ? game::SearchItem(itTool2ID) : NULL;
3456 }
3457 
3458 /**
3459  *
3460  * @param bAllowBreak
3461  * @param rpd
3462  * @param itSpawn
3463  * @param fsCreated
3464  * @return beware, can be NULL but still spawn lumps!
3465  */
CheckBreakItem(bool bAllowBreak,recipedata & rpd,item * itSpawn,festring & fsCreated)3466 item* crafthandle::CheckBreakItem(bool bAllowBreak, recipedata& rpd, item* itSpawn, festring& fsCreated)
3467 {
3468   bool bBreak = rpd.bSpawnBroken;
3469 
3470   if(bAllowBreak && bBreak && !itSpawn->IsBroken()){
3471     bool bCanBeBroken = itSpawn->CanBeBroken();
3472     if(bCanBeBroken){
3473       /**
3474        * IMPORTANT!!!
3475        *
3476        * can only break after placed somewhere like on player's inv
3477        *
3478        * breaking it with
3479        *   itSpawn->Break(NULL);
3480        * on the same turn it was spawned will create inconsistent inventory,
3481        * The last item will point to invalid memory and may segfault anywhere from next turn on,
3482        * or cause unpredictable results,
3483        * so do not use it!
3484        *
3485        * This below was taken from Break() and seems safe.
3486        * TODO create a method there like SetSelfAsBroken() to re-use the code to grant they will be in sync
3487        */
3488       if(itSpawn->SetConfigIfPossible(rpd.itSpawnCfg | BROKEN)){
3489         itSpawn->SetSize(itSpawn->GetSize() >> 1);
3490       }else{
3491         bCanBeBroken=false; // missing BROKEN config at .dat file but no problem, see below
3492       }
3493     }
3494 
3495     if(!bCanBeBroken){
3496       /**
3497        * things that can't be broken are special.
3498        * if it can't be broken, will just create a messy lump.
3499        */
3500       ADD_MESSAGE("Your lack of skill broke %s into pieces!",itSpawn->GetName(DEFINITE).CStr());
3501       craftcore::PrepareRemains(rpd,itSpawn->GetMainMaterial(),CIT_LUMP);
3502       if(itSpawn->GetSecondaryMaterial()!=NULL)
3503         craftcore::PrepareRemains(rpd,itSpawn->GetSecondaryMaterial(),CIT_LUMP);
3504       craftcore::SendToHellSafely(itSpawn);
3505       itSpawn=NULL; //because the result can be 2 item* (2 lumps, one for each material Main and Secondary!)
3506       fsCreated << "a funny looking lump";
3507     }
3508   }
3509 
3510   return itSpawn;
3511 }
3512 
CreateMaterial(material * matCopyFrom)3513 material* craftcore::CreateMaterial(material* matCopyFrom)
3514 {
3515   material* mat = matCopyFrom->Duplicate();
3516 
3517   DBG4(mat->GetSpoilLevel(),matCopyFrom->GetSpoilLevel(),mat->GetRustLevel(),matCopyFrom->GetRustLevel());
3518   if( //TODO temporary, remove as soon all looks good, faster than scanning the whole code manually to grant it works...
3519       mat->GetSpoilLevel() != matCopyFrom->GetSpoilLevel() ||
3520       mat->GetRustLevel() != matCopyFrom->GetRustLevel()
3521   ) ABORT("material full duplication failed");
3522 
3523 //  return CreateMaterial(true/*dummy*/,NULL,matCopyFrom);
3524   return mat;
3525 }
CreateMaterial(bool bMain,recipedata & rpd)3526 material* craftcore::CreateMaterial(bool bMain,recipedata& rpd)
3527 {
3528   material* mat = material::MakeMaterial(
3529     (bMain ? rpd.itSpawnMatMainCfg : rpd.itSpawnMatSecCfg),
3530     (bMain ? rpd.itSpawnMatMainVol : rpd.itSpawnMatSecVol)
3531   );
3532   return mat;
3533 }
3534 
3535 #define MAIN_MATERIAL true
3536 #define SECONDARY_MATERIAL false
SpawnItem(recipedata & rpd,festring & fsCreated)3537 item* crafthandle::SpawnItem(recipedata& rpd, festring& fsCreated)
3538 {
3539   rpd.rc.integrityCheck();
3540 
3541   item* itSpawn = NULL;
3542   material* matS = NULL;
3543   bool bAllowBreak=false;DBG3(rpd.itSpawnType,rpd.itSpawnCfg,rpd.itSpawnMatSecCfg);
3544   switch(rpd.itSpawnType){
3545     case CIT_POTION:
3546       /**
3547        * IMPORTANT!!!
3548        * do not use NO_MATERIALS here,
3549        * apparently the main material is always required for it
3550        * and the main material would remain uninitialized (instead of NULL TODO fix that?)
3551        * leading to SEGFAULT when trying to set the main material!
3552        */
3553       itSpawn = potion::Spawn(rpd.itSpawnCfg); //may be a vial
3554       if(rpd.itSpawnMatSecCfg>0)
3555         matS = liquid::Spawn(rpd.itSpawnMatSecCfg,rpd.itSpawnMatSecVol);
3556       break;
3557     case CIT_PROTOTYPE:
3558       itSpawn = protosystem::CreateItemToCraft(rpd.fsItemSpawnSearchPrototype);
3559 
3560       bAllowBreak=true;
3561       break;
3562     case CIT_STONE:
3563       itSpawn = stone::Spawn(rpd.itSpawnCfg, NO_MATERIALS);
3564       break;
3565     case CIT_LUMP:
3566       itSpawn = lump::Spawn(rpd.itSpawnCfg, NO_MATERIALS);
3567       break;
3568     case CIT_STICK:
3569       itSpawn = stick::Spawn(rpd.itSpawnCfg, NO_MATERIALS);
3570       break;
3571   }
3572 
3573   if(itSpawn==NULL)
3574     ABORT("craft spawned no item.");
3575 
3576   itemcontainer* ic = dynamic_cast<itemcontainer*>(itSpawn);
3577   if(ic!=NULL){
3578     if(ic->IsLocked())
3579       ic->SetIsLocked(false); //to prevent annoyance of not having the key TODO should instead be always a broken lock considering the complexity of the locking system itself?
3580   }
3581 
3582   if(rpd.itSpawnMatMainCfg==0 || rpd.itSpawnMatMainVol==0)
3583     ABORT("main material and/or volume is 0 %lu %lu",rpd.itSpawnMatMainCfg,rpd.itSpawnMatMainVol);
3584 
3585 //  material* matM = material::MakeMaterial(rpd.itSpawnMatMainCfg,rpd.itSpawnMatMainVol);
3586 //  matM->SetSpoilCounter(rpd.itSpawnMatMainSpoilLevel);
3587   material* matM = craftcore::CreateMaterial(MAIN_MATERIAL,rpd);
3588   delete itSpawn->SetMainMaterial(matM);
3589 
3590   if(rpd.itSpawnMatSecCfg==0)
3591     craftcore::EmptyContentsIfPossible(rpd,itSpawn);
3592   else{
3593     if(matS==NULL)
3594       matS = craftcore::CreateMaterial(SECONDARY_MATERIAL,rpd);
3595 //      matS = material::MakeMaterial(rpd.itSpawnMatSecCfg,rpd.itSpawnMatSecVol);
3596     if(matS!=NULL){
3597 //      matS->SetSpoilCounter(rpd.itSpawnMatSecSpoilLevel);
3598       delete itSpawn->SetSecondaryMaterial(matS);
3599     }
3600   }
3601 
3602   itSpawn->MoveTo(rpd.rc.H()->GetStack());
3603 
3604   if(!fsCreated.IsEmpty())
3605     fsCreated << " ";
3606   fsCreated << "You crafted ";
3607 
3608   // this check is more detailed and must be when the item is completely ready!
3609   if(!craftcore::canBeCrafted(itSpawn)){
3610     ABORT(
3611       "Dear developer, for the sake of balance and challenge do not create recipes for:\n"
3612       "- Quest items.\n"
3613       "- Magical items as rings, amulets, wands, scrolls, horns etc.\n"
3614       "Crafting any of this would be unbalanced as hell and unrealistic given your characters upbringing.\n"
3615       "You're after all a slave, with no knowledge of magic, and crafting magical items should be beyond most craftsmen.\n"
3616       "This attempt: %s",
3617       itSpawn->GetName(DEFINITE).CStr()
3618     );
3619   }
3620 
3621   itSpawn = CheckBreakItem(bAllowBreak, rpd, itSpawn, fsCreated);
3622 
3623   if(itSpawn!=NULL){
3624     if(fsCreated.GetSize()<200) // this will prevent a crash about "stack smashing detected". TODO to test it and provide a better solution, just comment this `if` line and split a corpse in 40 parts or more
3625       fsCreated << itSpawn->GetName(INDEFINITE);
3626 
3627     if(rpd.bCanBeBroken && !itSpawn->IsBroken()){
3628       // this may break it too!
3629       if(rpd.lDamageFinalItem>0){
3630         long lDmg=RAND()%rpd.lDamageFinalItem;
3631         itSpawn->ReceiveDamage(rpd.rc.H(), lDmg, PHYSICAL_DAMAGE);
3632         DBG5(itSpawn->GetName(DEFINITE).CStr(), lDmg, rpd.lDamageFinalItem, itSpawn->GetStrengthValue(), itSpawn->GetStrengthModifier());
3633         if(!itSpawn->Exists()){ //it may break (creating a cloned broken item) and the original will vanish!
3634           craftcore::SendToHellSafely(itSpawn);
3635           itSpawn=NULL;
3636         }
3637       }
3638     }
3639 
3640     if(itSpawn)
3641       craftcore::FinishSpawning(rpd,itSpawn);
3642 
3643     return itSpawn;
3644   }
3645   return NULL;
3646 }
3647 
CraftWorkTurn(recipedata & rpd)3648 void crafthandle::CraftWorkTurn(recipedata& rpd){ DBG1(rpd.iRemainingTurnsToFinish);
3649   //this may mess gradative work:  rpd.iRemainingTurnsToFinish -= rpd.rc.H()->StateIsActivated(HASTE) ? 2 : 1;
3650   rpd.iRemainingTurnsToFinish--;
3651   if(rpd.iRemainingTurnsToFinish<0)
3652     rpd.iRemainingTurnsToFinish=0;
3653   rpd.bSuccesfullyCompleted = rpd.iRemainingTurnsToFinish==0;
3654 
3655   if(RAND()%2==0 ? rpd.iRemainingTurnsToFinish%3==0 : rpd.iRemainingTurnsToFinish%5==0){ // to avoid unnecessarily spamming hiteffects
3656     // keep this preference order!
3657     lsquare* lsqrHF=NULL;
3658     if(!lsqrHF)lsqrHF=rpd.lsqrPlaceAt;
3659     if(!lsqrHF)lsqrHF=rpd.v2AnvilLocation.Is0()     ? NULL : rpd.rc.H()->GetNearLSquare(rpd.v2AnvilLocation);
3660     if(!lsqrHF)lsqrHF=rpd.v2WorkbenchLocation.Is0() ? NULL : rpd.rc.H()->GetNearLSquare(rpd.v2WorkbenchLocation);
3661     if(!lsqrHF)lsqrHF=rpd.v2TailoringWorkbenchLocation.Is0() ? NULL : rpd.rc.H()->GetNearLSquare(rpd.v2TailoringWorkbenchLocation);
3662     if(!lsqrHF)lsqrHF=rpd.v2PlaceAt.Is0()           ? NULL : rpd.rc.H()->GetNearLSquare(rpd.v2PlaceAt);
3663     if(!lsqrHF)lsqrHF=rpd.v2ForgeLocation.Is0()     ? NULL : rpd.rc.H()->GetNearLSquare(rpd.v2ForgeLocation);
3664     if(!lsqrHF)lsqrHF=rpd.v2XplodAt.Is0()           ? NULL : rpd.rc.H()->GetNearLSquare(rpd.v2XplodAt);
3665     if(lsqrHF){
3666       hiteffectSetup* pHitEff=new hiteffectSetup();
3667 
3668       pHitEff->Type = (rpd.itTool || rpd.itTool2) ? WEAPON_ATTACK : UNARMED_ATTACK;
3669       pHitEff->WhoHits=rpd.rc.H();
3670       pHitEff->HitAtSquare=lsqrHF;
3671 
3672       item* itHF=NULL; //keep the below order
3673       if(!itHF)itHF = rpd.itTool  ? rpd.itTool : NULL;
3674       if(!itHF)itHF = rpd.itTool2 ? rpd.itTool2 : NULL;
3675       if(!itHF)itHF = rpd.rc.H()->GetRightArm() ? rpd.rc.H()->GetRightArm() : NULL;
3676       if(!itHF)itHF = rpd.rc.H()->GetLeftArm()  ? rpd.rc.H()->GetLeftArm() : NULL;
3677       if(itHF){
3678         pHitEff->lItemEffectReferenceID = itHF->GetID();
3679         lsqrHF->AddHitEffect(*pHitEff);
3680       }
3681       delete pHitEff;
3682     }
3683   }
3684 
3685   if(rpd.bGradativeCraftOverride){
3686     GradativeCraftOverride(rpd);
3687   }else{
3688     if(rpd.bSuccesfullyCompleted)
3689     {
3690       festring fsMsg("");
3691 
3692       if(rpd.itSpawnType!=CIT_NONE){DBGLN;
3693         for(int i=0;i<rpd.itSpawnTot;i++){
3694           item* itSp = SpawnItem(rpd,fsMsg);
3695           if(itSp!=NULL) //TODO apply degradation to lumps as the item broke (therefore is NULL)?
3696             craftcore::CopyDegradationIfPossible(rpd,itSp);
3697         }
3698       }
3699 
3700       if(rpd.otSpawnType!=CTT_NONE){DBGLN;
3701         SpawnTerrain(rpd,fsMsg);
3702       }
3703 
3704       fsMsg << DestroyIngredients(rpd);
3705       fsMsg << ".";
3706 
3707       ADD_MESSAGE(fsMsg.CStr());
3708     }
3709   }
3710 }
3711 
3712 /**
3713  * Always 1 to many.
3714  * ex.:
3715  * - one big meltable lump into ingots
3716  * - one big stick into tiny ones
3717  * - one rock into smaller ones
3718  * - one organic
3719  */
GradativeCraftOverride(recipedata & rpd)3720 void crafthandle::GradativeCraftOverride(recipedata& rpd)
3721 {DBGLN;
3722   if(rpd.ingredientsIDs.size()!=1)
3723     ABORT("incompatible gradative mode and ingredients count %lu",rpd.ingredientsIDs.size());
3724 
3725   material* matM = game::SearchItem(rpd.ingredientsIDs[0])->GetMainMaterial();DBGLN;
3726   long matMRemVol = matM->GetVolume();
3727 
3728   /**
3729    * ex.: tot=20 base=10 remain=9
3730    * base  rem    rem*tot
3731    * __  = __ ; x=_____=18 ; tot-x=SpawnNow=2
3732    * tot   x       base
3733    */
3734   float fRemain = 0;
3735   if(rpd.iBaseTurnsToFinish>1)
3736     fRemain = (rpd.iRemainingTurnsToFinish*rpd.itSpawnTot)/((float)rpd.iBaseTurnsToFinish);
3737   int iSpawnNow = rpd.itSpawnTot-fRemain;
3738   ulong spawnedVol = rpd.itSpawnMatMainVol*iSpawnNow;
3739   DBG7(iSpawnNow,spawnedVol,matMRemVol,rpd.itSpawnTot,rpd.iBaseTurnsToFinish,rpd.iRemainingTurnsToFinish,fRemain);
3740   if(iSpawnNow==0)
3741     return; //holding until required time is met ex.: each spawned requires more than one turn, even if a fraction.
3742 
3743   recipedata rpdTmp = rpd; // a copy to avoid messing it
3744   if(rpdTmp.itSpawnType==CIT_STICK && rpdTmp.fDifficulty<=1.0)
3745     rpdTmp.fDifficulty=1.1; //just a little bit difficult to allow fumbling
3746   for(int i=0;i<iSpawnNow;i++){
3747     if(rpdTmp.itSpawnType==CIT_STICK)
3748       CheckFumble(rpdTmp,false);
3749 
3750     festring fsMsg;
3751 
3752     /**
3753      * IMPORTANT!!! the duplicator would vanish with the item ID that is being duplicated (the original one)
3754      * so the duplication source item's ID will vanish. TODO could it be safely kept at DuplicateToStack() ?
3755      * ex.:
3756     item* itDup = itSpawn->DuplicateToStack(rpd.rc.H()->GetStack());
3757      * so just avoiding using it here.
3758      */
3759     item* itSpawned = SpawnItem(rpdTmp,fsMsg);
3760     if(itSpawned==NULL)
3761       ABORT("gradative craft should not spawn things that can be broken %s",rpdTmp.dbgInfo().CStr()); //if broken, will be NULL and the inventory may contain lumps
3762     craftcore::CopyDegradation(matM,itSpawned->GetMainMaterial());
3763 
3764     ADD_MESSAGE("%s.",fsMsg.CStr());
3765 
3766     if(rpdTmp.itSpawnType==CIT_STICK)
3767       rpdTmp.bSpawnBroken=false; //reset for next stick fumble check
3768   }
3769 
3770   rpd.itSpawnTot -= iSpawnNow;
3771   rpd.iBaseTurnsToFinish = rpd.iRemainingTurnsToFinish; //TODO should a total turns required be always kept?
3772 
3773   if(rpd.itSpawnTot<0)
3774     ABORT("crafting remaining to spawn went negative %d %s",rpd.itSpawnTot,rpd.dbgInfo().CStr());
3775   if(rpd.itSpawnTot==0){
3776     rpd.bSuccesfullyCompleted=true;
3777     if(rpd.iRemainingTurnsToFinish>0){
3778       ADD_MESSAGE("You worked well and gained %d minutes!",rpd.iRemainingTurnsToFinish); //TODO despite this actually means a lack of calc precision :P, not less fun tho :)
3779       rpd.iRemainingTurnsToFinish=0; //overriding for consistency.
3780     }
3781   }
3782 
3783   if(spawnedVol==matMRemVol)
3784     DestroyIngredients(rpd); //cant be left with 0 volume
3785   else if(spawnedVol>matMRemVol) // Can happen with sol stone, TODO something about it but don't crash.
3786   {
3787     ADD_MESSAGE("Your work disappears in a puff of logic!");
3788     DestroyIngredients(rpd);
3789   }
3790   else
3791     matM->SetVolume(matMRemVol-spawnedVol); //the remaining volume becomes the action control/limit
3792 
3793   matMRemVol=matM->GetVolume();
3794   DBG7(iSpawnNow,spawnedVol,matMRemVol,rpd.itSpawnTot,rpd.iBaseTurnsToFinish,rpd.iRemainingTurnsToFinish,fRemain);
3795 }
3796 
CraftSkillAdvance(recipedata & rpd)3797 void craftcore::CraftSkillAdvance(recipedata& rpd){
3798   /**
3799    * the minimum to advance 1st level on success is at GetLevelMap(1)
3800    */
3801   int iAddCraftSkill = rpd.rc.H()->GetCWeaponSkill(CRAFTING)->GetLevelMap(1) * rpd.fDifficulty;
3802   if(rpd.fDifficulty <= 1.0) iAddCraftSkill /= 10.0; // too easy stuff will learn less
3803   if(rpd.bSpawnBroken) iAddCraftSkill /= 3.0; // learns something if fumble
3804   if(iAddCraftSkill<1) iAddCraftSkill=1; // add a minimum
3805   DBG1(iAddCraftSkill);
3806   rpd.rc.H()->GetCWeaponSkill(CRAFTING)->AddHit(iAddCraftSkill);
3807 }
3808 
CheckFumble(recipedata & rpd,bool & bCriticalFumble,int & iFumblePower)3809 bool craftcore::CheckFumble(recipedata& rpd, bool& bCriticalFumble,int& iFumblePower)
3810 {
3811   /**
3812    * To fumble, base reference is 15% chance at a craft skill of 20.
3813    * ex.: Craft skill of 10 will have 30% fumble chance.
3814    */
3815   int iLuckPerc=RAND()%100; //0 to 99
3816   float fBaseCraftSkillToNormalFumble=20.0*rpd.fDifficulty;
3817   static const int iBaseFumbleChancePerc=15;
3818   int iFumbleBase=iBaseFumbleChancePerc/(craftcore::CraftSkill(rpd.rc.H())/fBaseCraftSkillToNormalFumble); //ex.: 30%
3819   if(iFumbleBase>98)iFumbleBase=98; //%1 granted luck as it is 0-99
3820   int iDiv=0;
3821   iDiv=1;if(iFumbleBase>iDiv && iLuckPerc<=(iFumbleBase/iDiv))iFumblePower++; //ex.: <=30%
3822   iDiv=2;if(iFumbleBase>iDiv && iLuckPerc<=(iFumbleBase/iDiv))iFumblePower++; //ex.: <=15%
3823   iDiv=4;if(iFumbleBase>iDiv && iLuckPerc<=(iFumbleBase/iDiv))iFumblePower++; //ex.: <= 7%
3824   iDiv=8;if(iFumbleBase>iDiv && iLuckPerc<=(iFumbleBase/iDiv))iFumblePower++; //ex.: <= 3%
3825   if(iLuckPerc==0){
3826     iFumblePower++; //always have 1% granted fumble chance
3827     bCriticalFumble=true;
3828   }
3829   //current max chance per round of spawning broken is 5%
3830   int iTry=RAND()%100; //0 to 99
3831   DBG8(iTry,iFumblePower,iFumbleBase,fBaseCraftSkillToNormalFumble,iLuckPerc,rpd.bSpawnBroken,rpd.iRemainingTurnsToFinish,rpd.lDamageFinalItem);
3832   if(bCriticalFumble || iTry<=iFumblePower)
3833     return true;
3834 
3835   return false;
3836 }
3837 
CheckFumble(recipedata & rpd,bool bChangeTurns)3838 void crafthandle::CheckFumble(recipedata& rpd,bool bChangeTurns)
3839 {
3840   rpd.rc.integrityCheck();
3841 //  if(!v2XplodAt.Is0()){DBGSV2(v2XplodAt);
3842   if(rpd.fDifficulty>1.0){
3843     int xplodXtra=0;
3844     for(int i=0;i<rpd.iStrongerXplod;i++)
3845       xplodXtra+=RAND()%5;
3846 
3847     bool bCriticalFumble=false;
3848     int iFumblePower=5;
3849     bool bFumbled = craftcore::CheckFumble(rpd,bCriticalFumble,iFumblePower);
3850     if(bFumbled){
3851       if(rpd.bCanBeBroken){
3852         if(bCriticalFumble){
3853           rpd.bSpawnBroken=true;
3854           if(bChangeTurns)
3855             rpd.iRemainingTurnsToFinish/=3; // takes much less time to craft it will be broken
3856         }else{
3857           if(bChangeTurns)
3858             rpd.iRemainingTurnsToFinish-=3; // takes less time to craft it can break at the end
3859           rpd.lDamageFinalItem += iFumblePower;
3860         }
3861       }else{
3862         /**
3863          * Things that cant be broken (half-usable) later will be harder to craft.
3864          * They will "break" (spawn as lump) with just a normal fumble.
3865          * So dont wast player's time to craft nothing useful.
3866          */
3867         if(bChangeTurns)
3868           rpd.iRemainingTurnsToFinish=1;
3869         rpd.bSpawnBroken=true; //dont need a critical here
3870       }
3871     }
3872 
3873     bool bXplod = bFumbled;
3874     if(rpd.bOnlyXplodIfCriticalFumble && !bCriticalFumble)
3875       bXplod=false;
3876 
3877     rpd.xplodStr=0;
3878     if(bXplod){
3879       rpd.xplodStr = iFumblePower;
3880       if(rpd.xplodStr>0){DBG2(rpd.xplodStr,rpd.dbgInfo().CStr());
3881         rpd.xplodStr+=RAND()%5+xplodXtra; //reference: weak lantern xplod str is 5
3882         //TODO anvil should always be near the forge. Anvil have no sparks. Keeping messages like that til related code is improved
3883       }
3884     }
3885 
3886 //    DBG9(fBaseCraftSkillToNormalFumble,iFumbleBase,iLuckPerc,iFumblePower,bXplod,rpd.xplodStr,rpd.bCanBeBroken,rpd.bSpawnBroken,rpd.iBaseTurnsToFinish);
3887   }
3888 }
3889 
CheckIngredients(recipedata & rpd)3890 void crafthandle::CheckIngredients(recipedata& rpd){
3891   rpd.rc.integrityCheck();
3892   for(int i=0;i<rpd.ingredientsIDs.size();i++){DBG1(rpd.ingredientsIDs[i]);
3893     item* it=game::SearchItem(rpd.ingredientsIDs[i]);DBGLN;
3894     if(it==NULL){ //ABORT("ingredient id %d not found",rpd.ingredientsIDs[i]);
3895       /**
3896        * ex.: if something catches fire and is destroyed before the crafting ends.
3897        */
3898       ADD_MESSAGE("a required ingredient was destroyed...");DBG1(rpd.ingredientsIDs[i]);
3899       rpd.bFailedTerminateCancel=true; //TODO a crash happens in this line, how? memory corruption? if the tiny explosions trigger things on the floor like wands
3900       break;
3901     }
3902 
3903     // a magpie or siren may have taken it
3904     if(it->GetSquareUnder()!=rpd.lsqrActor){
3905       ADD_MESSAGE("%s ingredient went missing...",it->GetName(DEFINITE).CStr());
3906       rpd.bFailedTerminateCancel=true;
3907       break;
3908     }
3909 
3910     //TODO once: apply wands, release rings/ammys effects, xplod str 5+ if enchanteds +1 +2 etc
3911     if(!craftcore::canBeCrafted(it)){ //basically contains some kind of magic
3912       if(it->GetEnchantment()!=0)
3913         rpd.iStrongerXplod+=abs(it->GetEnchantment());
3914       else
3915         rpd.iStrongerXplod++; //TODO could add based on how hazardous the magic type is ex. fireball wand would be +100 or something like that..
3916     }
3917   }
3918 }
3919 
CheckFacilities(recipedata & rpd)3920 void crafthandle::CheckFacilities(recipedata& rpd){
3921   rpd.rc.integrityCheck();
3922   if(!rpd.v2AnvilLocation.Is0()){
3923     olterrain* ot = game::GetCurrentLevel()->GetLSquare(rpd.v2AnvilLocation)->GetOLTerrain();
3924     if(ot==NULL || ot->GetConfig()!=ANVIL){
3925       ADD_MESSAGE("The anvil was destroyed!");
3926       rpd.bFailedTerminateCancel=true;
3927     }
3928 
3929     rpd.v2XplodAt=rpd.v2AnvilLocation;
3930   }else
3931   if(!rpd.v2ForgeLocation.Is0()){
3932     olterrain* otForge = game::GetCurrentLevel()->GetLSquare(rpd.v2ForgeLocation)->GetOLTerrain();
3933     if(otForge==NULL || otForge->GetConfig()!=FORGE){
3934       ADD_MESSAGE("The forge was destroyed!");
3935       rpd.bFailedTerminateCancel=true;
3936     }
3937 
3938     rpd.v2XplodAt=rpd.v2ForgeLocation;
3939   }else
3940   if(!rpd.v2WorkbenchLocation.Is0()){
3941     olterrain* otWorkbench = game::GetCurrentLevel()->GetLSquare(rpd.v2WorkbenchLocation)->GetOLTerrain();
3942     if(otWorkbench==NULL || otWorkbench->GetConfig()!=WORK_BENCH){
3943       ADD_MESSAGE("The workbench was destroyed!");
3944       rpd.bFailedTerminateCancel=true;
3945     }
3946 
3947     //TODO workbench should be damaged w/o explosions (that is area effect related to fire/forge)
3948     rpd.bOnlyXplodIfCriticalFumble=true; //TODO kept as WIP
3949     //explode/sparks at workbench doesnt make much sense: rpd.v2XplodAt=rpd.v2WorkbenchLocation;
3950   }else
3951   if(!rpd.v2TailoringWorkbenchLocation.Is0()){
3952     olterrain* otTWorkbench = game::GetCurrentLevel()->GetLSquare(rpd.v2TailoringWorkbenchLocation)->GetOLTerrain();
3953     if(otTWorkbench==NULL || otTWorkbench->GetConfig()!=TAILORING_BENCH){
3954       ADD_MESSAGE("The tailoring workbench was destroyed!");
3955       rpd.bFailedTerminateCancel=true;
3956     }
3957 
3958     //TODO workbench should be damaged w/o explosions (that is area effect related to fire/forge)
3959     rpd.bOnlyXplodIfCriticalFumble=true; //TODO kept as WIP
3960   }
3961 }
3962 
checkTool(ulong id,lsquare * lsqrActor)3963 item* checkTool(ulong id,lsquare* lsqrActor){
3964   item* it=NULL;
3965   if(id!=0){
3966     it = game::SearchItem(id); //must keep searching it as it may have been destroyed.
3967     //TODO if a tool was broken and gets fixed, it's old ID will vanish!!! how to handle it!??!?!
3968     if(it==NULL){
3969       ADD_MESSAGE("One unmodified tool to craft this is missing or destroyed.");
3970       return NULL;
3971     }
3972     DBGEXEC(if(it!=NULL)DBGSV2(it->GetLSquareUnder()->GetPos()));
3973   }
3974 
3975   if(it!=NULL && it->GetLSquareUnder()!=lsqrActor)//rpd.itTool!=Actor->GetMainWielded())
3976   {DBGLN; //TODO re-mainWield it
3977     ADD_MESSAGE("%s went missing.",it->GetName(DEFINITE).CStr());
3978     return NULL;
3979   }
3980 
3981   return it;
3982 }
3983 
CheckTools(recipedata & rpd)3984 void crafthandle::CheckTools(recipedata& rpd){
3985   rpd.lsqrActor = rpd.rc.H()->GetLSquareUnder(); DBGSV2(rpd.lsqrActor->GetPos());
3986 
3987   if(rpd.itToolID>0){
3988     rpd.itTool=checkTool(rpd.itToolID,rpd.lsqrActor);
3989     if(rpd.itTool==NULL)
3990       rpd.bFailedTerminateCancel=true;
3991   }
3992 
3993   if(rpd.itTool2ID>0){
3994     rpd.itTool2=checkTool(rpd.itTool2ID,rpd.lsqrActor);
3995     if(rpd.itTool2ID>0 && rpd.itTool2==NULL)
3996       rpd.bFailedTerminateCancel=true;
3997   }
3998 
3999   if(rpd.bFailedTerminateCancel)
4000     rpd.itTool=rpd.itTool2=NULL; //consistency or have the two (if required), or have none
4001 
4002   DBG8(rpd.itToolID,rpd.itTool,rpd.itTool2ID,rpd.itTool2,rpd.itSpawnCfg,rpd.otSpawnCfg,rpd.itSpawnType,rpd.otSpawnType);
4003 }
4004 
CheckEverything(recipedata & rpd,character * CurrentActor)4005 void crafthandle::CheckEverything(recipedata& rpd, character* CurrentActor){
4006   rpd.rc.integrityCheck(CurrentActor);
4007 
4008   if(!rpd.IsFailedSuspendOrCancel())
4009     if(craftcore::CheckHumanoid(CurrentActor)==NULL)
4010       rpd.bFailedSuspend=true; //may have polymorphed
4011 
4012   if(!rpd.IsFailedSuspendOrCancel())
4013     if(rpd.rc.H()!=CurrentActor){
4014       /**
4015        * it is of ultimate importance to update the current actor
4016        */
4017       rpd.rc.SetHumanoid(CurrentActor);
4018     }
4019 
4020   if(!rpd.IsFailedSuspendOrCancel())
4021     if(!craftcore::CheckArms(rpd.rc.H()))
4022       rpd.bFailedSuspend=true;
4023 
4024   if(!rpd.IsFailedSuspendOrCancel())
4025     CheckTools(rpd);
4026 
4027   DBG2(DBGAV2(CurrentActor->GetPos()),DBGAV2(rpd.v2PlayerCraftingAt));
4028   if(CurrentActor->GetPos() != rpd.v2PlayerCraftingAt){ //in case player is teleported
4029     ADD_MESSAGE("You need to move back to where you started crafting.");
4030     rpd.bFailedSuspend=true;
4031   }
4032 
4033   rpd.rc.integrityCheck();
4034   DBGSV2(rpd.v2PlaceAt);
4035   if(!rpd.IsFailedSuspendOrCancel()){
4036     if(rpd.otSpawnType!=CTT_NONE){
4037       rpd.lsqrPlaceAt = CurrentActor->GetNearLSquare(rpd.v2PlaceAt); //TODO near? it is absolute map pos... confusing method name or I missed something???
4038       olterrain* oltExisting = rpd.lsqrPlaceAt->GetOLTerrain();
4039       if(oltExisting!=NULL){DBGLN;
4040     //  ADD_MESSAGE("%s cannot be placed there.", rpd.otSpawn->GetName(DEFINITE).CStr()); //TODO like in case something is placed there before ending the construction?
4041         ADD_MESSAGE("Unable to place it there."); //TODO like in case something is placed there before ending the construction? but what and how?
4042         rpd.bFailedTerminateCancel=true;
4043       }
4044     }
4045   }
4046 
4047   if(!rpd.IsFailedSuspendOrCancel())
4048     CheckIngredients(rpd);
4049 
4050   if(!rpd.IsFailedSuspendOrCancel())
4051     CheckFacilities(rpd);
4052 
4053   if(!rpd.IsFailedSuspendOrCancel())
4054     CheckFumble(rpd);
4055 
4056   rpd.rc.integrityCheck();
4057 }
4058 
DestroyIngredients(recipedata & rpd)4059 cfestring crafthandle::DestroyIngredients(recipedata& rpd){
4060   festring fsIng,fsIngP;
4061   festring fsIngPrev,fsIngPPrev;
4062   int iCountEqual=1;
4063   festring fsIngMsg("");
4064   for(int i=0;i<rpd.ingredientsIDs.size();i++){DBG1(rpd.ingredientsIDs[i]);
4065     item* it=game::SearchItem(rpd.ingredientsIDs[i]);DBGLN;
4066     if(it==NULL)ABORT("ingredient id %lu not found %s",rpd.ingredientsIDs[i],rpd.dbgInfo().CStr());
4067     it->RemoveFromSlot();DBGLN;
4068 
4069     bool bSendToHell=true;
4070     if(rpd.otSpawnMatMainCfg!=0)
4071       if(it->GetMainMaterial()->GetConfig() != rpd.otSpawnMatMainCfg)
4072         if(rpd.rc.H()->GetPos()!=rpd.lsqrPlaceAt->GetPos()){
4073           /**
4074            * TODO this diff pos check shouldnt be necessary, CanMoveOn() should suffice but didnt work as it always can't move on, therefore always "true" TODO confirm again...
4075           if(!rpd.rc.H()->CanMoveOn(rpd.lsqrPlaceAt))
4076            */
4077 
4078           /**
4079            * this keep still available but inaccessible ingredients differing from constructed main material
4080            * TODO but as ghost they would still be recoverable, keep as trick/feature/secret? or destroy them? or just prevent diff materials at all during their selection?
4081            */
4082 
4083           bSendToHell=false;
4084         }
4085 
4086     if(bSendToHell){DBGLN;
4087       craftcore::SendToHellSafely(it);
4088     }else{DBGLN;
4089       //this way, the lower quality wall will still contain all stones in a non destructive way, is more fair
4090       //TODO what about amulet of phasing or ghost mode?
4091       it->MoveTo(rpd.lsqrPlaceAt->GetStack());
4092     }DBGLN;
4093 
4094     fsIng.Empty();fsIng << it->GetName(INDEFINITE);DBGLN;
4095     fsIngP.Empty();fsIngP << it->GetName(PLURAL);DBGLN;
4096 
4097     bool bNewType = fsIngPrev!=fsIng;DBGLN;
4098 
4099     bool bDumpPrev = false;
4100     if(bNewType)
4101       bDumpPrev=true;
4102     if(i==rpd.ingredientsIDs.size()-1){DBGLN;
4103       bDumpPrev=true;
4104       fsIngPrev=fsIng;
4105     }
4106 
4107     if(bDumpPrev){DBGLN;
4108       if(fsIngMsg.GetSize()>0)
4109         fsIngMsg<<", ";
4110 
4111       if(iCountEqual>1){
4112         fsIngMsg << (iCountEqual+1) << " " << fsIngPPrev;
4113       }else{
4114         fsIngMsg << fsIngPrev;
4115       }
4116 
4117       iCountEqual=1;
4118     }else
4119       iCountEqual++;
4120 
4121     fsIngPrev.Empty();fsIngPrev<<fsIng;
4122     fsIngPPrev.Empty();fsIngPPrev<<fsIngP;
4123   }
4124 
4125   if(fsIngMsg.GetSize()>0) //TODO this needs improving, for plural etc, to look good
4126     return festring() << " using " << fsIngMsg.CStr();
4127 
4128   return "";
4129 }
4130 
4131 /**
4132  *
4133  * @param rpd
4134  * @param mat
4135  * @param ForceType CIT_... stick, lump, stone
4136  * @param volume is the main material volume, it is important to be set before item->CalculateAll()
4137  * @return
4138  */
PrepareRemains(recipedata & rpd,material * matWorkWith,int ForceType,long NewMaterialVolume)4139 item* craftcore::PrepareRemains(recipedata& rpd, material* matWorkWith, int ForceType, long NewMaterialVolume) //TODO force type could be a class (type) reference?
4140 {
4141   if(matWorkWith==NULL)
4142     ABORT("NULL remains material");
4143 
4144   DBG2(matWorkWith->GetName(DEFINITE).CStr(),matWorkWith->GetVolume());
4145   bool bLiquid = matWorkWith->IsLiquid();
4146   if(matWorkWith->IsPowder())bLiquid=false; //TODO if explosive could have a chance to xplod use the fumble function TODO generalize it
4147 
4148   if(dynamic_cast<gas*>(matWorkWith)!=NULL)return NULL; //TODO should have a chance to release the gas effect if not 100% :)
4149 
4150   if(bLiquid){
4151     rpd.rc.H()->SpillFluid(NULL,liquid::Spawn(matWorkWith->GetConfig(),matWorkWith->GetVolume())); //TODO use a fumble check to determine on floor or on character (worse)
4152     return NULL;
4153   }
4154 
4155   item* itTmp = NULL;
4156 
4157   int Type = CIT_NONE;
4158 
4159   if(ForceType!=CIT_NONE){
4160     Type=ForceType;DBGLN;
4161   }else{
4162     /**
4163      * TODO
4164      * create leather/cloth pieces, seweing tools and cloth crafting.
4165      * Chain mail should be this too and require metal cutting tool.
4166      */
4167     if(Type==CIT_NONE) //specific lumps
4168       if(craftcore::IsMeltable(matWorkWith) || matWorkWith->IsFlesh() || (matWorkWith->GetCategoryFlags() & CAN_BE_TAILORED))
4169         Type = CIT_LUMP;
4170 
4171     if(Type==CIT_NONE)
4172       if(IsWooden(matWorkWith) || IsBone(matWorkWith))
4173         Type = CIT_STICK;
4174 
4175     /**
4176      * the requested could be a shaped stone, not suitable to all re-uses.
4177      * also see item creation that already won't use full volume of non meltable stones
4178      * TODO mat->IsSolid() will not help to determine if it should be a stone, ex.: ruby material returned false :(
4179      */
4180     if(Type==CIT_NONE)
4181       Type = CIT_STONE;
4182 
4183     /**
4184      * TODO generic fallback should be lump
4185     if(itTmp==NULL){
4186       Type = CIT_LUMP;DBGLN;
4187     }
4188     */
4189   }
4190 
4191   switch(Type){
4192     case CIT_LUMP:DBGLN;
4193       itTmp = lump::Spawn(0, NO_MATERIALS);
4194       break;
4195     case CIT_STICK:DBGLN;
4196       itTmp = stick::Spawn(0, NO_MATERIALS);
4197       break;
4198     case CIT_STONE:DBGLN;
4199       /**
4200        * the requested could be a shaped stone, not suitable to all re-uses.
4201        * also see item creation that already won't use full volume of non meltable stones
4202        */
4203       itTmp = stone::Spawn(0, NO_MATERIALS);
4204       break;
4205   }
4206 
4207 //    delete itTmp->SetMainMaterial(material::MakeMaterial(mat->GetConfig(),mat->GetVolume()));
4208   /**
4209    * the material must be completely ready/configured before assigning it as
4210    * main or secondary, as there will happen important calculations, including
4211    * emitation!
4212    */
4213   material* matNew=CreateMaterial(matWorkWith);
4214   matNew->SetVolume(NewMaterialVolume);
4215   craftcore::CopyDegradation(matWorkWith,matNew);
4216   delete itTmp->SetMainMaterial(matNew);
4217 
4218   FinishSpawning(rpd,itTmp);
4219 
4220   ADD_MESSAGE("%s was recovered.", itTmp->GetName(DEFINITE).CStr());
4221 
4222   return itTmp;
4223 }
4224 
FinishSpawning(recipedata & rpd,item * itSpawn)4225 void craftcore::FinishSpawning(recipedata& rpd,item* itSpawn){
4226   itSpawn->MoveTo(rpd.rc.H()->GetStack());DBGLN;
4227   DBG3("EmitDbgSpawned",itSpawn->GetEmitation(),itSpawn->GetVolume());
4228   if(itSpawn->GetEmitation()>0){ //TODO is there a better way to do this emitation fix?
4229     // uses the previous emitation to fix everywhere before recalculating item emitation
4230     rpd.rc.H()->SignalEmitationDecrease(itSpawn->GetEmitation());
4231     rpd.rc.H()->CalculateEmitation();
4232 
4233     rpd.rc.H()->GetLSquareUnder()->SignalEmitationDecrease(itSpawn->GetEmitation());
4234     rpd.rc.H()->GetLSquareUnder()->CalculateLuminance();
4235 
4236     // last
4237     itSpawn->SignalEmitationDecrease(itSpawn->GetEmitation());
4238     itSpawn->CalculateEmitation();
4239   }
4240 }
4241 
4242 std::vector<uint> vBone;
IsBone(material * mat)4243 bool craftcore::IsBone(material* mat)
4244 {
4245   static bool bDummyInit = [](){
4246     vBone.push_back(BONE);
4247     vBone.push_back(DRAGON_BONE);
4248     vBone.push_back(WHALE_BONE);
4249     vBone.push_back(WRAITH_BONE);
4250     vBone.push_back(OMMEL_BONE);
4251     // new IDs wont be sequential, so no range...
4252     return true;
4253   }();bDummyInit=true;//assigning just to remove IDE's unused warnings.. is dead code anyway, should not exist...
4254 
4255   if(IsMeltable(mat))
4256     return false;
4257 
4258   for(auto pcfg = vBone.begin(); pcfg != vBone.end(); pcfg++)
4259     if(mat->GetConfig() == *pcfg)
4260       return true;
4261 
4262   return false;
4263 }
4264 
IsWooden(material * mat)4265 bool craftcore::IsWooden(material* mat){DBG3(mat->GetConfig(),FUNGI_WOOD,PETRIFIED_WOOD);
4266   if(IsMeltable(mat)){
4267     return false;DBGLN;
4268   }
4269   if(mat->GetConfig()>=FUNGI_WOOD && mat->GetConfig()<=PETRIFIED_WOOD) //if a new wood type is ever added, use a vector like vBone
4270     return true;
4271   return false;DBGLN;
4272 }
4273 
CitType(item * it)4274 int craftcore::CitType(item* it)
4275 {
4276   if(dynamic_cast<stick*>(it)!=NULL)return CIT_STICK;
4277   if(dynamic_cast<lump*>(it)!=NULL)return CIT_LUMP;
4278   if(dynamic_cast<stone*>(it)!=NULL)return CIT_STONE;
4279   if(dynamic_cast<potion*>(it)!=NULL)return CIT_POTION;
4280   return CIT_NONE;
4281 }
4282 
IsMeltable(item * it)4283 truth craftcore::IsMeltable(item* it)
4284 {
4285   return IsMeltable(it->GetMainMaterial(),it->GetSecondaryMaterial());
4286 }
4287 
IsMeltable(material * matM,material * matS)4288 truth craftcore::IsMeltable(material* matM,material* matS)
4289 {
4290   if(!craftcore::IsMeltable(matM))
4291     return false;
4292   if(matS!=NULL && !craftcore::IsMeltable(matS))
4293     return false;
4294   return true;
4295 }
4296 
IsMeltable(material * mat)4297 truth craftcore::IsMeltable(material* mat){
4298   if(mat->GetCategoryFlags() & IS_METAL)
4299     return true;
4300   if(mat->GetConfig()==GLASS)
4301     return true;
4302   //TODO find more meltables and add them here
4303   return false;
4304 }
4305 
IsDegradedMat(material * mat)4306 bool IsDegradedMat(material* mat){
4307   if(mat==NULL)return false;
4308   if(mat->GetBurnLevel()>0)return true;
4309   if(mat->GetSpoilLevel()>0)return true;
4310   if(mat->GetRustLevel()>0)return true;
4311   return false;
4312 }
IsDegraded(item * it,bool bShowMsg)4313 bool craftcore::IsDegraded(item* it,bool bShowMsg){
4314   bool b=false;
4315   if(IsDegradedMat(it->GetMainMaterial()))b=true;
4316   if(IsDegradedMat(it->GetSecondaryMaterial()))b=true;
4317 
4318   if(b && bShowMsg)
4319     ADD_MESSAGE("%s is too degraded to work with.",it->GetName(DEFINITE).CStr());
4320 
4321   return b;
4322 }
4323 
CopyDegradation(item * itFrom,material * matTo)4324 void craftcore::CopyDegradation(item* itFrom,material* matTo)
4325 {
4326   material* matFromM = itFrom->GetMainMaterial();
4327   if(matFromM!=NULL){
4328     CopyDegradation(matFromM,matTo);
4329   }else{
4330     /**
4331      * w/o main material there is:
4332      * - no rust available
4333      * - no burn available
4334      * //TODO will this make things easier to player?
4335      * PS.: this ends being probably only for corpses.
4336      */
4337     ushort SC=0;
4338     while(true){ //TODO reverse SpoilLevel into SpoilCounter value
4339       matTo->SetSpoilCounter(SC);
4340       if(matTo->GetSpoilLevel()>=itFrom->GetSpoilLevel())
4341         break;
4342       SC+=5; // from slowest at organic::Be
4343     }
4344   }
4345 }
CopyDegradation(material * matFrom,material * matTo)4346 void craftcore::CopyDegradation(material* matFrom,material* matTo)
4347 {
4348   if(dynamic_cast<organic*>(matTo)!=NULL)
4349     ((organic*)matTo)->SetSpoilCounter(((organic*)matFrom)->GetSpoilCounter());
4350   matTo->SetBurnLevel(matFrom->GetBurnLevel(),false);
4351   matTo->SetRustLevel(matFrom->GetRustLevel());
4352 }
4353 
CopyDegradationIfPossible(recipedata & rpd,item * itTo)4354 void craftcore::CopyDegradationIfPossible(recipedata& rpd, item* itTo)
4355 {
4356   /**
4357    * will work with simple recipes only
4358    * where the work is from one simple ingredient
4359    */
4360   if(rpd.ingredientsIDs.size()==1){
4361     item* itIng = game::SearchItem(rpd.ingredientsIDs[0]);
4362     if(itIng->GetSecondaryMaterial()==NULL)
4363       CopyDegradation(itIng,itTo->GetMainMaterial());
4364   }
4365 }
4366 
Save(outputfile & SaveFile)4367 void craftcore::Save(outputfile& SaveFile)
4368 {
4369   int size = vSuspended.size();
4370   SaveFile << size; //DBG2(size,SaveFile.GetFullFilename().CStr()); //token
4371   if(size>0){
4372     for(int i=0;i<size;i++)
4373       vSuspended[i].Save(SaveFile);
4374   }
4375 }
4376 
Load(inputfile & SaveFile)4377 void craftcore::Load(inputfile& SaveFile)
4378 {
4379   ClearSuspendedList(); //make sure it is always cleaned from memory!
4380   if(game::GetCurrentSavefileVersion()<133)
4381     return;
4382 
4383   int size = 0;
4384   SaveFile >> size; //DBG2(size,SaveFile.GetFullFilename().CStr()); //token
4385   if(size>0){
4386     for(int i=0;i<size;i++){
4387       recipedata rpd;
4388       rpd.Load(SaveFile);
4389       vSuspended.push_back(rpd);
4390     }
4391   }
4392 }
4393 
4394 //recipeprototype::recipeprototype(recipespawner Spawner, cchar* ClassID)
4395 //: Spawner(Spawner), ClassID(ClassID) { Index = protocontainer<recipedatacore>::Add(this); }
4396 //
4397 //recipedatacore* recipeprototype::SpawnAndLoad(inputfile& SaveFile) const
4398 //{
4399 //  recipedatacore* prpd = Spawner(0);
4400 //  prpd->Load(SaveFile);
4401 //  return prpd;
4402 //}
4403