防具にエンチャントをランダムに追加する

Modを作ろう

防具にエンチャントをランダムに追加してみます。

敵が着ている防具にエンチャントをランダムに付与してみます。

Fallout New Vegasを遊んでいたころ、レアモンスターに持たせる防具を作ってみよう – ランダムエンチャント編という記事を書いたのですが、この時はNVSEの制限の問題で実現出来ませんでした。Skyrim SEになってようやく出来ました。

エンチャントを用意する

まず、CKでもSSEEditでもいいので、エンチャントを用意します。Magic Effectからです。

ヴァニラのレコードをコピーしてバリエーションを増やしました。全部で13種類です。

次に、防具に紐づけるためのObject Effectを用意します。

手で作るのは面倒なので、SSEEditのスクリプトでもってサクっと作ります。強さは5段階にしました。

Object Effectを生成する例 (pascal)

{
  generate object effect
}


unit userscript;

var
  base: IInterface;


function Initialize: integer;
begin
  base := RecordByFormID(FileByIndex(0), $000AD461, True);

  if not Assigned(base) then begin
    AddMessage('Can''t copy base record as new');
    Result := 1;
    Exit;
  end;
end;


function Process(e: IInterface): Integer;
var
  i: integer;
  ee, effects, ef: IInterface;
begin
  // abort if this element is not a magic effect
  if Signature(e) <> 'MGEF' then Exit;

  for i := 1 to 5 do begin
    // create new form
    ee := wbCopyElementToFile(base, GetFile(e), True, True);

    if not Assigned(ee) then begin
      AddMessage('Can''t copy base record as new');
      Result := 1;
      Exit;
    end;

    // change editor id
    SetElementEditValues(ee, 'EDID', GetElementEditValues(e, 'EDID') + '0' + IntToStr(i));
    SetElementEditValues(ee, 'FULL', GetElementEditValues(e, 'FULL'));
    SetElementEditValues(ee, 'ENIT\Base Enchantment', 0);
    SetElementNativeValues(ee, 'ENIT\Enchantment Cost', 0);
    SetElementNativeValues(ee, 'ENIT\Enchantment Amount', 0);

    effects := ElementByPath(ee, 'Effects');

    if not Assigned(effects) then
      effects := Add(ee, 'Effects', false);

    ef := ElementByIndex(effects, 0);

    if Pos('Regenerate', GetElementEditValues(e, 'FULL')) >= 1 then
      SetElementNativeValues(ef, 'EFIT\Magnitude', i)
    else
      SetElementNativeValues(ef, 'EFIT\Magnitude', i * 10);

    SetElementNativeValues(ef, 'EFID - Base Effect', FixedFormID(e));
  end;
end;

end.

このコードを名前を付けて保存します。SSEEdit.exeのあるフォルダを開きます。Edit Scriptsフォルダがあるので、その中にコードを置きます。

SSEEditでMagic Effectを選択します。さっき作った13種類をまとめて選択です。

右クリックしてApply Scriptを選びます。さきほどのコードを選んで実行です。

こんな感じにObject Effectが出来たら成功です。

種類が13個で強さが5段階なので、全部で65通りです。

アクターの防具にエンチャントを付与する

ポイントはSKSE64のWornObject Scriptです。こいつは本当に便利で凄いのですが、情報がほとんどないので説明しておきます。

エンチャントを付与する関数は3つあります。

一つ目はArmorのSetEnchantmentで、ObjectReferenceではなく、ベースとなっているArmorに付与するものです。ですので、ゲーム内のすべての防具にエンチャントが付いてしまいます。

二つ目はObjectReferenceのSetEnchantmentです。こちらなら、その個体に付きます。CK Wikiに個別記事ページはなく、概要があるのみです。"Changes an item's player-made enchantment to another." と書いてあるのが若干気になります。ObjectReferenceなので、コンテナの中(アクターのインベントリ)にあるアイテムは、一度DropObject関数を使って外に出して、ObjectReferenceを取得する必要があります。アクターが着ていた場合は脱げてしまうので、見た目的にも処理内容的にも、あまりよろしくないです。

最後に、WornObjectのSetEnchantmentです。こちらも機能的にはObjectReferenceのものと同じみたいです。こちらはアクターが装備中の武器と防具に限定されますが、DropObjectで外に出す必要がなく、直接操作できます。

Papyrusのサンプルコードを書くと、おそらくこんな感じです。

WornObject.SetEnchantmentの例 (papyrus)

Function AddEnchant(Actor akTarget, Enchantment aiSource)
    Int iSlotMask = 0x00000004 ; 胴装備

    if akTarget.GetWornForm(iSlotMask)
        WornObject.SetEnchantment(Actor = akTarget, handSlot = 0, slotMask = iSlotMask, source = aiSource, maxCharge = 0.0)
    endif
