forked from ttronrud/Bannerlord-DistinguishedService
-
Notifications
You must be signed in to change notification settings - Fork 3
/
PromotionManager.cs
1492 lines (1376 loc) · 75.9 KB
/
PromotionManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Author: Thor Tronrud
* PromotionManager.cs:
*
* Pretty monolothic, and by accretion, not necessity. Acts as a big
* state object with a lot of static methods providing the utilities
* used to promote basic troops to companions.
*
* It is fed a list of nominees by the Battle Behaviour class and presents
* them to the player.
*
* Also includes additional dialogue and supporting methods.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using TaleWorlds.CampaignSystem;
using TaleWorlds.CampaignSystem.Actions;
using TaleWorlds.CampaignSystem.CampaignBehaviors;
using TaleWorlds.CampaignSystem.CharacterDevelopment;
using TaleWorlds.CampaignSystem.Conversation;
using TaleWorlds.CampaignSystem.Extensions;
using TaleWorlds.CampaignSystem.MapEvents;
using TaleWorlds.CampaignSystem.Party;
using TaleWorlds.CampaignSystem.Roster;
using TaleWorlds.CampaignSystem.Settlements;
using TaleWorlds.Core;
using TaleWorlds.Library;
using TaleWorlds.Localization;
using TaleWorlds.ObjectSystem;
namespace DistinguishedService
{
class PromotionManager
{
Random rand; //Single random object to use
//Distinguished Service Specific Lists
public static PromotionManager __instance = null; //our static instance
public List<CharacterObject> nominations; //Who's currently nominated for a promotion?
public List<int> killcounts; //What's their killcount?
public static bool MyLittleWarbandLoaded = false; //is MLWB loaded? We'll need compatibility adjustments -_-
//Settings values
public bool using_extern_namelist { get; set; } //are we using an external namelist?
public string extern_namelist { get; set; } //What is it?
public int max_nominations { get; set; } //How many mooks can be promoted at once?
public int tier_threshold { get; set; } //Minimum tier (-1 = end)
public int inf_kill_threshold { get; set; } //Type-specific kill minimums to qualify
public int cav_kill_threshold { get; set; }
public int ran_kill_threshold { get; set; }
public bool fill_perks { get; set; } //fill perks automatically on promotion?
public float outperform_percentile { get; set; } //What percentile of kills should the nominee lie above?
public int up_front_cost { get; set; } //Do they cost money to promote?
public bool respect_companion_limit { get; set; } //Do we care about the game's companion limit?
public bool ignore_cautions { get; set; } //Do you want to know if something might break?
private int base_additional_skill_points { get; set; } //How many base skill points do we give these companions?
private int leadership_points_per_50_extra_skill_points { get; set; } //And a bonus for high leadership
private int skp_per_excess_kill { get; set; } //How many extra skills points do excess kills grant?
public bool select_skills_randomly { get; set; } //No player input on skill selections? For games with lots of companions
public int num_skill_rounds { get; set; } //How many rounds of skill selection do we go through? (Primary, secondary, tertiary, etc...)
public int num_skill_bonuses { get; set; } //How many specific skills can be selected per round?
public float ai_promotion_chance { get; set; } //Can AI lords promote troops?
public int max_ai_comp_per_party { get; set; } //How many do we allow in their party at once?
public PromotionManager()
{
//string path = Path.Combine(BasePath.Name, "Modules", "DistinguishedService", "Settings.xml");
//start with what we know will work
string path = Path.Combine(TaleWorlds.ModuleManager.ModuleHelper.GetModuleFullPath("DistinguishedService110"), "Settings.xml");
//check for a settings in the modules folder
//if it exists, use it instead!
if (File.Exists(Path.Combine(BasePath.Name, "Modules", "DistinguishedService110", "Settings.xml")))
{
InformationManager.DisplayMessage(new InformationMessage("Using Modules/DistinguishedService110 Settings", Color.FromUint(4282569842U)));
path = Path.Combine(BasePath.Name, "Modules", "DistinguishedService110", "Settings.xml");
}
Settings currentsettings;
using (Stream stream = (Stream)new FileStream(path, FileMode.Open))
currentsettings = (Settings)new XmlSerializer(typeof(Settings)).Deserialize(stream);
//Set from settings
this.tier_threshold = currentsettings.tier_threshold;
this.max_nominations = currentsettings.max_nominations;
this.inf_kill_threshold = currentsettings.inf_kill_threshold;
this.cav_kill_threshold = currentsettings.cav_kill_threshold;
this.ran_kill_threshold = currentsettings.ran_kill_threshold;
this.outperform_percentile = currentsettings.outperform_percentile;
this.skp_per_excess_kill = currentsettings.skillpoints_per_excess_kill;
this.up_front_cost = currentsettings.up_front_cost;
this.fill_perks = currentsettings.fill_in_perks;
this.respect_companion_limit = currentsettings.respect_companion_limit;
this.base_additional_skill_points = currentsettings.base_additional_skill_points;
this.leadership_points_per_50_extra_skill_points = currentsettings.leadership_points_per_50_extra_skill_points;
this.num_skill_bonuses = currentsettings.number_of_skill_bonuses;
this.num_skill_rounds = currentsettings.number_of_skill_rounds;
this.select_skills_randomly = currentsettings.select_skills_randomly;
this.ai_promotion_chance = currentsettings.ai_promotion_chance;
this.max_ai_comp_per_party = currentsettings.max_ai_companions_per_party;
rand = new Random();
nominations = new List<CharacterObject>();
killcounts = new List<int>();
this.using_extern_namelist = currentsettings.NAMES_FROM_EXTERNAL_FILE;
this.extern_namelist = Path.Combine(TaleWorlds.ModuleManager.ModuleHelper.GetModuleFullPath("DistinguishedService110"), currentsettings.EXTERNAL_NAME_FILE);
//Do the same with the namelist -- use the easier-to-access Modules folder preferentially
if(File.Exists(Path.Combine(BasePath.Name, "Modules", "DistinguishedService110", currentsettings.EXTERNAL_NAME_FILE)))
{
this.extern_namelist = Path.Combine(BasePath.Name, "Modules", "DistinguishedService110", currentsettings.EXTERNAL_NAME_FILE);
}
if (this.using_extern_namelist)
{
InformationManager.DisplayMessage(new InformationMessage("USING EXTERNAL NAMELIST FILE!\nThis file will be written back to/edited to knock out used names", Color.FromUint(4282569842U)));
}
//set other values from settings
this.fill_perks = currentsettings.fill_in_perks;
//Output final mod state to user, set static instance
InformationManager.DisplayMessage(new InformationMessage("Max nominations = " + this.max_nominations + "\nTier Thresh = " + this.tier_threshold + "\nKill Thresh:\nInf = " + this.inf_kill_threshold + " cav = " + this.cav_kill_threshold + " ran = " + this.ran_kill_threshold + "\nPerformance Thresh = " + this.outperform_percentile, Color.FromUint(4282569842U)));
PromotionManager.__instance = this;
//Display warnings if chosen settings will cause non-player-controlled events
//e.g. auto perk selection, auto-promotion, ignoring companion limit
if (currentsettings.upgrade_to_hero)
{
InformationManager.DisplayMessage(new InformationMessage("CAUTION: Troops will auto-promote to heros at tier " + this.tier_threshold + "\nChange settings if this is unintended.", Colors.Yellow));
}
if (this.fill_perks)
{
InformationManager.DisplayMessage(new InformationMessage("CAUTION: New hero perks will be assigned automatically.\nChange settings if this is unintended.", Colors.Yellow));
}
if(!this.respect_companion_limit)
{
InformationManager.DisplayMessage(new InformationMessage("CAUTION: Ignoring companion limit.", Colors.Yellow));
}
}
//Called when the battle is considered "over"
//Doing it now sidesteps the UI elements being rendered underneath
//the end-of-battle loading screen, which was a pretty insidious bug
//The PM instance's nominations and killcounts are populated from the Battle Behaviour
//and in this method we go through and make sure the nominations are valid
public void OnPCBattleEndedResults()
{
//check if we care about the companion limit, and if there's room
if (this.respect_companion_limit && Clan.PlayerClan.Companions.Count >= Clan.PlayerClan.CompanionLimit)
{
InformationManager.DisplayMessage(new InformationMessage(new TextObject("At maximum allowed companions. No nominations possible.").ToString(), Colors.Blue));
return;
}
//Create a list of nominations and killcounts stripped of invalid entries
List<CharacterObject> _stripped_noms = new List<CharacterObject>();
List<int> _stripped_kcs = new List<int>();
//bonus from nomination list
if (nominations.Count > 0 && killcounts.Count > 0)
{
for (int i = 0; i < nominations.Count; i++)
{
if (MobileParty.MainParty.MemberRoster.Contains(nominations[i]) && nominations[i] != null && nominations[i].HitPoints > 0)
{
_stripped_noms.Add(nominations[i]);
_stripped_kcs.Add(killcounts[i]);
}
}
}
//Finally, the list of selected characterobjects
//up to the companion limit, if we're respecting it
List<CharacterObject> coList;
int mod_nominations = this.max_nominations;
double num = rand.NextDouble();
//If COs are in the final cut list, order them by killcount, and present them to the player
//We reference two methods -- genInquiryElements, which creates the little presentation box for the unit,
//and OnNomineeSelect, which takes each selected nominee and performs the "promotion"
if (_stripped_noms.Count > 0)
{
coList = new List<CharacterObject>(_stripped_noms).OrderBy<CharacterObject, int>(o => _stripped_kcs[_stripped_noms.IndexOf(o)]).Reverse().ToList();
_stripped_kcs = new List<int>(_stripped_kcs).OrderBy<int, int>(o => _stripped_kcs[_stripped_kcs.IndexOf(o)]).Reverse().ToList();
//check if number of possible nominations would put us over the companion limit
if (this.respect_companion_limit && (this.max_nominations + Clan.PlayerClan.Companions.Count) > Clan.PlayerClan.CompanionLimit)
{
mod_nominations = Clan.PlayerClan.CompanionLimit - Clan.PlayerClan.Companions.Count;
}
else
{
mod_nominations = this.max_nominations;
}
MBInformationManager.ShowMultiSelectionInquiry(new MultiSelectionInquiryData("Distinguished Soldiers", "Several soldiers made names for themselves in this battle. You can choose up to " + mod_nominations + " (or none, by exiting) to fight at your side as a companion.", this.GenInquiryelements(coList, _stripped_kcs), true, mod_nominations, "DONE", "RANDOM", new Action<List<InquiryElement>>(OnNomineeSelect), (Action<List<InquiryElement>>)null, ""), true);
return;
}
}
//First util function -- takes a character object list and killcount, creates a
//corresponding list of InquiryElements showing the unit's preview and killcount tooltip
public List<InquiryElement> GenInquiryelements(List<CharacterObject> _cos, List<int> _kills)
{
List<InquiryElement> _ies = new List<InquiryElement>();
for (int q = 0; q < _cos.Count; q++)
{
if (MobileParty.MainParty.MemberRoster.Contains(_cos[q]))
{
_ies.Add(new InquiryElement((object)_cos[q], _cos[q].Name.ToString(), new ImageIdentifier(CharacterCode.CreateFrom((BasicCharacterObject)_cos[q])), true, _kills[q].ToString() + " kills"));
}
}
return _ies;
}
//Second util function -- Takes the list of selected inquiry elements, and feeds them through the
//Hero-creation system
public void OnNomineeSelect(List<InquiryElement> ies)
{
foreach (InquiryElement ie in ies)
{
//CharacterObject is actually just the "identifier", cast to a different type
CharacterObject _co_ = (CharacterObject)(ie.Identifier);
//We're going to try to use "over-performance", so we want to scrape the
//killcount from the tooltip
int kc = 0;
string killhint = ie.Hint.Split(' ')[0];
if (int.TryParse(killhint, out kc))
{
InformationManager.DisplayMessage(new InformationMessage(_co_.Name + " got " + kc.ToString(), Colors.Red));
}
else
{
kc = -1;
}
//Finally, invoke the hero-creation, and remove the CO from the player party
//We could do the final verification check before, but I'd rather err on
//the side of the player getting a hero they didn't technically earn than
//the player burning a selection accidentally
this.PromoteUnit(_co_, kc);
if (MobileParty.MainParty.MemberRoster.Contains(_co_))
{
MobileParty.MainParty.MemberRoster.RemoveTroop(_co_);
}
}
}
//Third util function -- Simply returns whether a CO is qualified to be
//nominated or not
//Since end-tiers aren't uniform, we have to check if there are any upgrade targets
//for the default branch
public static bool IsSoldierQualified(CharacterObject co)
{
if (co == null)
{
return false;
}
if (PromotionManager.__instance.tier_threshold < 0)
{
if (co.UpgradeTargets == null || co.UpgradeTargets.Length == 0)
{
return true;
}
}
else
{
if (co.Tier >= PromotionManager.__instance.tier_threshold)
{
return true;
}
}
return false;
}
//Fourth util function -- Get a name from the provided external namelist
//It's technically bad form to run file IO each time, but this way
//the namelist can be modified on the fly, while the game is running
public string GetNameFromExternalFile()
{
string o = "ABORT"; //fuck-up code
if (File.Exists(extern_namelist))
{
try
{
string[] lines = System.IO.File.ReadAllLines(extern_namelist);
if (lines.Length == 0)
return "ABORT";
int sel_index = MBRandom.RandomInt(0, lines.Length);
o = lines[sel_index];
string new_text = "";
for (int i = 0; i < lines.Length; i++)
{
if (i != sel_index)
{
new_text += lines[i] + "\n";
}
}
File.WriteAllText(extern_namelist, new_text);
return o;
}
catch (Exception e)
{
InformationManager.DisplayMessage(new InformationMessage(e.Message, Colors.Red));
return "ABORT";
}
}
else
{
return "ABORT";
}
}
//Fifth util function -- Get a name suffix from the mod's internal list
//We build up a list of valid suffices and randomly pick from them at the end
//while it's relatively incomplete, it can be fleshed out pretty easily
//
//TODO would probably be to make this use file IO, and maybe XML parsing to
//allow external lists
public string GetNameSuffix(CharacterObject co)
{
List<string> potential_suff = new List<string>();
if (co.IsRanged)
{
potential_suff.AppendList(NameList.archer_suff);
}
else if (!co.FirstBattleEquipment[EquipmentIndex.Horse].IsEmpty)// || co.IsMounted)
{
potential_suff.AppendList(NameList.cavalry_suff);
}
else
{
potential_suff.AppendList(NameList.infantry_suff);
}
switch (co.Culture.GetCultureCode())
{
case CultureCode.Aserai:
potential_suff.AppendList(NameList.aserai_suff);
break;
case CultureCode.Battania:
potential_suff.AppendList(NameList.battanian_suff);
break;
case CultureCode.Khuzait:
potential_suff.AppendList(NameList.khuzait_suff);
break;
case CultureCode.Sturgia:
potential_suff.AppendList(NameList.sturgian_suff);
break;
case CultureCode.Nord:
potential_suff.AppendList(NameList.sturgian_suff);
break;
case CultureCode.Vakken:
potential_suff.AppendList(NameList.sturgian_suff);
break;
case CultureCode.Darshi:
potential_suff.AppendList(NameList.aserai_suff);
break;
case CultureCode.Vlandia:
potential_suff.AppendList(NameList.vlandian_suff);
break;
default:
break;
}
if (co.Culture.StringId == "criminals")
{
potential_suff.Clear();
potential_suff.AppendList(NameList.criminal_suff);
}
else if (co.Culture.StringId == "criminals2")
{
potential_suff.Clear();
potential_suff.AppendList(NameList.criminal2_suff);
}
else if (co.Culture.StringId == "criminals3")
{
potential_suff.Clear();
potential_suff.AppendList(NameList.criminal3_suff);
}
else if (co.Culture.StringId == "pirates")
{
potential_suff.Clear();
potential_suff.AppendList(NameList.pirates_suff);
}
else if (co.Culture.StringId == "shift_sand")
{
potential_suff.Clear();
potential_suff.AppendList(NameList.shiftingsands_suff);
}
return potential_suff[MBRandom.RandomInt(potential_suff.Count)];
}
//Sixth util function -- Take a hero and nerf all their equipment
//by applying the game's "companion" modifier
public void AdjustEquipment(Hero _h)
{
Equipment eq = _h.BattleEquipment;
ItemModifier itemModifier1 = MBObjectManager.Instance.GetObject<ItemModifier>("companion_armor");
ItemModifier itemModifier2 = MBObjectManager.Instance.GetObject<ItemModifier>("companion_weapon");
ItemModifier itemModifier3 = MBObjectManager.Instance.GetObject<ItemModifier>("companion_horse");
for (EquipmentIndex index = EquipmentIndex.WeaponItemBeginSlot; index < EquipmentIndex.NumEquipmentSetSlots; ++index)
{
EquipmentElement equipmentElement = eq[index];
if (equipmentElement.Item != null)
{
if (equipmentElement.Item.ArmorComponent != null)
eq[index] = new EquipmentElement(equipmentElement.Item, itemModifier1);
else if (equipmentElement.Item.HorseComponent != null)
eq[index] = new EquipmentElement(equipmentElement.Item, itemModifier3);
else if (equipmentElement.Item.WeaponComponent != null)
eq[index] = new EquipmentElement(equipmentElement.Item, itemModifier2);
}
}
}
//Seventh util function -- add variance to the game's main RPG traits
//Making a hero with a "reputation" that we could potentially use
//in the future for inter-companion (and inter-lord) conflict
public void AddTraitVariance(Hero hero)
{
foreach (TraitObject trait in TraitObject.All)
{
if (trait == DefaultTraits.Honor || trait == DefaultTraits.Mercy || (trait == DefaultTraits.Generosity || trait == DefaultTraits.Valor) || trait == DefaultTraits.Calculating)
{
int num1 = hero.CharacterObject.GetTraitLevel(trait);
float num2 = MBRandom.RandomFloat;
//skew towards player's traits
if (Hero.MainHero.GetTraitLevel(trait) >= 0.9)
{
num2 *= 1.2f;
}
if ((double)num2 < 0.1)
{
--num1;
if (num1 < -1)
num1 = -1;
}
if ((double)num2 > 0.9)
{
++num1;
if (num1 > 1)
num1 = 1;
}
int num3 = MBMath.ClampInt(num1, trait.MinValue, trait.MaxValue);
hero.SetTraitLevel(trait, num3);
}
}
}
//Here's the primary function for this mod--
//it takes in a CharacterObject, kill count, and option for player selection of skills
//and creates a hero from that CO, tweaks that hero's skills and attributes,
//and adds them to the player's party
public void PromoteUnit(CharacterObject co, int kills = -1, bool pick_skills = true)
{
//Basic check against whether the CO exists
CharacterObject nco = Game.Current.ObjectManager.GetObject<CharacterObject>(co.StringId);
co = nco;
if (co == null)
{
return;
}
//This set of functions attempts to populate the Hero template we want to mold into the input CharacterObject
//We first start with more stringent criteria (e.g. first check against the Culture's wanderer templates),
//and if all of that has fallen through, we'll just take anything at all that matches the male/female
CharacterObject wanderer = co.Culture.NotableAndWandererTemplates.GetRandomElementWithPredicate<CharacterObject>((Func<CharacterObject, bool>)(x => x.Occupation == Occupation.Wanderer && x.IsFemale == co.IsFemale && x.CivilianEquipments != null));
if (wanderer == null)
{
if (!ignore_cautions)
InformationManager.DisplayMessage(new InformationMessage("CAUTION: No wanderer template with culture " + co.Culture.Name + " available.\nChoosing randomly instead.", Colors.Yellow));
wanderer = CharacterObject.PlayerCharacter.Culture.NotableAndWandererTemplates.GetRandomElementWithPredicate<CharacterObject>((Func<CharacterObject, bool>)(x => x.Occupation == Occupation.Wanderer && x.IsFemale == co.IsFemale && x.CivilianEquipments != null));
}
//final fallback...
if (wanderer == null)
{
if (!ignore_cautions)
InformationManager.DisplayMessage(new InformationMessage("WARNING: SOMETHING ACTUALLY WENT WRONG WITH WANDERER TEMPLATES\nPICKING COMPLETELY RANDOMLY", Colors.Red));
wanderer = CharacterObject.PlayerCharacter.Culture.NotableAndWandererTemplates.GetRandomElementWithPredicate<CharacterObject>((Func<CharacterObject, bool>)(x => x.Occupation == Occupation.Wanderer && x.IsFemale == co.IsFemale));
}
if (wanderer == null)
{
InformationManager.DisplayMessage(new InformationMessage("WARNING: Could not find valid wanderer template. You broke something.", Colors.Red));
return;
}
//Past this point, we've populated the wanderer template, so we can actually create the hero
//Using some of the game's in-built hero creation tools
Hero specialHero = HeroCreator.CreateSpecialHero(wanderer, (Settlement)null, (Clan)null, (Clan)null, rand.Next(20, 50));
bool external_name_successful = false;
string new_name = "ABORT";
if (using_extern_namelist)
{
new_name = GetNameFromExternalFile();
if (new_name.Equals("ABORT"))
{
external_name_successful = false;
}
else
{
external_name_successful = true;
specialHero.SetName(new TextObject(new_name + GetNameSuffix(co)), new TextObject(new_name));
}
}
if (!using_extern_namelist || !external_name_successful)
{
specialHero.SetName(new TextObject(specialHero.FirstName.ToString() + GetNameSuffix(co)), specialHero.FirstName);
}
specialHero.Culture = co.Culture;
//Default formation class seems to be read only, so I could't change it
specialHero.CharacterObject.DefaultFormationGroup = co.DefaultFormationGroup;
specialHero.ChangeState(Hero.CharacterStates.Active);
AddCompanionAction.Apply(Clan.PlayerClan, specialHero);
AddHeroToPartyAction.Apply(specialHero, MobileParty.MainParty, true);
CampaignEventDispatcher.Instance.OnHeroCreated(specialHero, false);
AddTraitVariance(specialHero);
float adjusted_cost = this.up_front_cost;
//GI gives 30% discount
if (Hero.MainHero.GetPerkValue(DefaultPerks.Trade.GreatInvestor))
{
adjusted_cost *= 0.7f;
}
//PiP gives 25% discount
if (Hero.MainHero.GetPerkValue(DefaultPerks.Steward.PaidInPromise))
{
adjusted_cost *= 0.75f;
}
GiveGoldAction.ApplyBetweenCharacters(Hero.MainHero, specialHero, (int)adjusted_cost);
//Has met is now read only. So we're using setHasMet
specialHero.SetHasMet();
//special, equipment-formatting try-catch statement
try
{
if (MyLittleWarbandLoaded)
{
InformationManager.DisplayMessage(new InformationMessage("MLBW compatibility mode:\nUsing first equipment sets", Colors.Yellow));
specialHero.BattleEquipment.FillFrom(co.FirstBattleEquipment);
specialHero.CivilianEquipment.FillFrom(co.FirstCivilianEquipment);
}
else
{
specialHero.BattleEquipment.FillFrom(co.RandomBattleEquipment);
specialHero.CivilianEquipment.FillFrom(co.RandomCivilianEquipment);
}
this.AdjustEquipment(specialHero);
}
catch (Exception e)
{
if (!ignore_cautions)
{
InformationManager.DisplayMessage(new InformationMessage("CAUTION: Something went wrong with this unit's equipment.\nAborting process, not everything might be added properly.", Colors.Yellow));
Debug.Print("Equipment format issue, providing default equipment instead! Exception details:\n" + e.Message);
}
//leave them naked, alone, and afraid
}
specialHero.HeroDeveloper.SetInitialLevel(co.Level);
Dictionary<SkillObject, int> baseline_skills = new Dictionary<SkillObject, int>();
foreach (SkillObject sk in Skills.All)
{
baseline_skills[sk] = Math.Min(co.GetSkillValue(sk), 300);
//specialHero.HeroDeveloper.SetInitialSkillLevel(sk, co.GetSkillValue(sk));
}
int curr_sk = 0;
foreach (SkillObject sk in Skills.All)
{
curr_sk = specialHero.GetSkillValue(sk);
specialHero.HeroDeveloper.ChangeSkillLevel(sk, baseline_skills[sk] - curr_sk);
}
int skp_to_assign = base_additional_skill_points + 50 * Hero.MainHero.GetSkillValue(DefaultSkills.Leadership) / leadership_points_per_50_extra_skill_points;
if (kills > 0)
{
if (co.IsMounted)
{
skp_to_assign += PromotionManager.__instance.skp_per_excess_kill * (kills - PromotionManager.__instance.cav_kill_threshold);
}
else if (co.IsRanged)
{
skp_to_assign += PromotionManager.__instance.skp_per_excess_kill * (kills - PromotionManager.__instance.ran_kill_threshold);
}
else
{
skp_to_assign += PromotionManager.__instance.skp_per_excess_kill * (kills - PromotionManager.__instance.inf_kill_threshold);
}
}
if (!select_skills_randomly)
{
for (int i = 1; i <= num_skill_rounds; i++)
{
AssignSkills(specialHero, skp_to_assign / i, this.num_skill_bonuses, "Round " + i.ToString(), co.Name.ToString());
}
}
else
{
for (int i = 1; i <= num_skill_rounds; i++)
{
AssignSkillsRandomly(specialHero, skp_to_assign / i, this.num_skill_bonuses);
}
}
int tot_to_add = specialHero.HeroDeveloper.UnspentAttributePoints;
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Vigor, 2, false);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Control, 2, false);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Cunning, 2, false);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Endurance, 2, false);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Intelligence, 2, false);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Social, 2, false);
tot_to_add -= 12;
int to_add = 0;
if (tot_to_add > 0)
{
if (co.IsMounted)
{
to_add = rand.Next(3);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Endurance, to_add, false);
tot_to_add -= to_add;
}
else if (co.IsRanged)
{
to_add = rand.Next(3);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Control, to_add, false);
tot_to_add -= to_add;
}
else
{
to_add = rand.Next(3);
specialHero.HeroDeveloper.AddAttribute(DefaultCharacterAttributes.Vigor, to_add, false);
tot_to_add -= to_add;
}
List<CharacterAttribute> shuffled_attrs = new List<CharacterAttribute>(Attributes.All);
Shuffle(shuffled_attrs);
foreach (CharacterAttribute ca in shuffled_attrs)
{
to_add = rand.Next(2);
specialHero.HeroDeveloper.AddAttribute(ca, to_add, false);
tot_to_add -= to_add;
if (tot_to_add <= 0)
break;
}
}
if (this.fill_perks)
{
CharacterDevelopmentCampaignBehavior cdcb = CharacterDevelopmentCampaignBehavior.GetCampaignBehavior<CharacterDevelopmentCampaignBehavior>();
if (cdcb != null)
cdcb.DevelopCharacterStats(specialHero);
}
specialHero.HeroDeveloper.UnspentAttributePoints = 0;
}
//Eighth and Ninth Util functions -- Assign skills to the nascent hero
//either through player selection, or randomly
//For player selection, we create inquiry elements for each "soft" skill,
//and allow the player to choose several to give a skill bump to
//Randomly, we replace player choice with a switch statement
//
//We also cap out at 300 to avoid... Problems...
public void AssignSkills(Hero specialHero, int skill_points_to_assign, int num_skills_to_select, string title_prefix, string prev = " basic soldier")
{
//Add options only if points can be added to them in a valid way
List<InquiryElement> iqes = new List<InquiryElement>();
if (specialHero.GetSkillValue(DefaultSkills.Scouting) < 300)
iqes.Add(new InquiryElement("scout_bonus", "Ranged with the Scouts", null, true, "+" + skill_points_to_assign.ToString() + " scouting"));
if (specialHero.GetSkillValue(DefaultSkills.Crafting) < 300)
iqes.Add(new InquiryElement("smithing_bonus", "Repaired the party's weapons", null, true, "+" + skill_points_to_assign.ToString() + " smithing"));
if (specialHero.GetSkillValue(DefaultSkills.Athletics) < 300)
iqes.Add(new InquiryElement("athletics_bonus", "Trained for combat", null, true, "+" + skill_points_to_assign.ToString() + " athletics"));
if (specialHero.GetSkillValue(DefaultSkills.Riding) < 300)
iqes.Add(new InquiryElement("riding_bonus", "Rode horses", null, true, "+" + skill_points_to_assign.ToString() + " riding"));
if (specialHero.GetSkillValue(DefaultSkills.Tactics) < 300)
iqes.Add(new InquiryElement("tactics_bonus", "Studied past battles", null, true, "+" + skill_points_to_assign.ToString() + " tactics"));
if (specialHero.GetSkillValue(DefaultSkills.Roguery) < 300)
iqes.Add(new InquiryElement("roguery_bonus", "Sold loot in the black market", null, true, "+" + skill_points_to_assign.ToString() + " roguery"));
if (specialHero.GetSkillValue(DefaultSkills.Charm) < 300)
iqes.Add(new InquiryElement("charm_bonus", "Chatted everyone up", null, true, "+" + skill_points_to_assign.ToString() + " charm"));
if (specialHero.GetSkillValue(DefaultSkills.Leadership) < 300)
iqes.Add(new InquiryElement("leadership_bonus", "Organized duties for the party", null, true, "+" + skill_points_to_assign.ToString() + " leadership"));
if (specialHero.GetSkillValue(DefaultSkills.Trade) < 300)
iqes.Add(new InquiryElement("trade_bonus", "Bought and sold items from towns you visited", null, true, "+" + skill_points_to_assign.ToString() + " trade"));
if (specialHero.GetSkillValue(DefaultSkills.Steward) < 300)
iqes.Add(new InquiryElement("steward_bonus", "Helped handle the party's accounts", null, true, "+" + skill_points_to_assign.ToString() + " stewardship"));
if (specialHero.GetSkillValue(DefaultSkills.Medicine) < 300)
iqes.Add(new InquiryElement("medicine_bonus", "Helped as a medic", null, true, "+" + skill_points_to_assign.ToString() + " medicine"));
if (specialHero.GetSkillValue(DefaultSkills.Engineering) < 300)
iqes.Add(new InquiryElement("engineering_bonus", "Helped construct and take down the camp", null, true, "+" + skill_points_to_assign.ToString() + " engineering"));
MultiSelectionInquiryData msid = new MultiSelectionInquiryData("Select " + title_prefix + " Skill Focuses", specialHero.Name + " (previously a " + prev + ") has finally found glory on the battlefield. Before this, and besides training for battle they...", iqes, true, num_skills_to_select, "Accept", "Refuse", (Action<List<InquiryElement>>)((List<InquiryElement> ies) =>
{
int diff = 0;
foreach (InquiryElement ie in ies)
{
switch ((string)ie.Identifier)
{
case "scout_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Scouting); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Scouting, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "smithing_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Crafting); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Crafting, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "athletics_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Athletics); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Athletics, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "riding_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Riding); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Riding, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "tactics_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Tactics); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Tactics, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "roguery_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Roguery); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Roguery, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "charm_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Charm); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Charm, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "leadership_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Leadership); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Leadership, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "trade_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Trade); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Trade, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "steward_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Steward); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Steward, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "medicine_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Medicine); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Medicine, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
case "engineering_bonus":
try
{
diff = 300 - specialHero.GetSkillValue(DefaultSkills.Engineering); //cap out at 300
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Engineering, Math.Min(skill_points_to_assign, diff));
}
catch { }
break;
}
try
{
specialHero.HeroDeveloper.CheckInitialLevel();
}
catch (Exception e)
{
if (!ignore_cautions)
InformationManager.DisplayMessage(new InformationMessage("CAUTION: Reflection call to CheckInitialLevel failed. Potential version issue.", Colors.Yellow));
}
}
}),
(Action<List<InquiryElement>>)null);
MBInformationManager.ShowMultiSelectionInquiry(msid, true);
//InformationManager.ShowMultiSelectionInquiry(msid, true);
}
public void AssignSkillsRandomly(Hero specialHero, int skill_points_to_assign, int num_skills_to_select)
{
for (int i = 0; i < num_skills_to_select; i++)
{
switch ((int)(MBRandom.RandomFloat * 12) + 1)
{
case 1:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Scouting, skill_points_to_assign);
}
catch { }
break;
case 2:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Crafting, skill_points_to_assign);
}
catch { }
break;
case 3:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Athletics, skill_points_to_assign);
}
catch { }
break;
case 4:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Riding, skill_points_to_assign);
}
catch { }
break;
case 5:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Tactics, skill_points_to_assign);
}
catch { }
break;
case 6:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Roguery, skill_points_to_assign);
}
catch { }
break;
case 7:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Charm, skill_points_to_assign);
}
catch { }
break;
case 8:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Leadership, skill_points_to_assign);
}
catch { }
break;
case 9:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Trade, skill_points_to_assign);
}
catch { }
break;
case 10:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Steward, skill_points_to_assign);
}
catch { }
break;
case 11:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Medicine, skill_points_to_assign);
}
catch { }
break;
case 12:
try
{
specialHero.HeroDeveloper.ChangeSkillLevel(DefaultSkills.Engineering, skill_points_to_assign);
}
catch { }
break;
}
}
int diff = 0;
foreach (SkillObject sk in Skills.All)
{
diff = 300 - specialHero.GetSkillValue(sk);
if (diff < 0)
{
specialHero.HeroDeveloper.ChangeSkillLevel(sk, diff); //subtract
}
}
try
{
specialHero.HeroDeveloper.CheckInitialLevel();
}
catch (Exception e)
{
if (!ignore_cautions)
InformationManager.DisplayMessage(new InformationMessage("CAUTION: Reflection call to CheckInitialLevel failed. Potential version issue.", Colors.Yellow));
}
}
//Tenth util function -- shuffle a list of any type randomly
public void Shuffle<T>(IList<T> list)
{
int n = list.Count;
while (n > 1)
{
n--;
int k = rand.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
//Setting specific functions, added as event triggers
//Recruit to hero -- if you recruit a qualified unit, turn them into a hero immediately
public void recruit_to_hero(CharacterObject troop, int amount)
{
if (!IsSoldierQualified(troop) || !MobileParty.MainParty.MemberRoster.Contains(troop))
return;
for (int i = 0; i < amount; i++)
{
if (this.respect_companion_limit && Clan.PlayerClan.Companions.Count >= Clan.PlayerClan.CompanionLimit)
{ //stop giving companions if over companion limit and respecting it
MobileParty.MainParty.MemberRoster.RemoveTroop(troop, i);
return;
}
PromotionManager.__instance.PromoteUnit(troop);
}
MobileParty.MainParty.MemberRoster.RemoveTroop(troop, amount);
}
//And finally, for upgraded units
public void upgrade_to_hero(CharacterObject upgradeFromTroop, CharacterObject upgradeToTroop, int number)
{
if (!IsSoldierQualified(upgradeToTroop))
return;
for (int i = 0; i < number; i++)
{
if (this.respect_companion_limit && Clan.PlayerClan.Companions.Count >= Clan.PlayerClan.CompanionLimit)
{ //stop giving companions if over companion limit and respecting it
MobileParty.MainParty.MemberRoster.RemoveTroop(upgradeToTroop, i);
return;
}
PromotionManager.__instance.PromoteUnit(upgradeToTroop);
}
MobileParty.MainParty.MemberRoster.RemoveTroop(upgradeToTroop, number);
}
//Console commands to both test out functionality, and allow players to set up