315 lines
14 KiB
JavaScript
315 lines
14 KiB
JavaScript
//=============================================================================
|
||
// RPG Maker MZ - KN_MapBattleBattleSimulator.js
|
||
//=============================================================================
|
||
|
||
/*:ja
|
||
* @target MZ
|
||
* @author kurogoma knights
|
||
* @plugindesc シンプルなマップバトルシミュレーター
|
||
*/
|
||
|
||
(() => {
|
||
'use strict';
|
||
const script = document.currentScript;
|
||
const param = PluginManagerEx.createParameter(script);
|
||
|
||
// 定数設定(すべて \c[16] で表示、先制は \c[6]、ボスは \c[2]、HP0は赤(\c[18]))
|
||
const COLOR = "\\c[16]";
|
||
const RESET = "\\c[0]";
|
||
const RED = "\\c[18]";
|
||
const UP_COLOR = "\\c[24]"; // ステータス上昇時(緑)
|
||
const DOWN_COLOR = "\\c[18]"; // ステータス低下時(赤)
|
||
|
||
// 数値を右寄せ(指定桁数)
|
||
function formatNum(num, width) {
|
||
return String(num).padStart(width, " ");
|
||
}
|
||
|
||
// ラベルと数値の整形(ラベルと数値の間にスペースを入れる)
|
||
function formatLabelValue(label, value, width) {
|
||
return COLOR + label + RESET + " " + formatNum(value, width);
|
||
}
|
||
|
||
// ステータス増減に応じた色付き数値フォーマット(種族特攻用)
|
||
function formatColoredValue(label, current, base, width) {
|
||
let valueStr;
|
||
if (current > base) {
|
||
valueStr = UP_COLOR + formatNum(current, width) + RESET;
|
||
} else if (current < base) {
|
||
valueStr = DOWN_COLOR + formatNum(current, width) + RESET;
|
||
} else {
|
||
valueStr = formatNum(current, width);
|
||
}
|
||
return COLOR + label + RESET + " " + valueStr;
|
||
}
|
||
|
||
// 威力表示用ヘルパー:固定値ボーナスがある場合は (基礎値+補正値) 形式で表示
|
||
function formatPowerWithBonus(base, bonus) {
|
||
if (bonus > 0) {
|
||
return "(" + base + UP_COLOR + "+" + bonus + RESET + ")";
|
||
} else if (bonus < 0) {
|
||
return "(" + base + DOWN_COLOR + bonus + RESET + ")";
|
||
}
|
||
return String(base);
|
||
}
|
||
|
||
class MapBattleSimulator {
|
||
// simulateBattle(): 実際のHPは変更せず、局所変数上でシミュレーションする。
|
||
// 返り値は各側の最終HP、受けたダメージ、ログ、勝者、及び先制側("actor" or "enemy")を含む。
|
||
static simulateBattle(actor, enemy) {
|
||
const ctx = this._prepareBattleContext(actor, enemy);
|
||
this._runSimulation(ctx);
|
||
return this._buildResult(ctx);
|
||
}
|
||
|
||
// バトルコンテキスト準備(ステータス・ターン順・ダメージ計算)
|
||
static _prepareBattleContext(actor, enemy) {
|
||
// 種族特攻バフを適用したアクターのステータスを取得
|
||
const actorAtk = Kurogoma.RaceEffectManager.getModifiedActorAtk(enemy, actor.atk);
|
||
const actorDef = Kurogoma.RaceEffectManager.getModifiedActorDef(enemy, actor.def);
|
||
const actorAgi = Kurogoma.RaceEffectManager.getModifiedActorAgi(enemy, actor.agi);
|
||
|
||
const actorStats = {
|
||
name: actor.name(),
|
||
startHp: actor.hp,
|
||
hp: actor.hp,
|
||
atk: actorAtk,
|
||
def: actorDef,
|
||
agi: actorAgi
|
||
};
|
||
|
||
// 種族特攻を適用した敵のステータスを取得
|
||
const enemyAtk = Kurogoma.RaceEffectManager.getModifiedAtk(enemy, enemy.atk);
|
||
const enemyDef = Kurogoma.RaceEffectManager.getModifiedDef(enemy, enemy.def);
|
||
const enemyAgi = Kurogoma.RaceEffectManager.getModifiedAgi(enemy, enemy.agi);
|
||
|
||
const enemyStats = {
|
||
name: enemy.name(),
|
||
startHp: enemy.hp,
|
||
hp: enemy.hp,
|
||
atk: enemyAtk,
|
||
def: enemyDef,
|
||
agi: enemyAgi
|
||
};
|
||
|
||
// 先攻・攻撃回数を計算
|
||
const turnOrder = this._calcTurnOrder(actorStats.agi, enemyStats.agi);
|
||
|
||
// ダメージ計算
|
||
const artifactMods = Kurogoma.ArtifactBonus.getModifiers();
|
||
const damage = this._calcDamage(actorStats, enemyStats, artifactMods);
|
||
|
||
return {
|
||
actor,
|
||
enemy,
|
||
actorStats,
|
||
enemyStats,
|
||
turnOrder,
|
||
artifactMods,
|
||
damage,
|
||
actorHits: 0,
|
||
enemyHits: 0
|
||
};
|
||
}
|
||
|
||
// 先攻・攻撃回数計算
|
||
static _calcTurnOrder(actorAgi, enemyAgi) {
|
||
const hasFirstStrike = Kurogoma.ArtifactBonus.hasFirstStrikeArtifact();
|
||
let actorAttackCount, enemyAttackCount, first;
|
||
|
||
if (hasFirstStrike && actorAgi >= enemyAgi) {
|
||
actorAttackCount = Math.min(1 + Math.floor((actorAgi - enemyAgi) / 5), 3);
|
||
enemyAttackCount = 1;
|
||
first = "actor";
|
||
} else if (hasFirstStrike && actorAgi < enemyAgi) {
|
||
actorAttackCount = 1;
|
||
enemyAttackCount = Math.min(1 + Math.floor((enemyAgi - actorAgi) / 5), 3);
|
||
first = "actor";
|
||
} else if (actorAgi >= enemyAgi) {
|
||
actorAttackCount = Math.min(1 + Math.floor((actorAgi - enemyAgi) / 5), 3);
|
||
enemyAttackCount = 1;
|
||
first = "actor";
|
||
} else {
|
||
enemyAttackCount = Math.min(1 + Math.floor((enemyAgi - actorAgi) / 5), 3);
|
||
actorAttackCount = 1;
|
||
first = "enemy";
|
||
}
|
||
|
||
return { actorAttackCount, enemyAttackCount, first };
|
||
}
|
||
|
||
// ダメージ計算
|
||
static _calcDamage(actorStats, enemyStats, artifactMods) {
|
||
const calcBaseDamage = (atk, def) => Math.max(atk - def, 1);
|
||
const actorBaseDamage = calcBaseDamage(actorStats.atk, enemyStats.def);
|
||
const enemyBaseDamage = calcBaseDamage(enemyStats.atk, actorStats.def);
|
||
|
||
const actorDamage = Math.max(actorBaseDamage + artifactMods.attackBonus, 1 + artifactMods.attackBonus);
|
||
const enemyDamage = Math.max(enemyBaseDamage - artifactMods.defenseBonus, 0);
|
||
|
||
return { actorBaseDamage, enemyBaseDamage, actorDamage, enemyDamage };
|
||
}
|
||
|
||
// 戦闘シミュレーション(ループ)
|
||
static _runSimulation(ctx) {
|
||
const { actorStats, enemyStats, turnOrder, damage } = ctx;
|
||
const { actorAttackCount, enemyAttackCount, first } = turnOrder;
|
||
const { actorDamage, enemyDamage } = damage;
|
||
|
||
while (actorStats.hp > 0 && enemyStats.hp > 0) {
|
||
if (first === "actor") {
|
||
for (let i = 0; i < actorAttackCount; i++) {
|
||
enemyStats.hp -= actorDamage;
|
||
ctx.actorHits++;
|
||
}
|
||
if (enemyStats.hp <= 0) break;
|
||
for (let i = 0; i < enemyAttackCount; i++) {
|
||
actorStats.hp -= enemyDamage;
|
||
ctx.enemyHits++;
|
||
}
|
||
} else {
|
||
for (let i = 0; i < enemyAttackCount; i++) {
|
||
actorStats.hp -= enemyDamage;
|
||
ctx.enemyHits++;
|
||
}
|
||
if (actorStats.hp <= 0) break;
|
||
for (let i = 0; i < actorAttackCount; i++) {
|
||
enemyStats.hp -= actorDamage;
|
||
ctx.actorHits++;
|
||
}
|
||
}
|
||
}
|
||
actorStats.hp = Math.max(actorStats.hp, 0);
|
||
enemyStats.hp = Math.max(enemyStats.hp, 0);
|
||
}
|
||
|
||
// ステートアイコン取得
|
||
static _getStateIcons(battler, rotationIndex) {
|
||
if (!battler || !battler.states) return "";
|
||
const stateList = battler.states().filter(state => state && state.iconIndex > 0);
|
||
if (stateList.length === 0) return "";
|
||
|
||
const index = rotationIndex % stateList.length;
|
||
const currentState = stateList[index];
|
||
return `\\i[${currentState.iconIndex}]`;
|
||
}
|
||
|
||
// 名前にアイコン追加
|
||
static _appendNameIcons(ctx) {
|
||
const { actor, enemy, actorStats, enemyStats, turnOrder } = ctx;
|
||
|
||
// ローテーションインデックスを初期化
|
||
if (actor._stateIconRotationIndex === undefined) {
|
||
actor._stateIconRotationIndex = 0;
|
||
}
|
||
if (enemy._stateIconRotationIndex === undefined) {
|
||
enemy._stateIconRotationIndex = 0;
|
||
}
|
||
|
||
// 固定アイコン(ボス、先制、敗北イベント)を名前に追加
|
||
if ($gameSystem.isBossBattle && $gameSystem.isBossBattle()) {
|
||
enemyStats.name += "\\i[421]";
|
||
}
|
||
if (enemy.enemy().meta.DefeatEvent) {
|
||
enemyStats.name += "\\i[84]";
|
||
}
|
||
if (turnOrder.first === "actor") {
|
||
actorStats.name += "\\i[82]";
|
||
} else {
|
||
enemyStats.name += "\\i[82]";
|
||
}
|
||
|
||
// ステートアイコンを取得して追加
|
||
const actorStateIcons = this._getStateIcons(actor, actor._stateIconRotationIndex);
|
||
const enemyStateIcons = this._getStateIcons(enemy, enemy._stateIconRotationIndex);
|
||
|
||
if (actorStateIcons) actorStats.name += actorStateIcons;
|
||
if (enemyStateIcons) enemyStats.name += enemyStateIcons;
|
||
}
|
||
|
||
// ログ行生成
|
||
static _buildLog(stats, baseStats, levelStr, hpLine, powerStr, attackCount, totalPower) {
|
||
return "\\> " + stats.name + "\n" +
|
||
"\\> " + levelStr + formatColoredValue("ATK", stats.atk, baseStats.atk, 2) + " " +
|
||
formatColoredValue("DEF", stats.def, baseStats.def, 2) + " " +
|
||
formatLabelValue("SPD", stats.agi, 2) + "\n" +
|
||
hpLine + "\n" +
|
||
"\\> " + COLOR + "Power" + RESET + " " + powerStr + "x" + attackCount +
|
||
" " + COLOR + "Total" + RESET + " " + totalPower;
|
||
}
|
||
|
||
// 結果生成
|
||
static _buildResult(ctx) {
|
||
const { actor, enemy, actorStats, enemyStats, turnOrder, artifactMods, damage, actorHits, enemyHits } = ctx;
|
||
const { actorAttackCount, enemyAttackCount, first } = turnOrder;
|
||
const { actorBaseDamage, enemyBaseDamage, actorDamage, enemyDamage } = damage;
|
||
|
||
// 名前にアイコン追加
|
||
this._appendNameIcons(ctx);
|
||
|
||
const enemyTotalPower = enemyDamage * enemyHits;
|
||
const actorTotalPower = actorDamage * actorHits;
|
||
|
||
// レベル表示(敵のみ)
|
||
const enemyLevelStr = enemy.enemy().meta.EnemyLevel
|
||
? COLOR + "Lv." + RESET + String(enemy.enemy().meta.EnemyLevel).padStart(2, " ") + " "
|
||
: "";
|
||
|
||
// HPログ行
|
||
const enemyFinalHPStr = enemyStats.hp === 0 ? RED + formatNum(enemyStats.hp, 3) + RESET : formatNum(enemyStats.hp, 3);
|
||
const actorFinalHPStr = actorStats.hp === 0 ? RED + formatNum(actorStats.hp, 3) + RESET : formatNum(actorStats.hp, 3);
|
||
const enemyHpLine = "\\> " + COLOR + "HP" + RESET + " " +
|
||
formatNum(enemyStats.startHp, 3) + " → " +
|
||
enemyFinalHPStr + (enemyStats.hp === 0 ? " \\i[1]" : "");
|
||
const actorHpLine = "\\> " + COLOR + "HP" + RESET + " " +
|
||
formatNum(actorStats.startHp, 3) + " → " +
|
||
actorFinalHPStr + (actorStats.hp === 0 ? " \\i[1]" : "");
|
||
|
||
// 威力表示
|
||
const actorPowerStr = formatPowerWithBonus(actorBaseDamage, artifactMods.attackBonus);
|
||
const enemyPowerStr = formatPowerWithBonus(enemyBaseDamage, -artifactMods.defenseBonus);
|
||
|
||
// ログ生成
|
||
const enemyLog = this._buildLog(
|
||
enemyStats, enemy, enemyLevelStr, enemyHpLine, enemyPowerStr, enemyAttackCount, enemyTotalPower
|
||
);
|
||
const actorLog = this._buildLog(
|
||
actorStats, actor, "", actorHpLine, actorPowerStr, actorAttackCount, actorTotalPower
|
||
);
|
||
|
||
return {
|
||
enemyLog,
|
||
actorLog,
|
||
enemyFinalHp: enemyStats.hp,
|
||
actorFinalHp: actorStats.hp,
|
||
enemyDamageTaken: actorTotalPower,
|
||
actorDamageTaken: enemyTotalPower,
|
||
winner: (actorStats.hp > enemyStats.hp) ? "actor" : "enemy",
|
||
first,
|
||
actor,
|
||
enemy
|
||
};
|
||
}
|
||
|
||
// runBattle():simulateBattle() の結果に基づき実際のHPにダメージを適用し、結果を返す
|
||
static runBattle(actor, enemy) {
|
||
const simResult = MapBattleSimulator.simulateBattle(actor, enemy);
|
||
const actorDamageTaken = actor.hp - simResult.actorFinalHp;
|
||
const enemyDamageTaken = enemy.hp - simResult.enemyFinalHp;
|
||
actor.gainHp(-actorDamageTaken);
|
||
enemy.gainHp(-enemyDamageTaken);
|
||
return {
|
||
enemyLog: simResult.enemyLog,
|
||
actorLog: simResult.actorLog,
|
||
enemyDamageTaken: enemyDamageTaken,
|
||
actorDamageTaken: actorDamageTaken,
|
||
winner: simResult.winner,
|
||
first: simResult.first
|
||
};
|
||
}
|
||
}
|
||
|
||
window.Kurogoma = window.Kurogoma || {};
|
||
window.Kurogoma.MapBattleSimulator = MapBattleSimulator;
|
||
|
||
})();
|