Navigation Menu

Skip to content

Commit

Permalink
Progress CastSpellDo
Browse files Browse the repository at this point in the history
Once started CastSpellDo will monitor spells readied by player and execute target task when a matching spell is readied. Player does not actually have to cast spell on anything - just ready it. This matches classic.
Daggerfall Unity will support custom and standard versions of spells provided all effects match classic spell. This does not match classic but seems to overcome a classic limitation more than intended design. Can be rebuilt to work like classic if needed.
Fixed pronoun macros in N0B00Y08. Note that gender of warrior Foe spawned can be different to quest Person. Currently no way to reliably link these in quest system.
Added hack to ignore fatigue damage from spells on non-hostile foes to support N0B00Y08. Would prefer a quest action to whitelist spells on Foes for this purpose.
Enabled 3x quests in QuestList-Classic related to CastSpellDo for testing. N0B00Y08 is now passing, N0B00Y16 and N0B40Y07 are still in testing.
  • Loading branch information
Interkarma committed Nov 3, 2018
1 parent addd362 commit 8835b10
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 12 deletions.
20 changes: 20 additions & 0 deletions Assets/Scripts/Game/Entities/DaggerfallEntityBehaviour.cs
Expand Up @@ -11,6 +11,7 @@

using UnityEngine;
using DaggerfallWorkshop.Game.MagicAndEffects;
using DaggerfallWorkshop.Game.Questing;

namespace DaggerfallWorkshop.Game.Entity
{
Expand Down Expand Up @@ -93,9 +94,28 @@ void Update()
/// <param name="assignMultiplier">Optionally assign fatigue multiplier.</param>
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);
}

/// <summary>
/// Check if this entity is a hostile enemy.
/// Currently only used to block damage and aggro from Sleep spell in N0B00Y08.
/// </summary>
/// <returns>True if this entity is a hostile enemy.</returns>
bool IsHostileEnemy()
{
EnemyMotor enemyMotor = transform.GetComponent<EnemyMotor>();
return enemyMotor && enemyMotor.IsHostile;
}

/// <summary>
/// Cause damage to entity health with additional logic.
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions Assets/Scripts/Game/MagicAndEffects/EntityEffectBundle.cs
Expand Up @@ -9,6 +9,7 @@
// Notes:
//

using DaggerfallConnect.Save;
using DaggerfallWorkshop.Game.Entity;
using DaggerfallWorkshop.Game.Items;

Expand Down Expand Up @@ -89,5 +90,27 @@ public EntityEffectBundle(EffectBundleSettings settings, DaggerfallEntityBehavio
}

#endregion

#region Public Methods

/// <summary>
/// Checks if effect bundle contains an effect matching a classic effect record.
/// </summary>
/// <param name="effectRecord">Effect record to compare with native bundle effects.</param>
/// <returns>True if bundle contains effect matching classic effect record.</returns>
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
}
}
21 changes: 21 additions & 0 deletions Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
}
91 changes: 87 additions & 4 deletions Assets/Scripts/Game/Questing/Actions/CastSpellDo.cs
Expand Up @@ -11,19 +11,25 @@

using System.Text.RegularExpressions;
using FullSerializer;
using DaggerfallConnect.Save;
using DaggerfallWorkshop.Utility;
using DaggerfallWorkshop.Game.MagicAndEffects;

namespace DaggerfallWorkshop.Game.Questing
{
/// <summary>
/// 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.
/// </summary>
public class CastSpellDo : ActionTemplate
{
int spellID;
int spellID = -1;
SpellRecord.EffectRecordData[] classicEffects;
Symbol taskSymbol;

EntityEffectBundle lastReadySpell;

public override string Pattern
{
get
Expand All @@ -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)
Expand All @@ -49,24 +57,98 @@ 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;
}

public override object GetSaveData()
{
SaveData_v1 data = new SaveData_v1();
data.spellID = spellID;
data.classicEffects = classicEffects;
data.taskSymbol = taskSymbol;

return data;
Expand All @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions Assets/Scripts/Game/Questing/QuestMachine.cs
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions Assets/StreamingAssets/Quests/N0B00Y08.txt
Expand Up @@ -14,7 +14,7 @@ QuestorOffer: [1000]
<ce> sponsored the Guild in many of our
<ce> enterprises, and as we have made
<ce> enemies, so has %g. We have word
<ce> from %g3 courier that a witch has
<ce> from %g2 courier that a witch has
<ce> cursed %g2 with unending insomnia.
<ce> A simple Sleep spell would cure
<ce> %g2 -- perhaps you are willing
Expand All @@ -39,7 +39,7 @@ AcceptQuest: [1002]
<ce> Very good. Now, I told the courier
<ce> that we wouldn't use any spell but
<ce> Sleep on _warrior_.
<ce> Given %g3 previous experience with
<ce> Given %g2 previous experience with
<ce> cursed spells, you can hardly blame
<ce> %g2 for this anxiety. You'll find
<ce> _warrior_ in _castle_
Expand All @@ -51,14 +51,14 @@ AcceptQuest: [1002]
QuestComplete: [1004]
<ce> I hear that _warrior_
<ce> dropped into sublime torpescence
<ce> right on %g3 feet. I do hope %g
<ce> right on %g2 feet. I do hope %g
<ce> wakes before next New Life. As for
<ce> you, my fine %ra, you
<ce> have certainly earned your _gold_
<ce> 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.

Expand Down Expand Up @@ -97,7 +97,7 @@ QuestLogEntry: [1010]
Message: 1011
<ce> _warrior_ falls
<ce> into a profound sleep, still
<ce> on %g3 feet.
<ce> on %g2 feet.


-- Symbols used in the QRC file:
Expand Down
6 changes: 3 additions & 3 deletions Assets/StreamingAssets/Tables/QuestList-Classic.txt
Expand Up @@ -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.
Expand All @@ -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."
Expand Down

0 comments on commit 8835b10

Please sign in to comment.