Maximum Carnage

Modの紹介

グロテスクなゴア表現を追加します。D-Wonさんの作品です。

Nexus

配布の設定は要確認

Actorにスペルを配布するためにSPIDを使用しています。SPIDの設定ファイルはMaximumCarnage_DISTR.iniになります。この設定は修正した方がいいかもしれません。

NPCの設定ですが、私の環境ではLeveled NPCの一部に配布されていませんでした。そこでFox Raceを加えることで強引に配布させたところ、概ね大丈夫のようでした。

;MC_Gore Human
Spell = 0x0012CBF6 - MaximumCarnage.esp | ActorTypeNPC , Charmed Vigilant , Spellsword | NONE | NONE | NONE | NONE | 100
Spell = 0x0012CBF6 - MaximumCarnage.esp | NONE | 0x00109C7C
;MC_Gore Human Magic
Spell = 0x001FF321 - MaximumCarnage.esp | ActorTypeNPC , Charmed Vigilant , Spellsword | NONE | NONE | NONE | NONE | 100
Spell = 0x001FF321 - MaximumCarnage.esp | NONE | 0x00109C7C

ドラウグルはなぜか名前指定になっています。これだと日本語化環境では配布されません。Faction指定にしたら大丈夫そうでした。

;MC_Gore Draugr
;Spell = 0x0012CBFB - MaximumCarnage.esp | Draugr , Restless Draugr , Draugr Wight , Draugr Scourge , Draugr Deathlord , Draugr Overlord , Draugr Wight Lord , Draugr Scourge Lord , Draugr Death Overlord , Draugr Thrall , Red Eagle , Mikrul Gauldurson , Sigdis Gauldurson , Jyrik Gauldurson , Guardian Torsten , Guardian Saerek | NONE | NONE | NONE | NONE | 100
Spell = 0x0012CBFB - MaximumCarnage.esp | NONE | 0x0002430D , NOT 0x00109C7C
;MC_Gore Draugr Magic
;Spell = 0x00209190 - MaximumCarnage.esp | Draugr , Restless Draugr , Draugr Wight , Draugr Scourge , Draugr Deathlord , Draugr Overlord , Draugr Wight Lord , Draugr Scourge Lord , Draugr Death Overlord , Draugr Thrall , Red Eagle , Mikrul Gauldurson , Sigdis Gauldurson , Jyrik Gauldurson , Guardian Torsten , Guardian Saerek | NONE | NONE | NONE | NONE | 100
Spell = 0x00209190 - MaximumCarnage.esp | NONE | 0x0002430D , NOT 0x00109C7C

スクリプトの負荷に注意

スペルが物理攻撃版と魔法攻撃版でわかれています。

以降、NPCの場合で解説します。

物理攻撃版

物理攻撃版はOnHitイベントを捕捉していますが、ほぼNo Waitなので、状況次第では高負荷になります。

わざわざBusyステートに移行させているにもかかわらず、条件に一致しない場合はステートを即戻しているので、あまり負荷対策になっていません。1秒ほど待機時間を入れると、かなり負荷が下がると思います。ただし、処理を間引くことで負荷を下げているため、攻撃が重なると無視されやすくなる点には注意します。応答性と負荷のトレードオフなので仕方がないです。

mc_deatheffectsscript.psc (papyrus)

; 普段はReadyステートに遷移させておく
Event OnEffectStart(Actor Target, Actor Caster)
    ; 中略
    GoToState("Ready")
EndEvent

; ステート遷移用関数を定義する
Function GoToReady()
EndFunction

Function GoToKillmove()
EndFunction

; 普段のステート
State Ready
    Event OnAnimationEvent(ObjectReference akSource, string asEventName)
        ; 略
    EndEvent

    ; 攻撃を受けたらBusyステートに遷移して、しばらくしたらReadyステートに戻る
    Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
        GotoState("Busy")
        ; 中略
        Utility.Wait(1.0)
        GoToReady()
    EndEvent
EndState

; 攻撃を受けてしばらくOnHitを無視しているときのステート
State Busy
    ; BusyステートからReadyステートに戻る
    Function GoToReady()
        GotoState("Ready")
    EndFunction
EndState

