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