-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPlayerEntity.cs
2741 lines (2418 loc) · 135 KB
/
PlayerEntity.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
// Project: Daggerfall Tools For Unity
// Copyright: Copyright (C) 2009-2020 Daggerfall Workshop
// Web Site: http://www.dfworkshop.net
// License: MIT License (http://www.opensource.org/licenses/mit-license.php)
// Source Code: https://github.com/Interkarma/daggerfall-unity
// Original Author: Gavin Clayton ([email protected])
// Contributors: Numidium
//
// Notes:
//
using UnityEngine;
using System;
using System.Collections.Generic;
using DaggerfallConnect;
using DaggerfallConnect.Arena2;
using DaggerfallConnect.Save;
using DaggerfallWorkshop.Game.Banking;
using DaggerfallWorkshop.Game.Formulas;
using DaggerfallWorkshop.Game.Items;
using DaggerfallWorkshop.Game.Player;
using DaggerfallWorkshop.Game.UserInterfaceWindows;
using DaggerfallWorkshop.Game.Utility;
using DaggerfallWorkshop.Utility;
using DaggerfallWorkshop.Game.Serialization;
using DaggerfallWorkshop.Game.Guilds;
using DaggerfallWorkshop.Game.MagicAndEffects;
using DaggerfallWorkshop.Game.MagicAndEffects.MagicEffects;
using DaggerfallWorkshop.Game.Questing;
using DaggerfallWorkshop.Utility.AssetInjection;
namespace DaggerfallWorkshop.Game.Entity
{
/// <summary>
/// Implements DaggerfallEntity with properties specific to a Player.
/// </summary>
public class PlayerEntity : DaggerfallEntity
{
#region Fields
public const string vampireSpellTag = "vampire";
public const string lycanthropySpellTag = "lycanthrope";
bool godMode = false;
bool noTargetMode = false;
bool preventEnemySpawns = false;
bool preventNormalizingReputations = false;
bool isResting = false;
//bool hasStartedInitialVampireQuest = false;
//byte vampireClan = 0;
//uint lastTimeVampireNeedToKillSatiated = 0;
const int testPlayerLevel = 1;
const string testPlayerName = "Nameless";
protected RaceTemplate raceTemplate;
protected int faceIndex;
protected PlayerReflexes reflexes;
protected ItemCollection wagonItems = new ItemCollection();
protected ItemCollection otherItems = new ItemCollection();
protected DaggerfallUnityItem lightSource;
protected int goldPieces = 0;
protected PersistentFactionData factionData = new PersistentFactionData();
protected PersistentGlobalVars globalVars = new PersistentGlobalVars();
protected PlayerNotebook notebook = new PlayerNotebook();
protected short[] skillUses;
protected uint[] skillsRecentlyRaised = new uint[2];
protected uint timeOfLastSkillIncreaseCheck = 0;
protected uint timeOfLastSkillTraining = 0;
protected uint timeOfLastStealthCheck = 0;
protected int startingLevelUpSkillSum = 0;
protected int currentLevelUpSkillSum = 0;
protected bool readyToLevelUp = false;
protected bool oghmaLevelUp = false;
protected short[] sGroupReputations = new short[11];
protected int biographyResistDiseaseMod = 0;
protected int biographyResistMagicMod = 0;
protected int biographyAvoidHitMod = 0;
protected int biographyResistPoisonMod = 0;
protected int biographyFatigueMod = 0;
protected int biographyReactionMod = 0;
protected uint timeForThievesGuildLetter = 0;
protected uint timeForDarkBrotherhoodLetter = 0;
protected int thievesGuildRequirementTally = 0;
protected int darkBrotherhoodRequirementTally = 0;
private const int InviteSent = 100;
protected uint timeToBecomeVampireOrWerebeast = 0;
protected uint lastTimePlayerAteOrDrankAtTavern = 0;
protected RegionDataRecord[] regionData = new RegionDataRecord[62];
protected Crimes crimeCommitted = 0;
protected bool haveShownSurrenderToGuardsDialogue = false;
protected bool arrested = false;
protected short halfOfLegalRepPlayerLostFromCrime = 0;
private List<RoomRental_v1> rentedRooms = new List<RoomRental_v1>();
// Fatigue loss per in-game minute
public const int DefaultFatigueLoss = 11;
public const int ClimbingFatigueLoss = 22;
public const int RunningFatigueLoss = 88;
public const int SwimmingFatigueLoss = 44;
public const int JumpingFatigueLoss = 11;
private int breathUpdateTally = 0;
private int runningTallyCounter = 0;
private float guardsArriveCountdown = 0;
DaggerfallLocation guardsArriveCountdownLocation;
private bool CheckedCurrentJump = false;
PlayerMotor playerMotor = null;
ClimbingMotor climbingMotor = null;
protected uint lastGameMinutes = 0; // Being tracked in order to perform updates based on changes in the current game minute
private bool gameStarted = false;
bool displayingExhaustedPopup = false;
const int socialGroupCount = 11;
int[] reactionMods = new int[socialGroupCount]; // Indices map to FactionFile.SocialGroups 0-10 - do not serialize, set by live effects
bool enemyAlertActive = false;
uint lastEnemyAlertTime;
// Player-only constant effects
// Note: These properties are intentionally not serialized. They should only be set by live effects.
public bool IsAzurasStarEquipped { get; set; }
#endregion
#region Properties
public bool GodMode { get { return godMode; } set { godMode = value; } }
public bool NoTargetMode { get { return noTargetMode; } set { noTargetMode = value; } }
public bool PreventEnemySpawns { get { return preventEnemySpawns; } set { preventEnemySpawns = value; } }
public bool PreventNormalizingReputations { get { return preventNormalizingReputations; } set { preventNormalizingReputations = value; } }
public bool IsResting { get { return isResting; } set { isResting = value; } }
public Races Race { get { return (Races)RaceTemplate.ID; } }
public RaceTemplate RaceTemplate { get { return GetLiveRaceTemplate(); } }
public RaceTemplate BirthRaceTemplate { get { return raceTemplate; } set { raceTemplate = value; } }
public int FaceIndex { get { return faceIndex; } set { faceIndex = value; } }
public PlayerReflexes Reflexes { get { return reflexes; } set { reflexes = value; } }
public ItemCollection WagonItems { get { return wagonItems; } set { wagonItems.ReplaceAll(value); } }
public ItemCollection OtherItems { get { return otherItems; } set { otherItems.ReplaceAll(value); } }
public DaggerfallUnityItem LightSource { get { return lightSource; } set { lightSource = value; } }
public int GoldPieces { get { return goldPieces; } set { goldPieces = value; } }
public PersistentFactionData FactionData { get { return factionData; } }
public PersistentGlobalVars GlobalVars { get { return globalVars; } }
public PlayerNotebook Notebook { get { return notebook; } }
public short[] SkillUses { get { return skillUses; } set { skillUses = value; } }
public uint[] SkillsRecentlyRaised { get { return skillsRecentlyRaised; } set { skillsRecentlyRaised = value; } }
public uint TimeOfLastSkillIncreaseCheck { get { return timeOfLastSkillIncreaseCheck; } set { timeOfLastSkillIncreaseCheck = value; } }
public uint TimeOfLastSkillTraining { get { return timeOfLastSkillTraining; } set { timeOfLastSkillTraining = value; } }
public uint TimeOfLastStealthCheck { get { return timeOfLastStealthCheck; } set { timeOfLastStealthCheck = value; } }
public int StartingLevelUpSkillSum { get { return startingLevelUpSkillSum; } set { startingLevelUpSkillSum = value; } }
public int CurrentLevelUpSkillSum { get { return currentLevelUpSkillSum; } internal set { currentLevelUpSkillSum = value; } }
public bool ReadyToLevelUp { get { return readyToLevelUp; } set { readyToLevelUp = value; } }
public bool OghmaLevelUp { get { return oghmaLevelUp; } set { oghmaLevelUp = value; } }
public short[] SGroupReputations { get { return sGroupReputations; } set { sGroupReputations = value; } }
public int BiographyResistDiseaseMod { get { return biographyResistDiseaseMod; } set { biographyResistDiseaseMod = value; } }
public int BiographyResistMagicMod { get { return biographyResistMagicMod; } set { biographyResistMagicMod = value; } }
public int BiographyAvoidHitMod { get { return biographyAvoidHitMod; } set { biographyAvoidHitMod = value; } }
public int BiographyResistPoisonMod { get { return biographyResistPoisonMod; } set { biographyResistPoisonMod = value; } }
public int BiographyFatigueMod { get { return biographyFatigueMod; } set { biographyFatigueMod = value; } }
public int BiographyReactionMod { get { return biographyReactionMod; } set { biographyReactionMod = value; } }
public uint TimeForThievesGuildLetter { get { return timeForThievesGuildLetter; } set { timeForThievesGuildLetter = value; } }
public uint TimeForDarkBrotherhoodLetter { get { return timeForDarkBrotherhoodLetter; } set { timeForDarkBrotherhoodLetter = value; } }
public int ThievesGuildRequirementTally { get { return thievesGuildRequirementTally; } set { thievesGuildRequirementTally = value; } }
public int DarkBrotherhoodRequirementTally { get { return darkBrotherhoodRequirementTally; } set { darkBrotherhoodRequirementTally = value; } }
public uint TimeToBecomeVampireOrWerebeast { get { return timeToBecomeVampireOrWerebeast; } set { timeToBecomeVampireOrWerebeast = value; } }
public uint LastTimePlayerAteOrDrankAtTavern { get { return lastTimePlayerAteOrDrankAtTavern; } set { lastTimePlayerAteOrDrankAtTavern = value; } }
public float CarriedWeight { get { return Items.GetWeight() + (goldPieces * DaggerfallBankManager.goldUnitWeightInKg); } }
public float WagonWeight { get { return WagonItems.GetWeight(); } }
public RegionDataRecord[] RegionData { get { return regionData; } set { regionData = value; } }
public uint LastGameMinutes { get { return lastGameMinutes; } set { lastGameMinutes = value; } }
public List<RoomRental_v1> RentedRooms { get { return rentedRooms; } set { rentedRooms = value; } }
public Crimes CrimeCommitted { get { return crimeCommitted; } set { SetCrimeCommitted(value); } }
public bool HaveShownSurrenderToGuardsDialogue { get { return haveShownSurrenderToGuardsDialogue; } set { haveShownSurrenderToGuardsDialogue = value; } }
public bool Arrested { get { return arrested; } set { arrested = value; } }
public bool InPrison { get ; set ; }
public bool IsInBeastForm { get; set; }
public List<string> BackStory { get; set; }
public VampireClans PreviousVampireClan { get; set; }
public bool EnemyAlertActive { get { return enemyAlertActive; } }
public int DaedraSummonDay { get; set; }
public int DaedraSummonIndex { get; set; }
#endregion
#region Constructors
public PlayerEntity(DaggerfallEntityBehaviour entityBehaviour)
: base(entityBehaviour)
{
StartGameBehaviour.OnNewGame += StartGameBehaviour_OnNewGame;
OnExhausted += PlayerEntity_OnExhausted;
}
#endregion
#region Public Methods
public bool GetSkillRecentlyIncreased(DFCareer.Skills skill)
{
return (skillsRecentlyRaised[(int)skill / 32] & (1 << ((int)skill % 32))) != 0;
}
public void SetSkillRecentlyIncreased(int index)
{
skillsRecentlyRaised[index / 32] |= (uint)(1 << (index % 32));
}
public void ResetSkillsRecentlyRaised()
{
Array.Clear(skillsRecentlyRaised, 0, 2);
}
public RaceTemplate GetLiveRaceTemplate()
{
// Look for racial override effect
RacialOverrideEffect racialOverrideEffect = GameManager.Instance.PlayerEffectManager.GetRacialOverrideEffect();
if (racialOverrideEffect != null)
return racialOverrideEffect.CustomRace;
else
return raceTemplate;
}
public RoomRental_v1 GetRentedRoom(int mapId, int buildingKey)
{
foreach (RoomRental_v1 room in rentedRooms)
if (room.mapID == mapId && room.buildingKey == buildingKey)
return room;
return null;
}
public List<RoomRental_v1> GetRentedRooms(int mapId)
{
return rentedRooms.FindAll(r => r.mapID == mapId);
}
public void RemoveExpiredRentedRooms()
{
rentedRooms.RemoveAll(r => {
if (GetRemainingHours(r) < 1) {
SaveLoadManager.StateManager.RemovePermanentScene(DaggerfallInterior.GetSceneName(r.mapID, r.buildingKey));
return true;
} else
return false;
});
}
public static int GetRemainingHours(RoomRental_v1 room)
{
if (room == null)
return -1;
double remainingSecs = (double)(room.expiryTime - DaggerfallUnity.Instance.WorldTime.Now.ToSeconds());
return (int)Math.Ceiling((remainingSecs / DaggerfallDateTime.SecondsPerHour));
}
public void ChangeReactionMod(FactionFile.SocialGroups socialGroup, int amount)
{
int index = (int)socialGroup;
if (index >= 0 && index < reactionMods.Length)
reactionMods[index] += amount;
}
public int GetReactionMod(FactionFile.SocialGroups socialGroup)
{
int index = (int)socialGroup;
if (index >= 0 && index < reactionMods.Length)
return reactionMods[index];
else
return 0;
}
/// <summary>
/// Enemy alert is raised by hostile enemies or attempting to rest near hostile enemies.
/// Enemy alert is lowered when killing a hostile enemy or after some time has passed.
/// </summary>
public void SetEnemyAlert(bool alert)
{
enemyAlertActive = alert;
if (alert)
lastEnemyAlertTime = DaggerfallUnity.Instance.WorldTime.DaggerfallDateTime.ToClassicDaggerfallTime();
}
public override void FixedUpdate()
{
// Handle events that are called by classic's update loop
if (GameManager.ClassicUpdate && playerMotor)
{
// Tally running skill. Running tallies so quickly in classic that it might be a bug or oversight.
// Here we use a rate of 1/4 that observed for classic.
if (playerMotor.IsRunning && !playerMotor.IsRiding)
{
if (runningTallyCounter == 3)
{
TallySkill(DFCareer.Skills.Running, 1);
runningTallyCounter = 0;
}
else
runningTallyCounter++;
}
// Handle breath when underwater and not water breathing
if (GameManager.Instance.PlayerEnterExit.IsPlayerSubmerged && !GameManager.Instance.PlayerEntity.IsWaterBreathing)
{
if (currentBreath == 0)
{
currentBreath = GameManager.Instance.GuildManager.DeepBreath(MaxBreath);
}
if (breathUpdateTally > 18)
{
--currentBreath;
if (Race == Races.Argonian && (UnityEngine.Random.Range(0, 2) == 1))
++currentBreath;
breathUpdateTally = 0;
}
else
++breathUpdateTally;
if (currentBreath <= 0)
SetHealth(0);
}
else
currentBreath = 0;
}
}
public override void Update(DaggerfallEntityBehaviour sender)
{
if (SaveLoadManager.Instance.LoadInProgress)
return;
if (CurrentHealth <= 0)
return;
if (guardsArriveCountdown > 0)
{
guardsArriveCountdown -= Time.deltaTime;
if (guardsArriveCountdown <= 0 && guardsArriveCountdownLocation == GameManager.Instance.StreamingWorld.CurrentPlayerLocationObject)
SpawnCityGuards(true);
}
if (playerMotor == null)
playerMotor = GameManager.Instance.PlayerMotor;
if (climbingMotor == null)
climbingMotor = GameManager.Instance.ClimbingMotor;
uint gameMinutes = DaggerfallUnity.Instance.WorldTime.DaggerfallDateTime.ToClassicDaggerfallTime();
if (gameMinutes < lastGameMinutes)
{
throw new Exception(string.Format("lastGameMinutes {0} greater than gameMinutes: {1}", lastGameMinutes, gameMinutes));
}
// Wait until game has started and the game time has been set.
// If the game time is taken before then "30" is returned, which causes an initial player fatigue loss
// after loading or starting a game with a non-30 minute.
if (!gameStarted && !GameManager.Instance.StateManager.GameInProgress)
return;
else if (!gameStarted)
gameStarted = true;
// Lower active enemy alert if more than 8 hours have passed since alert was raised
const int alertDecayMinutes = 8 * DaggerfallDateTime.MinutesPerHour;
if (enemyAlertActive && (gameMinutes - lastEnemyAlertTime) > alertDecayMinutes)
SetEnemyAlert(false);
if (playerMotor != null)
{
// Values are < 1 so fatigue loss is slower
const float atleticismMultiplier = 0.9f;
const float improvedAtleticismMultiplier = 0.8f;
// UESP describes Athleticism relating to fatigue/stamina as "decreases slower when running, jumping, climbing, and swimming."
// https://en.uesp.net/wiki/Daggerfall:ClassMaker#Special_Advantages
// In this implementation, players with athleticism will lose fatigue 10% slower, otherwise at normal rate
// If player also has improved athleticism enchantment, they will lose fatigue 20% slower
// TODO: Determine actual improvement multiplier to fatigue loss, possibly move to FormulaHelper
float fatigueLossMultiplier = 1.0f;
if (career.Athleticism)
fatigueLossMultiplier = (ImprovedAthleticism) ? improvedAtleticismMultiplier : atleticismMultiplier;
// Apply per-minute events
if (lastGameMinutes != gameMinutes)
{
// Apply fatigue loss to the player
int amount = (int)(DefaultFatigueLoss * fatigueLossMultiplier);
if (climbingMotor != null && climbingMotor.IsClimbing)
amount = (int)(ClimbingFatigueLoss * fatigueLossMultiplier);
else if (playerMotor.IsRunning && !playerMotor.IsStandingStill)
amount = (int)(RunningFatigueLoss * fatigueLossMultiplier);
else if (GameManager.Instance.PlayerEnterExit.IsPlayerSwimming)
{
if (Race != Races.Argonian && Dice100.FailedRoll(Skills.GetLiveSkillValue(DFCareer.Skills.Swimming)))
amount = (int)(SwimmingFatigueLoss * fatigueLossMultiplier);
TallySkill(DFCareer.Skills.Swimming, 1);
}
if (!isResting)
DecreaseFatigue(amount);
// Make magically-created items that have expired disappear
items.RemoveExpiredItems();
}
// Reduce fatigue when jumping and tally jumping skill
if (!CheckedCurrentJump && playerMotor.IsJumping)
{
DecreaseFatigue((int)(JumpingFatigueLoss * fatigueLossMultiplier));
TallySkill(DFCareer.Skills.Jumping, 1);
CheckedCurrentJump = true;
}
// Reset jump fatigue check when grounded
if (CheckedCurrentJump && !playerMotor.IsJumping)
{
CheckedCurrentJump = false;
}
}
// Adjust regional prices and update climate weathers and diseases whenever the date has changed.
uint lastDay = lastGameMinutes / 1440;
uint currentDay = gameMinutes / 1440;
int daysPast = (int)(currentDay - lastDay);
if (daysPast > 0)
{
FormulaHelper.UpdateRegionalPrices(ref regionData, daysPast);
GameManager.Instance.WeatherManager.SetClimateWeathers();
GameManager.Instance.WeatherManager.UpdateWeatherFromClimateArray = true;
RemoveExpiredRentedRooms();
LoanChecker.CheckOverdueLoans(lastGameMinutes);
}
// Normalize legal reputation and update faction power and regional conditions every certain number of days
uint minutesPassed = gameMinutes - lastGameMinutes;
for (int i = 0; i < minutesPassed; ++i)
{
// Normalize legal reputations towards 0
if (((i + lastGameMinutes) % 161280) == 0 && !preventNormalizingReputations) // 112 days
NormalizeReputations();
// Update faction powers
if (((i + lastGameMinutes) % 10080) == 0) // 7 days
RegionPowerAndConditionsUpdate(false);
// Update regional conditions
// In classic this version of the function is called within the conditional for the above, when both
// (i + lastGameMinutes) % 10080) == 0 and (i + lastGameMinutes) % 54720) == 0) are true, or every 266 days.
// I'm pretty sure it was supposed to be every 38 days.
if (((i + lastGameMinutes) % 54720) == 0) // 38 days
{
RegionPowerAndConditionsUpdate(true);
StartRacialOverrideQuest(false);
}
if (((i + lastGameMinutes) % 120960) == 0) // 84 days
StartRacialOverrideQuest(true);
}
// Enemy spawns are prevented after time is raised for fast travel, jail time, and vampire transformation
// Classic also prevents enemy spawns during loitering,
// but this seems counterintuitive so it's not implemented in DF Unity for now
if (!preventEnemySpawns)
{
bool updatedGuards = false;
for (uint l = 0; l < (gameMinutes - lastGameMinutes); ++l)
{
// Catch up time and break if something spawns. Don't spawn encounters while player is swimming in water (same as classic).
if (!GameManager.Instance.PlayerEnterExit.IsPlayerSwimming && IntermittentEnemySpawn(l + lastGameMinutes + 1))
break;
// Confirm regionData is available
if (regionData == null || regionData.Length == 0)
break;
// Handle guards appearing for low-legal rep player
int regionIndex = GameManager.Instance.PlayerGPS.CurrentRegionIndex;
if (regionData[regionIndex].LegalRep < -10 && Dice100.SuccessRoll(5))
{
crimeCommitted = Crimes.Criminal_Conspiracy;
SpawnCityGuards(false);
}
// Handle guards appearing for banished player
if ((regionData[regionIndex].SeverePunishmentFlags & 1) != 0 && Dice100.SuccessRoll(10))
{
crimeCommitted = Crimes.Criminal_Conspiracy;
SpawnCityGuards(false);
}
// If enemy guards have been spawned, any new NPC guards should be made into enemyMobiles
if (!updatedGuards)
MakeNPCGuardsIntoEnemiesIfGuardsSpawned();
updatedGuards = true;
}
}
lastGameMinutes = gameMinutes;
// Allow enemy spawns again if they have been disabled
if (preventEnemySpawns)
preventEnemySpawns = false;
// Allow normalizing reputations again if it was disabled
if (preventNormalizingReputations)
preventNormalizingReputations = false;
HandleStartingCrimeGuildQuests();
// Reset surrender to guards dialogue if no guards are nearby
if (haveShownSurrenderToGuardsDialogue && GameManager.Instance.HowManyEnemiesOfType(MobileTypes.Knight_CityWatch, true) == 0)
{
haveShownSurrenderToGuardsDialogue = false;
}
}
void StartRacialOverrideQuest(bool isCureQuest)
{
RacialOverrideEffect racialEffect = GameManager.Instance.PlayerEffectManager.GetRacialOverrideEffect();
if (racialEffect != null)
racialEffect.StartQuest(isCureQuest);
}
public bool IntermittentEnemySpawn(uint Minutes)
{
// Define minimum distance from player based on spawn locations
const int minDungeonDistance = 8;
const int minLocationDistance = 10;
const int minWildernessDistance = 10;
//TODO: if (InOutsideWater)
// return;
// Do not allow spawns if not enough time has passed or spawns are suppressed for any reason
// Note - should not need the preventEnemySpawns check here as IntermittentEnemySpawn() is only called by Update() !preventEnemySpawns
bool timeForSpawn = ((Minutes / 12) % 12) == 0;
if (!timeForSpawn || preventEnemySpawns)
return false;
// Spawns when player is outside
if (!GameManager.Instance.PlayerEnterExit.IsPlayerInside)
{
uint timeOfDay = Minutes % 1440; // 1440 minutes in a day
if (GameManager.Instance.PlayerGPS.IsPlayerInLocationRect)
{
if (timeOfDay < 360 || timeOfDay > 1080)
{
// In a location area at night
if (FormulaHelper.RollRandomSpawn_LocationNight() == 0)
{
GameObjectHelper.CreateFoeSpawner(true, RandomEncounters.ChooseRandomEnemy(false), 1, minLocationDistance);
return true;
}
}
}
else
{
if (timeOfDay >= 360 && timeOfDay <= 1080)
{
// Wilderness during day
if (FormulaHelper.RollRandomSpawn_WildernessDay() != 0)
return false;
}
else
{
// Wilderness at night
if (FormulaHelper.RollRandomSpawn_WildernessNight() != 0)
return false;
}
GameObjectHelper.CreateFoeSpawner(true, RandomEncounters.ChooseRandomEnemy(false), 1, minWildernessDistance);
return true;
}
}
// Spawns when player is inside
if (GameManager.Instance.PlayerEnterExit.IsPlayerInside)
{
// Spawns when player is inside a dungeon
if (GameManager.Instance.PlayerEnterExit.IsPlayerInsideDungeon)
{
if (isResting)
{
if (FormulaHelper.RollRandomSpawn_Dungeon() == 0)
{
// TODO: Not sure how enemy type is chosen here.
GameObjectHelper.CreateFoeSpawner(false, RandomEncounters.ChooseRandomEnemy(false), 1, minDungeonDistance);
return true;
}
}
}
}
return false;
}
// Recreation of guard spawning based on classic
public void SpawnCityGuards(bool immediateSpawn)
{
// Only spawn if player is not in a dungeon, and if there are 10 or fewer existing guards
if (!GameManager.Instance.PlayerEnterExit.IsPlayerInsideDungeon && GameManager.Instance.HowManyEnemiesOfType(MobileTypes.Knight_CityWatch, false, true) <= 10)
{
// Handle indoor guard spawning
if (GameManager.Instance.PlayerEnterExit.IsPlayerInside && GameManager.Instance.PlayerEnterExit.IsPlayerInsideOpenShop)
{
Vector3 lowestDoorPos;
Vector3 lowestDoorNormal;
if (GameManager.Instance.PlayerEnterExit.Interior.FindLowestInteriorDoor(out lowestDoorPos, out lowestDoorNormal))
{
lowestDoorPos += lowestDoorNormal * (GameManager.Instance.PlayerController.radius + 0.1f);
int guardCount = UnityEngine.Random.Range(2, 6);
for (int i = 0; i < guardCount; i++)
{
SpawnCityGuard(lowestDoorPos, Vector3.forward);
}
}
return;
}
DaggerfallLocation dfLocation = GameManager.Instance.StreamingWorld.CurrentPlayerLocationObject;
if (dfLocation == null)
return;
PopulationManager populationManager = dfLocation.GetComponent<PopulationManager>();
if (populationManager == null)
return;
// If immediateSpawn, then guards will be created without any countdown
if (immediateSpawn)
{
int guardsSpawnedFromNPCs = 0;
// Try to spawn guards from nearby NPCs. This has the benefits that the spawn position will be valid,
// and that guards will tend to appear from the more crowded areas and not from nothing.
// Note: Classic disables the NPC that the guard is spawned from. Classic guards do not spawn looking at the player.
for (int i = 0; i < populationManager.PopulationPool.Count; i++)
{
if (!populationManager.PopulationPool[i].npc.isActiveAndEnabled)
continue;
Vector3 directionToMobile = populationManager.PopulationPool[i].npc.Motor.transform.position - GameManager.Instance.PlayerMotor.transform.position;
if (directionToMobile.magnitude <= 77.5f)
{
// Spawn from guard mobile NPCs first
if (populationManager.PopulationPool[i].npc.IsGuard)
{
SpawnCityGuard(populationManager.PopulationPool[i].npc.transform.position, populationManager.PopulationPool[i].npc.transform.forward);
populationManager.PopulationPool[i].npc.gameObject.SetActive(false);
++guardsSpawnedFromNPCs;
}
// Next try non-guards
else if (Vector3.Angle(directionToMobile, GameManager.Instance.PlayerMotor.transform.forward) >= 105.469
&& UnityEngine.Random.Range(0, 4) == 0)
{
SpawnCityGuard(populationManager.PopulationPool[i].npc.transform.position, populationManager.PopulationPool[i].npc.transform.forward);
++guardsSpawnedFromNPCs;
}
}
}
// If no guards spawned from nearby NPCs, spawn randomly with a foeSpawner
if (guardsSpawnedFromNPCs == 0)
{
GameObjectHelper.CreateFoeSpawner(true, MobileTypes.Knight_CityWatch, UnityEngine.Random.Range(2, 5 + 1), 12.8f, 51.2f);
}
}
else
// Spawn guards if player seen by an NPC
{
bool seen = false;
bool seenByGuard = false;
for (int i = 0; i < populationManager.PopulationPool.Count; i++)
{
if (!populationManager.PopulationPool[i].npc.isActiveAndEnabled)
continue;
Vector3 toPlayer = GameManager.Instance.PlayerMotor.transform.position - populationManager.PopulationPool[i].npc.Motor.transform.position;
if (toPlayer.magnitude <= 77.5f && Vector3.Angle(toPlayer, populationManager.PopulationPool[i].npc.Motor.transform.forward) <= 95)
{
// Check if line of sight to target
RaycastHit hit;
// Set origin of ray to approximate eye position
Vector3 eyePos = populationManager.PopulationPool[i].npc.Motor.transform.position;
eyePos.y += .7f;
// Set destination to the player's approximate eye position
CharacterController controller = GameManager.Instance.PlayerEntityBehaviour.transform.GetComponent<CharacterController>();
Vector3 playerEyePos = GameManager.Instance.PlayerMotor.transform.position;
playerEyePos.y += controller.height / 3;
// Check if npc sees player
Vector3 eyeToTarget = playerEyePos - eyePos;
Ray ray = new Ray(eyePos, eyeToTarget.normalized);
if (Physics.Raycast(ray, out hit, 77.5f))
{
// Check if hit was player
DaggerfallEntityBehaviour entity = hit.transform.gameObject.GetComponent<DaggerfallEntityBehaviour>();
if (entity == GameManager.Instance.PlayerEntityBehaviour)
seen = true;
if (populationManager.PopulationPool[i].npc.IsGuard)
seenByGuard = true;
}
}
if (seenByGuard)
{
SpawnCityGuard(populationManager.PopulationPool[i].npc.transform.position, populationManager.PopulationPool[i].npc.transform.forward);
populationManager.PopulationPool[i].npc.gameObject.SetActive(false);
}
}
// Player seen by a non-guard NPC but not by any guard NPCs. Start a countdown until guards arrive.
if (!seenByGuard && seen)
{
guardsArriveCountdown = UnityEngine.Random.Range(5, 10 + 1);
// Also track location so guards don't appear if player leaves during countdown
guardsArriveCountdownLocation = dfLocation;
}
}
}
}
public GameObject SpawnCityGuard(Vector3 position, Vector3 direction)
{
GameObject[] cityWatch = GameObjectHelper.CreateFoeGameObjects(position, MobileTypes.Knight_CityWatch, 1);
cityWatch[0].transform.forward = direction;
EnemyMotor enemyMotor = cityWatch[0].GetComponent<EnemyMotor>();
// Classic does not do anything special to make guards aware of the player, but since they're responding to a crime, standing around
// unaware of the player can be perceived as a bug.
enemyMotor.MakeEnemyHostileToAttacker(GameManager.Instance.PlayerEntityBehaviour);
// Set a longer giveUpTimer than usual in case it takes more than the usual timer to get to the player position
enemyMotor.GiveUpTimer *= 3;
cityWatch[0].SetActive(true);
return cityWatch[0];
}
void MakeNPCGuardsIntoEnemiesIfGuardsSpawned()
{
if (GameManager.Instance.HowManyEnemiesOfType(MobileTypes.Knight_CityWatch, true) > 0)
{
DaggerfallLocation dfLocation = GameManager.Instance.StreamingWorld.CurrentPlayerLocationObject;
if (dfLocation == null)
return;
PopulationManager populationManager = dfLocation.GetComponent<PopulationManager>();
if (populationManager == null)
return;
for (int i = 0; i < populationManager.PopulationPool.Count; i++)
{
if (!populationManager.PopulationPool[i].npc.isActiveAndEnabled)
continue;
// Spawn from guard mobile NPCs
if (populationManager.PopulationPool[i].npc.IsGuard)
{
SpawnCityGuard(populationManager.PopulationPool[i].npc.transform.position, populationManager.PopulationPool[i].npc.transform.forward);
populationManager.PopulationPool[i].npc.gameObject.SetActive(false);
}
}
}
}
/// <summary>
/// Resets entity to initial state.
/// </summary>
public void Reset()
{
equipTable.Clear();
items.Clear();
wagonItems.Clear();
otherItems.Clear();
lightSource = null;
spellbook.Clear();
factionData.Reset();
globalVars.Reset();
notebook.Clear();
SetEntityDefaults();
startingLevelUpSkillSum = 0;
currentLevelUpSkillSum = 0;
goldPieces = 0;
timeOfLastSkillIncreaseCheck = 0;
timeOfLastSkillTraining = 0;
rentedRooms.Clear();
DaedraSummonDay = DaedraSummonIndex = 0;
if (skillUses != null)
System.Array.Clear(skillUses, 0, skillUses.Length);
// Clear any world variation this player entity has triggered
WorldDataVariants.Clear();
}
/// <summary>
/// Assigns player entity settings from a character document.
/// </summary>
public void AssignCharacter(CharacterDocument character, int level = 1, int maxHealth = 0, bool fillVitals = true)
{
if (character == null)
{
SetEntityDefaults();
return;
}
this.level = level;
this.gender = character.gender;
this.raceTemplate = character.raceTemplate;
this.career = character.career;
this.name = character.name;
this.faceIndex = character.faceIndex;
this.stats = character.workingStats;
this.skills = character.workingSkills;
this.reflexes = character.reflexes;
this.maxHealth = character.maxHealth;
this.currentHealth = character.currentHealth;
this.currentMagicka = character.currentSpellPoints;
this.sGroupReputations[0] = character.reputationCommoners;
this.sGroupReputations[1] = character.reputationMerchants;
this.sGroupReputations[2] = character.reputationScholars;
this.sGroupReputations[3] = character.reputationNobility;
this.sGroupReputations[4] = character.reputationUnderworld;
this.currentFatigue = character.currentFatigue;
this.skillUses = character.skillUses;
this.skillsRecentlyRaised[0] = character.skillsRaisedThisLevel1;
this.skillsRecentlyRaised[1] = character.skillsRaisedThisLevel2;
this.startingLevelUpSkillSum = character.startingLevelUpSkillSum;
this.minMetalToHit = (WeaponMaterialTypes)character.minMetalToHit;
this.armorValues = character.armorValues;
this.timeToBecomeVampireOrWerebeast = character.timeToBecomeVampireOrWerebeast;
this.lastTimePlayerAteOrDrankAtTavern = character.lastTimePlayerAteOrDrankAtTavern;
this.timeOfLastSkillTraining = character.lastTimePlayerBoughtTraining;
this.timeForThievesGuildLetter = character.timeForThievesGuildLetter;
this.timeForDarkBrotherhoodLetter = character.timeForDarkBrotherhoodLetter;
this.darkBrotherhoodRequirementTally = character.darkBrotherhoodRequirementTally;
this.thievesGuildRequirementTally = character.thievesGuildRequirementTally;
//this.hasStartedInitialVampireQuest = character.hasStartedInitialVampireQuest != 0;
//this.vampireClan = character.vampireClan;
//this.lastTimeVampireNeedToKillSatiated = character.lastTimeVampireNeedToKillSatiated;
// Trim name strings as these might contain trailing whitespace characters from classic save
name = name.Trim();
career.Name = career.Name.Trim();
BackStory = character.backStory;
if (maxHealth <= 0)
this.maxHealth = FormulaHelper.RollMaxHealth(this);
else
this.maxHealth = maxHealth;
if (fillVitals)
FillVitalSigns();
timeOfLastSkillIncreaseCheck = DaggerfallUnity.Instance.WorldTime.Now.ToClassicDaggerfallTime();
DaggerfallUnity.LogMessage("Assigned character " + this.name, true);
}
/// <summary>
/// Assigns character items and spells from classic save tree.
/// Spells are stored as a child of spellbook item container, which is why they are imported along with items.
/// </summary>
public void AssignItemsAndSpells(SaveTree saveTree)
{
// Find character record, should always be a singleton
CharacterRecord characterRecord = (CharacterRecord)saveTree.FindRecord(RecordTypes.Character);
if (characterRecord == null)
return;
// Find all character-owned items
List<SaveTreeBaseRecord> itemRecords = saveTree.FindRecords(RecordTypes.Item, characterRecord);
// Filter for container-based inventory items
List<SaveTreeBaseRecord> filteredRecords = saveTree.FilterRecordsByParentType(itemRecords, RecordTypes.Container);
// Add interim Daggerfall Unity items
foreach (var record in filteredRecords)
{
// Get container parent
ContainerRecord containerRecord = (ContainerRecord)record.Parent;
// Some (most likely hacked) classic items have 0 or 65535 in image data bitfield
// Discard these items as they will likely have other bad attributes such as an impossible weight
// The goal here is just to prevent game from crashing due to bad item data
if ((record as ItemRecord).ParsedData.image1 == 0 || (record as ItemRecord).ParsedData.image1 == 0xffff)
continue;
// Create item
DaggerfallUnityItem newItem = new DaggerfallUnityItem((ItemRecord)record);
// Import spells if this is a spellbook record
try
{
if (newItem.ItemGroup == ItemGroups.MiscItems && newItem.GroupIndex == 0)
ImportSpells(containerRecord);
}
catch (Exception ex)
{
// Failed to import spellbook - log and give player empty spellbook
Debug.LogWarningFormat("Failed to import spellbook record. Exception {0}", ex.Message);
GameManager.Instance.ItemHelper.AddSpellbookItem(GameManager.Instance.PlayerEntity);
}
// Grabbed trapped soul if needed
if (newItem.ItemGroup == ItemGroups.MiscItems && newItem.GroupIndex == 1)
{
if (record.Children.Count > 0)
{
TrappedSoulRecord soulRecord = (TrappedSoulRecord)record.Children[0];
newItem.TrappedSoulType = (MobileTypes)soulRecord.RecordRoot.SpriteIndex;
}
else
newItem.TrappedSoulType = MobileTypes.None;
}
// Add existence time limit if item is flagged as having been made through the "Create Item" effect
if (((record as ItemRecord).ParsedData.flags & 0x1000) != 0)
{
newItem.TimeForItemToDisappear = record.RecordRoot.Time;
}
// Add to local inventory or wagon
if (containerRecord.IsWagon)
wagonItems.AddItem(newItem);
else
items.AddItem(newItem);
// Equip to player if equipped in save
for (int i = 0; i < characterRecord.ParsedData.equippedItems.Length; i++)
{
if (characterRecord.ParsedData.equippedItems[i] == record.RecordRoot.RecordID)
equipTable.EquipItem(newItem, true, false);
}
}
}
/// <summary>
/// Assigns spells from spellbook item.
/// </summary>
void ImportSpells(ContainerRecord record)
{
// Must have a populated spellbook container with spells
if (record == null || record.Children.Count == 0 ||
record.Children[0].Children == null || record.Children[0].Children.Count == 0)
{
return;
}
// Read spell records in spellbook container
foreach(SpellRecord spell in record.Children[0].Children)
{
spell.ReadNativeSpellData();
EffectBundleSettings bundle;
if (!GameManager.Instance.EntityEffectBroker.ClassicSpellRecordDataToEffectBundleSettings(spell.ParsedData, BundleTypes.Spell, out bundle))
{
Debug.LogErrorFormat("Failed to create effect bundle while importing classic spell '{0}'.", spell.ParsedData.spellName);
continue;
}
AddSpell(bundle);
}
}
/// <summary>
/// Assigns guild memberships to player from classic save tree.
/// </summary>
public void AssignGuildMemberships(SaveTree saveTree)
{
// Find character record, should always be a singleton
CharacterRecord characterRecord = (CharacterRecord)saveTree.FindRecord(RecordTypes.Character);
if (characterRecord == null)
return;
// Find all guild memberships, and add Daggerfall Unity guild memberships
List<SaveTreeBaseRecord> guildMembershipRecords = saveTree.FindRecords(RecordTypes.GuildMembership, characterRecord);
GameManager.Instance.GuildManager.ImportMembershipData(guildMembershipRecords);
}
/// <summary>
/// Assigns diseases and poisons to player from classic save tree.
/// </summary>
public void AssignDiseasesAndPoisons(SaveTree saveTree, out LycanthropyTypes lycanthropyType)
{
lycanthropyType = LycanthropyTypes.None;