どんなModなのか
攻撃を受けるとランダムで身体が血で汚れます。これだけならバニラにもありますが、このModの血は放っておいても取れません。血の表現は4段階あり、徐々にひどくなっていきます。
時間が経つと汚れていきます。こちらも4段階あります。こちらはバニラにはない機能です。血を放置すると汚れにかわります。
雨にうたれるか水の中で泳ぐと、血はすぐに落ちて汚れにかわります。ひどい汚れは少し落ちますが、完全には落ちません。
このModで追加される石鹸を使って身体を洗うことで、完全に綺麗な状態になります。
ここまでがプレイヤーだけの機能です。
NPCの機能はまったくの別物
Descriptionを読んでもよくわからなかったので、プラグインのレコードとスクリプトのソースコードを読んで解析しました。
鍛冶屋と浮浪者は常に汚れています。どうにかして洗ってあげると落ちるようです。興味がなかったのでよくわかりません。それ以外のNPCは汚れません。
すべてのNPCと一部のクリーチャーは返り血の表現があります。ただし、プレイヤーのものとは全く異なり、ヘルスがある程度低下するとランダムで血が表示されて、時間経過で消えるというものです。
フォロワーに求めていた機能としては、思っていたのと違ったので残念でした。そこで、プレイヤーと同等の処理を作ってみました。
SPIDの設定ファイルを変更
アビリティ(常時発動型のスペル)の配布はSPIDを使っています。ですから設定ファイルを少し書き換えるだけで済むため簡単です。
DirtandBlood__DISTR.ini (ini)
; All NPCs except Follower
;Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|ActorTypeNPC|NONE|NONE|NONE|100
Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|ActorTypeNPC|-0x5C84D|NONE|NONE|100
; All NPCs except Follower
;Spell = 0x822~Dirt and Blood - Dynamic Visuals.esp|ActorTypeNPC|NONE|NONE|NONE|100
Spell = 0x822~Dirt and Blood - Dynamic Visuals.esp|ActorTypeNPC|-0x5C84D|NONE|NONE|100
;
Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|ActorTypeAnimal|NONE|NONE|NONE|100
Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|ActorTypeDragon|NONE|NONE|NONE|100
Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|ActorTypeGiant|NONE|NONE|NONE|100
Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|ActorTypeTroll|NONE|NONE|NONE|100
Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|-PlayerKeyword|0xCDD84|NONE|NONE|100
Spell = 0xDE6~Dirt and Blood - Dynamic Visuals.esp|NONE|0x131F4|NONE|NONE|100
; Dirt for Follower
Spell = 0x800~Dirt and Blood - Follower.esp|ActorTypeNPC|0x5C84D
自前パッチのプラグイン名を Dirt and Blood - Follower.esp としています。
パッチを作る
プラグインの名前は Dirt and Blood - Follower.esp です。
フォロワー専用アビリティと、プレイヤーとフォロワーの洗浄用クエストを作ります。
それから、Dirt and Blood - Dynamic Visuals.espに含まれる一部のSpellはプレイヤー前提の作りになってしまっているので、フォロワーにも使い回せるようにConditionsのRun OnをPlayerRefからSubjectにしています。
フォロワー用アビリティ
常時発動型のアビリティとしてSpellとMagic Effectを用意します。条件は特に要りません。
以下のスクリプトを付けます。
Dirty_FollowerScript (papyrus)
Scriptname Dirty_FollowerScript extends ActiveMagicEffect
Spell Dirty_Spell_Blood1
Spell Dirty_Spell_Blood2
Spell Dirty_Spell_Blood3
Spell Dirty_Spell_Blood4
Spell Dirty_Spell_Dirt1
Spell Dirty_Spell_Dirt2
Spell Dirty_Spell_Dirt3
Spell Dirty_Spell_Dirt4
Spell Dirty_Spell_Clean
Spell Dirty_Spell_Swimming
Spell Dirty_Spell_IsRaining
GlobalVariable Dirty_RandomDirtDuration
GlobalVariable Dirty_PlayerHitBloodyChance
Actor MySelf
Event OnEffectStart(Actor akTarget, Actor akCaster)
Debug.Trace("Dirty_FollowerScript OnEffectStart akTarget=" + akTarget)
MySelf = akTarget
Dirty_Spell_Blood1 = Game.GetFormFromFile(0x809, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Blood2 = Game.GetFormFromFile(0x80A, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Blood3 = Game.GetFormFromFile(0x80B, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Blood4 = Game.GetFormFromFile(0x839, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Dirt1 = Game.GetFormFromFile(0x806, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Dirt2 = Game.GetFormFromFile(0x807, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Dirt3 = Game.GetFormFromFile(0x808, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Dirt4 = Game.GetFormFromFile(0x838, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Clean = Game.GetFormFromFile(0x80C, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_Swimming = Game.GetFormFromFile(0x825, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_Spell_IsRaining = Game.GetFormFromFile(0x85D, "Dirt and Blood - Dynamic Visuals.esp") as Spell
Dirty_RandomDirtDuration = Game.GetFormFromFile(0x829, "Dirt and Blood - Dynamic Visuals.esp") as GlobalVariable
Dirty_PlayerHitBloodyChance = Game.GetFormFromFile(0x82A, "Dirt and Blood - Dynamic Visuals.esp") as GlobalVariable
MySelf.AddSpell(Dirty_Spell_Swimming)
MySelf.AddSpell(Dirty_Spell_IsRaining)
UntilNextTime()
EndEvent
Event OnUpdateGameTime()
Debug.Trace("Dirty_FollowerScript OnUpdateGameTime MySelf=" + MySelf)
If Dirty_RandomDirtDuration.GetValue() == 3
; nothing
else
If MySelf.HasSpell(Dirty_Spell_Dirt4)
; nothing
elseif MySelf.HasSpell(Dirty_Spell_Dirt3)
MySelf.AddSpell(Dirty_Spell_Dirt4, false)
MySelf.RemoveSpell(Dirty_Spell_Dirt3)
elseif MySelf.HasSpell(Dirty_Spell_Dirt2)
MySelf.AddSpell(Dirty_Spell_Dirt3, false)
MySelf.RemoveSpell(Dirty_Spell_Dirt2)
elseif MySelf.HasSpell(Dirty_Spell_Dirt1)
MySelf.AddSpell(Dirty_Spell_Dirt2, false)
MySelf.RemoveSpell(Dirty_Spell_Dirt1)
elseif MySelf.HasSpell(Dirty_Spell_Clean)
MySelf.AddSpell(Dirty_Spell_Dirt1, false)
MySelf.RemoveSpell(Dirty_Spell_Clean)
elseif MySelf.HasSpell(Dirty_Spell_Blood4)
MySelf.AddSpell(Dirty_Spell_Dirt4, false)
MySelf.RemoveSpell(Dirty_Spell_Blood4)
elseif MySelf.HasSpell(Dirty_Spell_Blood3)
MySelf.AddSpell(Dirty_Spell_Dirt4, false)
MySelf.RemoveSpell(Dirty_Spell_Blood3)
elseif MySelf.HasSpell(Dirty_Spell_Blood2)
MySelf.AddSpell(Dirty_Spell_Dirt3, false)
MySelf.RemoveSpell(Dirty_Spell_Blood2)
elseif MySelf.HasSpell(Dirty_Spell_Blood1)
MySelf.AddSpell(Dirty_Spell_Dirt2, false)
MySelf.RemoveSpell(Dirty_Spell_Blood1)
Endif
Endif
UntilNextTime()
EndEvent
Function UntilNextTime()
Int RandomTime
If Dirty_RandomDirtDuration.GetValue() == 0
RandomTime = Utility.RandomInt(48, 96)
elseif Dirty_RandomDirtDuration.GetValue() == 1
RandomTime = Utility.RandomInt(24, 48)
elseif Dirty_RandomDirtDuration.GetValue() == 2
RandomTime = Utility.RandomInt(12, 24)
elseif Dirty_RandomDirtDuration.GetValue() == 3
RandomTime = 24
endif
RegisterForSingleUpdateGameTime(RandomTime)
EndFunction
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
;Debug.Trace("Dirty_FollowerScript OnHit MySelf=" + MySelf)
GoToState("Busy")
if !abHitBlocked && (akSource as Weapon) && !MySelf.HasSpell(Dirty_Spell_Blood4) && MySelf.GetActorValuePercentage("Health") < 1.0
int hit_chance
float hit_applies
hit_chance = Utility.RandomInt(1, 100)
hit_applies = Dirty_PlayerHitBloodyChance.GetValue()
if abPowerAttack || abSneakAttack || (MySelf.GetActorValue("Health") < 50.0)
hit_chance -= 20
endif
;Debug.Trace("Dirty_FollowerScript OnHit hit_chance=" + hit_chance + " <= hit_applies=" + hit_applies)
if hit_chance <= hit_applies
if MySelf.HasSpell(Dirty_Spell_Blood3)
MySelf.AddSpell(Dirty_Spell_Blood4, false)
MySelf.RemoveSpell(Dirty_Spell_Blood3)
elseif MySelf.HasSpell(Dirty_Spell_Blood2)
MySelf.AddSpell(Dirty_Spell_Blood3, false)
MySelf.RemoveSpell(Dirty_Spell_Blood2)
Elseif MySelf.HasSpell(Dirty_Spell_Blood1)
MySelf.AddSpell(Dirty_Spell_Blood2, false)
MySelf.RemoveSpell(Dirty_Spell_Blood1)
elseif MySelf.HasSpell(Dirty_Spell_Dirt4)
MySelf.AddSpell(Dirty_Spell_Blood4, false)
MySelf.RemoveSpell(Dirty_Spell_Dirt4)
elseif MySelf.HasSpell(Dirty_Spell_Dirt3)
MySelf.AddSpell(Dirty_Spell_Blood3, false)
MySelf.RemoveSpell(Dirty_Spell_Dirt3)
elseif MySelf.HasSpell(Dirty_Spell_Dirt2)
MySelf.AddSpell(Dirty_Spell_Blood2, false)
MySelf.RemoveSpell(Dirty_Spell_Dirt2)
elseif MySelf.HasSpell(Dirty_Spell_Dirt1)
MySelf.AddSpell(Dirty_Spell_Blood1, false)
MySelf.RemoveSpell(Dirty_Spell_Dirt1)
elseif MySelf.HasSpell(Dirty_Spell_Clean)
MySelf.AddSpell(Dirty_Spell_Blood1, false)
MySelf.RemoveSpell(Dirty_Spell_Clean)
else
MySelf.AddSpell(Dirty_Spell_Blood1, false)
endif
endif
endif
Utility.Wait(1.0)
GoToState("")
EndEvent
State Busy
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
EndEvent
EndState
とりあえず、これでフォロワーに血がついて汚れるようになります。
洗浄用クエスト
クエストはStart Game Enabledをオフにしておきます。優先度はとにかく高く、100にしました。
クエストに以下のスクリプトを付けます。
Dirty_QuestSoapScript (papyrus)
Scriptname Dirty_QuestSoapScript extends Quest
Actor Property PlayerRef Auto
Spell Property Dirty_SoapEffectSpell Auto
Spell Property Dirty_Spell_Dirt1 Auto
Spell Property Dirty_Spell_Dirt2 Auto
Spell Property Dirty_Spell_Dirt3 Auto
Spell Property Dirty_Spell_Dirt4 Auto
Spell Property Dirty_Spell_Blood1 Auto
Spell Property Dirty_Spell_Blood2 Auto
Spell Property Dirty_Spell_Blood3 Auto
Spell Property Dirty_Spell_Blood4 Auto
Spell Property Dirty_Spell_Clean Auto
Sound Property Dirty_WashingMarkerSound auto
Message Property Dirty_MessageBathWeapon Auto
Message Property Dirty_MessageBathCombat Auto
Message Property Dirty_MessageBathSoap Auto
Formlist property Dirty_ListofSoaps Auto
GlobalVariable Property Dirty_WashingAutomaticClothes Auto
GlobalVariable Property Dirty_SoundEffect Auto
Bool Property IsPlayerReady = false Auto Hidden
Form[] Clothing
Event OnInit()
if IsRunning()
IsPlayerReady = UseSoap(PlayerRef)
if IsPlayerReady
MySendModEvent("DirtyBatheStart")
; force 3rd person
Game.ForceThirdPerson()
Game.DisablePlayerControls(True, True, False, False, True, False, True)
if Dirty_WashingAutomaticClothes.GetValue() == 1
Clothing = Undress(PlayerRef)
endif
Utility.Wait(0.1)
PlayBatheAnimation(PlayerRef)
if Dirty_WashingAutomaticClothes.GetValue() == 1
Reequip(PlayerRef, Clothing)
endif
; enable controls
Game.EnablePlayerControls()
MySendModEvent("DirtyBatheFinish")
else
Debug.Notification("You have run out of soap")
endif
Utility.Wait(10.0)
Stop()
endif
EndEvent
Bool Function UseSoap(Actor akTarget)
If akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(0)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(0))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(1)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(1))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(2)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(2))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(3)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(3))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(4)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(4))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(5)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(5))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(6)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(6))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(7)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(7))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(8)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(8))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(9)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(9))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(10)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(10))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(11)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(11))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(12)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(12))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(13)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(13))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(14)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(14))
elseif akTarget.GetItemCount(Dirty_ListofSoaps.GetAt(15)) > 0
akTarget.RemoveItem(Dirty_ListofSoaps.GetAt(15))
else
return false
Endif
return true
EndFunction
Form[] Function Undress(Actor akTarget)
weapon weapon1
weapon weapon2
if Game.GetModByName("All Geared Up Derivative.esp") != 255
Spell Display = Game.GetFormFromFile(0x02003000, "All Geared Up Derivative.esp") As Spell
akTarget.RemoveSpell(Display)
endif
Form[] kClothing = new Form[33]
Int Index = kClothing.Length
While Index
Index -= 1
Int ArmorSlotMask = Armor.GetMaskForSlot(Index + 30)
kClothing[Index] = akTarget.GetWornForm(ArmorSlotMask)
If kClothing[Index] && kClothing[Index].IsPlayable()
akTarget.UnequipItem(kClothing[Index], False, True)
EndIf
EndWhile
Weapon1 = akTarget.GetEquippedWeapon()
Weapon2 = akTarget.GetEquippedWeapon(True)
;akTarget.Unequipall()
if Weapon1
akTarget.UnequipItem(Weapon1, False, True)
endif
if Weapon2
akTarget.UnequipItem(Weapon2, False, True)
endif
return kClothing
EndFunction
Function Reequip(Actor akTarget, Form[] akClothing)
if Game.GetModByName("All Geared Up Derivative.esp") != 255
Spell Display = Game.GetFormFromFile(0x02003000, "All Geared Up Derivative.esp") As Spell
akTarget.AddSpell(Display)
endif
Int ClothingIndex = akClothing.Length
While ClothingIndex
ClothingIndex -= 1
If akClothing[ClothingIndex]
akTarget.EquipItem(akClothing[ClothingIndex], False, True)
EndIf
EndWhile
;akTarget.EquipItemEx(Weapon1, 1, false, true)
;akTarget.EquipItemEx(Weapon2, 2, false, true)
EndFunction
Function PlayBatheAnimation(Actor akTarget)
if Dirty_SoundEffect.GetValue() == 1
int instanceID = Dirty_WashingMarkerSound.play(akTarget)
Sound.SetInstanceVolume(instanceID, 1.0)
endif
; lather up
Debug.SendAnimationEvent(akTarget, "IdleForceDefaultState")
Utility.Wait(0.1)
Debug.SendAnimationEvent(akTarget, "IdleSearchingChest")
;Utility.Wait(3.0)
Utility.Wait( Utility.RandomFloat(3.0, 3.5) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(0.3)
Debug.SendAnimationEvent(akTarget, "IdleWarmArms")
;Utility.Wait(1.5)
Utility.Wait( Utility.RandomFloat(1.5, 2.0) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
akTarget.AddSpell(Dirty_SoapEffectSpell, False)
Utility.Wait(0.3)
Debug.SendAnimationEvent(akTarget, "IdleWarmArms")
;Utility.Wait(1.5)
Utility.Wait( Utility.RandomFloat(1.5, 2.0) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(1.0)
Debug.SendAnimationEvent(akTarget, "IdleWipeBrow")
;Utility.Wait(3.0)
Utility.Wait( Utility.RandomFloat(3.0, 3.5) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(0.3)
; wash
Debug.SendAnimationEvent(akTarget, "IdleWarmArms")
;Utility.Wait(1.5)
Utility.Wait( Utility.RandomFloat(1.5, 2.0) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(0.3)
Debug.SendAnimationEvent(akTarget, "IdleWarmArms")
;Utility.Wait(1.5)
Utility.Wait( Utility.RandomFloat(1.5, 2.0) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(0.3)
Debug.SendAnimationEvent(akTarget, "IdleWarmArms")
;Utility.Wait(1.5)
Utility.Wait( Utility.RandomFloat(1.5, 2.0) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(0.3)
Debug.SendAnimationEvent(akTarget, "IdleWarmArms")
;Utility.Wait(1.5)
Utility.Wait( Utility.RandomFloat(1.5, 2.0) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(1.0)
; rinse off
Debug.SendAnimationEvent(akTarget, "IdleWarmHandsCrouched")
;Utility.Wait(3.0)
Utility.Wait( Utility.RandomFloat(3.0, 3.5) )
akTarget.RemoveSpell(Dirty_SoapEffectSpell)
akTarget.RemoveSpell(Dirty_Spell_Dirt1)
akTarget.RemoveSpell(Dirty_Spell_Dirt2)
akTarget.RemoveSpell(Dirty_Spell_Dirt3)
akTarget.RemoveSpell(Dirty_Spell_Dirt4)
akTarget.RemoveSpell(Dirty_Spell_Blood1)
akTarget.RemoveSpell(Dirty_Spell_Blood2)
akTarget.RemoveSpell(Dirty_Spell_Blood3)
akTarget.RemoveSpell(Dirty_Spell_Blood4)
akTarget.AddSpell(Dirty_Spell_Clean, False)
Utility.Wait(1.0)
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(1.0)
Debug.SendAnimationEvent(akTarget, "IdleWipeBrow")
;Utility.Wait(3.0)
Utility.Wait( Utility.RandomFloat(3.0, 3.5) )
Debug.SendAnimationEvent(akTarget, "IdleStop")
Utility.Wait(0.3)
EndFunction
Function MySendModEvent(String asEventName)
Int h = ModEvent.Create(asEventName)
if h
ModEvent.Send(h)
endif
EndFunction
近くにいるフォロワーを検知するReference Aliasを好きな数だけ用意します。
条件はこんな感じです。
現在雇用中のActor、ロード中のセル内限定、プレイヤーから2000ユニット以内です。
OptionalとAllow Reservedをオンにするのを忘れないようにします。
洗浄中にじっとさせておくPackageを作って割り当てていますが、フォロワー拡張を使うのであればなくてもいいと思います。
それぞれのReference Aliasには以下のスクリプトを付けます。
Dirty_AliasSoapScript (papyrus)
Scriptname Dirty_AliasSoapScript extends ReferenceAlias
Form[] Clothing
Event OnInit()
Actor kSelfRef = GetActorReference()
if !kSelfRef
return
endif
Dirty_QuestSoapScript kQuest = GetOwningQuest() as Dirty_QuestSoapScript
if kQuest.IsRunning()
Utility.Wait(2.0)
if kQuest.IsPlayerReady
if kQuest.Dirty_WashingAutomaticClothes.GetValue() == 1
Clothing = kQuest.Undress(kSelfRef)
endif
;Utility.Wait(0.1)
Utility.Wait( Utility.RandomFloat(0.1, 3.0) )
kQuest.PlayBatheAnimation(kSelfRef)
if kQuest.Dirty_WashingAutomaticClothes.GetValue() == 1
kQuest.Reequip(kSelfRef, Clothing)
endif
endif
endif
EndEvent
クエストを稼働させると洗浄開始
何らかの形で上記のクエストを開始させる手段を用意します。私は自前のフォロワー拡張に追加しました。
石鹸の所持チェックがあって、持っていたらプレイヤーとフォロワーが同時に洗浄を始めます。洗い終わったらクエストも停止します。
このModは元々はインベントリの石鹸を使うと洗い始めるのですが、コンテナ間を移動させるとうまく動作しなくて、一度捨てて拾い直すと動くようになります。また、使ったらメニューを手動で閉じる必要があります。これらが面倒なので、パッチに全部含めました。
フォロワーのエフェクトシェーダーがなぜかたまに消える
スペル(アビリティ)は、紐づいているMagic Effectの効果が発動し、エフェクトシェーダーが表示され、付いているスクリプトが稼働します。
原因はよくわかりませんが、Magic Effectは動いているのにエフェクトシェーダーだけが消えることがあります。そもそもエフェクトシェーダーは1つのActorにたいして同時に1つしか表示できません。フレッシュ系の魔法、破壊魔法を受けた時の炎や氷結、ゴア系Modのゴア表現などと被ります。後から発動したMagic Effectが優先されるようです。
仕方がないので、自前のフォロワー拡張に定期的にフォロワーに血や汚れのアビリティを付け直す処理を加えてお茶を濁しました。
スペルのエフェクトシェーダーからRaceMenuのNiOverrideに変更する
スペルのエフェクトシェーダーは他のスペルと同時に使えないのですが、RaceMenuの肌にタトゥーなどを追加するスキンオーバーレイ機能を使って表現すると、スペルと同時に使用できるようになります。
Bathing in Skyrim Special Editionという同類の汚れを表現するModに、このNiOverrideを使う例があったので、それを参考に実装してみました。
既存の汚れや血のMagic Effectからエフェクトシェーダーの指定を取り除き、かわりにスクリプトを付けてオーバーレイの脱着処理を行います。
これでフレッシュ系魔法をかけても汚れが消えなくなりました。
エフェクトシェーダーはActor単位でかかるため、何を着ていても表面にエフェクトが描画されます。また、頭から足までまるごと対象になります。一方でオーバーレイの場合はノード単位なので、顔、手、足、胴体の4箇所でそれぞれ個別にエフェクトをかけることができます。また、肌にエフェクトが付くため、防具を着ていると隠れます。返り血はエフェクトシェーダーが、汚れはオーバーレイがいいと思いました。
オーバーレイは複数同時にかけられるので、返り血と汚れを同時に付けることも可能です。