EndFunction

改良した方法

FormListを5つ用意します。強さに応じて5つです。

それぞれにEnchantを入れていきます。CKのフィルタを使うと楽です。

Questを作ってスクリプトを紐づけます。コードはこんな感じです。

エンチャントをランダムに返す(改良版) (papyrus)

Scriptname eeQuestEnchantmentScript Extends Quest

FormList[] Property eeListEnchantment Auto


Enchantment Function GetRandomArmorEnchantment(Int aiLevel)
    Int iTier = Utility.RandomInt(0, aiLevel) / 10

    if iTier >= eeListEnchantment.Length
        iTier = eeListEnchantment.Length - 1
    endif

    Int i = Utility.RandomInt(0, eeListEnchantment[iTier].GetSize() - 1)

    return eeListEnchantment[iTier].GetAt(i) as Enchantment
EndFunction

プロパティを紐づけます。FormListの配列に、強さの弱い順に置いていきます。

引数のaiLevelはアクターのレベルをそのまま入れます。

対象のアクターが装備中の防具にエンチャントを付与するコードが以下になります。

アクターの装備にエンチャントを付与する (papyrus)

Function AddRandomEnchantment(Actor akTarget)
    Form[] akItemList = akTarget.GetContainerForms()
    Int iIndex = akItemList.Length

    while iIndex > 0
        iIndex -= 1

        if akItemList[iIndex] && akTarget.IsEquipped(kItemList[iIndex])
            Int iSlotMask = (akItemList[iIndex] as Armor).GetSlotMask()

            if !WornObject.GetEnchantment(MySelf, handSlot = 0, slotMask = iSlotMask)
                WornObject.SetEnchantment(MySelf, handSlot = 0, slotMask = iSlotMask, source = GetRandomArmorEnchantment(akTarget.GetLevel()), maxCharge = 0.0)
            endif
        endif
    endwhile
EndFunction

効率の悪い方法

このやり方はオススメしません。読まなくていいです。

さて、エンチャント(Object Effect)が大量にあって、とても手で書いていられませんので、機械に書かせます。

まず、Object Effectの一覧を出します。以下のコードをSSEEditでObject Effectを選択してApply Scriptで実行です。

Object EffectからPapyrusのコードを出力させる例 (pascal)

{
  generate enchantment code
}


unit userscript;


var
  slPapyrus1: TStringList;
  slPapyrus2: TStringList;
  i: integer;
function Initialize: integer;
begin
  slPapyrus1 := TStringList.Create;
  slPapyrus2 := TStringList.Create;
  i := 0;
end;


function Process(e: IInterface): Integer;
var
  eid: string;
begin
  // abort if this element is not an enchantment
  if Signature(e) 'ENCH' then Exit;

  eid := GetElementEditValues(e, 'EDID');
  slPapyrus1.Add('Enchantment Property ' + eid + ' Auto');
  slPapyrus2.Add('kEnchantments[' + IntToStr(i) + '] = ' + eid);

  i := i + 1;
end;


function Finalize: integer;
var
  fname: string;
  slPapyrus2b: TStringList;
  i, c: integer;
begin
  fname := ProgramPath + 'Edit Scripts\enchantment1.psc';
  AddMessage('Saving list to ' + fname);
  slPapyrus1.SaveToFile(fname);
  slPapyrus1.Free;

  slPapyrus2b := TStringList.Create;
  c := slPapyrus2.Count;
  slPapyrus2b.Add('Enchantment[] kEnchantments = new Enchantment[' + IntToStr(c) + ']');

  for i := 0 to c - 1 do begin
    slPapyrus2b.Add(slPapyrus2[i]);
  end;

  slPapyrus2.Free;
  slPapyrus2b.Add('return kEnchantments[Utility.RandomInt(0, ' + IntToStr(c - 1) + ')]');

  fname := ProgramPath + 'Edit Scripts\enchantment2.psc';
  AddMessage('Saving list to ' + fname);
  slPapyrus2b.SaveToFile(fname);
  slPapyrus2b.Free;
end;

end.

enchantment1.pscがPropertyの一覧です。CKで紐づけしましょう。enchantment2.pscが配列です。ランダムに選ぶ処理を追加しましょう。

このままだと、無作為にランダムなので、敵の強さを加味した上で選ばれるようにしたいです。そこで、PHPで再加工するプログラムを書きます。

エンチャントの配列の一覧をレベル順に並べ替える例 (php)

<?php
error_reporting(E_ALL);
mb_internal_encoding('UTF-8');

$template = isset($_POST['template']) ? $_POST['template'] : '';
$result = '';

