【WFRP4e@FVTT】効果のリファクタリングとは?

Foundry Virtual Tabletopで動作するウォーハンマーRPGシステムに関する翻訳メモ。翻訳元はWFRP4eシステムのリポジトリ(GitHub)にあるドキュメント「What is the Effect Refactor」です。なお、画像は元サイトをご確認ください。

効果のリファクタリングとは?

数年前、私は Foundry のアクティブ効果ドキュメントに拡張機能を実装しました。これにより、私 (およびプロセスを学んだユーザー) は、呪文、異能などのほぼすべての動作を作成できます。具体的には、コードを挿入できるさまざまな「トリガー」を提供しました。ただし、この初期段階ではかなり扱いにくいことがわかりました。これは、このようなものを作成する最初の試みであり、他のウォーハンマーシステムの開発を通じて、プロセスを繰り返しました。最後に、新しい Foundry V11 データベース機能を使用して、すべての開発をこのシステムに戻し、機能するものを採用し、機能しないものを無視/変更する時が来ました。これが効果のリファクタリングです。

旧バージョンの何が問題だったのか?

基本的に、混乱を招き、一貫性がなく、まだ制限が多すぎたため、新しい Foundry バージョンがリリースされるたびに、さまざまな修正と変更がパッチされました。

明らかな制限は、私が作業していたアーキテクチャでした。V10 以前では、アクティブ効果を効果の真の「孫」としてサポートしていませんでした。効果を持つアイテムをアクターに追加すると、その効果は直接の子としてアクターに コピー され、その結果、所有するアイテム (編集不可) とアクターの 2 つの完全に独立した効果が作成されます。Winds of Magic のローブまたは魔法の杖アイテムを構成したことがある場合は、アイテムの効果をアクターに追加する前に *編集する必要がある理由がこれです。

V11 では、この点が改善され、アクターは孫のアクティブ効果をコピーせずに「使用」でき、編集できます。

では、リファクタリングは何をしますか?

呪文を唱えるときに、《我流魔術》いのボーナスをクリックするのを忘れたことはありませんか?

ロール・ダイアログに関係のない異能/効果・ボーナスが大量に表示されることにうんざりしていませんか?

攪乱のペナルティを完全に忘れていたりしませんか?

リファクタリングにより、これらすべてが修正されます。ただし、これは表面をなぞっただけです。

効果・範囲/オーラ

効果を範囲の型に貼付できるようになりました。以前の実装では部分的にしか実装されていませんでしたが、現在は、範囲内のアクターに効果を持続的に(または 1 回だけ)適用する型を作成できます。さらに、オーラは、トークンに添付された範囲の型を使用して、その中の誰にでも効果を追加する同様の仕組みです。

複数のスクリプト

スクリプト・システムの主な改善点は、1 つの効果に複数のスクリプトを設定できることです。以前は、効果に対して設定できるスクリプト/トリガーは1つだけだったため、アイテムの仕組みを実行するために複数のトリガーが必要な場合は、複数のアクティブ効果が必要でした。これは、以下の「作成時」機能と併用すると特に便利です。

作成時/削除時のスクリプト

効果は、作成時および削除時にスクリプトを実行できるようになりました。これらのスクリプトは、いくつかの便利な機能を実行できます。

たとえば、異能《鋭敏感覚》は、5つの感覚のうち1つを選択してボーナスを適用できます。したがって、アイテムがアクター上に作成されると、即座スクリプトを使用して、ユーザーがタレントで改善する感覚を選択するためのダイアログを表示し、〈知覚:感覚〉〈知覚:視覚〉など、選択したものに変更することができます。または、よく使用される別の例として、Winds of Magic のローブがあります。この場合、手動で効果の名前を変更する必要がなく、アイテムをアクターに追加すると、どの秘術魔法体系にボーナスを適用するかを選択するダイアログが提供されます。作成スクリプトの詳細については、「即座」または「アイテムを追加」トリガーを参照してください。

これが可能なのは、効果が 複数のスクリプト(上述参照)をサポートしているためです。これにより、作成スクリプトだけでなく、アイテムに必要な機能も定義できます。

ロール・ダイアログ

おそらくこのアップデートで最も強力な側面であるロール・ダイアログの内部は、特に異能/効果ボーナス一覧を改善するため完全に見直されました。このリストは、さまざまなソースから効果を含む ダイアログ・モディファイア に変換されました。重要な点として、これらのモディファイアはスクリプトを介して非表示にしたりアクティブにしたりできるため、チャーム テストをロールするときには、チャーム テストに関連するモディファイアのみが表示されます (効果自体の中にスクリプトとして記述された決定)。ダイアログ スクリプトの側面について詳しく知りたい場合は、「ダイアログ・トリガー」を参照してください。