; キルムーブの発動を検知している間のステート
State Killmove
    ; 自分が攻撃側であることが判明したらReadyステートに戻る
    ; 自分が被害者側であることが判明したらBusyステートに遷移する
    Event OnAnimationEvent(ObjectReference akSource, string asEventName)
        if asEventName == "Decapitate"
            GotoState("Busy")
        elseif asEventName == "weaponSwing"
            GoToReady()
            killmove = false
            UnregisterForAnimationEvent(akSource, "weaponSwing")
        endif
    EndEvent

    Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
        GotoState("Busy")

        if !(akSource as Weapon)
            ; 武器でないなら無視
            Utility.Wait(1.0)
            GoToKillmove()
        elseif !TargetActor.IsDead()
            ; 死んでないなら無視
            Utility.Wait(1.0)
            GoToKillmove()
        else
            ; 武器によって殺されたので処理
            position = (TargetRef.GetHeadingAngle(akAggressor) as Int)
            weap = (akSource as Weapon)
            GotoState("kState" + weap.GetWeaponType())
        endif
    EndEvent

    Function GoToReady()
        GotoState("Ready")
    EndFunction

    Function GoToKillmove()
        GotoState("Killmove")
    EndFunction
EndState

魔法攻撃版

魔法版はOnMagicEffectApplyイベントを捕捉しています。これは非常に悪手だと思います。特にAbility型のCloakスペルとの相性が最悪で、あっという間に高負荷になるのでOnHitを使うように書き換えた方がいいでしょう。

mc_deatheffectsmagicscript.psc (papyrus)

Int lastEffectType = 0

; 普段はReadyステートに遷移しておく
Event OnEffectStart(Actor Target, Actor Caster)
    ; 中略
    ;RegisterForAnimationEvent(TargetRef, "KillMoveStart")
    GoToState("Ready")
EndEvent

; OnCellDetachは全ステートで受け取る
Event OnCellDetach()
    if TargetActor.IsDead()
        if torched
            TargetActor.AttachAshPile(DefaultAshPile1)
            TargetActor.SetCriticalStage(4)
        elseif burned
            TargetActor.AttachAshPile(MC_AshPile1)
            TargetActor.SetCriticalStage(4)
        elseif iced
            TargetActor.AttachAshPile(DefaultAshPileIce)
            TargetActor.SetCriticalStage(4)
        else
            RemoveGore()
        endif
    endif
EndEvent

; 死んだらゴア発動
Event OnDeath(Actor akKiller)
    UnregisterForUpdate()
    ;UnregisterForAnimationEvent(TargetRef, "weaponSwing")
    ;UnregisterForAnimationEvent(TargetRef, "KillMoveStart")
    GotoState("Dead")

    if lastEffectType >= 1
        ElementalType(lastEffectType)
    endif
EndEvent

; 魔法の効果切れ
Event OnUpdate()
    lastEffectType = 0
EndEvent

; ステート遷移用
Function GoToReady()
EndFunction

; エンチャントからMagic Effectを得る
Function ParseEnchantmentMagicEffect(Enchantment akEnchantment)
    Int iNumEffect = akEnchantment.GetNumEffects()
    Int i = 0

    while i < iNumEffect
        MagicEffect kEffect = akEnchantment.GetNthEffectMagicEffect(i)

        if kEffect.GetAssociatedSkill() == "Destruction"
            Int iType = 0

            if kEffect.HasKeyword(MagicDamageFire)
                iType = 1
            elseif kEffect.HasKeyword(MagicDamageFrost)
                iType = 2
            elseif kEffect.HasKeyword(MagicDamageShock)
                iType = 3
            endif

            if iType >= 1
                UnregisterForUpdate()
                RegisterForSingleUpdate(5.0)
                dt = kEffect.GetDeliveryType()
                lastEffectType = iType
                ;Debug.Trace(TargetActor + " lastEffectType=" + lastEffectType)
                return
            endif
        endif

        i += 1
    endwhile
EndFunction

; スペルからMagic Effectを得る
Function ParseSpellMagicEffect(Spell akSpell)
    Int iNumEffect = akSpell.GetNumEffects()
    Int i = 0

    while i < iNumEffect
        MagicEffect kEffect = akSpell.GetNthEffectMagicEffect(i)

        if kEffect.GetAssociatedSkill() == "Destruction"
            Int iType = 0

            if kEffect.HasKeyword(MagicDamageFire)
                iType = 1
            elseif kEffect.HasKeyword(MagicDamageFrost)
                iType = 2
            elseif kEffect.HasKeyword(MagicDamageShock)
                iType = 3
            endif

            if iType >= 1
                UnregisterForUpdate()
                RegisterForSingleUpdate(5.0)
                dt = kEffect.GetDeliveryType()
                lastEffectType = iType
                ;Debug.Trace(TargetActor + " lastEffectType=" + lastEffectType)
                return
            endif
        endif

        i += 1
    endwhile
EndFunction

