//=============================================================================
// RTB_ArmyBattle.js
//=============================================================================
// 版本: 2.5.1 (Save Before Call)
// 描述: 英雄带兵战斗系统 - 彻底修复首次攻击不显示
//=============================================================================
/*:
* @target MZ
* @plugindesc v2.5.1 英雄带兵战斗系统 (数据保存版)
* @author YourName
* @help
* ============================================================================
* 插件说明
* ============================================================================
* 此插件将RPG Maker MZ的默认战斗界面改造为带兵打仗风格。
*
* 主要特性:
* - 上方显示敌人头像、名称、HP条(隐藏默认敌人战斗图)
* - 攻击时,左侧出现我方部队,右侧出现敌方部队
* - 兵模图片从 /img/pictures/ 文件夹读取
* - 若未配置映射,自动使用默认图片:Actor1_1, Actor1_2, Actor1_3
*
* @param generalMapping
* @text 将领图片映射
* @desc 角色ID与将领图片的对应关系。格式: ["1:图片名","2:图片名"]
* @type string[]
* @default ["1:Actor1_1"]
*
* @param supportMapping
* @text 支援兵器映射
* @desc 防具ID与支援兵器图片的对应关系。格式: ["1:图片名","2:图片名"]
* @type string[]
* @default ["1:Actor1_2"]
*
* @param troopMapping
* @text 兵种映射
* @desc 武器ID:兵模图片:类型:每单位人力。
* @type string[]
* @default ["1:Actor1_3:infantry:1"]
*
* @param enemyGeneralMapping
* @text 敌将图片映射
* @desc 敌人ID与将领图片的对应关系。
* @type string[]
* @default ["1:Monster_1"]
*
* @param enemyTroopMapping
* @text 敌兵种映射
* @desc 敌人ID:兵模图片:类型:每单位人力。
* @type string[]
* @default ["1:Monster_2:infantry:1"]
*
* @param enemyAvatarMapping
* @text 敌人头像映射
* @desc 敌人ID与头像图片的对应关系,用于上方敌人列表显示。
* @type string[]
* @default ["1:Monster_3"]
*/
(() => {
'use strict';
console.log('[RTB] 插件初始化开始');
//=============================================================================
// 参数解析 (完全容错)
//=============================================================================
const parameters = PluginManager.parameters('RTB_ArmyBattle');
function safeJsonParse(str, defaultValue = []) {
if (!str) return defaultValue;
try {
return JSON.parse(str);
} catch (e) {
console.warn('[RTB] 参数解析失败,使用默认值。', str);
return defaultValue;
}
}
const GENERAL_MAP_RAW = safeJsonParse(parameters.generalMapping, ["1:Actor1_1"]);
const SUPPORT_MAP_RAW = safeJsonParse(parameters.supportMapping, ["1:Actor1_2"]);
const TROOP_MAP_RAW = safeJsonParse(parameters.troopMapping, ["1:Actor1_3:infantry:1"]);
const ENEMY_GENERAL_MAP_RAW = safeJsonParse(parameters.enemyGeneralMapping, ["1:Monster_1"]);
const ENEMY_TROOP_MAP_RAW = safeJsonParse(parameters.enemyTroopMapping, ["1:Monster_2:infantry:1"]);
const ENEMY_AVATAR_MAP_RAW = safeJsonParse(parameters.enemyAvatarMapping, ["1:Monster_3"]);
function parseSimpleMapping(rawArray) {
const map = new Map();
for (const entry of rawArray) {
if (typeof entry !== 'string') continue;
const parts = entry.split(':');
if (parts.length >= 2) {
const id = Number(parts[0]);
const name = parts[1].trim();
if (!isNaN(id) && name) map.set(id, name);
}
}
return map;
}
function parseTroopMapping(rawArray) {
const map = new Map();
for (const entry of rawArray) {
if (typeof entry !== 'string') continue;
const parts = entry.split(':');
if (parts.length >= 4) {
const id = Number(parts[0]);
const imageName = parts[1].trim();
const type = parts[2].trim().toLowerCase();
const unitHp = Number(parts[3]);
if (!isNaN(id) && imageName) {
map.set(id, { imageName, type, unitHp });
}
}
}
return map;
}
const GENERAL_MAP = parseSimpleMapping(GENERAL_MAP_RAW);
const SUPPORT_MAP = parseSimpleMapping(SUPPORT_MAP_RAW);
const TROOP_MAP = parseTroopMapping(TROOP_MAP_RAW);
const ENEMY_GENERAL_MAP = parseSimpleMapping(ENEMY_GENERAL_MAP_RAW);
const ENEMY_TROOP_MAP = parseTroopMapping(ENEMY_TROOP_MAP_RAW);
const ENEMY_AVATAR_MAP = parseSimpleMapping(ENEMY_AVATAR_MAP_RAW);
console.log('[RTB] 映射加载完成');
//=============================================================================
// 工具函数
//=============================================================================
function calculateTroopCount(currentHp, troopType, unitHp) {
if (currentHp <= 0) return 0;
const base = (unitHp && unitHp > 0) ? unitHp : (troopType === 'armor' ? 10 : 1);
return Math.max(1, Math.ceil(currentHp / base));
}
function getActorForceData(actor) {
const actorId = actor.actorId();
const weapon = actor.weapons()[0];
const armor = actor.armors()[0];
let generalImage = GENERAL_MAP.get(actorId) || 'Actor1_1';
let supportImage = armor ? (SUPPORT_MAP.get(armor.id) || 'Actor1_2') : '';
let troopImage = 'Actor1_3';
let troopType = 'infantry';
let unitHp = 1;
if (weapon) {
const troopInfo = TROOP_MAP.get(weapon.id);
if (troopInfo) {
troopImage = troopInfo.imageName;
troopType = troopInfo.type;
unitHp = troopInfo.unitHp;
}
}
const soldierCount = calculateTroopCount(actor.hp, troopType, unitHp);
return { generalImage, supportImage, troopImage, troopType, soldierCount };
}
function getEnemyForceData(enemy) {
const enemyId = enemy.enemyId();
const troopInfo = ENEMY_TROOP_MAP.get(enemyId);
let troopImage = troopInfo ? troopInfo.imageName : 'Monster_2';
let troopType = troopInfo ? troopInfo.type : 'infantry';
let unitHp = troopInfo ? troopInfo.unitHp : 1;
let generalImage = ENEMY_GENERAL_MAP.get(enemyId) || 'Monster_1';
const soldierCount = calculateTroopCount(enemy.hp, troopType, unitHp);
return { generalImage, troopImage, troopType, soldierCount };
}
//=============================================================================
// 窗口:上方敌人状态列表
//=============================================================================
class Window_TroopStatus extends Window_Base {
initialize() {
const w = Graphics.boxWidth;
const h = 180;
super.initialize(new Rectangle(0, 0, w, h));
this._enemies = [];
this.opacity = 192;
this.refresh();
}
setEnemies(enemies) {
this._enemies = enemies || [];
this.refresh();
}
refresh() {
this.contents.clear();
if (!this._enemies.length) return;
const itemWidth = 200;
const padding = 10;
const startX = 20;
const y = 0;
this._enemies.forEach((enemy, index) => {
const x = startX + index * (itemWidth + padding);
this.drawEnemyItem(enemy, x, y, itemWidth);
});
}
drawEnemyItem(enemy, x, y, width) {
const enemyId = enemy.enemyId();
const avatarName = ENEMY_AVATAR_MAP.get(enemyId) || 'Monster_3';
try {
const bitmap = ImageManager.loadPicture(avatarName);
this.contents.blt(bitmap, 0, 0, 48, 48, x, y, 48, 48);
} catch (e) {
try {
this.drawIcon(1, x, y);
} catch (e2) {}
}
this.drawText(enemy.name(), x + 54, y, width - 54, 'left');
const hpY = y + 24;
const hpWidth = width - 54;
const rate = enemy.hpRate();
const barHeight = 12;
this.contents.fillRect(x + 54, hpY, hpWidth, barHeight, '#000000');
const color1 = ColorManager.textColor(20);
const fillW = Math.max(0, Math.floor((hpWidth - 2) * rate));
this.contents.fillRect(x + 55, hpY + 1, fillW, barHeight - 2, color1);
this.drawText(`${enemy.hp}/${enemy.mhp}`, x + 54, hpY + barHeight, hpWidth, 'right');
}
update() {
super.update();
if (this._enemies.length) this.refresh();
}
}
//=============================================================================
// 隐藏默认敌人精灵
//=============================================================================
const _Spriteset_Battle_createEnemies = Spriteset_Battle.prototype.createEnemies;
Spriteset_Battle.prototype.createEnemies = function() {
_Spriteset_Battle_createEnemies.call(this);
if (this._enemySprites) {
this._enemySprites.forEach(sprite => sprite.visible = false);
}
};
const _Spriteset_Battle_update = Spriteset_Battle.prototype.update;
Spriteset_Battle.prototype.update = function() {
_Spriteset_Battle_update.call(this);
if (this._enemySprites) {
this._enemySprites.forEach(sprite => sprite.visible = false);
}
};
//=============================================================================
// 场景战斗扩展:创建容器和部队显示方法
//=============================================================================
const _Scene_Battle_createDisplayObjects = Scene_Battle.prototype.createDisplayObjects;
Scene_Battle.prototype.createDisplayObjects = function() {
_Scene_Battle_createDisplayObjects.call(this);
this._troopStatusWindow = new Window_TroopStatus();
this.addWindow(this._troopStatusWindow);
this._troopStatusWindow.setEnemies($gameTroop.members());
// 确保容器存在
if (this._spriteset) {
this._spriteset._leftArmyContainer = new PIXI.Container();
this._spriteset._rightArmyContainer = new PIXI.Container();
// 添加到 _baseSprite 或直接添加到 spriteset
const target = this._spriteset._baseSprite || this._spriteset;
target.addChild(this._spriteset._leftArmyContainer);
target.addChild(this._spriteset._rightArmyContainer);
}
};
const _Scene_Battle_update = Scene_Battle.prototype.update;
Scene_Battle.prototype.update = function() {
_Scene_Battle_update.call(this);
if (this._troopStatusWindow) {
this._troopStatusWindow.setEnemies($gameTroop.members());
}
};
Scene_Battle.prototype.clearArmySprites = function() {
const spriteset = this._spriteset;
if (!spriteset) return;
if (spriteset._leftArmyContainer) spriteset._leftArmyContainer.removeChildren();
if (spriteset._rightArmyContainer) spriteset._rightArmyContainer.removeChildren();
if (this._armyClearTimer) {
clearTimeout(this._armyClearTimer);
this._armyClearTimer = null;
}
};
Scene_Battle.prototype.showLeftArmy = function(actorData) {
const spriteset = this._spriteset;
if (!spriteset || !spriteset._leftArmyContainer) return;
spriteset._leftArmyContainer.removeChildren();
const centerX = 200;
const baseY = Graphics.boxHeight - 200;
const addSprite = (img, x, y) => {
if (!img) return;
try {
const bitmap = ImageManager.loadPicture(img);
const sprite = new Sprite(bitmap);
sprite.x = x;
sprite.y = y;
sprite.scale.set(0.8, 0.8);
sprite.anchor.set(0.5, 1);
spriteset._leftArmyContainer.addChild(sprite);
} catch (e) {
console.warn(`[RTB] 加载左侧图片失败: ${img}`);
}
};
addSprite(actorData.generalImage, centerX - 40, baseY - 60);
if (actorData.supportImage) {
addSprite(actorData.supportImage, centerX + 60, baseY - 60);
}
if (actorData.soldierCount > 0) {
const count = Math.min(actorData.soldierCount, 8);
for (let i = 0; i < count; i++) {
const offsetX = (i % 4) * 30 - 45;
const offsetY = Math.floor(i / 4) * 25 + 20;
addSprite(actorData.troopImage, centerX + offsetX, baseY + offsetY);
}
}
};
Scene_Battle.prototype.showRightArmy = function(enemyData) {
const spriteset = this._spriteset;
if (!spriteset || !spriteset._rightArmyContainer) return;
spriteset._rightArmyContainer.removeChildren();
const centerX = Graphics.boxWidth - 200;
const baseY = Graphics.boxHeight - 200;
const addSprite = (img, x, y) => {
if (!img) return;
try {
const bitmap = ImageManager.loadPicture(img);
const sprite = new Sprite(bitmap);
sprite.x = x;
sprite.y = y;
sprite.scale.set(0.8, 0.8);
sprite.anchor.set(0.5, 1);
spriteset._rightArmyContainer.addChild(sprite);
} catch (e) {
console.warn(`[RTB] 加载右侧图片失败: ${img}`);
}
};
addSprite(enemyData.generalImage, centerX - 40, baseY - 60);
if (enemyData.soldierCount > 0) {
const count = Math.min(enemyData.soldierCount, 8);
for (let i = 0; i < count; i++) {
const offsetX = (i % 4) * 30 - 45;
const offsetY = Math.floor(i / 4) * 25 + 20;
addSprite(enemyData.troopImage, centerX + offsetX, baseY + offsetY);
}
}
};
//=============================================================================
// 拦截行动开始 (关键修正:先保存数据,再调原版)
//=============================================================================
const _BattleManager_startAction = BattleManager.startAction;
BattleManager.startAction = function() {
const action = this._action;
const subject = this._subject;
// 【关键】立即保存当前行动者和目标,避免被原版方法清空
const isActorAction = subject && subject.isActor() && action;
let actorData = null;
let enemyData = null;
if (isActorAction) {
actorData = getActorForceData(subject);
const targets = action.makeTargets();
let target = targets.find(t => t.isEnemy());
if (!target) {
for (const t of targets) {
if (t.isEnemy()) { target = t; break; }
}
}
if (target) enemyData = getEnemyForceData(target);
}
// 执行原版逻辑
_BattleManager_startAction.call(this);
// 显示部队
if (isActorAction) {
const scene = SceneManager._scene;
if (!scene) return;
console.log('[RTB] 角色行动,显示部队');
if (scene._armyClearTimer) {
clearTimeout(scene._armyClearTimer);
scene._armyClearTimer = null;
}
scene.clearArmySprites();
scene.showLeftArmy(actorData);
if (enemyData) scene.showRightArmy(enemyData);
// 设置清除定时器
scene._armyClearTimer = setTimeout(() => scene.clearArmySprites(), 600);
}
};
const _Scene_Battle_terminate = Scene_Battle.prototype.terminate;
Scene_Battle.prototype.terminate = function() {
if (this._armyClearTimer) {
clearTimeout(this._armyClearTimer);
this._armyClearTimer = null;
}
this.clearArmySprites();
_Scene_Battle_terminate.call(this);
};
console.log('[RTB] 插件加载完成');
})();