ダイアログのリワークはすべてのユーザーに最も影響を与えるため、ここでは最高主席魔導師シュープリーム・パトリアークである強力なティルス・ゴルマンを取り上げます。ティルスは熟練した呪文使いで、異能と装備から呪文に多数のボーナスがあります。

発動:《我流魔術》2、《絶対音感》2、ヴォランスの杖、アグニの火の石
魔封交信:《エーテル順応》3、ヴォランスの杖

リファクタリング前:焔の魔法体系呪文を発動するとき、多くのユーザーが気づいているとおり、ボーナスの一覧には無関係な異能ボーナスで埋め尽くされていることに注意してください。

【画像】

リファクタリング後:一覧がかなり小さくなりました。なぜでしょうか?それぞれの異能のスクリプトが、ダイアログにボーナス値を表示するタイミングと非表示にするタイミングを伝えているからです。また、重要な点として、このダイアログには何も入力していないため、ダイアログは適用する必要があるとわかっている関連ボーナスを自動的に選択しています。どのようにでしょうか?これもスクリプトが決定しています。

【画像】

次に、ティルスは〈話術〉テストを試行します。ダイアログは次のようになります。この場合、何も自動的に選択されませんでしたが、ダイアログ修飾子がフィルタリングされる方法により、これらがこのテストに関連する可能性がある唯一の修飾子であることがわかり、使用する必要がある場合は選択できます。

【画像】

TLDR;ゲームが壊れないようにするにはどうすればいいですか?

効果のリファクタリングを利用する場合は、次の手順を検討してください

  1. まず最初にバックアップしたことを確認する。マイグレーションが実行済みと思いますので、このリファクタリングによって何も「壊れる」ことはないでしょうが、稀な場合は常に存在します。

  2. モジュール・コンテンツを再初期化

  3. プレイヤー・キャラクターなどのすべての重要なアクターのすべての異能、呪文および祈祷を置き換える必要があります。より選択的にしたい場合は、効果のリファクタリング・スプレッドシート を参照して、具体的にどの異能が変更されているか確認してください。

これを自動化する方法はありますか?

はい!ある程度は可能です。さまざまなモジュールのすべてのアクター (1,000 を超える) に対して上記の手順を実行する必要はありませんよね?

(注意)
続行する前にワールドをバックアップしてください

  1. ワールドからカスタム以外のアクターとアイテムをすべて削除します。公式モジュールのアクターが数百ある場合は、すでに移行が完了しているため、この移行に送る必要はありません。このプロセスが完了したら、事典から再インポートするだけです。

  2. 以下のコードをコンソール(F12)にコピーしてEnterを押下する。

このコードは、ワールド内のすべてのアクターに対して一連の手順を実行します。コードを実行する前に、プロセスを理解してください。

  1. アクターが所有するすべてのアイテムについて、交換アイテムを検索

    • 交換アイテムは、元アイテムの「sourceId」を利用て取得する(存在する場合)。存在しない場合は、名前と種類が同じアイテムを検索します。(「sourceId」はアイテムの取得元を示す識別子であり、アイテムに情報が存在する場合と存在しない場合があります。)

  2. 古いアイテムから保持するデータを決定し、新しいアイテムへ反映

    • たとえば、アクターが呪文を習得している場合は、新しいアイテムも習得する必要があります。

  3. 新しいアイテムまたは古いアイテムがアクターを変更する場合(《分別》が【知力】を変更するなど)、変更を相殺する際には特別な注意が必要です。この移行によって特性やスキルの最終値が変更されるのは望ましくないため、新しいアイテムで変更が検出された場合は、新しい変更を相殺するようにアクターが変更されます。(カスタム・アクターの場合は、おそらく問題になりません)

  4. 古いアイテムを削除し、新しいアイテムを追加

(注意)

このマイグレーションにおいて、特に交換アイテムを探すときに、多くの問題が発生する可能性があります。例えば、アクターの武器と防具は、「武器を掲げよ!」で用意されている代替品に置き換えられている場合があります。アクターのキャリア、異能、呪文、武器を確認してください。

更に重要な点:既存の事典アイテムからアイテムが作成された場合、上書きされる危険性があります。

async function updateEffectsRefactor(actor, update=false)
{
    let items = actor.items.contents;
    let toDelete = [];
    let toAdd = [];
    let actorUpdate = {};
    for(let item of items)
    {
        let newItem = await findReplacement(item);
        if (newItem)
        {
            let data = keepData(item, newItem)      
            let offset = {};
            offsetChanges(item, newItem, offset);
            applyOffset(offset, actorUpdate, actor)
            toAdd.push(data);
            toDelete.push(item.id)
        }
    }

    let summary = 
    `${actor.name}
    Deleting ${toDelete.map(i => actor.items.get(i).name).join(", ")}
    Adding ${toAdd.map(i => i.name)}
    `
    console.log(summary);

    if (toDelete.length && update)
    {
        await actor.deleteEmbeddedDocuments("Item", toDelete);
    }
    if (toAdd && update)
    {
        await actor.createEmbeddedDocuments("Item", toAdd, {keepId : true});
        await actor.update(actorUpdate);
    }
}

