nyacronomicon/js/plugins/KN_MapBattleActionManager.js

562 lines
No EOL
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//=============================================================================
// RPG Maker MZ - KN_MapBattleActionManager.js
//=============================================================================
/*:ja
* @target MZ
* @author kurogoma knights
* @base PluginCommonBase
* @plugindesc マップバトルのアクション(アイテム、スキル、攻撃とか)の管理プラグイン
*/
(() => {
'use strict';
const script = document.currentScript;
const param = PluginManagerEx.createParameter(script);
const TP_RECOVER = 15; // 敵撃破時のTP回復量
const RUN_WAIT = 6; // バトル実行ウェイト
//
// マップバトルのアクション管理クラス
//
class MapBattleActionManager {
static actionHandlers = new Map(); // アクション処理を登録するMap
static logs = [];
// アクション処理を登録(アクション名をキーとして処理を登録)
static registerAction(key, handler) {
this.actionHandlers.set(key, handler);
}
// 敵に対する使用回数制限を取得(遺物ボーナスを含む)
static getUsageLimit(target, actor, type = 'skill') {
if (!target) return 1;
const baseLimit = type === 'skill'
? (target._skillUsageLimit || 1)
: (target._itemUsageLimit || 1);
// 遺物ボーナス
const modifiers = Kurogoma.ArtifactBonus.getModifiers();
const bonus = type === 'skill'
? (modifiers.skillUsageBonus || 0)
: (modifiers.itemUsageBonus || 0);
return baseLimit + bonus;
}
// 敵に対する残り使用可能回数を取得
static getRemainingUsageCount(target, actor, type = 'skill') {
const limit = this.getUsageLimit(target, actor, type);
const used = type === 'skill'
? (target._skillUsageCount || 0)
: (target._itemUsageCount || 0);
return Math.max(0, limit - used);
}
// 使用回数チェック
static checkUsageLimit(target, actor, type = 'skill') {
const remaining = this.getRemainingUsageCount(target, actor, type);
if (remaining <= 0) {
const limit = this.getUsageLimit(target, actor, type);
const typeName = type === 'skill' ? 'skill' : 'item';
notify(`Cannot use any more ${typeName}s on this enemy! (${limit}/${limit} used)`);
return false;
}
return true;
}
// 使用回数を増やす
static incrementUsageCount(target, type = 'skill') {
if (!target) return;
if (type === 'skill') {
if (!target._skillUsageCount) target._skillUsageCount = 0;
target._skillUsageCount += 1;
} else {
if (!target._itemUsageCount) target._itemUsageCount = 0;
target._itemUsageCount += 1;
}
}
// 使用回数を減らす(キャンセル時用)
static decrementUsageCount(target, type = 'skill') {
if (!target) return;
if (type === 'skill') {
if (!target._skillUsageCount) target._skillUsageCount = 0;
target._skillUsageCount = Math.max(0, target._skillUsageCount - 1);
} else {
if (!target._itemUsageCount) target._itemUsageCount = 0;
target._itemUsageCount = Math.max(0, target._itemUsageCount - 1);
}
}
// 共通処理:アクション実行前の準備
static beforeActionExecution(context) {
// 使用回数制限チェックitemがある場合のみ
if (context.item && context.target && context.item.meta.CategoryType) {
const type = context.item.meta.CategoryType; // 'skill' or 'item'
const canUse = this.checkUsageLimit(context.target, context.subject, type);
if (!canUse) {
context._usageLimitExceeded = true;
return;
}
// 使用可能なら先にカウントを増やす(キャンセル時に減らす)
this.incrementUsageCount(context.target, type);
context._usageType = type; // キャンセル時に戻すために保存
}
}
// 共通処理:アクション実行後の処理
static afterActionExecution(context) {
// アイテム効果によるステート付与を適用
if (context.item) {
applyStatesFromItem(context.target, context.item);
}
}
// アクション処理を実行
// ログ表示は MapBattleActionManager 内で完結させる(呼び出し側は logs を気にしない)
static executeAction(key, context) {
const handler = this.actionHandlers.get(key);
if (handler) {
// 共通の前処理
this.beforeActionExecution(context);
// 使用回数制限超過の場合はアクションを実行しない
if (!context._usageLimitExceeded) {
// アクション固有の処理
handler(context);
}
// 共通の後処理
this.afterActionExecution(context);
} else {
console.warn(`アクション「${key}」の処理が登録されていません。`);
}
// ハンドラ内でメッセージや選択肢がセットされている場合は、実行元のインタプリタをメッセージ待ちにする
const interpreter = context?.interpreter;
const hasChoices = !!($gameMessage._choices && $gameMessage._choices.length > 0);
const hasTexts = !!($gameMessage._texts && $gameMessage._texts.length > 0);
if ((hasChoices || hasTexts) && interpreter && typeof interpreter.setWaitMode === 'function') {
interpreter.setWaitMode('message');
}
}
}
window.Kurogoma = window.Kurogoma || {};
window.Kurogoma.MapBattleActionManager = MapBattleActionManager;
//
// アクション定義
//
let key = "";
key = "テストアクション";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
const scripts = [
() => {
Kurogoma.EventUtils.notify("Shiro has collapsed...");
$gameMap.getInterpreter().wait(60);
},
() => {
console.log("end");
},
]
// ここでscriptsを標準イベント形式(355と655を使ったもの)に変換
// 実行をインタプリタに持たせたのでこのままでは動かんと思うから注意
const list = convertScriptToEventCommand(scripts);
// const commonEvent = $dataCommonEvents[5];
// $gameMap.getInterpreter().setupChild(commonEvent.list, $gameMap.getInterpreter().eventId());
// console.log(commonEvent.list);
// コモンっぽく再生
$gameMap.getInterpreter().setupChild(list, $gameMap.getInterpreter().eventId());
// console.log(list);
});
key = "攻撃";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context; // 分割代入
const actor = subject;
const enemy = target;
// シミュレーション結果を取得
const simResult = Kurogoma.MapBattleSimulator.simulateBattle(actor, enemy);
// 先制側がどちらかsimResult.first が "actor" または "enemy"
if (simResult.first === "actor") {
// アクターが先制の場合:まずアクターの攻撃結果(エネミーが受けたダメージ)を表示
if (simResult.enemyDamageTaken > 0) {
// character: 0 → エネミー用
popupDamage(0, simResult.enemyDamageTaken, false);
notify(`${actor.name()} attacks! Dealt \\c[2]${simResult.enemyDamageTaken}\\c[0] damage to ${enemy.name()}!`);
}
// 次にエネミーの反撃結果(アクターが受けたダメージ)
if (simResult.actorDamageTaken > 0) {
// character: -1 → アクター用
popupDamage(-1, simResult.actorDamageTaken, false);
notify(`${enemy.name()} counterattacks! Dealt \\c[2]${simResult.actorDamageTaken}\\c[0] damage to ${actor.name()}!`);
}
} else {
// エネミーが先制の場合:まずエネミーの攻撃結果(アクターが受けたダメージ)を表示
if (simResult.actorDamageTaken > 0) {
popupDamage(-1, simResult.actorDamageTaken, false);
notify(`${enemy.name()} attacks! Dealt \\c[2]${simResult.actorDamageTaken}\\c[0] damage to ${actor.name()}!`);
}
// 次にアクターの反撃結果(エネミーが受けたダメージ)を表示
if (simResult.enemyDamageTaken > 0) {
popupDamage(0, simResult.enemyDamageTaken, false);
notify(`${actor.name()} counterattacks! Dealt \\c[2]${simResult.enemyDamageTaken}\\c[0] damage to ${enemy.name()}!`);
}
}
// 演出
playSe("Attack2");
playShake();
// シミュレーション結果に基づいて実際にダメージを適用
const runResult = Kurogoma.MapBattleSimulator.runBattle(actor, enemy);
// うぇいと
interpreter.wait(RUN_WAIT);
});
key = "勝利";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
const eventId = interpreter.eventId();
// イベント削除するとイベント参照が消えて座標バグるプラグインがあるので例えばTemplateEventとCharacterPopupDamageの組み合わせ
$gameSelfSwitches.setValue([$gameMap.mapId(), eventId, 'D'], true); // こっちを使う
// $gameMap.eraseEvent(eventId); // イベント削除
// ボスタイプ判定RaceEffectManagerを使用
const isBigBoss = Kurogoma.RaceEffect.isEnemyBigBoss(target);
const isMidBoss = Kurogoma.RaceEffect.isEnemyMidBoss(target);
if (isBigBoss) {
// 大ボス撃破時の特殊演出
$gameScreen.startFlash([255, 255, 255, 170], 60); // レベルあがるとそっちで上書きされてそう→対策済み(フラグで制御)
$gameScreen.startShake(9, 9, 60); // 攻撃でシェイクしてるから注意 たぶんこっちが上書きされる?
playSe("VXA/Collapse4");
notify(`\\c[6][Boss Defeated] ${target.name()} was defeated!\\c[0]`);
notify(`Gained \\c[14]${target.exp()}\\c[0] EXP!`);
interpreter.wait(60); // ここの関数全体処理されてからウェイトされるから注意
} else if (isMidBoss) {
// 中ボス撃破時の演出
playSe("VXA/Collapse3");
notify(`${target.name()} was defeated! Gained ${target.exp()} EXP!`);
} else {
// 雑魚敵撃破時
playSe("VXA/Collapse1");
notify(`${target.name()} was defeated! Gained ${target.exp()} EXP!`);
}
// TP回復遺物で2倍になる可能性あり
const tpRecovery = Kurogoma.ArtifactBonus.hasDoubleTpRecoveryArtifact() ? TP_RECOVER * 2 : TP_RECOVER;
subject.gainTp(tpRecovery);
// 経験値加算
$gameTemp._skipLevelUpFlash = isBigBoss || isMidBoss; // ボス撃破演出を優先
subject.changeExp(subject.currentExp() + target.exp(), false); // レベルアップ表示なし
$gameTemp._skipLevelUpFlash = false;
});
key = "敗北";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
const eventId = interpreter.eventId();
// 疑似非同期を検討したが、呼び出し元の同期/非同期が混在すると流れ読みづらい/デバッグしづらい/その他デメリットが多数
// いさぎよく敗北はコモンに任せることにした。現在イベント側からよんでる
// const commonEvent = $dataCommonEvents[6]; // 敗北コモン
// $gameMap.getInterpreter().setupChild(commonEvent.list, $gameMap.getInterpreter().eventId());
});
const damageActionConfigs = [
{ key: "ねこぱんち", costMeta: "TP消費", costType: "tp" },
{ key: "超ねこぱんちの書", costMeta: "TP消費", costType: "tp" },
{ key: "超ねこぱんちの書+", costMeta: "TP消費", costType: "tp" },
{ key: "ねこだましの書", costMeta: "TP消費", costType: "tp" },
{ key: "ねこだましの書+", costMeta: "TP消費", costType: "tp" },
{ key: "ねこキックの書", costMeta: "TP消費", costType: "tp" },
{ key: "ねこキックの書+", costMeta: "TP消費", costType: "tp" },
{ key: "絶ねこぱんちの書", costMeta: "TP消費", costType: "tp" },
{ key: "絶ねこぱんちの書+", costMeta: "TP消費", costType: "tp" },
{ key: "サンダー", costMeta: "MP消費", costType: "mp" },
{ key: "ジャッジボルトの書", costMeta: "MP消費", costType: "mp" },
{ key: "ジャッジボルトの書+", costMeta: "MP消費", costType: "mp" },
{ key: "サンダーブレイクの書", costMeta: "MP消費", costType: "mp" },
{ key: "サンダーブレイクの書+", costMeta: "MP消費", costType: "mp" },
{ key: "ライトニングピアスの書", costMeta: "MP消費", costType: "mp" },
{ key: "ライトニングピアスの書+", costMeta: "MP消費", costType: "mp" },
{ key: "ショックバインドの書", costMeta: "MP消費", costType: "mp" },
{ key: "ショックバインドの書+", costMeta: "MP消費", costType: "mp" },
{ key: "ヴォーテクスの書", costMeta: "MP消費", costType: "mp" },
{ key: "ヴォーテクスの書+", costMeta: "MP消費", costType: "mp" },
{ key: "うに", se: "Explosion2" },
{ key: "ダイナマイト", se: "Explosion2" },
{ key: "ウィークパウダー", se: "Paralyze1" },
{ key: "ブレイクパウダー", se: "Paralyze1" },
{ key: "スロウパウダー", se: "Paralyze1" },
{ key: "オールダウンパウダー", se: "Paralyze1" },
];
damageActionConfigs.forEach((config) => registerDamageItemAction(config));
function registerDamageItemAction({ key, costMeta, costType, se = "Thunder1" }) {
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
const damage = calculateItemDamage(subject, target, item);
playSe(se);
// MP/TPコスト処理
if (costType === "mp") {
const costValue = Number(item.meta[costMeta] ?? 0);
subject.gainMp(-costValue);
} else if (costType === "tp") {
const costValue = Number(item.meta[costMeta] ?? 0);
subject.gainTp(-costValue);
}
// ダメージがある場合のみHP変更と表示
if (damage > 0) {
interpreter.changeHp(target, -damage, false); // 1は残る
$gameScreen.startShake(5, 5, 3);
popupDamage(0, damage, false);
notify(`Used ${item.name}!`);
notify(`Dealt ${damage} damage to ${target.name()}!`);
} else {
notify(`Used ${item.name}!`);
}
});
}
key = "シロ式究極魔法489";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
// 実行処理を関数化してコールバック内から呼ぶ
const doAction = () => {
const damage = calculateItemDamage(subject, target, item);
playSe("Explosion2");
interpreter.changeHp(target, -damage, false);
subject.gainHp(-damage);
$gameScreen.startShake(5, 5, 3);
popupDamage(-1, damage, false);
notify(`Used ${item.name}!`);
notify(`${subject.name()} took ${damage} damage!`);
};
// 選択肅をセット(はい = index 0, いいえ = index 1
$gameMessage.setChoices(["Yes", "No"], 0, 1);
$gameMessage.setChoiceCallback((n) => {
if (n === 0) {
doAction();
} else {
notify("Use cancelled.");
// キャンセル時はカウントを戻す
const type = item.meta.CategoryType || 'skill';
MapBattleActionManager.decrementUsageCount(target, type);
}
});
$gameMessage.add("I have a very bad feeling about this...!\nAre you sure?");
});
key = "生体破壊爆弾";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
const eventId = interpreter.eventId();
$gameParty.gainItem(item, -1);
playSe("Fire8"); // サウンド再生
$gameScreen.startShake(5, 5, 6); // 画面の揺れ効果
$gameSelfSwitches.setValue([$gameMap.mapId(), eventId, 'D'], true); // イベント削除するとバグるからこっち
// $gameMap.eraseEvent(eventId); // イベント削除
notify(`Used ${item.name}!`);
notify(`${target.name()} was obliterated!`); // ログ出力
});
key = "チートコード";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
$gameParty.gainItem(item, -1);
playSe("Flash1");
$gameScreen.startFlash([255, 255, 255, 170], 6)
target.isBoss = false; // 属性リセット
notify(`Used ${item.name}!`);
notify(`${target.name()}'s traits were stripped!`);
});
key = "煙玉";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
$gameParty.gainItem(item, -1);
playSe("Magic4");
interpreter.character(0).forceMoveRoute({ // https://rpgmaker-script-wiki.xyz/moverout_mv.php
"list": [
{ "code": 42, "parameters": [128] }, // 不透明度
{ "code": 37 }, // すり抜けON
{ "code": 15, "parameters": [300] }, // ウェイト
{ "code": 42, "parameters": [255] }, // 不透明度
{ "code": 38 }, // すり抜けOFF
{ "code": 0 }, // ルートエンド(必須)
],
"repeat": false,
"skippable": true,
})
notify(`Used ${item.name}!`);
notify(`${target.name()} has lost sight of ${subject.name()}...`);
});
key = "毒瓶";
MapBattleActionManager.registerAction(key, (context) => {
const { subject, target, item, interpreter } = context;
const damage = calculateItemDamage(subject, target, item);
// 実行処理を関数化してコールバック内から呼ぶ
const doAction = () => {
const damage = calculateItemDamage(subject, target, item);
playSe("Explosion2");
interpreter.changeHp(target, -damage, false);
subject.gainHp(-damage);
$gameScreen.startShake(5, 5, 3);
popupDamage(-1, damage, false);
notify(`Used ${item.name}!`);
notify(`${subject.name()} took ${damage} damage!`);
};
// 選択肅をセット(はい = index 0, いいえ = index 1
$gameMessage.setChoices(["Yes", "No"], 0, 1);
$gameMessage.setChoiceCallback((n) => {
if (n === 0) {
doAction();
} else {
notify("Use cancelled.");
// キャンセル時はカウントを戻す
const type = item.meta.CategoryType || 'skill';
MapBattleActionManager.decrementUsageCount(target, type);
}
});
$gameMessage.add("Are you really sure you want to drink this...!?");
});
//
// helper utils
//
// アイテムの効果からステート付与を適用
function applyStatesFromItem(target, item) {
const effectIds = extractStateIdsFromEffects(item.effects);
const stateIds = [...new Set(effectIds)];
if (!stateIds.length) {
return;
}
applyStateIds(target, stateIds);
}
// アイテム効果からステートIDを抽出
function extractStateIdsFromEffects(effects) {
if (!effects?.length) {
return [];
}
return effects
.filter(effect => effect.code === 21)
.map(effect => Number(effect.dataId))
.filter(id => Number.isFinite(id) && id > 0);
}
// ステートIDリストを対象に付与
function applyStateIds(target, stateIds) {
if (!stateIds.length) {
return;
}
const applied = [];
const invalid = [];
stateIds.forEach(id => {
const state = $dataStates[id];
if (state) {
target.addState(id);
applied.push(state.name || `ID:${id}`);
} else {
invalid.push(id);
}
});
if (invalid.length) {
console.warn(`[KN_MapBattle] 存在しないステートID: ${invalid.join(', ')}`);
}
if (!applied.length) {
return;
}
const name = target.name();
const message = `Applied ${applied.join(', ')} to ${name}!`;
notify(message);
}
// アイテムダメージ計算
function calculateItemDamage(subject, target, item) {
// ダメージタイプが「なし」(type: 0)の場合は0を返す
if (item.damage.type === 0) {
return 0;
}
const action = new Game_Action(subject);
action.setItemObject(item);
const rawValue = action.makeDamageValue(target, false);
return Math.max(0, Math.round(rawValue));
}
// EventUtils
window.Kurogoma = window.Kurogoma || {};
Kurogoma.EventUtils = {
// イベント全中断...流れを追うのが煩雑になるから使わない方向で組みたい。
suspendAllEvent() {
for (let i = $gameMap._interpreter; i; i = i._childInterpreter) {
i.command115();
}
},
playShake() {
$gameScreen.startShake(5, 5, 3);
},
// SE
playSe(name) {
const se = { name: name, volume: 90, pitch: 100, pan: 0 };
AudioManager.playSe(se);
},
// 通知メッセージ表示
notify(message, iconIndex = 0) {
Torigoya.NotifyMessage.Manager.notify(
new Torigoya.NotifyMessage.NotifyItem({ message: message, icon: iconIndex })
);
},
// pop
popupDamage(character, damage, isCritical) {
const interpreter = $gameMap.getInterpreter();
const args = {
character: String(character),
value: String(damage),
setting: JSON.stringify({ reverse: "false", type: "HP", critical: String(isCritical) }),
};
PluginManager.callCommand(interpreter, "CharacterPopupDamage", "POPUP_DAMAGE", args);
},
};
const { playShake, suspendAllEvent, playSe, notify, popupDamage } = window.Kurogoma.EventUtils;
})();