Dirt and Blood – Dynamic Visual Effectsを改造してみた

Modを作ろう

返り血や汚れを追加します。フォロワーに対応させてみました。

どんな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箇所でそれぞれ個別にエフェクトをかけることができます。また、肌にエフェクトが付くため、防具を着ていると隠れます。返り血はエフェクトシェーダーが、汚れはオーバーレイがいいと思いました。

オーバーレイは複数同時にかけられるので、返り血と汚れを同時に付けることも可能です。

タイトルとURLをコピーしました