async function findReplacement(item)
{
    let sourceId = item.getFlag("core", "sourceId")
    if (sourceId)
    {
        let sourceItem = await fromUuid(sourceId);
        if (sourceItem)
        {
            return sourceItem;
        }
    }
    return game.wfrp4e.utility.findItem(item.name, item.type)
}

function keepData(oldItem, newItem)
{
    let keep = {
        _id : oldItem._id,
        name : oldItem.name,
        img: oldItem.img,
        "system.description.value" : oldItem.system.description.value,
        "system.gmdescription.value" : oldItem.system.gmdescription.value
    }
    if (oldItem.system.quantity?.value)
    {
        keep["system.quantity.value"] = oldItem.system.quantity.value;
    }
    if (oldItem.system.tests?.value)
    {
        keep["system.tests.value"] = oldItem.system.tests.value;
    }
    if (oldItem.system.location?.value)
    {
        keep["system.location.value"] = oldItem.system.location.value
    }
    if (oldItem.system.worn?.value)
    {
        keep["system.worn.value"] = oldItem.system.worn?.value
    }
    if (oldItem.system.worn)
    {
        keep["system.worn"] = oldItem.system.worn
    }
    if (oldItem.system.equipped)
    {
        keep["system.equipped"] = oldItem.system.equipped
    }
    if (oldItem.system.advances)
    {
        keep["system.advances"] = oldItem.system.advances
    }
    if (oldItem.system.modifier?.value)
    {
        keep["system.modifier.value"] = oldItem.system.modifier.value;
    }
    if (oldItem.system.memorized?.value)
    {
        keep["system.memorized.value"] = oldItem.system.memorized.value;
    }
    if (oldItem.system.skill?.value)
    {
        keep["system.skill.value"] = oldItem.system.skill.value;
    }
    if (oldItem.system.ingredients)
    {
        keep["system.ingredients"] = oldItem.system.ingredients;
        keep["system.currentIng"] = oldItem.system.currentIng;
    }
    if (oldItem.system.wind?.value)
    {
        keep["system.wind"] = oldItem.system.wind;
    }
    if (oldItem.system.current?.value)
    {
        keep["system.current.value"] = oldItem.system.current.value;
    }
    if (oldItem.system.complete?.value)
    {
        keep["system.complete.value"] = oldItem.system.complete.value;
    }
    if (oldItem.type == "trait")
    {
        keep.system = oldItem.system
    }
    return mergeObject(newItem.toObject(), keep);
}

function offsetChanges(oldItem, newItem, offsets)
{
    let oldChanges = oldItem.effects.contents.reduce((changes, effect) => changes.concat(effect.changes), []).filter(i => i.mode == 2);
    let newChanges = newItem.effects.contents.reduce((changes, effect) => changes.concat(effect.changes), []).filter(i => i.mode == 2);
    let diffChanges = {};

    let oldTotals = oldChanges.reduce((totals, change) => {
        if (totals[change.key])
        {
            totals[change.key] += Number(change.value);
        }
        else 
        {
            totals[change.key] = Number(change.value);
        }
        return totals
    }, {});

    let newTotals = newChanges.reduce((totals, change) => {
        if (totals[change.key])
        {
            totals[change.key] += Number(change.value);
        }
        else 
        {
            totals[change.key] = Number(change.value);
        }
        return totals
    }, {});


    for(let newTotalKey in newTotals)
    {
        let diff = newTotals[newTotalKey] - (oldTotals[newTotalKey] || 0)
        diffChanges[newTotalKey] = diff;
    }

    for(let diffKey in diffChanges)
    {
        let current = getProperty(offsets, diffKey) || 0;

        current -= diffChanges[diffKey]

        setProperty(offsets, diffKey, current);
    }

    if (!isEmpty(diffChanges))
    {
        console.log(`@@@ Diff Changes for $${oldItem.name} - ${newItem.name} @@@`)
        console.log(diffChanges);
    }
}

function applyOffset(offset, update, actor)
{
    for(let key in flattenObject(offset))
    {
        let current = getProperty(update, key) || getProperty(actor._source, key);
        current += getProperty(offset, key);
        setProperty(update, key, current);
    }
}

for(let actor of game.actors.contents)
{
    await updateEffectsRefactor(actor, true);
}

訳注というか、思いついたこと

リファクタリングされた効果、大変便利ですね。ローカライズ対応が大変で完全な対応は完了していないのですが、元には戻れません。最後に用意されているスクリプトは便利ですが、ワールドの全アクターを処理しちゃうので気軽に使えないんですよね。個人的には「所有権のあるアクター」、「選択したトークンに紐づいたアクター」だけを処理するのが良さそうですね。


この記事が気に入ったらサポートをしてみませんか?