public virtual TextFile.Token[] GetRandomTokens(int id, bool dfRand = false) { TextFile.Token[] sourceTokens = GetRSCTokens(id); // Build a list of token subrecords List <TextFile.Token> currentStream = new List <TextFile.Token>(); List <TextFile.Token[]> tokenStreams = new List <TextFile.Token[]>(); for (int i = 0; i < sourceTokens.Length; i++) { // If we're at end of subrecord then start a new stream if (sourceTokens[i].formatting == TextFile.Formatting.SubrecordSeparator) { tokenStreams.Add(currentStream.ToArray()); currentStream.Clear(); continue; } // Otherwise keep adding to current stream currentStream.Add(sourceTokens[i]); } // Complete final stream tokenStreams.Add(currentStream.ToArray()); // Select a random token stream int index = dfRand ? (int)(DFRandom.rand() % tokenStreams.Count) : UnityEngine.Random.Range(0, tokenStreams.Count); // Select the next to last item from the array if the length of the last one is zero index = (tokenStreams[index].Length == 0 ? index - 1 : index); return(tokenStreams[index]); }
void FixedUpdate() { // Unable to attack if AI disabled or paralyzed if (GameManager.Instance.DisableAI || entityBehaviour.Entity.IsParalyzed) { return; } // Countdown to next melee attack MeleeTimer -= Time.deltaTime; if (MeleeTimer < 0) { MeleeTimer = 0; } EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; int speed = entity.Stats.LiveSpeed; // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack // attempts at higher speeds, so it seems backwards. if (GameManager.ClassicUpdate && (DFRandom.rand() % speed >= (speed >> 3) + 6 && MeleeTimer == 0)) { if (!MeleeAnimation()) { return; } ResetMeleeTimer(); } }
public override string PaintingPrefix2() { // %pp2 DFRandom.rand(); // Classic uses every other value. TextFile.Token[] tokens = DaggerfallUnity.Instance.TextProvider.GetRandomTokens(paintingPp2, true); MacroHelper.ExpandMacros(ref tokens); return((tokens.Length > 0) ? tokens[0].text : "%pp2[idxError]"); }
public override string ImperialName() { // %imp string[] names = { "Pelagius", "Cephorus", "Uriel", "Cassynder", "Voragiel", "Trabbatus" }; DFRandom.Seed = (uint)parent.GetHashCode(); uint rand = DFRandom.rand() % 6; return(names[rand]); }
void Update() { // If a melee attack has reached the damage frame we can run a melee attempt if (mobile.DoMeleeDamage) { MeleeDamage(); mobile.DoMeleeDamage = false; } // If a bow attack has reached the shoot frame we can shoot an arrow else if (mobile.ShootArrow) { BowDamage(); // TODO: Shoot 3D projectile instead of doing an instant hit mobile.ShootArrow = false; DaggerfallAudioSource dfAudioSource = GetComponent <DaggerfallAudioSource>(); if (dfAudioSource) { dfAudioSource.PlayOneShot((int)SoundClips.ArrowShoot, 1, 1.0f); } } // Countdown to next melee attack meleeTimer -= Time.deltaTime; if (meleeTimer < 0) { meleeTimer = 0; } EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; int speed = entity.Stats.LiveSpeed; // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack // attempts at higher speeds, so it seems backwards. if (classicUpdate && (DFRandom.rand() % speed >= (speed >> 3) + 6 && meleeTimer == 0)) { MeleeAnimation(); meleeTimer = Random.Range(1500, 3001); meleeTimer -= 50 * (GameManager.Instance.PlayerEntity.Level - 10); // Note: In classic, what happens here is // meleeTimer += 450 * (enemydata[130] - 2); // Apparently this was meant to reference the game reflexes setting, // which is stored in playerentitydata[130]. // Instead enemydata[130] seems to instead always be 0, the equivalent of // "very high" reflexes, regardless of what the game reflexes are. // Here, we use the reflexes data as was intended. meleeTimer += 450 * ((int)GameManager.Instance.PlayerEntity.Reflexes - 2); if (meleeTimer > 100000 || meleeTimer < 0) { meleeTimer = 1500; } meleeTimer /= 980; // Approximates classic frame update } }
public override string ImperialName() { // %imp string[] names = { "Pelagius", "Cephorus", "Uriel", "Cassynder", "Voragiel", "Trabbatus" }; System.Random random = new System.Random(); DFRandom.Seed = (uint)random.Next(); uint rand = DFRandom.rand() % 6; return(names[rand]); }
public override string ArtistName() { // %an DFRandom.rand(); // Classic uses every other value. uint rand = DFRandom.rand() & 1; Genders gender = (Genders)rand; rand = DFRandom.rand(); NameHelper.BankTypes race = (NameHelper.BankTypes)(rand & 7); return(DaggerfallUnity.Instance.NameHelper.FullName(race, gender)); }
public bool SetNewRulerData(int factionID) { if (factionDict.ContainsKey(factionID)) { FactionFile.FactionData faction = factionDict[factionID]; faction.rulerPowerBonus = DFRandom.random_range_inclusive(0, 50) + 20; uint random = DFRandom.rand() << 16; faction.rulerNameSeed = DFRandom.rand() | random; return(true); } return(false); }
void FixedUpdate() { const int speedFloor = 8; // Unable to attack if AI disabled or paralyzed if (GameManager.Instance.DisableAI || entityBehaviour.Entity.IsParalyzed) { return; } // Unable to attack when playing certain oneshot anims if (mobile && mobile.IsPlayingOneShot() && mobile.OneShotPauseActionsWhilePlaying()) { return; } // Countdown to next melee attack MeleeTimer -= Time.deltaTime; if (MeleeTimer < 0) { MeleeTimer = 0; } // Get entity speed and enforce a lower limit so Drain Speed does not prevent attack ever firing EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; int speed = entity.Stats.LiveSpeed; if (speed < speedFloor) { speed = speedFloor; } // Slow down enemy frame rate based on floored speed value // If enemy is still at maximum speed then divisor is 1 and will experience no change to frame rate mobile.FrameSpeedDivisor = entity.Stats.PermanentSpeed / speed; // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack // attempts at higher speeds, so it seems backwards. if (GameManager.ClassicUpdate && (DFRandom.rand() % speed >= (speed >> 3) + 6 && MeleeTimer == 0)) { if (!MeleeAnimation()) { return; } ResetMeleeTimer(); } }
void Update() { if (GameManager.Instance.DisableAI) { return; } // If a melee attack has reached the damage frame we can run a melee attempt if (mobile.DoMeleeDamage) { MeleeDamage(); mobile.DoMeleeDamage = false; } // If a bow attack has reached the shoot frame we can shoot an arrow else if (mobile.ShootArrow) { ShootBow(); mobile.ShootArrow = false; DaggerfallAudioSource dfAudioSource = GetComponent <DaggerfallAudioSource>(); if (dfAudioSource) { dfAudioSource.PlayOneShot((int)SoundClips.ArrowShoot, 1, 1.0f); } } // Countdown to next melee attack MeleeTimer -= Time.deltaTime; if (MeleeTimer < 0) { MeleeTimer = 0; } EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; int speed = entity.Stats.LiveSpeed; // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack // attempts at higher speeds, so it seems backwards. if (GameManager.ClassicUpdate && (DFRandom.rand() % speed >= (speed >> 3) + 6 && MeleeTimer == 0)) { if (!MeleeAnimation()) { return; } ResetMeleeTimer(); } }
// Gets random surname for Nord names that follow 0+1+"sen" pattern string GetRandomNordSurname(NameBank nameBank) { // Get set parts string[] partsA, partsB; partsA = nameBank.sets[0].parts; partsB = nameBank.sets[1].parts; // Generate strings uint index = DFRandom.rand() % (uint)partsA.Length; string stringA = partsA[index]; index = DFRandom.rand() % (uint)partsB.Length; string stringB = partsB[index]; return(stringA + stringB + "sen"); }
// Gets random Redguard name which follows 0+1+2+3(75%) (male), 0+1+2+4 (female) pattern string GetRandomRedguardName(NameBank nameBank, Genders gender) { // Get set parts string[] partsA, partsB, partsC, partsD; if (gender == Genders.Male) { partsA = nameBank.sets[0].parts; partsB = nameBank.sets[1].parts; partsC = nameBank.sets[2].parts; partsD = nameBank.sets[3].parts; } else { partsA = nameBank.sets[0].parts; partsB = nameBank.sets[1].parts; partsC = nameBank.sets[2].parts; partsD = nameBank.sets[4].parts; } // Generate strings uint index = DFRandom.rand() % (uint)partsA.Length; string stringA = partsA[index]; index = DFRandom.rand() % (uint)partsB.Length; string stringB = partsB[index]; index = DFRandom.rand() % (uint)partsC.Length; string stringC = partsC[index]; string stringD = string.Empty; if (gender == Genders.Female || (DFRandom.rand() % 100 < 75)) { index = DFRandom.rand() % (uint)partsD.Length; stringD = partsD[index]; } return(stringA + stringB + stringC + stringD); }
// Gets random first name by gender for names that follow 0+1 (male), 2+3 (female) pattern string GetRandomFirstName(NameBank nameBank, Genders gender) { // Get set parts string[] partsA, partsB; if (gender == Genders.Male) { partsA = nameBank.sets[0].parts; partsB = nameBank.sets[1].parts; } else { partsA = nameBank.sets[2].parts; partsB = nameBank.sets[3].parts; } // Generate strings uint index = DFRandom.rand() % (uint)partsA.Length; string stringA = partsA[index]; index = DFRandom.rand() % (uint)partsB.Length; string stringB = partsB[index]; return(stringA + stringB); }
public TextFile.Token[] InitPaintingInfo(int paintingTextId = 250) { GetMacroDataSource(); if (ItemGroup == ItemGroups.Paintings && dataSource.paintingInfo == null) { DFRandom.srand(message); uint paintingIndex = DFRandom.rand() % 180; dataSource.paintingFileIdx = paintingIndex & 7; char paintingFileChar = (char)((paintingIndex >> 3) + 97); dataSource.paintingFilename = paintingFileChar + "paint.cif"; byte[] paintingRecord = DaggerfallUnity.Instance.ContentReader.PaintFileReader.Read(paintingIndex); Debug.LogFormat("painting file: {0}, index: {1}, cif idx: {2}, record: {3} {4} {5}", dataSource.paintingFilename, paintingIndex, dataSource.paintingFileIdx, paintingRecord[0], paintingRecord[1], paintingRecord[2]); dataSource.paintingSub = GetPaintingRecordPart(paintingRecord, 0, 9) + 6100; // for %sub macro dataSource.paintingAdj = GetPaintingRecordPart(paintingRecord, 10, 19) + 6200; // for %adj macro dataSource.paintingPp1 = GetPaintingRecordPart(paintingRecord, 20, 29) + 6300; // for %pp1 macro dataSource.paintingPp2 = GetPaintingRecordPart(paintingRecord, 30, 39) + 6400; // for %pp2 macro ITextProvider textProvider = DaggerfallUnity.Instance.TextProvider; dataSource.paintingInfo = textProvider.GetRandomTokens(paintingTextId, true); } return(dataSource.paintingInfo); }
// Get random monster name. // Monster1: 0+(50% +1)+2 // Monster2: 0+(50% +1)+2+(if female, +3) // Monster3: (if male, 25% +3 + " ")+0+1+2 string GetRandomMonsterName(Genders gender) { BankTypes type = (BankTypes)UnityEngine.Random.Range(8, 9 + 1); // Get random Monster1 or Monster2 for now. NameBank nameBank = bankDict[type]; // Get set parts string[] partsA, partsB, partsC, partsD; partsA = nameBank.sets[0].parts; partsB = nameBank.sets[1].parts; partsC = nameBank.sets[2].parts; partsD = null; string stringA = string.Empty; string stringB = string.Empty; string stringC = string.Empty; string stringD = string.Empty; uint index = 0; // Additional set for Monster2 and Monster3 if (nameBank.sets.Length >= 4) { partsD = nameBank.sets[3].parts; } // Generate strings if (type != BankTypes.Monster3) // Monster1 or Monster2 { index = DFRandom.rand() % (uint)partsA.Length; stringA = partsA[index]; stringB = string.Empty; if (DFRandom.rand() % 50 < 25) { index = DFRandom.rand() % (uint)partsB.Length; stringB = partsB[index]; } index = DFRandom.rand() % (uint)partsC.Length; stringC = partsC[index]; // Additional set for Monster2 female if (partsD != null && gender == Genders.Female) { index = DFRandom.rand() % (uint)partsD.Length; stringD = partsD[index]; } } else // Monster3 { if (gender == Genders.Female || DFRandom.rand() % 100 >= 25) { index = DFRandom.rand() % (uint)partsA.Length; stringA = partsA[index]; } else { index = DFRandom.rand() % (uint)partsD.Length; stringA = partsD[index] + " "; index = DFRandom.rand() % (uint)partsA.Length; stringB = partsA[index]; } index = DFRandom.rand() % (uint)partsB.Length; stringC = partsB[index]; index = DFRandom.rand() % (uint)partsC.Length; stringD = partsC[index]; } return(stringA + stringB + stringC + stringD); }
public static int CalculateAttackDamage(Entity.DaggerfallEntity attacker, Entity.DaggerfallEntity target, int weaponEquipSlot, int enemyAnimStateRecord) { if (attacker == null || target == null) { return(0); } int minBaseDamage = 0; int maxBaseDamage = 0; int damageModifiers = 0; int damage = 0; int chanceToHitMod = 0; int backstabbingLevel = 0; Entity.PlayerEntity player = GameManager.Instance.PlayerEntity; Items.DaggerfallUnityItem weapon = attacker.ItemEquipTable.GetItem((Items.EquipSlots)weaponEquipSlot); short skillID = 0; // Choose whether weapon-wielding enemies use their weapons or weaponless attacks. // In classic, weapon-wielding enemies use the damage values of their weapons // instead of their weaponless values. // For some enemies this gives lower damage than similar-tier monsters // and the weaponless values seems more appropriate, so here // enemies will choose to use their weaponless attack if it is more damaging. Entity.EnemyEntity AIAttacker = attacker as Entity.EnemyEntity; if (AIAttacker != null && weapon != null) { int weaponAverage = ((minBaseDamage + maxBaseDamage) / 2); int noWeaponAverage = ((AIAttacker.MobileEnemy.MinDamage + AIAttacker.MobileEnemy.MaxDamage) / 2); if (noWeaponAverage > weaponAverage) { // Use hand-to-hand weapon = null; } } if (weapon != null) { // If the attacker is using a weapon, check if the material is high enough to damage the target if (target.MinMetalToHit > (Items.WeaponMaterialTypes)weapon.NativeMaterialValue) { if (attacker == player) { DaggerfallUI.Instance.PopupMessage(UserInterfaceWindows.HardStrings.materialIneffective); } return(0); } // Get weapon skill used skillID = weapon.GetWeaponSkillIDAsShort(); } else { skillID = (short)DFCareer.Skills.HandToHand; } chanceToHitMod = attacker.Skills.GetLiveSkillValue(skillID); Entity.EnemyEntity AITarget = null; if (target != player) { AITarget = target as Entity.EnemyEntity; } if (attacker == player) { // Apply swing modifiers. Not applied to hand-to-hand in classic. FPSWeapon onscreenWeapon = GameManager.Instance.WeaponManager.ScreenWeapon; if (onscreenWeapon != null) { // The Daggerfall manual groups diagonal slashes to the left and right as if they are the same, but they are different. // Classic does not apply swing modifiers to hand-to-hand. if (onscreenWeapon.WeaponState == WeaponStates.StrikeUp) { damageModifiers += -4; chanceToHitMod += 10; } if (onscreenWeapon.WeaponState == WeaponStates.StrikeDownRight) { damageModifiers += -2; chanceToHitMod += 5; } if (onscreenWeapon.WeaponState == WeaponStates.StrikeDownLeft) { damageModifiers += 2; chanceToHitMod += -5; } if (onscreenWeapon.WeaponState == WeaponStates.StrikeDown) { damageModifiers += 4; chanceToHitMod += -10; } } if (weapon != null) { // Apply weapon proficiency if (((int)attacker.Career.ExpertProficiencies & weapon.GetWeaponSkillUsed()) != 0) { damageModifiers += ((attacker.Level / 3) + 1); chanceToHitMod += attacker.Level; } } // Apply hand-to-hand proficiency. Hand-to-hand proficiency is not applied in classic. else if (((int)attacker.Career.ExpertProficiencies & (int)(DFCareer.ProficiencyFlags.HandToHand)) != 0) { damageModifiers += ((attacker.Level / 3) + 1); chanceToHitMod += attacker.Level; } // Apply racial bonuses if (weapon != null) { if (player.RaceTemplate.ID == (int)Entity.Races.DarkElf) { damageModifiers += (attacker.Level / 4); chanceToHitMod += (attacker.Level / 4); } else if (skillID == (short)DFCareer.Skills.Archery) { if (player.RaceTemplate.ID == (int)Entity.Races.WoodElf) { damageModifiers += (attacker.Level / 3); chanceToHitMod += (attacker.Level / 3); } } else if (player.RaceTemplate.ID == (int)Entity.Races.Redguard) { damageModifiers += (attacker.Level / 3); chanceToHitMod += (attacker.Level / 3); } } // Apply backstabbing if (enemyAnimStateRecord % 5 > 2) // Facing away from player { chanceToHitMod += attacker.Skills.GetLiveSkillValue(DFCareer.Skills.Backstabbing); attacker.TallySkill(DFCareer.Skills.Backstabbing, 1); // backstabbing backstabbingLevel = attacker.Skills.GetLiveSkillValue(DFCareer.Skills.Backstabbing); } } // Choose struck body part int[] bodyParts = { 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6 }; int struckBodyPart = bodyParts[UnityEngine.Random.Range(0, bodyParts.Length)]; // Get damage for weaponless attacks if (skillID == (short)DFCareer.Skills.HandToHand) { if (attacker == player) { if (CalculateSuccessfulHit(attacker, target, chanceToHitMod, null, struckBodyPart)) { minBaseDamage = CalculateHandToHandMinDamage(attacker.Skills.GetLiveSkillValue(DFCareer.Skills.HandToHand)); maxBaseDamage = CalculateHandToHandMaxDamage(attacker.Skills.GetLiveSkillValue(DFCareer.Skills.HandToHand)); damage = UnityEngine.Random.Range(minBaseDamage, maxBaseDamage + 1); // Apply damage modifiers. damage += damageModifiers; // Apply strength modifier. It is not applied in classic despite what the in-game description for the Strength attribute says. damage += DamageModifier(attacker.Stats.LiveStrength); // Handle backstabbing if (backstabbingLevel > 0 && UnityEngine.Random.Range(1, 101) <= backstabbingLevel) { damage *= 3; string backstabMessage = UserInterfaceWindows.HardStrings.successfulBackstab; DaggerfallUI.Instance.PopupMessage(backstabMessage); } } } else // attacker is monster { // Handle multiple attacks by AI int attackNumber = 0; while (attackNumber < 3) // Classic supports up to 5 attacks but no monster has more than 3 { if (attackNumber == 0) { minBaseDamage = AIAttacker.MobileEnemy.MinDamage; maxBaseDamage = AIAttacker.MobileEnemy.MaxDamage; } else if (attackNumber == 1) { minBaseDamage = AIAttacker.MobileEnemy.MinDamage2; maxBaseDamage = AIAttacker.MobileEnemy.MaxDamage2; } else if (attackNumber == 2) { minBaseDamage = AIAttacker.MobileEnemy.MinDamage3; maxBaseDamage = AIAttacker.MobileEnemy.MaxDamage3; } if (DFRandom.rand() % 100 < 50 && minBaseDamage > 0 && CalculateSuccessfulHit(attacker, target, chanceToHitMod, null, struckBodyPart)) { damage += UnityEngine.Random.Range(minBaseDamage, maxBaseDamage + 1); } ++attackNumber; } } } // Handle weapon attacks else { if (CalculateSuccessfulHit(attacker, target, chanceToHitMod, weapon, struckBodyPart)) { damage = UnityEngine.Random.Range(weapon.GetBaseDamageMin(), weapon.GetBaseDamageMax() + 1); damage += damageModifiers; // Apply modifiers for Skeletal Warrior. if (AITarget != null && AITarget.CareerIndex == (int)Entity.MonsterCareers.SkeletalWarrior) { if (weapon.NativeMaterialValue == (int)Items.WeaponMaterialTypes.Silver) { damage *= 2; } if ((weapon.flags & 0x10) == 0) // not a blunt weapon { damage /= 2; } } // Apply strength modifier damage += DamageModifier(attacker.Stats.LiveStrength); // Apply material modifier. // The in-game display in Daggerfall of weapon damages with material modifiers is incorrect. The material modifier is half of what the display suggests. damage += weapon.GetWeaponMaterialModifier(); if (damage < 1) { damage = 0; } damage += GetBonusOrPenaltyByEnemyType(attacker, AITarget); if (backstabbingLevel > 1 && UnityEngine.Random.Range(1, 100 + 1) <= backstabbingLevel) { damage *= 3; string backstabMessage = UserInterfaceWindows.HardStrings.successfulBackstab; DaggerfallUI.Instance.PopupMessage(backstabMessage); } } } damage = Mathf.Max(0, damage); // If damage was done by a weapon, damage the weapon and armor of the hit body part. // In classic, shields are never damaged, only armor specific to the hitbody part is. // Here, if an equipped shield covers the hit body part, it takes damage instead. if (weapon != null && damage > 0) { weapon.DamageThroughPhysicalHit(damage, attacker); Items.DaggerfallUnityItem shield = target.ItemEquipTable.GetItem(Items.EquipSlots.LeftHand); bool shieldTakesDamage = false; if (shield != null) { Items.BodyParts[] protectedBodyParts = shield.GetShieldProtectedBodyParts(); for (int i = 0; (i < protectedBodyParts.Length) && !shieldTakesDamage; i++) { if (protectedBodyParts[i] == (Items.BodyParts)struckBodyPart) { shieldTakesDamage = true; } } } if (shieldTakesDamage) { shield.DamageThroughPhysicalHit(damage, target); } else { Items.EquipSlots hitSlot = Items.DaggerfallUnityItem.GetEquipSlotForBodyPart((Items.BodyParts)struckBodyPart); Items.DaggerfallUnityItem armor = target.ItemEquipTable.GetItem(hitSlot); if (armor != null) { armor.DamageThroughPhysicalHit(damage, target); } } } return(damage); }
public static string GetName(int seed, DFLocation.BuildingTypes type, int factionID, string locationName, string regionName) { const string firstNameTitleVar = "%ef"; const string cityNameTitleVar = "%cn"; const string royalTitleVar = "%rt"; string a = string.Empty, b = string.Empty; string result = string.Empty; bool singleton = false; FactionFile.FactionData factionData; DFRandom.srand(seed); switch (type) { case DFLocation.BuildingTypes.HouseForSale: return("House for sale"); case DFLocation.BuildingTypes.Tavern: b = TavernsB[DFRandom.random_range(0, TavernsB.Length)]; a = TavernsA[DFRandom.random_range(0, TavernsA.Length)]; break; case DFLocation.BuildingTypes.GeneralStore: b = GeneralStoresB[DFRandom.random_range(0, GeneralStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.WeaponSmith: b = WeaponStoresB[DFRandom.random_range(0, WeaponStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Armorer: b = ArmorStoresB[DFRandom.random_range(0, ArmorStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Bookseller: b = BookStoresB[DFRandom.random_range(0, BookStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.ClothingStore: b = ClothingStoresB[DFRandom.random_range(0, ClothingStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Alchemist: b = AlchemyStoresB[DFRandom.random_range(0, AlchemyStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.GemStore: b = GemStoresB[DFRandom.random_range(0, GemStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.PawnShop: b = PawnStoresB[DFRandom.random_range(0, PawnStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.FurnitureStore: b = FurnitureStoresB[DFRandom.random_range(0, FurnitureStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Library: b = LibraryStoresB[DFRandom.random_range(0, LibraryStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Bank: // Banks always appear to be named "The Bank of RegionName" b = regionName; a = "The Bank of"; break; case DFLocation.BuildingTypes.GuildHall: // Guild halls get the name from faction data if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionID, out factionData)) { a = factionData.name; singleton = true; } break; case DFLocation.BuildingTypes.Temple: // Temples get name from faction data - always seem to be first child of factionID if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionID, out factionData)) { if (factionData.children.Count > 0) { FactionFile.FactionData firstChild; if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionData.children[0], out firstChild)) { a = firstChild.name; singleton = true; } } } break; case DFLocation.BuildingTypes.Palace: // Main palace names come from TEXT.RSC (e.g. "Castle Daggerfall") // Other palaces are just named "Palace" (still need to confirm behaviour) int textId = 0; if (locationName == "Daggerfall") { textId = 475; } else if (locationName == "Wayrest") { textId = 476; } else if (locationName == "Sentinel") { textId = 477; } if (textId > 0) { TextFile.Token[] nameTokens = DaggerfallUnity.Instance.TextProvider.GetRSCTokens(textId); foreach (TextFile.Token token in nameTokens) { if (token.formatting == TextFile.Formatting.Text) { a = token.text; break; } } a = a.TrimEnd('.'); // remove character '.' from castle text record entry if it is last character } else { a = "Palace"; } singleton = true; break; default: // Do nothing for unknown/unsupported building type // Houses can actually change names based on active quests return(string.Empty); } // Replace %cn a = a.Replace(cityNameTitleVar, locationName); // Replace %ef if (a.Contains(firstNameTitleVar)) { // Need to burn a rand() for %ef roll to be correct. // Classic is always doing this when expanding a macro. DFRandom.rand(); // In classic, the function expanding the %ef macro uses a global variable containing the current // region race. However, this variable is never updated when the character travels // and remains at 0. This explains why the Breton name bank is always used for shops. // This global variable is probably a leftover from Daggerfall early development as, // with the exception of %lp, which presents a similar issue and always returns // "High Rock", all naming functions use a global array of 62 fixed race values, one for each region. // As with %lp, we choose to fix the original bug in DFU and use this array, meaning that // all shops in Hammerfell now use Redguard names. NameHelper.BankTypes nameBank = (NameHelper.BankTypes)MapsFile.RegionRaces[GameManager.Instance.PlayerGPS.CurrentRegionIndex]; string firstName = DaggerfallUnity.Instance.NameHelper.FirstName(nameBank, Game.Entity.Genders.Male); a = a.Replace(firstNameTitleVar, firstName); } // Replace %rt based on faction ruler if (a.Contains(royalTitleVar)) { a = a.Replace(royalTitleVar, MacroHelper.RegentTitle(null)); } // Final text is "{a} {b}" for two-part names or just "{a}" for singleton names if (!singleton) { result = string.Format("{0} {1}", a, b); } else { result = a; } return(result); }
public static string GetName(int seed, DFLocation.BuildingTypes type, int factionID, string locationName, string regionName) { const string firstNameTitleVar = "%ef"; const string cityNameTitleVar = "%cn"; const string royalTitleVar = "%rt"; string a = string.Empty, b = string.Empty; string result = string.Empty; bool singleton = false; FactionFile.FactionData factionData; DFRandom.srand(seed); switch (type) { case DFLocation.BuildingTypes.HouseForSale: return("House for sale"); case DFLocation.BuildingTypes.Tavern: b = TavernsB[DFRandom.random_range(0, TavernsB.Length)]; a = TavernsA[DFRandom.random_range(0, TavernsA.Length)]; break; case DFLocation.BuildingTypes.GeneralStore: b = GeneralStoresB[DFRandom.random_range(0, GeneralStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.WeaponSmith: b = WeaponStoresB[DFRandom.random_range(0, WeaponStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Armorer: b = ArmorStoresB[DFRandom.random_range(0, ArmorStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Bookseller: b = BookStoresB[DFRandom.random_range(0, BookStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.ClothingStore: b = ClothingStoresB[DFRandom.random_range(0, ClothingStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Alchemist: b = AlchemyStoresB[DFRandom.random_range(0, AlchemyStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.GemStore: b = GemStoresB[DFRandom.random_range(0, GemStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.PawnShop: b = PawnStoresB[DFRandom.random_range(0, PawnStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.FurnitureStore: b = FurnitureStoresB[DFRandom.random_range(0, FurnitureStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Library: b = LibraryStoresB[DFRandom.random_range(0, LibraryStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Bank: // Banks always appear to be named "The Bank of RegionName" b = regionName; a = "The Bank of"; break; case DFLocation.BuildingTypes.GuildHall: // Guild halls get the name from faction data if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionID, out factionData)) { a = factionData.name; singleton = true; } break; case DFLocation.BuildingTypes.Temple: // Temples get name from faction data - always seem to be first child of factionID if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionID, out factionData)) { if (factionData.children.Count > 0) { FactionFile.FactionData firstChild; if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionData.children[0], out firstChild)) { a = firstChild.name; singleton = true; } } } break; case DFLocation.BuildingTypes.Palace: // Main palace names come from TEXT.RSC (e.g. "Castle Daggerfall") // Other palaces are just named "Palace" (still need to confirm behaviour) int textId = 0; if (locationName == "Daggerfall") { textId = 475; } else if (locationName == "Wayrest") { textId = 476; } else if (locationName == "Sentinel") { textId = 477; } if (textId > 0) { TextFile.Token[] nameTokens = DaggerfallUnity.Instance.TextProvider.GetRSCTokens(textId); foreach (TextFile.Token token in nameTokens) { if (token.formatting == TextFile.Formatting.Text) { a = token.text; break; } } a = a.TrimEnd('.'); // remove character '.' from castle text record entry if it is last character } else { a = "Palace"; } singleton = true; break; default: // Do nothing for unknown/unsupported building type // Houses can actually change names based on active quests return(string.Empty); } // Replace %cn a = a.Replace(cityNameTitleVar, locationName); // Replace %ef if (a.Contains(firstNameTitleVar)) { // Need to burn a rand() for %ef roll to be correct // What is Daggerfall rolling here? DFRandom.rand(); // Observation finds nameplates only seem to use male Breton namebank string firstName = DaggerfallUnity.Instance.NameHelper.FirstName(NameHelper.BankTypes.Breton, Game.Entity.Genders.Male); a = a.Replace(firstNameTitleVar, firstName); } // Replace %rt based on faction ruler if (a.Contains(royalTitleVar)) { a = a.Replace(royalTitleVar, MacroHelper.RegentTitle(null)); } // Final text is "{a} {b}" for two-part names or just "{a}" for singleton names if (!singleton) { result = string.Format("{0} {1}", a, b); } else { result = a; } return(result); }
public override string PaintingSubject() { // %sub DFRandom.rand(); // Classic uses every other value. TextFile.Token[] tokens = DaggerfallUnity.Instance.TextProvider.GetRandomTokens(paintingSub, true); return((tokens.Length > 0) ? tokens[0].text : "%sub[idxError]"); }
void Update() { // Change sound presets if (Presets != lastPresets) { // Clear settings lastPresets = Presets; rainLoop = null; cricketsLoop = null; // Stop playing any loops if (loopAudioSource.isPlaying) { loopAudioSource.Stop(); loopAudioSource.clip = null; loopAudioSource.loop = false; } ApplyPresets(); StartWaiting(); } // Start rain loop if not running if ((Presets == AmbientSoundPresets.Rain || Presets == AmbientSoundPresets.Storm) && rainLoop == null) { rainLoop = PlayLoop(SoundClips.AmbientRaining, 1f); } // Start crickets loop if not running if ((Presets == AmbientSoundPresets.ClearNight) && cricketsLoop == null) { cricketsLoop = PlayLoop(SoundClips.AmbientCrickets, 1f); } // Tick counters waitCounter += Time.deltaTime; waterWaitCounter += Time.deltaTime; if (waitCounter > waitTime) { PlayEffects(); StartWaiting(); } // Play water sound effects. Timing based on classic. if (waterWaitCounter > GameManager.classicUpdateInterval) { if (playerEnterExit && playerEnterExit.blockWaterLevel != 10000) { // Chance to play gentle water sound at water surface if (DFRandom.rand() < 50) { Vector3 waterSoundPosition = playerBehaviour.transform.position; waterSoundPosition.y = playerEnterExit.blockWaterLevel * -1 * MeshReader.GlobalScale; waterSoundPosition.x += Random.Range(-3f, 3f); waterSoundPosition.z += Random.Range(-3f, 3f); SpatializedPlayOneShot(SoundClips.WaterGentle, waterSoundPosition, 1f); } // Chance to play water bubbles sound if player is underwater if (playerEnterExit.IsPlayerSubmerged && DFRandom.rand() < 100) { AmbientPlayOneShot(SoundClips.AmbientWaterBubbles, 1f); } } waterWaitCounter = 0; } }
void SelectCurrentSong() { if (currentPlaylist == null || currentPlaylist.Length == 0) { return; } int index = 0; // General MIDI song selection { uint gameMinutes = DaggerfallUnity.Instance.WorldTime.DaggerfallDateTime.ToClassicDaggerfallTime(); DFRandom.srand(gameMinutes / 1440); uint random = DFRandom.rand(); if (currentPlaylist == NightSongs) { index = (int)(random % NightSongs.Length); } else if (currentPlaylist == SunnySongs) { index = (int)(random % SunnySongs.Length); } else if (currentPlaylist == CloudySongs) { index = (int)(random % CloudySongs.Length); } else if (currentPlaylist == OvercastSongs) { index = (int)(random % OvercastSongs.Length); } else if (currentPlaylist == RainSongs) { index = (int)(random % RainSongs.Length); } else if (currentPlaylist == SnowSongs) { index = (int)(random % SnowSongs.Length); } else if (currentPlaylist == TempleSongs && playerEnterExit) { byte[] templeFactions = { 0x52, 0x54, 0x58, 0x5C, 0x5E, 0x62, 0x6A, 0x24 }; uint factionOfPlayerEnvironment = playerEnterExit.FactionID; index = Array.IndexOf(templeFactions, (byte)factionOfPlayerEnvironment); if (index < 0) { byte[] godFactions = { 0x15, 0x16, 0x18, 0x1A, 0x1B, 0x1D, 0x21, 0x23 }; index = Array.IndexOf(godFactions, (byte)factionOfPlayerEnvironment); } } else if (currentPlaylist == TavernSongs) { index = (int)(gameMinutes / 1440 % TavernSongs.Length); } else if (currentPlaylist == DungeonInteriorSongs) { PlayerGPS gps = GameManager.Instance.PlayerGPS; ushort unknown2 = 0; int region = 0; if (gps.HasCurrentLocation) { unknown2 = (ushort)gps.CurrentLocation.Dungeon.RecordElement.Header.Unknown2; region = gps.CurrentRegionIndex; } DFRandom.srand(unknown2 ^ ((byte)region << 8)); random = DFRandom.rand(); index = (int)(random % 15); } else if (currentPlaylist == SneakingSongs) { index = UnityEngine.Random.Range(0, SneakingSongs.Length); } } currentSong = currentPlaylist[index]; currentSongIndex = index; }
void Update() { // Change sound presets if (Presets != lastPresets) { // Clear settings lastPresets = Presets; rainLoop = null; cricketsLoop = null; // Stop playing any loops if (dfAudioSource.AudioSource.isPlaying) { dfAudioSource.AudioSource.Stop(); dfAudioSource.AudioSource.clip = null; dfAudioSource.AudioSource.loop = false; } ApplyPresets(); StartWaiting(); } // Start rain loop if not running if ((Presets == AmbientSoundPresets.Rain || Presets == AmbientSoundPresets.Storm) && rainLoop == null) { rainLoop = dfAudioSource.GetAudioClip((int)SoundClips.AmbientRaining); dfAudioSource.AudioSource.clip = rainLoop; dfAudioSource.AudioSource.loop = true; dfAudioSource.AudioSource.spatialBlend = 0; dfAudioSource.AudioSource.Play(); } // Start crickets loop if not running if ((Presets == AmbientSoundPresets.ClearNight) && cricketsLoop == null) { cricketsLoop = dfAudioSource.GetAudioClip((int)SoundClips.AmbientCrickets); dfAudioSource.AudioSource.clip = cricketsLoop; dfAudioSource.AudioSource.loop = true; dfAudioSource.AudioSource.spatialBlend = 0; dfAudioSource.AudioSource.Play(); } // Tick counters waitCounter += Time.deltaTime; waterWaitCounter += Time.deltaTime; if (waitCounter > waitTime) { PlayEffects(); StartWaiting(); } // Play water sound effects. Timing and position based on classic behavior. // TODO: Make sound follow player's X and Z movement but play from Y coordinate of dungeon water, for a more dynamically 3d sound. // Currently the sound is a volume adjustment based on vertical distance from the water. if (waterWaitCounter > waterSoundWaitTime) { if (playerEnterExit == null) { playerEnterExit = GameManager.Instance.PlayerEnterExit; } if (playerEnterExit) { if (playerEnterExit.blockWaterLevel != 10000) { Entity.DaggerfallEntityBehaviour player = GameManager.Instance.PlayerEntityBehaviour; // Chance to play gentle water sound based on vertical distance between player and water surface if (DFRandom.rand() < 50) { float waterHeightInDFUnityUnits = (playerEnterExit.blockWaterLevel * -1 * MeshReader.GlobalScale); float volumeScale = Mathf.Clamp(1 - (Mathf.Abs(player.transform.position.y - waterHeightInDFUnityUnits) / 9), 0, 1); dfAudioSource.PlayOneShot((int)SoundClips.WaterGentle, 0, volumeScale); } // Chance to play underwater bubbling noise if player is underwater if (playerEnterExit.IsPlayerSubmerged) { if (DFRandom.rand() < 100) { dfAudioSource.PlayOneShot((int)SoundClips.AmbientWaterBubbles, 0); } } } } waterWaitCounter = 0; } }
void ParseFactions(string txt) { // Unmodded faction.txt contains multiples of same id // This resolver counter is used to give a faction a unique id if needed int resolverId = 1000; // Clear existing dictionary factionDict.Clear(); // First pass reads each faction text block in order List <string[]> factionBlocks = new List <string[]>(); using (StringReader reader = new StringReader(txt)) { List <string> currentblock = new List <string>(); while (true) { // Handle end of file string line = reader.ReadLine(); if (line == null) { // Store final block if (currentblock.Count > 0) { factionBlocks.Add(currentblock.ToArray()); } break; } // Ignore comment lines and empty lines if (line.StartsWith(";") || string.IsNullOrEmpty(line)) { continue; } // All factions blocks start with a '#' character if (line.Contains("#")) { // Store current block if (currentblock.Count > 0) { factionBlocks.Add(currentblock.ToArray()); } // Start new block currentblock.Clear(); } // Add line to current faction block currentblock.Add(line); } } // Second pass parses the text block into FactionData int lastPrecedingTabs = 0; FactionData previousFaction = new FactionData(); Stack <int> parentStack = new Stack <int>(); for (int i = 0; i < factionBlocks.Count; i++) { // Start a new faction FactionData faction = new FactionData(); string[] block = factionBlocks[i]; // Parent child relationship determined by preceding tabs int precedingTabs = CountPrecedingTabs(block[0]); if (precedingTabs > lastPrecedingTabs) { parentStack.Push(previousFaction.id); } else if (precedingTabs < lastPrecedingTabs) { while (parentStack.Count > precedingTabs) { parentStack.Pop(); } } lastPrecedingTabs = precedingTabs; // Set parent from top of stack if (parentStack.Count > 0) { faction.parent = parentStack.Peek(); } // Parse faction block ParseFactionData(ref block, ref faction); // Store faction just read if (!factionDict.ContainsKey(faction.id)) { factionDict.Add(faction.id, faction); } else { // Duplicate id detected faction.id = resolverId++; factionDict.Add(faction.id, faction); } // Key faction name to faction id if (!factionNameToIDDict.ContainsKey(faction.name)) { factionNameToIDDict.Add(faction.name, faction.id); } else { // Just ignoring duplicates for now // Currently only using name to id lookup for to find region faction quickly //UnityEngine.Debug.LogWarningFormat("Duplicate name detected " + faction.name); } // Calculate ruler name seed and ruler bonus in same manner as classic. These are not read from FACTION.TXT. uint random = DFRandom.rand() << 16; faction.rulerNameSeed = DFRandom.rand() | random; faction.rulerPowerBonus = DFRandom.random_range_inclusive(0, 50) + 20; previousFaction = faction; } }
public override void Update() { base.Update(); // Close immediately if no crime assigned to player if (playerEntity.CrimeCommitted == Entity.PlayerEntity.Crimes.None) { CloseWindow(); return; } if (state == 0) // Starting { regionIndex = GameManager.Instance.PlayerGPS.CurrentRegionIndex; int crimeType = (int)playerEntity.CrimeCommitted - 1; int legalRep = playerEntity.RegionData[regionIndex].LegalRep; // Get whether punishment is normal (fine/prison) or a severe punishment (banishment/execution) int threshold1 = 0; int threshold2 = 0; if (legalRep < 0) { threshold1 = -legalRep; if (threshold1 > 75) { threshold1 = 75; } threshold2 = -legalRep / 2; if (threshold2 > 75) { threshold2 = 75; } } if (Dice100.FailedRoll(threshold2) && Dice100.FailedRoll(threshold1)) { punishmentType = 2; // fine/prison } else { punishmentType = 0; // banishment or execution } // Calculate penalty amount int penaltyAmount = 0; if (legalRep >= 0) { penaltyAmount = PenaltyPerLegalRepPoint[crimeType] * legalRep + BasePenaltyAmounts[crimeType]; } else { penaltyAmount = BasePenaltyAmounts[crimeType] - PenaltyPerLegalRepPoint[crimeType] * legalRep; } if (penaltyAmount > MaximumPenaltyAmounts[crimeType]) { penaltyAmount = MaximumPenaltyAmounts[crimeType]; } else if (penaltyAmount < MinimumPenaltyAmounts[crimeType]) { penaltyAmount = MinimumPenaltyAmounts[crimeType]; } penaltyAmount /= 40; // Calculate days of prison and fine daysInPrison = 0; fine = 0; for (int i = 0; i < penaltyAmount; i++) { if ((DFRandom.rand() & 1) != 0) { fine += 40; } else { daysInPrison += 3; } } // If player can't pay fine, limit fine and add to prison sentence int playerGold = playerEntity.GetGoldAmount(); if (playerGold < fine) { daysInPrison += (fine - playerGold) / 40; fine = playerGold; } DaggerfallMessageBox messageBox; if (crimeType == 4 || crimeType == 3) // Assault or murder { // If player is a member of the Dark Brotherhood, they may be rescued for a violent crime Guilds.IGuild guild = GameManager.Instance.GuildManager.GetGuild((int)FactionFile.FactionIDs.The_Dark_Brotherhood); if (guild.IsMember()) { if (guild.Rank >= UnityEngine.Random.Range(0, 20)) { messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextDB)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; uiManager.PushWindow(messageBox); playerEntity.FillVitalSigns(); playerEntity.RaiseReputationForDoingSentence(); state = 100; return; } } } if (crimeType <= 2 || crimeType == 11) // Attempted breaking and entering, trespassing, breaking and entering, pickpocketing { // If player is a member of the Thieves Guild, they may be rescued for a thieving crime Guilds.IGuild guild = GameManager.Instance.GuildManager.GetGuild((int)FactionFile.FactionIDs.The_Thieves_Guild); if (guild.IsMember()) { if (guild.Rank >= UnityEngine.Random.Range(0, 20)) { messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextTG)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; messageBox.AllowCancel = false; uiManager.PushWindow(messageBox); playerEntity.FillVitalSigns(); playerEntity.RaiseReputationForDoingSentence(); state = 100; return; } } } messageBox = new DaggerfallMessageBox(uiManager, this, false, 105); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextStart)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.AddButton(DaggerfallMessageBox.MessageBoxButtons.Guilty); messageBox.AddButton(DaggerfallMessageBox.MessageBoxButtons.NotGuilty); messageBox.OnButtonClick += GuiltyNotGuilty_OnButtonClick; messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.AllowCancel = false; uiManager.PushWindow(messageBox); state = 1; // Done with initial message } else if (state == 2) // Found guilty { DaggerfallMessageBox messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextFoundGuilty)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; messageBox.AllowCancel = false; uiManager.PushWindow(messageBox); if (daysInPrison > 0) { state = 3; } else { // Give the reputation raise here if no prison time will be served. playerEntity.RaiseReputationForDoingSentence(); repositionPlayer = true; playerEntity.FillVitalSigns(); ReleaseFromPrison(); state = 100; } } else if (state == 3) // Serve prison sentence { playerEntity.InPrison = true; SwitchToPrisonScreen(); daysInPrisonLeft = daysInPrison; playerEntity.RaiseReputationForDoingSentence(); repositionPlayer = true; state = 100; } else if (state == 4) // Banished { DaggerfallMessageBox messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextBanished)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; messageBox.AllowCancel = false; uiManager.PushWindow(messageBox); playerEntity.RegionData[regionIndex].SeverePunishmentFlags |= 1; repositionPlayer = true; // Refill player vitals after banishment, otherwise player left with 1HP outside city gates playerEntity.FillVitalSigns(); state = 100; } // Note: Seems like an execution sentence can't be given in classic. It can't be given here, either. else if (state == 5) // Execution { DaggerfallMessageBox messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextExecuted)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; messageBox.AllowCancel = false; uiManager.PushWindow(messageBox); playerEntity.RegionData[regionIndex].SeverePunishmentFlags |= 2; state = 6; } else if (state == 6) // Reposition player at entrance { repositionPlayer = true; state = 100; } else if (state == 100) // Done { if (playerEntity.InPrison) { if (Input.GetKey(exitKey)) // Speed up prison day countdown. Not in classic. { prisonUpdateInterval = 0.001f; } else { prisonUpdateInterval = 0.3f; } if (prisonUpdateTimer == 0) { prisonUpdateTimer = Time.realtimeSinceStartup; } if (Time.realtimeSinceStartup < prisonUpdateTimer + prisonUpdateInterval) { return; } else { prisonUpdateTimer = Time.realtimeSinceStartup; UpdatePrisonScreen(); } } else { ReleaseFromPrison(); } } }
private void Move() { // Cancel movement and animations if paralyzed, but still allow gravity to take effect // This will have the (intentional for now) side-effect of making paralyzed flying enemies fall out of the air // Paralyzed swimming enemies will just freeze in place // Freezing anims also prevents the attack from triggering until paralysis cleared if (entityBehaviour.Entity.IsParalyzed) { mobile.FreezeAnims = true; if (swims) { controller.Move(Vector3.zero); } else { controller.SimpleMove(Vector3.zero); } return; } else { mobile.FreezeAnims = false; } // If hit, get knocked back if (knockBackSpeed > 0) { // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion, // making it last longer and do more damage if the enemy collides with something. if (knockBackSpeed > (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { knockBackSpeed = (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } if (knockBackSpeed > (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Hurt); } // Actual speed of motion is limited Vector3 motion; if (knockBackSpeed <= (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { motion = knockBackDirection * knockBackSpeed; } else { motion = knockBackDirection * (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } if (swims) { WaterMove(motion); } else if (flies || isLevitating) { controller.Move(motion * Time.deltaTime); } else { controller.SimpleMove(motion); } if (classicUpdate) { knockBackSpeed -= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); if (knockBackSpeed <= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Move); } } return; } // Monster speed of movement follows the same formula as for when the player walks EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; float moveSpeed = ((entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) / PlayerSpeedChanger.classicToUnitySpeedUnitRatio); // Reduced speed if playing a one-shot animation if (mobile.IsPlayingOneShot()) { moveSpeed /= AttackSpeedDivisor; } // As long as the target is detected, // giveUpTimer is reset to full if (senses.DetectedTarget) { giveUpTimer = 200; } // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop if (!senses.DetectedTarget && giveUpTimer > 0 && classicUpdate) { giveUpTimer--; } // Enemy will keep moving towards last known target position targetPos = senses.LastKnownTargetPos; // Remain idle after finishing any attacks if no target or after giving up finding the target if (entityBehaviour.Target == null || giveUpTimer == 0 || targetPos == EnemySenses.ResetPlayerPos) { if (!mobile.IsPlayingOneShot()) { mobile.ChangeEnemyState(MobileStates.Idle); } return; } // Flying enemies and slaughterfish aim for target face if (flies || isLevitating || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish)) { targetPos.y += 0.9f; } else { // Ground enemies target at their own height // This avoids short enemies from stepping on each other as they approach the target // Otherwise, their target vector aims up towards the target var playerController = GameManager.Instance.PlayerController; var deltaHeight = (playerController.height - controller.height) / 2; targetPos.y -= deltaHeight; } // Get direction & distance. var direction = targetPos - transform.position; float distance = direction.magnitude; // If attacking, randomly follow target with attack. if (mobile.Summary.EnemyState == MobileStates.PrimaryAttack) { if (!isAttackFollowsPlayerSet) { attackFollowsPlayer = (Random.Range(0f, 1f) > 0.5f); isAttackFollowsPlayerSet = true; } } else { isAttackFollowsPlayerSet = false; } if (attackFollowsPlayer) { transform.forward = direction.normalized; } // Bow attack for enemies that have the appropriate animation if (senses.TargetInSight && 360 * MeshReader.GlobalScale < distance && distance < 2048 * MeshReader.GlobalScale) { if (senses.TargetIsWithinYawAngle(22.5f)) { if (mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132) { // Random chance to shoot bow if (classicUpdate && DFRandom.rand() < 1000) { if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2 && mobile.Summary.EnemyState != MobileStates.RangedAttack1) { mobile.ChangeEnemyState(MobileStates.RangedAttack1); } else if (mobile.Summary.Enemy.HasRangedAttack2 && mobile.Summary.EnemyState != MobileStates.RangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack2); } } // Otherwise hold ground else if (!mobile.IsPlayingOneShot()) { mobile.ChangeEnemyState(MobileStates.Idle); } } //else if (spellPoints > 0 && canCastRangeSpells && DFRandom.rand() % 40 == 0) TODO: Ranged spell shooting // CastRangedSpell(); // Spell Cast Animation; else { // If no ranged attack, move towards target PursueTarget(direction, moveSpeed); } } else { if (!mobile.IsPlayingOneShot()) { mobile.ChangeEnemyState(MobileStates.Move); } TurnToTarget(direction.normalized); return; } } // Move towards target else if (distance > stopDistance) { PursueTarget(direction, moveSpeed); } else if (!senses.TargetIsWithinYawAngle(22.5f)) { TurnToTarget(direction.normalized); } //else //{ // TODO: Touch spells. //if (hasSpellPoints && attackCoolDownFinished && CanCastTouchSpells) //{ // Cast Touch Spell // Spell Cast Animation //} //} else if (!senses.DetectedTarget && mobile.Summary.EnemyState == MobileStates.Move) { mobile.ChangeEnemyState(MobileStates.Idle); } }
private void Move() { // If hit, get knocked back if (knockBackSpeed > 0) { // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion, // making it last longer and do more damage if the enemy collides with something. if (knockBackSpeed > (40 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10))) { knockBackSpeed = (40 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10)); } if (knockBackSpeed > (5 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Hurt); } // Actual speed of motion is limited Vector3 motion; if (knockBackSpeed <= (25 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10))) { motion = knockBackDirection * knockBackSpeed; } else { motion = knockBackDirection * (25 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10)); } if (swims) { WaterMove(motion); } else if (flies) { controller.Move(motion * Time.deltaTime); } else { controller.SimpleMove(motion); } if (classicUpdate) { knockBackSpeed -= (5 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10)); if (knockBackSpeed <= (5 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Move); } } return; } // Monster speed of movement follows the same formula as for when the player walks EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; float moveSpeed = ((entity.Stats.LiveSpeed + PlayerMotor.dfWalkBase) / PlayerMotor.classicToUnitySpeedUnitRatio); // Reduced speed if playing a one-shot animation if (mobile.IsPlayingOneShot()) { moveSpeed /= AttackSpeedDivisor; } // Remain idle when not hostile if (!isHostile) { mobile.ChangeEnemyState(MobileStates.Idle); return; } // If hostile but the enemy doesn't see the player, run the stealth check else if (senses.LastKnownPlayerPos == EnemySenses.ResetPlayerPos) { if (senses.StealthCheck()) { // Enemy noticed the player senses.LastKnownPlayerPos = GameManager.Instance.PlayerObject.transform.position; senses.DetectedPlayer = true; } else { // Enemy didn't notice the player mobile.ChangeEnemyState(MobileStates.Idle); senses.DetectedPlayer = false; return; } } // As long as the player is directly seen/heard, // giveUpTimer is reset to full if (senses.PlayerInSight || senses.PlayerInEarshot) { giveUpTimer = 200; } else if (giveUpTimer == 0) { // Player lost for too long, or wasn't in sight/earshot to begin with. Time to give up senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos; return; } // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop if (!senses.PlayerInSight && !senses.PlayerInEarshot && giveUpTimer > 0 && classicUpdate) { giveUpTimer--; } // Enemy will keep moving towards last known player position targetPos = senses.LastKnownPlayerPos; // Flying enemies and slaughterfish aim for player face if (flies || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish)) { targetPos.y += 0.9f; } else { // Ground enemies target at their own height // This avoids short enemies from stepping on each other as they approach the player // Otherwise, their target vector aims up towards the player var playerController = GameManager.Instance.PlayerController; var deltaHeight = (playerController.height - controller.height) / 2; targetPos.y -= deltaHeight; } // Get direction & distance. var direction = targetPos - transform.position; float distance = direction.magnitude; // If attacking, randomly follow player with attack. if (mobile.Summary.EnemyState == MobileStates.PrimaryAttack) { if (!isAttackFollowsPlayerSet) { attackFollowsPlayer = (Random.Range(0f, 1f) > 0.5f); isAttackFollowsPlayerSet = true; } } else { isAttackFollowsPlayerSet = false; } if (attackFollowsPlayer) { transform.forward = direction.normalized; } // Bow attack for enemies that have the appropriate animation if (senses.PlayerInSight && 360 * MeshReader.GlobalScale < distance && distance < 2048 * MeshReader.GlobalScale) { if (senses.TargetIsWithinYawAngle(22.5f)) { if (mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132) { // Random chance to shoot bow if (DFRandom.rand() < 1000) { if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2 && mobile.Summary.EnemyState != MobileStates.RangedAttack1) { mobile.ChangeEnemyState(MobileStates.RangedAttack1); } else if (mobile.Summary.Enemy.HasRangedAttack2 && mobile.Summary.EnemyState != MobileStates.RangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack2); } } // Otherwise hold ground else if (!mobile.IsPlayingOneShot()) { mobile.ChangeEnemyState(MobileStates.Idle); } } //else if (spellPoints > 0 && canCastRangeSpells && DFRandom.rand() % 40 == 0) TODO: Ranged spell shooting // CastRangedSpell(); // Spell Cast Animation; else { // If no ranged attack, move towards target PursueTarget(direction, moveSpeed); } } else { if (!mobile.IsPlayingOneShot()) { mobile.ChangeEnemyState(MobileStates.Move); } TurnToTarget(direction.normalized); return; } } // Move towards target else if (distance > stopDistance) { PursueTarget(direction, moveSpeed); } else if (!senses.TargetIsWithinYawAngle(22.5f)) { TurnToTarget(direction.normalized); } //else //{ // TODO: Touch spells. //if (hasSpellPoints && attackCoolDownFinished && CanCastTouchSpells) //{ // Cast Touch Spell // Spell Cast Animation //} //} else if (!senses.PlayerInSight && !senses.PlayerInEarshot) { mobile.ChangeEnemyState(MobileStates.Idle); } }
public static string GetName(int seed, DFLocation.BuildingTypes type, int factionID, string locationName, string regionName) { const string firstNameTitleVar = "%ef"; const string cityNameTitleVar = "%cn"; const string royalTitleVar = "%rt"; string a = string.Empty, b = string.Empty; string result = string.Empty; bool singleton = false; FactionFile.FactionData factionData; DFRandom.srand(seed); switch (type) { case DFLocation.BuildingTypes.HouseForSale: return("House for sale"); case DFLocation.BuildingTypes.Tavern: b = TavernsB[DFRandom.random_range(0, TavernsB.Length)]; a = TavernsA[DFRandom.random_range(0, TavernsA.Length)]; break; case DFLocation.BuildingTypes.GeneralStore: b = GeneralStoresB[DFRandom.random_range(0, GeneralStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.WeaponSmith: b = WeaponStoresB[DFRandom.random_range(0, WeaponStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Armorer: b = ArmorStoresB[DFRandom.random_range(0, ArmorStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Bookseller: b = BookStoresB[DFRandom.random_range(0, BookStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.ClothingStore: b = ClothingStoresB[DFRandom.random_range(0, ClothingStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Alchemist: b = AlchemyStoresB[DFRandom.random_range(0, AlchemyStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.GemStore: b = GemStoresB[DFRandom.random_range(0, GemStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.PawnShop: b = PawnStoresB[DFRandom.random_range(0, PawnStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.FurnitureStore: b = FurnitureStoresB[DFRandom.random_range(0, FurnitureStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Library: b = LibraryStoresB[DFRandom.random_range(0, LibraryStoresB.Length)]; a = StoresA[DFRandom.random_range(0, StoresA.Length)]; break; case DFLocation.BuildingTypes.Bank: // Banks always appear to be named "The Bank of RegionName" b = regionName; a = "The Bank of"; break; case DFLocation.BuildingTypes.GuildHall: // Guild halls get the name from faction data if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionID, out factionData)) { a = factionData.name; singleton = true; } break; case DFLocation.BuildingTypes.Temple: // Temples get name from faction data - always seem to be first child of factionID if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionID, out factionData)) { if (factionData.children.Count > 0) { FactionFile.FactionData firstChild; if (DaggerfallUnity.Instance.ContentReader.FactionFileReader.GetFactionData(factionData.children[0], out firstChild)) { a = firstChild.name; singleton = true; } } } break; case DFLocation.BuildingTypes.Palace: // Main palace names (e.g. "Castle Daggerfall" appear to be hardcoded in FALL.EXE // Other palaces are just named "Palace" // Need to confirm behaviour before implementing // Just calling everything "Palace" for now. a = "Palace"; singleton = true; break; default: // Do nothing for unknown/unsupported building type // Houses can actually change names based on active quests return(string.Empty); } // Replace %cn a = a.Replace(cityNameTitleVar, locationName); // Replace %ef if (a.Contains(firstNameTitleVar)) { // Need to burn a rand() for %ef roll to be correct // What is Daggerfall rolling here? DFRandom.rand(); // Observation finds nameplates only seem to use male Breton namebank string firstName = DaggerfallUnity.Instance.NameHelper.FirstName(NameHelper.BankTypes.Breton, Game.Entity.Genders.Male); a = a.Replace(firstNameTitleVar, firstName); } // Replace %rt based on faction ruler if (a.Contains(royalTitleVar)) { // Get factionID of this region FactionFile factionFile = DaggerfallUnity.Instance.ContentReader.FactionFileReader; int regionalFactionID = factionFile.GetFactionID(regionName); if (regionalFactionID != -1) { // Get faction data if (factionFile.GetFactionData(factionID, out factionData)) { // Get ruler title for this region string royalTile = RoyalTitles[factionData.ruler]; a = a.Replace(royalTitleVar, royalTile); } } } // Final text is "{a} {b}" for two-part names or just "{a}" for singleton names if (!singleton) { result = string.Format("{0} {1}", a, b); } else { result = a; } return(result); }
public override void Update() { base.Update(); if (state == 0) // Starting { regionIndex = GameManager.Instance.PlayerGPS.CurrentRegionIndex; int crimeType = (int)playerEntity.CrimeCommitted - 1; int legalRep = playerEntity.RegionData[regionIndex].LegalRep; // Get whether punishment is normal (fine/prison) or a severe punishment (banishment/execution) int threshold1 = 0; int threshold2 = 0; if (legalRep < 0) { threshold1 = -legalRep; if (threshold1 > 75) { threshold1 = 75; } threshold2 = -legalRep / 2; if (threshold2 > 75) { threshold2 = 75; } } if (UnityEngine.Random.Range(1, 101) > threshold2 && UnityEngine.Random.Range(1, 101) > threshold1) { punishmentType = 2; // fine/prison } else { punishmentType = 0; // banishment or execution } // Calculate penalty amount int penaltyAmount = 0; if (legalRep >= 0) { penaltyAmount = PenaltyPerLegalRepPoint[crimeType] * legalRep + BasePenaltyAmounts[crimeType]; } else { penaltyAmount = BasePenaltyAmounts[crimeType] - PenaltyPerLegalRepPoint[crimeType] * legalRep; } if (penaltyAmount > MaximumPenaltyAmounts[crimeType]) { penaltyAmount = MaximumPenaltyAmounts[crimeType]; } else if (penaltyAmount < MinimumPenaltyAmounts[crimeType]) { penaltyAmount = MinimumPenaltyAmounts[crimeType]; } penaltyAmount /= 40; // Calculate days of prison and fine daysInPrison = 0; fine = 0; for (int i = 0; i < penaltyAmount; i++) { if ((DFRandom.rand() & 1) != 0) { fine += 40; } else { daysInPrison += 3; } } // If player can't pay fine, limit fine and add to prison sentence int playerGold = playerEntity.GetGoldAmount(); if (playerGold < fine) { daysInPrison += (fine - playerGold) / 40; fine = playerGold; } DaggerfallMessageBox messageBox; if (crimeType == 4 || crimeType == 3) // Assault or murder { // If player is a member of the Dark Brotherhood, they may be rescued for a violent crime Guilds.Guild guild = GameManager.Instance.GuildManager.GetGuild((int)FactionFile.FactionIDs.The_Dark_Brotherhood); if (guild.IsMember()) { if (guild.Rank >= UnityEngine.Random.Range(0, 20)) { messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextDB)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; uiManager.PushWindow(messageBox); playerEntity.FillVitalSigns(); playerEntity.RaiseReputationForDoingSentence(); state = 100; return; } } } if (crimeType <= 2 || crimeType == 11) // Attempted breaking and entering, trespassing, breaking and entering, pickpocketing { // If player is a member of the Thieves Guild, they may be rescued for a thieving crime Guilds.Guild guild = GameManager.Instance.GuildManager.GetGuild((int)FactionFile.FactionIDs.The_Thieves_Guild); if (guild.IsMember()) { if (guild.Rank >= UnityEngine.Random.Range(0, 20)) { messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextTG)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; uiManager.PushWindow(messageBox); playerEntity.FillVitalSigns(); playerEntity.RaiseReputationForDoingSentence(); state = 100; return; } } } messageBox = new DaggerfallMessageBox(uiManager, this, false, 105); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextStart)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.AddButton(DaggerfallMessageBox.MessageBoxButtons.Guilty); messageBox.AddButton(DaggerfallMessageBox.MessageBoxButtons.NotGuilty); messageBox.OnButtonClick += GuiltyNotGuilty_OnButtonClick; messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; uiManager.PushWindow(messageBox); state = 1; // Done with initial message } else if (state == 2) // Found guilty { DaggerfallMessageBox messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextFoundGuilty)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; uiManager.PushWindow(messageBox); state = 3; } else if (state == 3) // Serve prison sentence { PositionPlayerAtLocationEntrance(); ServeTime(daysInPrison); playerEntity.RaiseReputationForDoingSentence(); state = 100; } else if (state == 4) // Banished { DaggerfallMessageBox messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextBanished)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; uiManager.PushWindow(messageBox); playerEntity.RegionData[regionIndex].SeverePunishmentFlags |= 1; PositionPlayerAtLocationEntrance(); state = 100; } // Note: Seems like an execution sentence can't be given in classic. It can't be given here, either. else if (state == 5) // Execution { DaggerfallMessageBox messageBox = new DaggerfallMessageBox(uiManager, this, false, 149); messageBox.SetTextTokens(DaggerfallUnity.Instance.TextProvider.GetRSCTokens(courtTextExecuted)); messageBox.ScreenDimColor = new Color32(0, 0, 0, 0); messageBox.ParentPanel.VerticalAlignment = VerticalAlignment.Bottom; messageBox.ClickAnywhereToClose = true; uiManager.PushWindow(messageBox); playerEntity.RegionData[regionIndex].SeverePunishmentFlags |= 2; state = 6; } else if (state == 6) // Reposition player at entrance { PositionPlayerAtLocationEntrance(); state = 100; } else if (state == 100) // Done { ReleaseFromJail(); } }
/// <summary> /// Make decision about what movement action to take. /// </summary> void Move() { // Cancel movement and animations if paralyzed, but still allow gravity to take effect // This will have the (intentional for now) side-effect of making paralyzed flying enemies fall out of the air // Paralyzed swimming enemies will just freeze in place // Freezing anims also prevents the attack from triggering until paralysis cleared if (entityBehaviour.Entity.IsParalyzed) { mobile.FreezeAnims = true; if ((swims || flies) && !isLevitating) { controller.Move(Vector3.zero); } else { controller.SimpleMove(Vector3.zero); } return; } mobile.FreezeAnims = false; // If hit, get knocked back if (knockBackSpeed > 0) { // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion, // making it last longer and do more damage if the enemy collides with something (TODO). if (knockBackSpeed > (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { knockBackSpeed = (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } if (knockBackSpeed > (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Hurt); } // Actual speed of motion is limited Vector3 motion; if (knockBackSpeed <= (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { motion = knockBackDirection * knockBackSpeed; } else { motion = knockBackDirection * (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } // Move in direction of knockback if (swims) { WaterMove(motion); } else if (flies || isLevitating) { controller.Move(motion * Time.deltaTime); } else { controller.SimpleMove(motion); } // Remove remaining knockback and restore animation if (classicUpdate) { knockBackSpeed -= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); if (knockBackSpeed <= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Move); } } // If a decent hit got in, reconsider whether to continue current tactic if (knockBackSpeed > (10 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { EvaluateMoveInForAttack(); } return; } // Apply gravity if (!flies && !swims && !isLevitating && !controller.isGrounded) { controller.SimpleMove(Vector3.zero); // Only return if actually falling. Sometimes mobiles can get stuck where they are !isGrounded but SimpleMove(Vector3.zero) doesn't help. // Allowing them to continue and attempt a Move() in the code below frees them, but we don't want to allow that if we can avoid it so they aren't moving // while falling, which can also accelerate the fall due to anti-bounce downward movement in Move(). if (lastPosition != transform.position) { return; } } // Monster speed of movement follows the same formula as for when the player walks float moveSpeed = (entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) * MeshReader.GlobalScale; // Get isPlayingOneShot for use below bool isPlayingOneShot = mobile.IsPlayingOneShot(); // Reduced speed if playing a one-shot animation with enhanced AI if (isPlayingOneShot && DaggerfallUnity.Settings.EnhancedCombatAI) { moveSpeed /= AttackSpeedDivisor; } // As long as the target is detected, // giveUpTimer is reset to full if (senses.DetectedTarget) { giveUpTimer = 200; } // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop if (classicUpdate && !senses.DetectedTarget && giveUpTimer > 0) { giveUpTimer--; } // Change to idle animation if haven't moved or rotated if (!mobile.IsPlayingOneShot()) { // Rotation is done at classic update rate, so check at classic update rate if (classicUpdate) { Vector3 currentDirection = transform.forward; currentDirection.y = 0; if (lastPosition == transform.position && lastDirection == currentDirection) { mobile.ChangeEnemyState(MobileStates.Idle); rotating = false; } else { mobile.ChangeEnemyState(MobileStates.Move); } lastDirection = currentDirection; } // Movement is done at regular update rate, so check at regular update rate else if (!rotating && lastPosition == transform.position) { mobile.ChangeEnemyState(MobileStates.Idle); } else { mobile.ChangeEnemyState(MobileStates.Move); } lastPosition = transform.position; } // Do nothing if no target or after giving up finding the target or if target position hasn't been acquired yet if (senses.Target == null || giveUpTimer == 0 || senses.PredictedTargetPos == EnemySenses.ResetPlayerPos) { SetChangeStateTimer(); bashing = false; return; } if (bashing) { if (senses.TargetInSight || senses.LastKnownDoor == null || !senses.LastKnownDoor.IsLocked) { bashing = false; } else { int speed = entity.Stats.LiveSpeed; if (classicUpdate && DFRandom.rand() % speed >= (speed >> 3) + 6 && attack.MeleeTimer == 0) { mobile.ChangeEnemyState(MobileStates.PrimaryAttack); attack.ResetMeleeTimer(); } return; } } bool targetPosIsEnemyPos = false; // Get location to move towards. Either the combat target's position or, if trying to avoid an obstacle or fall, // a location to try to detour around the obstacle/fall. if (avoidObstaclesTimer == 0 && (senses.PredictedTargetPos.y > transform.position.y || ClearPathToPosition(senses.PredictedTargetPos))) { targetPos = senses.PredictedTargetPos; // Flying enemies and slaughterfish aim for target face if (flies || isLevitating || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish)) { targetPos.y += 0.9f; } else { // Ground enemies target at their own height // This avoids short enemies from stepping on each other as they approach the target // Otherwise, their target vector aims up towards the target var targetController = senses.Target.GetComponent <CharacterController>(); var deltaHeight = (targetController.height - controller.height) / 2; targetPos.y -= deltaHeight; } tempMovePos = targetPos; targetPosIsEnemyPos = true; } // If detouring, use the detour position else if (avoidObstaclesTimer > 0) { targetPos = tempMovePos; } // Otherwise, go straight else { tempMovePos = transform.position + transform.forward * 2; targetPos = tempMovePos; } // Get direction & distance. var direction = (targetPos - transform.position).normalized; float distance = (targetPos - transform.position).magnitude; // Ranged attacks if (targetPosIsEnemyPos && senses.DetectedTarget && 360 * MeshReader.GlobalScale < senses.DistanceToTarget && senses.DistanceToTarget < 2048 * MeshReader.GlobalScale) { bool evaluateBow = mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132; bool evaluateRangedMagic = false; if (!evaluateBow) { evaluateRangedMagic = CanCastRangedSpell(); } if (evaluateBow || evaluateRangedMagic) { if (classicUpdate && senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos)) { if (!isPlayingOneShot) { if (evaluateBow) { // Random chance to shoot bow if (DFRandom.rand() < 1000) { if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack1); } else if (mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack2); } } } // Random chance to shoot spell else if (DFRandom.rand() % 40 == 0 && entityEffectManager.SetReadySpell(selectedSpell)) { mobile.ChangeEnemyState(MobileStates.Spell); } } } else { TurnToTarget(direction); } return; } } // Touch spells if (targetPosIsEnemyPos && senses.TargetInSight && senses.DetectedTarget && attack.MeleeTimer == 0 && senses.DistanceToTarget <= attack.MeleeDistance + senses.TargetRateOfApproach && CanCastTouchSpell() && entityEffectManager.SetReadySpell(selectedSpell)) { if (mobile.Summary.EnemyState != MobileStates.Spell) { mobile.ChangeEnemyState(MobileStates.Spell); } attack.ResetMeleeTimer(); return; } // Update advance/retreat decision if (moveInForAttackTimer <= 0 && avoidObstaclesTimer == 0) { EvaluateMoveInForAttack(); } // Update timers if (moveInForAttackTimer > 0) { moveInForAttackTimer -= Time.deltaTime; } if (avoidObstaclesTimer > 0 && senses.TargetIsWithinYawAngle(5.625f, targetPos)) { avoidObstaclesTimer -= Time.deltaTime; } if (avoidObstaclesTimer < 0) { avoidObstaclesTimer = 0; } if (checkingClockwiseTimer > 0) { checkingClockwiseTimer -= Time.deltaTime; } if (checkingClockwiseTimer < 0) { checkingClockwiseTimer = 0; } if (changeStateTimer > 0) { changeStateTimer -= Time.deltaTime; } // If detouring, attempt to move if (avoidObstaclesTimer > 0) { AttemptMove(direction, moveSpeed); } // Otherwise, if not still executing a retreat, approach target until close enough to be on-guard. // If decided to move in for attack, continue until within melee range. Classic always moves in for attack. else if ((!retreating && distance >= (stopDistance * 2.75)) || (distance > stopDistance && moveInForAttack)) { // If state change timer is done, or we are continuing an already started combatMove, we can move immediately if (changeStateTimer <= 0 || pursuing) { AttemptMove(direction, moveSpeed); } // Otherwise, look at target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction); } } // Back away from combat target if right next to it, or if decided to retreat and enemy is too close. // Classic AI never backs awwy. else if (DaggerfallUnity.Settings.EnhancedCombatAI && senses.TargetInSight && (distance < stopDistance * .50 || (!moveInForAttack && distance < (stopDistance * retreatDistanceMultiplier)))) { // If state change timer is done, or we are already executing a retreat, we can move immediately if (changeStateTimer <= 0 || retreating) { AttemptMove(direction, moveSpeed / 2, true); } // Otherwise, look at target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } } // Not moving, just look at target else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } else // Not moving, and no need to turn { SetChangeStateTimer(); pursuing = false; retreating = false; avoidObstaclesTimer = 0; } }
/// <summary> /// Make decision about what movement action to take. /// </summary> void Move() { // Cancel movement and animations if paralyzed, but still allow gravity to take effect // This will have the (intentional for now) side-effect of making paralyzed flying enemies fall out of the air // Paralyzed swimming enemies will just freeze in place // Freezing anims also prevents the attack from triggering until paralysis cleared if (entityBehaviour.Entity.IsParalyzed) { mobile.FreezeAnims = true; if ((swims || flies) && !isLevitating) { controller.Move(Vector3.zero); } else { controller.SimpleMove(Vector3.zero); } return; } mobile.FreezeAnims = false; // Apply gravity to non-moving AI if active (has a combat target) or nearby if ((entityBehaviour.Target != null || senses.WouldBeSpawnedInClassic) && !flies && !swims) { controller.SimpleMove(Vector3.zero); } // If hit, get knocked back if (knockBackSpeed > 0) { // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion, // making it last longer and do more damage if the enemy collides with something (TODO). if (knockBackSpeed > (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { knockBackSpeed = (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } if (knockBackSpeed > (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Hurt); } // Actual speed of motion is limited Vector3 motion; if (knockBackSpeed <= (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { motion = knockBackDirection * knockBackSpeed; } else { motion = knockBackDirection * (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } // Move in direction of knockback if (swims) { WaterMove(motion); } else if (flies || isLevitating) { controller.Move(motion * Time.deltaTime); } else { controller.SimpleMove(motion); } // Remove remaining knockback and restore animation if (classicUpdate) { knockBackSpeed -= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); if (knockBackSpeed <= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Move); } } // If a decent hit got in, reconsider whether to continue current tactic if (knockBackSpeed > (10 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { EvaluateMoveInForAttack(); } return; } // Monster speed of movement follows the same formula as for when the player walks float moveSpeed = (entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) * MeshReader.GlobalScale; // Reduced speed if playing a one-shot animation with enhanced AI if (mobile.IsPlayingOneShot() && DaggerfallUnity.Settings.EnhancedCombatAI) { moveSpeed /= AttackSpeedDivisor; } // As long as the target is detected, // giveUpTimer is reset to full if (senses.DetectedTarget) { giveUpTimer = 200; } // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop if (!senses.DetectedTarget && giveUpTimer > 0 && classicUpdate) { giveUpTimer--; } // Change to idle animation if haven't moved or rotated if (!mobile.IsPlayingOneShot()) { // Rotation is done at classic update rate, so check at classic update rate if (classicUpdate) { Vector3 currentDirection = transform.forward; currentDirection.y = 0; if (lastPosition == transform.position && lastDirection == currentDirection) { mobile.ChangeEnemyState(MobileStates.Idle); rotating = false; } else { mobile.ChangeEnemyState(MobileStates.Move); } lastDirection = currentDirection; } // Movement is done at regular update rate, so check at regular update rate else if (!rotating && lastPosition == transform.position) { mobile.ChangeEnemyState(MobileStates.Idle); } else { mobile.ChangeEnemyState(MobileStates.Move); } lastPosition = transform.position; } // Do nothing if no target or after giving up finding the target if (entityBehaviour.Target == null || giveUpTimer == 0) { SetChangeStateTimer(); return; } // Get predicted target position if (avoidObstaclesTimer == 0 && !lookingForDetour) { targetPos = senses.PredictedTargetPos; // Flying enemies and slaughterfish aim for target face if (flies || isLevitating || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish)) { targetPos.y += 0.9f; } else { // Ground enemies target at their own height // This avoids short enemies from stepping on each other as they approach the target // Otherwise, their target vector aims up towards the target var playerController = GameManager.Instance.PlayerController; var deltaHeight = (playerController.height - controller.height) / 2; targetPos.y -= deltaHeight; } tempMovePos = targetPos; } else { targetPos = tempMovePos; } // Get direction & distance. var direction = targetPos - transform.position; float distance = (targetPos - transform.position).magnitude; // Ranged attacks if (senses.TargetInSight && 360 * MeshReader.GlobalScale < senses.DistanceToTarget && senses.DistanceToTarget < 2048 * MeshReader.GlobalScale) { bool evaluateBow = mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132; bool evaluateRangedMagic = false; if (!evaluateBow) { evaluateRangedMagic = CanCastRangedSpell(); } if (evaluateBow || evaluateRangedMagic) { if (senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos)) { if (!mobile.IsPlayingOneShot()) { if (evaluateBow) { // Random chance to shoot bow if (classicUpdate && DFRandom.rand() < 1000) { if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack1); } else if (mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack2); } } } // Random chance to shoot spell else if (classicUpdate && DFRandom.rand() % 40 == 0 && entityEffectManager.SetReadySpell(selectedSpell)) { mobile.ChangeEnemyState(MobileStates.Spell); } } } else { TurnToTarget(direction.normalized); } return; } } if (senses.TargetInSight && attack.MeleeTimer == 0 && senses.DistanceToTarget <= attack.MeleeDistance + senses.TargetRateOfApproach && CanCastTouchSpell() && entityEffectManager.SetReadySpell(selectedSpell)) { if (mobile.Summary.EnemyState != MobileStates.Spell) { mobile.ChangeEnemyState(MobileStates.Spell); } attack.ResetMeleeTimer(); return; } // Update melee decision if (moveInForAttackTimer <= 0 && avoidObstaclesTimer == 0 && !lookingForDetour) { EvaluateMoveInForAttack(); } if (moveInForAttackTimer > 0) { moveInForAttackTimer -= Time.deltaTime; } if (avoidObstaclesTimer > 0) { avoidObstaclesTimer -= Time.deltaTime; } if (avoidObstaclesTimer < 0) { avoidObstaclesTimer = 0; } if (changeStateTimer > 0) { changeStateTimer -= Time.deltaTime; } // Looking for detour if (lookingForDetour) { CombatMove(direction, moveSpeed); } // Approach target until we are close enough to be on-guard, or continue to melee range if attacking else if ((!retreating && distance >= (stopDistance * 2.75)) || (distance > stopDistance && moveInForAttack)) { // If state change timer is done, or we are already pursuing, we can move if (changeStateTimer <= 0 || pursuing) { CombatMove(direction, moveSpeed); } // Otherwise, just keep an eye on target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } } // Back away if right next to target, if retreating, or if cooling down from attack // Classic AI never backs away else if (DaggerfallUnity.Settings.EnhancedCombatAI && (senses.TargetInSight && (distance < stopDistance * .50 || (!moveInForAttack && distance < (stopDistance * retreatDistanceMultiplier))))) { // If state change timer is done, or we are already retreating, we can move if (changeStateTimer <= 0 || retreating) { CombatMove(direction, moveSpeed / 2, true); } // Otherwise, just keep an eye on target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } } else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } else if (avoidObstaclesTimer > 0 && distance > 0.1f) { CombatMove(direction, moveSpeed); } else // Next to target { SetChangeStateTimer(); pursuing = false; retreating = false; avoidObstaclesTimer = 0; } }