State Ready
    Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
        GotoState("Busy")

        if akSource
            if akSource as Spell
                mage = akAggressor
                ParseSpellMagicEffect(akSource as Spell)
            elseif akSource as Enchantment
                mage = akAggressor
                ParseEnchantmentMagicEffect(akSource as Enchantment)
            endif
        endif

        Utility.Wait(1.0)
        GoToReady()
    EndEvent
EndState

State Busy
    Function GoToReady()
        GoToState("Ready")
    EndFunction
EndState

Function ElementalType(Int type)
    if !iced
        if type == 1
            if !burned
                ElementalDeath(1)
            endif
        elseif type == 2
            ElementalDeath(2)
        elseif type == 3
            if !torched
                ElementalDeath(3)
            endif
        endif
    endif
EndFunction

まず、 OnMagicEffectApplyとOnAnimationEventは使わないのですべて削除します。Killmove関連も使いませんので削除です。

OnHitで攻撃を受けたら、スペルまたはエンチャントである場合に属性を記憶します。リセットは5秒としました。属性攻撃を受けてから5秒以内に死んだらゴアを発動です。

エンチャントは物理攻撃、エンチャント攻撃の順にOnHitが発火した場合はエンチャント側が無視されてしまいます。これをエンチャント攻撃だけを受け取る方法が私にはわかりませんでした。ですが、雷撃付きクロスボウでテストした結果、概ね雷撃ゴアが発生したので大丈夫みたいです。

ElementalType関数はMagic Effectを受け取る処理から属性番号を受け取る処理に置き換えます。

あるいは、スペルに一定時間継続する効果を追加して、その効果の有無で判定すればOnHitすらなくせます。これなら判定漏れはありません。ただし競合しやすいのと、すべての破壊魔法に手を入れる必要があるのが難点です。

他の種族について

それぞれ物理と魔法でわかれていますが、基本的な考え方は同じです。

専用の処理が追加されていて微妙に異なっている点には注意します。

ウ○チについて

初期のバージョンではたくさん出たらしいです。いまのバージョンはMCMで無効にできて、有効にしても1回しか出ないようです。

手を加えてたくさん出るようにしてみました。

処理は魔法版のTorch関数になります。種族によってはないので注意します。

Torch関数 (papyrus)

Function Torch()
    torched = true

    ; なんかのエフェクトを再生させている
    (MC_EffectShaders.GetAt(0) as EffectShader).Play(TargetRef, 1.0)
    (MC_EffectShaders.GetAt(2) as EffectShader).Play(TargetRef, 2.0)

    ; 死体に物理的な力を加えることで痙攣を表現している
    TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 50.0)

    ; 適宜間隔をあける
    Utility.Wait(0.5)

    TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 100.0)

    Utility.Wait(0.5)

    TargetRef.ApplyHavokImpulse(0.000000, 1.000000, 1.00000, 100.0)

    Utility.Wait(1.0)

    (MC_EffectShaders.GetAt(2) as EffectShader).Play(TargetRef, 10.0)

    TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 50.0)

    Utility.Wait(Utility.RandomFloat(0.2, 1.0))

    TargetRef.ApplyHavokImpulse(1.000000, 0.000000, 1.00000, 50.0)

    Utility.Wait(Utility.RandomFloat(0.2, 1.0))

    TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 50.0)

    Utility.Wait(Utility.RandomFloat(0.2, 1.0))

    TargetRef.ApplyHavokImpulse(1.000000, 0.000000, 1.00000, 50.0)

    Utility.Wait(Utility.RandomFloat(0.2, 1.0))

    TargetRef.ApplyHavokImpulse(0.000000, 1.000000, 1.00000, 50.0)

    Utility.Wait(Utility.RandomFloat(0.2, 1.0))

    TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 50.0)

    Utility.Wait(Utility.RandomFloat(0.2, 1.0))

    if MC_Poop.GetValue() == 1
        ; これはNoneになるから動いていない
        ;(MC_BloodSpellsHumanoids.GetAt(10) as Spell).Cast(TargetRef, TargetRef)

        ; esp側で6番目を足しておく
        (MC_BloodSpellsHumanoids.GetAt(6) as Spell).Cast(TargetRef, TargetRef)

        ; explosionを発生させる。これがウ○チになる
        explod1 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)

        TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 150.0)
        Utility.Wait(1.0)
    endif

    (MC_EffectShaders.GetAt(4) as EffectShader).Play(TargetRef, -1.0)

    if MC_Poop.GetValue() == 1 && Utility.RandomInt(0, 9) == 0
        ; たくさん出すモード(10%で発生)
        explod2 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
        TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 150.0)
        Utility.Wait(0.2)

        explod3 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
        Utility.Wait(0.2)

        explod4 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
        Utility.Wait(0.2)

        Int i = Utility.RandomInt(0, 2)

        while i > 0
            i -= 1

            explod2.Delete()
            explod2 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 150.0)
            Utility.Wait(0.2)

            explod3.Delete()
            explod3 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            Utility.Wait(0.2)

            explod4.Delete()
            explod4 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            Utility.Wait(0.2)

            explod2.Delete()
            explod2 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 150.0)
            Utility.Wait(0.2)

            explod3.Delete()
            explod3 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            Utility.Wait(0.2)

            explod4.Delete()
            explod4 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            Utility.Wait(0.2)
        endwhile
    else
        ; 少し出すモード(それぞれ50%で発生、最大で3回分追加)
        if MC_Poop.GetValue() == 1 && Utility.RandomInt(0, 1) == 0
            explod2 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 150.0)
        else
            TargetRef.ApplyHavokImpulse(0.000000, 1.000000, 1.00000, 50.0)
        endif

        Utility.Wait(Utility.RandomFloat(0.2, 1.0))

        if MC_Poop.GetValue() == 1 && Utility.RandomInt(0, 1) == 0
            explod3 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 150.0)
        else
            TargetRef.ApplyHavokImpulse(0.000000, 1.000000, 1.00000, 50.0)
        endif

        Utility.Wait(Utility.RandomFloat(0.2, 1.0))

        if MC_Poop.GetValue() == 1 && Utility.RandomInt(0, 1) == 0
            explod4 = TargetRef.placeatme(MC_Explosions.GetAt(4) as Explosion, 1, false, false)
            TargetRef.ApplyHavokImpulse(0.000000, 0.000000, 1.00000, 150.0)
        else
            TargetRef.ApplyHavokImpulse(0.000000, 1.000000, 1.00000, 50.0)
        endif
    endif

    ; 発生させたexplosionは必ず消すこと
    if MC_Poop.GetValue() == 1
        Utility.Wait(1.0)

        if explod1
            explod1.Delete()
            explod1 = none
        endif

        if explod2
            explod2.Delete()
            explod2 = none
        endif

        if explod3
            explod3.Delete()
            explod3 = none
        endif

        if explod4
            explod4.Delete()
            explod4 = none
        endif
    endif
