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."