敵が着ている防具にエンチャントをランダムに付与してみます。
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にあります。大量の防具レコードを操作するのにはまったく向いていません。