EndFunction

互換性について

キルムーブの誤認識

キルムーブ発生時にゴアを適用しているため、特殊な状況下ではゴアが誤適用されてしまいます。

具体的にはzxlice BackStab and Parry SSE - Script Freeのバックスタブなどが該当します。

まず、不死属性 (IsEssentialがTrueを返すActor) が付いているNPCはゴアの対象外なので問題ありません。

不死属性が付いていないNPCがバックスタブを受けると、キルムーブを受けたと判定されてゴアが適用されてしまします。

フォロワーはEssentialが付いていないことがほとんどです。かわりにProtectedが付いています。なのでゴアが誤適用されうるのです。

キルムーブの処理で対象が死んでいることを確認するとよいでしょう。

刀のパワーアタックを認識しない

Immersive Weaponsで追加される刀ですが、パワーアタックを出したにもかかわらず、OnHitイベントのabPowerAttackが常にFalseを渡すせいで、刀ではゴアが発動しません。

Immersive Weaponsの刀にキーワードを付与してアニメーションを変更する例をDynamic Animation Replacerで解説しています。この環境での話になります。アニメーションを変更しなければ問題ないのかもしれません。

刀でパワーアタックと認識されない問題の修正 (papyrus)

Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
    GotoState("Busy")

    if TargetActor.IsDead()
        if abPowerAttack
            ; ここにゴア適用の処理
        elseif (akSource as Weapon) && akAggressor
            if akSource.HasKeywordString("WeapTypeKatana")
                ; ここにゴア適用の処理
                ; 上と全く同じでよい
            elseif akAggressor.HasKeyword(ActorTypeCreature)
                ; ここにクリーチャー用のゴア適用の処理
                ; 上とは少し違うので注意
            endif
        endif

        ; 死んでいるならステートを戻さずにこのまま終わってよい
        return
    endif

    Utility.Wait(1.0)
    GoToReady()
EndEvent

刀でとどめを刺した場合は常にゴアを発動させるようにしました。理想はパワーアタックを認識できることですが、abPowerAttack引数が使えないとなるとかなり回りくどい方法を取らないと無理なので、これで妥協です。

何が何でもパワーアタックを判定させるなら、イベントでパワーアタックの開始と終了を見てフラグを立てる、パワーアタック中のみ発動するスペルを用意してスペルの発動を見る、といった方法になると思います。

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