武器が壊れるようにしてみました。Horizonが用意した仕組みを使うと簡単です。
劣化に関する仕様
Fallout 3にはCND値というものがありました。武器を使っているとCND値が下がって性能が低下し、修理することでCND値が回復して性能が上昇するという仕組みです。
Skyrimにも強化値という名前で残っています。劣化の概念はありませんが、鍛冶で強化することで性能が上昇します。
Fallout 4では強化値もCND値も廃止になりました。かわりにアタッチメントで性能を強化することができます。アタッチメントは強化に限らず、設定次第であえて性能が下がるようにもできます。
Horizonはこのアタッチメントを使って劣化の概念を取り入れています。武器の初期状態として劣化していることがあるというだけで、一度修理してしまえばそれ以降は劣化することはありません。
設計
Horizonの劣化の仕組みを応用し、あるタイミングで再び劣化のアタッチメントを付与することで、武器を使用しているときの劣化、破損を実現してみます。
劣化のタイミングはいくつか考えられますが、ポイントが2つあります。まずゲームとして面白いかどうか、そして処理に無理がないかどうかです。
- 弾を発射したとき(銃器)、殴ったとき(近接武器)
- リロードしたとき(銃器)
- 防御したとき(近接武器)
- 装備、解除したとき
- 時間が経過したとき
時間経過で劣化はインベントリやコンテナの中身を精査して処理するということになりますが、すべての武器が劣化していくのは非現実的ですし、プレイヤーが入手した途端に劣化が始まるのも不自然なので、これはないと思います。
弾を発射したときに判定するのはありがちですが、オートマチックの場合、判定が高頻度で行われるので負荷が気になります。特に、アタッチメントの装着処理が重なると不具合の原因になるようなので、ますます難しくなります。
リロードが完了したときであれば、短時間で連続してリロードはできないので頻度の問題はなく、リロード完了時は武器を装備中であることが確定するため、何かと都合がよいのです。というわけでこれにしました。
近接武器の場合は殴ったときと防御したときでいいと思います。
サンプルソースコード
Questを1つ用意して紐付けます。
武器が劣化するサンプル (papyrus)
Scriptname SampleScript Extends Quest
Actor Property PlayerRef Const Auto Mandatory
ObjectMod Z_mod_Condition_Standard_1_Degraded1
ObjectMod Z_mod_Condition_Standard_2_Degraded2
ObjectMod Z_mod_Condition_Standard_3_Degraded3
ObjectMod Z_mod_Condition_Standard_4_Degraded4
ObjectMod Z_mod_Condition_Standard_D_Destroyed
Function Initialize()
RegisterForRemoteEvent(PlayerRef, "OnPlayerLoadGame")
EndFunction
Function Initialize2()
if Game.IsPluginInstalled("Z_Horizon.esp")
GoToState("HORIZON_READY")
else
GoToState("HORIZON_NOT_READY")
endif
EndFunction
Function ShowActorWeapon(Actor akTarget)
ObjectMod[] kOmodList = akTarget.GetWornItemMods(41)
if !kOmodList
Debug.Trace("ShowActorWeapon " + akTarget + " no weapon omod found")
return
endif
Int i = kOmodList.Length
while i > 0
i -= 1
ObjectMod kOmod = kOmodList[i]
Debug.Trace("omod " + i + " " + kOmod)
ObjectMod:PropertyModifier[] kProperties = kOmod.GetPropertyModifiers()
Int j = kProperties.Length
while j > 0
j -= 1
Debug.Trace(" properties " + j)
Debug.Trace(" target = " + kProperties[j].target)
Debug.Trace(" operator = " + kProperties[j].operator)
Debug.Trace(" object = " + kProperties[j].object)
Debug.Trace(" value1 = " + kProperties[j].value1)
Debug.Trace(" value2 = " + kProperties[j].value2)
endwhile
endwhile
EndFunction
Function TryToBreakWeapon(Actor akTarget, Int aiBipedSlot)
Form kWeapon = akTarget.GetEquippedWeapon(0)
if !kWeapon
Debug.Trace("TryToBreakWeapon " + akTarget + " no weapon equipped")
return
endif
ObjectMod[] kOmodList = akTarget.GetWornItemMods(aiBipedSlot)
if !kOmodList
Debug.Trace("TryToBreakWeapon " + akTarget + " " + kWeapon + " no weapon omod found")
return
endif
if kOmodList.Find(Z_mod_Condition_Standard_D_Destroyed) != -1
Debug.Trace("TryToBreakWeapon " + akTarget + " " + kWeapon + " is already destroyed")
elseif kOmodList.Find(Z_mod_Condition_Standard_4_Degraded4) != -1
akTarget.UnequipItem(kWeapon, abSilent = true)
akTarget.AttachModToInventoryItem(kWeapon, Z_mod_Condition_Standard_D_Destroyed)
Debug.Trace("TryToBreakWeapon " + akTarget + " " + kWeapon + " is destroyed")
Debug.Notification(kWeapon.GetName() + "は壊れた")
elseif kOmodList.Find(Z_mod_Condition_Standard_3_Degraded3) != -1
akTarget.AttachModToInventoryItem(kWeapon, Z_mod_Condition_Standard_4_Degraded4)
Debug.Trace("TryToBreakWeapon " + akTarget + " " + kWeapon + " is degraded 4")
Debug.Notification(kWeapon.GetName() + "は劣化した")
elseif kOmodList.Find(Z_mod_Condition_Standard_2_Degraded2) != -1
akTarget.AttachModToInventoryItem(kWeapon, Z_mod_Condition_Standard_3_Degraded3)
Debug.Trace("TryToBreakWeapon " + akTarget + " " + kWeapon + " is degraded 3")
Debug.Notification(kWeapon.GetName() + "は劣化した")
elseif kOmodList.Find(Z_mod_Condition_Standard_1_Degraded1) != -1
akTarget.AttachModToInventoryItem(kWeapon, Z_mod_Condition_Standard_2_Degraded2)
Debug.Trace("TryToBreakWeapon " + akTarget + " " + kWeapon + " is degraded 2")
Debug.Notification(kWeapon.GetName() + "は劣化した")
else
akTarget.AttachModToInventoryItem(kWeapon, Z_mod_Condition_Standard_1_Degraded1)
Debug.Trace("TryToBreakWeapon " + akTarget + " " + kWeapon + " is degraded 1")
Debug.Notification(kWeapon.GetName() + "は劣化した")
endif
EndFunction
Event OnQuestInit()
Initialize()
Initialize2()
EndEvent
Event Actor.OnPlayerLoadGame(Actor akActorRef)
Initialize2()
EndEvent
State HORIZON_READY
Event OnBeginState(string asOldState)
Z_mod_Condition_Standard_1_Degraded1 = Game.GetFormFromFile(0x12490E, "Z_Horizon.esp") as ObjectMod
Z_mod_Condition_Standard_2_Degraded2 = Game.GetFormFromFile(0x12491F, "Z_Horizon.esp") as ObjectMod
Z_mod_Condition_Standard_3_Degraded3 = Game.GetFormFromFile(0x12490F, "Z_Horizon.esp") as ObjectMod
Z_mod_Condition_Standard_4_Degraded4 = Game.GetFormFromFile(0x124920, "Z_Horizon.esp") as ObjectMod
Z_mod_Condition_Standard_D_Destroyed = Game.GetFormFromFile(0x124910, "Z_Horizon.esp") as ObjectMod
RegisterForAnimationEvent(PlayerRef, "reloadComplete")
RegisterForAnimationEvent(PlayerRef, "weaponSwing")
RegisterForHitEvent(PlayerRef, aiBlockFilter = 1)
EndEvent
Event OnAnimationEvent(ObjectReference akSource, string asEventName)
if asEventName == "weaponSwing"
if Utility.RandomInt(1, 100) == 1
TryToBreakWeapon(akSource as Actor, 33)
endif
else
if Utility.RandomInt(1, 50) == 1
TryToBreakWeapon(akSource as Actor, 41)
endif
endif
EndEvent
Event OnHit(ObjectReference akTarget, ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, \
bool abSneakAttack, bool abBashAttack, bool abHitBlocked, string apMaterial)
if Utility.RandomInt(1, 50) == 1
TryToBreakWeapon(akTarget as Actor, 33)
endif
RegisterForHitEvent(akTarget, aiBlockFilter = 1)
EndEvent
EndState
State HORIZON_NOT_READY
Event OnBeginState(string asOldState)
UnregisterForAnimationEvent(PlayerRef, "reloadComplete")
UnregisterForAnimationEvent(PlayerRef, "weaponSwing")
UnregisterForAllHitEvents()
EndEvent
EndState
解説
今回はHorizonの仕組みを流用するとしたので、Horizonのインストール状況を確認して、Horizonがあれば動作して、なければ動作しないようにしました。
マスター指定があると面倒なので、セーブデータのロード時にFormを拾ってくるようにしました。
リロード完了と殴ったときのタイミングはアニメーションイベントを捕捉します。
ちなみにリロード開始の方は、リロードアニメーション中に横に移動するとリロードをやり直すという不具合があるせいで短時間に連発することがあるのでダメです。
防御のタイミングはOnHitイベントを使います。Skyrimと違いフィルタが使えますので、プレイヤーが防御に成功したときだけイベントを受け取れるため、無駄な負荷をなくすことができます。
武器が破損した場合は使用不可になりますが、装備は自動的に解除されないので、スクリプトから装備を強制的に解除させています。
なお、装備不可にするにはバニラに用意されている「プレイヤーの装備不可」キーワードをつけるだけです。これは破損用のObjectModにキーワード付与の設定があります。
劣化は1段階ずつとしており、全部で6段階です。これはHorizon利用者なら知っていると思います。
劣化確率はリロードと防御が1/50、殴ったときが1/100としました。OnAnimationEvent関数とOnHit関数にて抽選しています。
アタッチメントの付与について
アタッチメントの付与ですが、2つあります。
まずはAttachModToInventoryItem関数です。これはコンテナにたいして実行します。ベース武器を指定して実行することで、コンテナ内の指定された武器にアタッチメントが付与されます。おなじ武器が複数個ある場合は対象を特定できず、どれが処理されるかはランダムのようです。
それからAttachMod関数ですが、これは武器のリファレンス自体に実行します。ですから対象を特定できます。コンテナ内にあるうちはリファレンスがないので、DropObjectで一度地面に落としてリファレンスを取得し、アタッチメントを付与したらコンテナに戻すという処理になります。
SkyrimであればSKSEがWornObjectという便利なFormを用意してくれているので、上のような問題がなくていいのですが、残念ながらF4SEにはないようです。
Biped Slot
装備スロットのことですが、銃器は41、近接武器は33になるようです。
CK Wikiにも詳細は書かれていないので、実際にゲーム内で確認するしかないと思います。
Function ShowBipedWeaponList() Int i = 32 while i < 43 Actor:WornItem kWornItem = PlayerRef.GetWornItem(i) Debug.Trace(i + " " + kWornItem.Item) ObjectMod[] kOmodList = PlayerRef.GetWornItemMods(i) Debug.Trace(i + " " + kOmodList.Length) i += 1 endwhile EndFunction