diff --git a/Assets/Scripts/Game/Entities/DaggerfallEntityBehaviour.cs b/Assets/Scripts/Game/Entities/DaggerfallEntityBehaviour.cs index 84e35d5fed..84419652ba 100644 --- a/Assets/Scripts/Game/Entities/DaggerfallEntityBehaviour.cs +++ b/Assets/Scripts/Game/Entities/DaggerfallEntityBehaviour.cs @@ -11,6 +11,7 @@ using UnityEngine; using DaggerfallWorkshop.Game.MagicAndEffects; +using DaggerfallWorkshop.Game.Questing; namespace DaggerfallWorkshop.Game.Entity { @@ -93,9 +94,28 @@ void Update() /// Optionally assign fatigue multiplier. public void DamageFatigueFromSource(IEntityEffect sourceEffect, int amount, bool assignMultiplier = false) { + // Skip fatigue damage from effects if this is a non-hostile enemy + // This is a hack to support N0B00Y08 otherwise warrior will aggro if player casts Sleep on them + // Warrior does not aggro in classic and it seems impossible to cast this class of spell on non-hostiles in classic + // Would prefer a better system such as a quest action to whitelist certain spells on a Foe resource + // But this will get job done in this case and we can expand/improve later + if (!IsHostileEnemy()) + return; + DamageFatigueFromSource(sourceEffect.Caster, amount, assignMultiplier); } + /// + /// Check if this entity is a hostile enemy. + /// Currently only used to block damage and aggro from Sleep spell in N0B00Y08. + /// + /// True if this entity is a hostile enemy. + bool IsHostileEnemy() + { + EnemyMotor enemyMotor = transform.GetComponent(); + return enemyMotor && enemyMotor.IsHostile; + } + /// /// Cause damage to entity health with additional logic. /// diff --git a/Assets/Scripts/Game/MagicAndEffects/EntityEffectBundle.cs b/Assets/Scripts/Game/MagicAndEffects/EntityEffectBundle.cs index 1ca3eac50c..0b939b7082 100644 --- a/Assets/Scripts/Game/MagicAndEffects/EntityEffectBundle.cs +++ b/Assets/Scripts/Game/MagicAndEffects/EntityEffectBundle.cs @@ -9,6 +9,7 @@ // Notes: // +using DaggerfallConnect.Save; using DaggerfallWorkshop.Game.Entity; using DaggerfallWorkshop.Game.Items; @@ -89,5 +90,27 @@ public EntityEffectBundle(EffectBundleSettings settings, DaggerfallEntityBehavio } #endregion + + #region Public Methods + + /// + /// Checks if effect bundle contains an effect matching a classic effect record. + /// + /// Effect record to compare with native bundle effects. + /// True if bundle contains effect matching classic effect record. + public bool HasMatchForClassicEffect(SpellRecord.EffectRecordData effectRecord) + { + int classicKey = BaseEntityEffect.MakeClassicKey((byte)effectRecord.type, (byte)effectRecord.subType); + foreach(EffectEntry entry in settings.Effects) + { + IEntityEffect effectTemplate = GameManager.Instance.EntityEffectBroker.GetEffectTemplate(entry.Key); + if (effectTemplate.Properties.ClassicKey == classicKey) + return true; + } + + return false; + } + + #endregion } } \ No newline at end of file diff --git a/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs b/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs index b154ef7981..612f1478d0 100644 --- a/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs +++ b/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs @@ -278,6 +278,7 @@ public bool SetReadySpell(EntityEffectBundle spell, bool noSpellPointCost = fals // Assign spell - caster only spells are cast instantly readySpell = spell; + RaiseOnNewReadySpell(spell); readySpellDoesNotCostSpellPoints = noSpellPointCost; if (readySpell.Settings.TargetType == TargetTypes.CasterOnly) instantCast = true; @@ -1313,6 +1314,7 @@ void EnemyCastReadySpell() } // Clear ready spell and reset casting - do not store last spell if casting from item + RaiseOnCastReadySpell(readySpell); lastSpell = readySpell; readySpell = null; readySpellCastingCost = 0; @@ -1355,6 +1357,7 @@ private void PlayerSpellCasting_OnReleaseFrame() } // Clear ready spell and reset casting - do not store last spell if casting from item + RaiseOnCastReadySpell(readySpell); lastSpell = (readySpellDoesNotCostSpellPoints) ? null : readySpell; readySpell = null; readySpellCastingCost = 0; @@ -1606,6 +1609,24 @@ protected virtual void RaiseOnAddIncumbentState() OnAddIncumbentState(); } + // OnNewReadySpell + public delegate void OnNewReadySpellEventHandler(EntityEffectBundle spell); + public event OnNewReadySpellEventHandler OnNewReadySpell; + protected virtual void RaiseOnNewReadySpell(EntityEffectBundle spell) + { + if (OnNewReadySpell != null) + OnNewReadySpell(spell); + } + + // OnCastReadySpell + public delegate void OnCastReadySpellEventHandler(EntityEffectBundle spell); + public event OnCastReadySpellEventHandler OnCastReadySpell; + protected virtual void RaiseOnCastReadySpell(EntityEffectBundle spell) + { + if (OnCastReadySpell != null) + OnCastReadySpell(spell); + } + #endregion } } \ No newline at end of file diff --git a/Assets/Scripts/Game/Questing/Actions/CastSpellDo.cs b/Assets/Scripts/Game/Questing/Actions/CastSpellDo.cs index e8f0bbf660..a7ad6c743e 100644 --- a/Assets/Scripts/Game/Questing/Actions/CastSpellDo.cs +++ b/Assets/Scripts/Game/Questing/Actions/CastSpellDo.cs @@ -11,19 +11,25 @@ using System.Text.RegularExpressions; using FullSerializer; +using DaggerfallConnect.Save; +using DaggerfallWorkshop.Utility; +using DaggerfallWorkshop.Game.MagicAndEffects; namespace DaggerfallWorkshop.Game.Questing { /// - /// Condition that fires when player casts a specific spell. + /// Executes target task when player readies a spell containing specific effects. /// Classic only accepts standard versions of spell, not custom spells created by player. - /// Daggerfall Unity makes no distinction between standard or custom spells and will instead match by effect. + /// Daggerfall Unity makes no distinction between standard or custom spells and will instead match by effects. /// public class CastSpellDo : ActionTemplate { - int spellID; + int spellID = -1; + SpellRecord.EffectRecordData[] classicEffects; Symbol taskSymbol; + EntityEffectBundle lastReadySpell; + public override string Pattern { get @@ -35,6 +41,8 @@ public override string Pattern public CastSpellDo(Quest parentQuest) : base(parentQuest) { + GameManager.Instance.PlayerEffectManager.OnNewReadySpell += PlayerEffectManager_OnNewReadySpell; + GameManager.Instance.PlayerEffectManager.OnCastReadySpell += PlayerEffectManager_OnCastReadySpell; } public override IQuestAction CreateNew(string source, Quest parentQuest) @@ -49,17 +57,90 @@ public override IQuestAction CreateNew(string source, Quest parentQuest) string sourceSpellName = match.Groups["aSpell"].Value; action.taskSymbol = new Symbol(match.Groups["aTask"].Value); - // TODO: Attempt to get spellID from table using source name + // Cache classic effects to match + Table spellsTable = QuestMachine.Instance.SpellsTable; + if (spellsTable.HasValue(sourceSpellName)) + { + action.spellID = int.Parse(spellsTable.GetValue("id", sourceSpellName)); + SpellRecord.SpellRecordData spellRecord; + if (GameManager.Instance.EntityEffectBroker.GetClassicSpellRecord(action.spellID, out spellRecord)) + { + action.classicEffects = spellRecord.effects; + } + else + { + QuestMachine.LogFormat("CastSpellDo could not find spell matching spellID '{0}' from spell '{1}'", spellID, sourceSpellName); + SetComplete(); + } + } + else + { + QuestMachine.LogFormat("CastSpellDo could not resolve spell '{0}' in Quests-Spells data table", sourceSpellName); + SetComplete(); + } return action; } + public override void Update(Task caller) + { + // Validate + if (spellID == -1 || classicEffects == null || classicEffects.Length == 0 || taskSymbol == null || lastReadySpell == null) + { + lastReadySpell = null; + return; + } + + // Compare readied effect properties to spell record + for (int i = 0; i < classicEffects.Length; i++) + { + // Effect slot must be populated + if (classicEffects[i].type == -1 || classicEffects[i].subType == -1) + continue; + + // Bundle must have contain native effects matching this classic effect + if (!lastReadySpell.HasMatchForClassicEffect(classicEffects[i])) + { + lastReadySpell = null; + return; + } + } + + // Only reached here if action is running and matching spell is cast + ParentQuest.StartTask(taskSymbol); + SetComplete(); + } + + public override void SetComplete() + { + base.SetComplete(); + GameManager.Instance.PlayerEffectManager.OnNewReadySpell -= PlayerEffectManager_OnNewReadySpell; + GameManager.Instance.PlayerEffectManager.OnCastReadySpell -= PlayerEffectManager_OnCastReadySpell; + } + + #region Event Handlers + + private void PlayerEffectManager_OnNewReadySpell(EntityEffectBundle spell) + { + // Store last ready spell to evaluate on next tick + lastReadySpell = spell; + } + + private void PlayerEffectManager_OnCastReadySpell(EntityEffectBundle spell) + { + // Clear last ready spell so player can't queue it up before entering location + lastReadySpell = null; + } + + #endregion + #region Serialization [fsObject("v1")] public struct SaveData_v1 { public int spellID; + public SpellRecord.EffectRecordData[] classicEffects; public Symbol taskSymbol; } @@ -67,6 +148,7 @@ public override object GetSaveData() { SaveData_v1 data = new SaveData_v1(); data.spellID = spellID; + data.classicEffects = classicEffects; data.taskSymbol = taskSymbol; return data; @@ -79,6 +161,7 @@ public override void RestoreSaveData(object dataIn) SaveData_v1 data = (SaveData_v1)dataIn; spellID = data.spellID; + classicEffects = data.classicEffects; taskSymbol = data.taskSymbol; } diff --git a/Assets/Scripts/Game/Questing/QuestMachine.cs b/Assets/Scripts/Game/Questing/QuestMachine.cs index b4b02e9887..a3ed852329 100644 --- a/Assets/Scripts/Game/Questing/QuestMachine.cs +++ b/Assets/Scripts/Game/Questing/QuestMachine.cs @@ -375,6 +375,7 @@ void RegisterActionTemplates() RegisterAction(new RumorMill(null)); RegisterAction(new MakePcDiseased(null)); RegisterAction(new CurePcDisease(null)); + RegisterAction(new CastSpellDo(null)); // Stubs - these actions are not complete yet // Just setting up so certain quests compile for now diff --git a/Assets/StreamingAssets/Quests/N0B00Y08.txt b/Assets/StreamingAssets/Quests/N0B00Y08.txt index 7ad6147a3e..4173fdcb47 100644 --- a/Assets/StreamingAssets/Quests/N0B00Y08.txt +++ b/Assets/StreamingAssets/Quests/N0B00Y08.txt @@ -14,7 +14,7 @@ QuestorOffer: [1000] sponsored the Guild in many of our enterprises, and as we have made enemies, so has %g. We have word - from %g3 courier that a witch has + from %g2 courier that a witch has cursed %g2 with unending insomnia. A simple Sleep spell would cure %g2 -- perhaps you are willing @@ -39,7 +39,7 @@ AcceptQuest: [1002] Very good. Now, I told the courier that we wouldn't use any spell but Sleep on _warrior_. - Given %g3 previous experience with + Given %g2 previous experience with cursed spells, you can hardly blame %g2 for this anxiety. You'll find _warrior_ in _castle_ @@ -51,14 +51,14 @@ AcceptQuest: [1002] QuestComplete: [1004] I hear that _warrior_ dropped into sublime torpescence - right on %g3 feet. I do hope %g + right on %g2 feet. I do hope %g wakes before next New Life. As for you, my fine %ra, you have certainly earned your _gold_ gold pieces. RumorsDuringQuest: [1005] -Poor _warrior_ has been awake for days. I think %g3 sanity is going. +Poor _warrior_ has been awake for days. I think %g2 sanity is going. <---> The witch who cursed _warrior_ with sleeplessness really did a job on %g2. @@ -97,7 +97,7 @@ QuestLogEntry: [1010] Message: 1011 _warrior_ falls into a profound sleep, still - on %g3 feet. + on %g2 feet. -- Symbols used in the QRC file: diff --git a/Assets/StreamingAssets/Tables/QuestList-Classic.txt b/Assets/StreamingAssets/Tables/QuestList-Classic.txt index 6e9ce341b2..aadc8fb0e6 100644 --- a/Assets/StreamingAssets/Tables/QuestList-Classic.txt +++ b/Assets/StreamingAssets/Tables/QuestList-Classic.txt @@ -50,9 +50,9 @@ N0C00Y12, MagesGuild, N, 0, Passed N0C00Y13, MagesGuild, N, 0, Passed N0B00Y04, MagesGuild, M, 0, Passed N0B00Y06, MagesGuild, M, 0, Passed --N0B00Y08, MagesGuild, M, 0, FAILED. Needs sleep spell +N0B00Y08, MagesGuild, M, 0, Passed -N0B00Y09, MagesGuild, M, 0, FAILED. Repute exceeds task not yet functional so invitation always arrives. --N0B00Y16, MagesGuild, M, 0, FAILED. Needs open spell +N0B00Y16, MagesGuild, M, 0, IN-TESTING -N0B00Y17, MagesGuild, M, 0, more testing needed N0B10Y01, MagesGuild, M, 10, Passed N0B10Y03, MagesGuild, M, 10, Passed. Repute tasks not yet existent. Corpses do not disappear at quest end. @@ -61,7 +61,7 @@ N0B20Y02, MagesGuild, M, 20, Passed. Corpses don't disappear at the end of the q N0B20Y05, MagesGuild, M, 20, Passed. Lacks text variables in intro -N0B21Y14, MagesGuild, M, 21, FAILED. GetCurrentRegionFaction() needed N0B30Y15, MagesGuild, M, 30, Passed. Lacks spells on imp --N0B40Y07, MagesGuild, M, 40, FAILED Banish Daedra +N0B40Y07, MagesGuild, M, 40, IN-TESTING -- Classic: Temples (general) -C0C00Y10, HolyOrder, N, 0, FAILED. Player log says "Exception during quest compile: An element with the same key already exists in the dictionary."