if ($template) {
    $enc_all = [];

    if ( preg_match_all('/=(.+?)$/ms', $template, $regs_all, PREG_SET_ORDER) ) {
        foreach ($regs_all as $regs) {
            $enc_base = trim($regs[1]);
            $enc_level = preg_replace('/^.+([0-9]+)$/', '$1', $enc_base);
            $enc_all[$enc_level][] = $enc_base;
            $result = print_r($enc, true);
        }

        $i = 0;

        foreach ($enc_all as $enc_level => $enc_list) {
            $result .= sprintf("\t; level %d\n", $enc_level);
            foreach ($enc_list as $enc) {
                $result .= sprintf("\tkEnchantments[%d] = %s\n", $i, $enc);
                $i++;
            }
        }

        $result .= sprintf("\t; misc\n");
        $result .= sprintf("\tInt iMaxLevel = %d\n", count($enc_all));
        $result .= sprintf("\tInt iItemPerLevel = %d\n", count($enc_list));
    } else {
        $result = 'no match error';
    }
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>テンプレ君</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<body>

<div class="container-fluid">
<form action="enchantment.php" method="post">

<div class="row">
    <div class="col">
        <div class="form-group">
            <label for="template">テンプレート</label>
            <textarea class="form-control" name="template" rows="10"><?php echo htmlspecialchars($template); ?></textarea>
            <small class="form-text text-muted">キーワードが変数varに格納される</small>
        </div>
    </div>
</div>

<button type="submit" class="btn btn-primary">変換</button>
<button type="button" class="btn btn-secondary" id="copy">クリップボードにコピー</button>

<div class="form-group">
    <label for="list">結果</label>
    <textarea class="form-control" rows="10" id="result"><?php echo htmlspecialchars($result); ?></textarea>
</div>

</form>
</div>

<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</script>

<script>
$('#copy').on('click', function(){
    $('#result').select();
    document.execCommand("Copy");
});
</script>

</body>
</html>

これをサーバに置いてページを開きます。enchantment2.pscの中身をそのままペーストして変換すると、こうなります。

エンチャントの配列をレベル順に並べ替えてみた例 (papyrus)

; level 1
    kEnchantments[0] = aaaEnchFortifyHealthConstantSelf01
    kEnchantments[1] = aaaEnchFortifyMagickaConstantSelf01
    kEnchantments[2] = aaaEnchFortifyStaminaConstantSelf01
    kEnchantments[3] = aaaEnchFortifyHealRateConstantSelf01
    kEnchantments[4] = aaaEnchFortifyMagickaRateConstantSelf01
    kEnchantments[5] = aaaEnchFortifyStaminaRateConstantSelf01
    kEnchantments[6] = aaaEnchResistFireConstantSelf01
    kEnchantments[7] = aaaEnchResistFrostConstantSelf01
    kEnchantments[8] = aaaEnchResistMagicConstantSelf01
    kEnchantments[9] = aaaEnchResistPoisonConstantSelf01
    kEnchantments[10] = aaaEnchResistShocktConstantSelf01
    kEnchantments[11] = aaaEnchFortifyDestruction01
    kEnchantments[12] = aaaEnchFortifyRestration01
    ; level 2
    kEnchantments[13] = aaaEnchFortifyHealthConstantSelf02
    kEnchantments[14] = aaaEnchFortifyMagickaConstantSelf02
    kEnchantments[15] = aaaEnchFortifyStaminaConstantSelf02
    kEnchantments[16] = aaaEnchFortifyHealRateConstantSelf02
    kEnchantments[17] = aaaEnchFortifyMagickaRateConstantSelf02
    kEnchantments[18] = aaaEnchFortifyStaminaRateConstantSelf02
    kEnchantments[19] = aaaEnchResistFireConstantSelf02
    kEnchantments[20] = aaaEnchResistFrostConstantSelf02
    kEnchantments[21] = aaaEnchResistMagicConstantSelf02
    kEnchantments[22] = aaaEnchResistPoisonConstantSelf02
    kEnchantments[23] = aaaEnchResistShocktConstantSelf02
    kEnchantments[24] = aaaEnchFortifyDestruction02
    kEnchantments[25] = aaaEnchFortifyRestration02
    ; level 3
    kEnchantments[26] = aaaEnchFortifyHealthConstantSelf03
    kEnchantments[27] = aaaEnchFortifyMagickaConstantSelf03
    kEnchantments[28] = aaaEnchFortifyStaminaConstantSelf03
    kEnchantments[29] = aaaEnchFortifyHealRateConstantSelf03
    kEnchantments[30] = aaaEnchFortifyMagickaRateConstantSelf03
    kEnchantments[31] = aaaEnchFortifyStaminaRateConstantSelf03
    kEnchantments[32] = aaaEnchResistFireConstantSelf03
    kEnchantments[33] = aaaEnchResistFrostConstantSelf03
    kEnchantments[34] = aaaEnchResistMagicConstantSelf03
    kEnchantments[35] = aaaEnchResistPoisonConstantSelf03
    kEnchantments[36] = aaaEnchResistShocktConstantSelf03
    kEnchantments[37] = aaaEnchFortifyDestruction03
    kEnchantments[38] = aaaEnchFortifyRestration03
    ; level 4
    kEnchantments[39] = aaaEnchFortifyHealthConstantSelf04
    kEnchantments[40] = aaaEnchFortifyMagickaConstantSelf04
    kEnchantments[41] = aaaEnchFortifyStaminaConstantSelf04
    kEnchantments[42] = aaaEnchFortifyHealRateConstantSelf04
    kEnchantments[43] = aaaEnchFortifyMagickaRateConstantSelf04
    kEnchantments[44] = aaaEnchFortifyStaminaRateConstantSelf04
    kEnchantments[45] = aaaEnchResistFireConstantSelf04
    kEnchantments[46] = aaaEnchResistFrostConstantSelf04
    kEnchantments[47] = aaaEnchResistMagicConstantSelf04
    kEnchantments[48] = aaaEnchResistPoisonConstantSelf04
    kEnchantments[49] = aaaEnchResistShocktConstantSelf04
    kEnchantments[50] = aaaEnchFortifyDestruction04
    kEnchantments[51] = aaaEnchFortifyRestration04
    ; level 5
    kEnchantments[52] = aaaEnchFortifyHealthConstantSelf05
    kEnchantments[53] = aaaEnchFortifyMagickaConstantSelf05
    kEnchantments[54] = aaaEnchFortifyStaminaConstantSelf05
    kEnchantments[55] = aaaEnchFortifyHealRateConstantSelf05
    kEnchantments[56] = aaaEnchFortifyMagickaRateConstantSelf05
    kEnchantments[57] = aaaEnchFortifyStaminaRateConstantSelf05
    kEnchantments[58] = aaaEnchResistFireConstantSelf05
    kEnchantments[59] = aaaEnchResistFrostConstantSelf05
    kEnchantments[60] = aaaEnchResistMagicConstantSelf05
    kEnchantments[61] = aaaEnchResistPoisonConstantSelf05
    kEnchantments[62] = aaaEnchResistShocktConstantSelf05
    kEnchantments[63] = aaaEnchFortifyDestruction05
    kEnchantments[64] = aaaEnchFortifyRestration05
    ; misc
    Int iMaxLevel = 5
    Int iItemPerLevel = 13

処理を加えて関数にしたのが、以下のコードになります。

エンチャントをランダムに返す (papyrus)

Enchantment Function GetRandomArmorEnchantment(Int aiLevel)
    Enchantment[] kEnchantments = new Enchantment[65]

    ; 中略

    Float fRandomMax = Math.pow(1.09, aiLevel) ; 1.09 ^ 50 = 74
    Int iBaseLevel = (Utility.RandomFloat(0.0, fRandomMax) / iItemPerLevel) as Int

    if iBaseLevel >= iMaxLevel
        iBaseLevel = iMaxLevel - 1
    endif

    iBaseLevel *= iItemPerLevel

    return kEnchantments[iBaseLevel + Utility.RandomInt(0, iItemPerLevel - 1)]
EndFunction

引数のaiLevelはアクターのレベルをそのまま入れます。

対象のアクターが装備中の防具にエンチャントを付与するコードが以下になります。

アクターの装備にエンチャントを付与する (papyrus)

Function AddRandomEnchantment(Actor akTarget)
    Form[] akItemList = akTarget.GetContainerForms()
    Int iIndex = akItemList.Length

    while iIndex > 0
        iIndex -= 1

        if akItemList[iIndex] && akTarget.IsEquipped(kItemList[iIndex])
            Int iSlotMask = (akItemList[iIndex] as Armor).GetSlotMask()

            if !WornObject.GetEnchantment(MySelf, handSlot = 0, slotMask = iSlotMask)
                WornObject.SetEnchantment(MySelf, handSlot = 0, slotMask = iSlotMask, source = GetRandomArmorEnchantment(akTarget.GetLevel()), maxCharge = 0.0)
            endif
        endif
    endwhile
EndFunction

まとめ

今回の記事はコードだらけになってしまいました。コードは一度書けばずっと使いまわせるので、「Papyrusは書けるけどPascalはちょっと…」という方は是非挑戦してみてください。「CKは触ったことがあるけどSSEEditはちょっと…」という方は、まずSSEEditを触るところからはじめてください。CKの売りはCell Viewにあります。大量の防具レコードを操作するのにはまったく向いていません。

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