.
Пример того, что он делает.
Для установки смените окончание\расшерение файла с txt на Js.
Code:
/*:
* @target MZ
* @plugindesc v7.5 Formation Grid: сетка + механики (без спрайтов/анимаций врагов)
* @author Custom
*
* @help
* ============================================================
* Battle Formation Grid v7.5
* ============================================================
* Сетка: бойцы внутри линии стоят СТОЛБИКОМ СНИЗУ ВВЕРХ,
* линии идут СЛЕВА НАПРАВО. Линия 0 — фронт.
*
* --- ТЕГИ СКИЛЛОВ / ПРЕДМЕТОВ ---
* <ranged> <melee>
* <pierceLines:N> <aoeLine:N> <aoeColumn:N> <aoeCross:N>
* <coverBack> (state)
* ============================================================
*
* @command openFormation
* @text Открыть построение
*
* @param attackIsMelee
* @text Базовая «Атака» = ближняя
* @type boolean
* @default true
*
* @param partySize
* @text Party: бойцов в линии
* @type number
* @min 1
* @max 6
* @default 3
*
* @param partyLines
* @text Party: линий
* @type number
* @min 1
* @max 6
* @default 3
*
* @param partyOriginX
* @text Party: Origin X
* @type number
* @default 540
*
* @param partyOriginY
* @text Party: Origin Y
* @type number
* @default 540
*
* @param partyColSpacing
* @text Party: шаг бойцов в линии
* @type number
* @default 80
*
* @param partyRowSpacing
* @text Party: шаг между линиями
* @type number
* @default 100
*
* @param partyMirror
* @text Party: Mirror
* @type boolean
* @default false
*
* @param partyFaceLeft
* @text Party: смотрят влево
* @type boolean
* @default true
*
* @param enemySize
* @text Enemy: бойцов в линии
* @type number
* @min 1
* @max 6
* @default 3
*
* @param enemyLines
* @text Enemy: линий
* @type number
* @min 1
* @max 6
* @default 3
*
* @param enemyOriginX
* @text Enemy: Origin X
* @type number
* @default 260
*
* @param enemyOriginY
* @text Enemy: Origin Y
* @type number
* @default 540
*
* @param enemyColSpacing
* @text Enemy: шаг бойцов в линии
* @type number
* @default 80
*
* @param enemyRowSpacing
* @text Enemy: шаг между линиями
* @type number
* @default 100
*
* @param enemyMirror
* @text Enemy: Mirror
* @type boolean
* @default true
*/
(() => {
"use strict";
const pluginName = "BattleFormationGrid";
const raw = PluginManager.parameters(pluginName);
const clamp = (v, a, b) => isNaN(v) ? a : Math.max(a, Math.min(b, v));
const P = {
attackIsMelee: String(raw.attackIsMelee) !== "false",
partySize: clamp(Number(raw.partySize || 3), 1, 6),
partyLines: clamp(Number(raw.partyLines || 3), 1, 6),
partyOriginX: Number(raw.partyOriginX || 540),
partyOriginY: Number(raw.partyOriginY || 540),
partyColSpacing: Number(raw.partyColSpacing || 80),
partyRowSpacing: Number(raw.partyRowSpacing || 100),
partyMirror: String(raw.partyMirror) === "true",
partyFaceLeft: String(raw.partyFaceLeft) !== "false",
enemySize: clamp(Number(raw.enemySize || 3), 1, 6),
enemyLines: clamp(Number(raw.enemyLines || 3), 1, 6),
enemyOriginX: Number(raw.enemyOriginX || 260),
enemyOriginY: Number(raw.enemyOriginY || 540),
enemyColSpacing: Number(raw.enemyColSpacing || 80),
enemyRowSpacing: Number(raw.enemyRowSpacing || 100),
enemyMirror: String(raw.enemyMirror) !== "false"
};
// ============================================================
// UI СЦЕНЫ ПОСТРОЕНИЯ
// ============================================================
const LAYOUT = {
grid: {
originX: 560, originY: 500,
stepX: 100, stepY: 90,
cellW: 100, cellH: 90,
innerPadding: 2
},
frontLabel: { x: 460, y: 260, charW: 30, charH: 26, fontSize: 22 },
lineLabel: { offsetY: 36, w: 140, h: 20, fontSize: 16 },
infoLabel: {
offsetY: 60, w: 180, h: 46,
nameFontSize: 17, hpFontSize: 12,
hpBarH: 10, hpBarMargin: 10, hpBarBottomGap: 10
},
helpWindow: { x: 40, y: 600, w: 1200, h: 70 },
linePanel: { x: 40, y: 50, w: 220, h: 450, rowH: 38 },
cursor: { extra: 8, thickness: 3 }
};
// ============================================================
// УТИЛИТЫ
// ============================================================
function actorSlot(actor) {
if (!actor) return -1;
const map = $gameParty._formationSlots;
if (map) {
const key = String(actor.actorId());
if (Object.prototype.hasOwnProperty.call(map, key)) return Number(map[key]);
}
return $gameParty.allMembers().indexOf(actor);
}
function battlerSlot(b) {
if (!b) return -1;
if (b.isActor && b.isActor()) return actorSlot(b);
return b.index();
}
function gridPos(index, size, lines, ox, oy, cs, rs, mirror) {
const member = index % size;
const line = Math.floor(index / size);
const dir = mirror ? -1 : 1;
return {
x: Math.round(ox + line * rs * dir),
y: Math.round(oy - member * cs)
};
}
function getLineOfBattler(b) {
if (!b) return -1;
const s = battlerSlot(b);
if (s < 0) return -1;
const size = b.isActor() ? P.partySize : P.enemySize;
return Math.floor(s / size);
}
function getFrontLivingLineOfUnit(unit) {
if (!unit) return -1;
let min = Infinity;
for (const m of unit.aliveMembers()) {
const l = getLineOfBattler(m);
if (l >= 0 && l < min) min = l;
}
return min === Infinity ? -1 : min;
}
function unitLayout(unit) {
const isPartyUnit = !!unit._actors;
return {
size: isPartyUnit ? P.partySize : P.enemySize,
lines: isPartyUnit ? P.partyLines : P.enemyLines
};
}
function cellsOfUnit(unit) {
const { size } = unitLayout(unit);
const cells = [];
for (const m of unit.aliveMembers()) {
const s = battlerSlot(m);
if (s < 0) continue;
cells.push({
member: m,
memberIdx: s % size,
line: Math.floor(s / size)
});
}
return cells;
}
function itemNote(action) { return (action.item() && action.item().note) || ""; }
function actionIsMelee(action) {
const note = itemNote(action);
if (/<ranged>/i.test(note)) return false;
if (/<melee>/i.test(note)) return true;
return P.attackIsMelee && action.isAttack();
}
function parsePierce(action) {
const m = /<pierceLines:(\d+)>/i.exec(itemNote(action));
return m ? clamp(Number(m[1]), 1, 6) : 1;
}
function parseAoeShapes(action) {
const note = itemNote(action);
const shapes = [];
const grab = (re, key) => {
const m = re.exec(note);
if (!m) return;
const n = m[1] ? clamp(Number(m[1]), 1, 36) : 36;
shapes.push({ key, n });
};
grab(/<aoeLine:(\d+)>/i, "line");
grab(/<aoeColumn:(\d+)>/i, "column");
grab(/<aoeCross:(\d+)>/i, "cross");
if (/<aoeLine>(?!\d)/i.test(note) && !/<aoeLine:\d+>/i.test(note))
shapes.push({ key: "line", n: 36 });
if (/<aoeColumn>(?!\d)/i.test(note) && !/<aoeColumn:\d+>/i.test(note))
shapes.push({ key: "column", n: 36 });
if (/<aoeCross>(?!\d)/i.test(note) && !/<aoeCross:\d+>/i.test(note))
shapes.push({ key: "cross", n: 36 });
return shapes;
}
// ============================================================
// ПОЗИЦИИ СПРАЙТОВ В БОЮ
// ============================================================
Sprite_Actor.prototype.setActorHome = function(index) {
const battler = this._actor || this._battler;
const slot = (battler && battler.isActor && battler.isActor())
? actorSlot(battler) : index;
const s = slot >= 0 ? slot : index;
const p = gridPos(
s,
P.partySize, P.partyLines,
P.partyOriginX, P.partyOriginY,
P.partyColSpacing, P.partyRowSpacing,
P.partyMirror
);
this.setHome(p.x, p.y);
};
Game_Enemy.prototype.screenX = function() {
return gridPos(this.index(), P.enemySize, P.enemyLines,
P.enemyOriginX, P.enemyOriginY, P.enemyColSpacing, P.enemyRowSpacing,
P.enemyMirror).x;
};
Game_Enemy.prototype.screenY = function() {
return gridPos(this.index(), P.enemySize, P.enemyLines,
P.enemyOriginX, P.enemyOriginY, P.enemyColSpacing, P.enemyRowSpacing,
P.enemyMirror).y;
};
Game_Actor.prototype.screenX = function() {
const s = actorSlot(this);
return gridPos(s >= 0 ? s : 0, P.partySize, P.partyLines,
P.partyOriginX, P.partyOriginY, P.partyColSpacing, P.partyRowSpacing,
P.partyMirror).x;
};
Game_Actor.prototype.screenY = function() {
const s = actorSlot(this);
return gridPos(s >= 0 ? s : 0, P.partySize, P.partyLines,
P.partyOriginX, P.partyOriginY, P.partyColSpacing, P.partyRowSpacing,
P.partyMirror).y;
};
// ============================================================
// ФИЛЬТР ПО ПЕРЕДНЕЙ ЛИНИИ
// ============================================================
// Может ли атакующий вообще достать кого-то ближним боем
// (должен стоять в своей передней живой линии).
function meleeAttackerCanReach(action) {
if (!action) return true;
const subject = action.subject();
if (!subject) return true;
const ownFront = getFrontLivingLineOfUnit(subject.friendsUnit());
if (ownFront < 0) return true;
return getLineOfBattler(subject) === ownFront;
}
// null — достижимо; иначе текст причины.
function meleeBlockReason(action, target) {
if (!action || !target) return null;
if (!actionIsMelee(action)) return null;
if (parsePierce(action) > 1) return null;
if (!action.isForOpponent()) return null;
if (!meleeAttackerCanReach(action))
return "Атака ближнего боя невозможна из задней линии.";
const front = getFrontLivingLineOfUnit(action.opponentsUnit());
if (front < 0) return null;
if (getLineOfBattler(target) !== front)
return "Слишком далеко! Цель за передней линией.";
return null;
}
function targetIsReachable(action, target) {
return meleeBlockReason(action, target) === null;
}
function filterByFrontLine(action, targets) {
if (!targets || !targets.length) return targets;
if (!actionIsMelee(action)) return targets;
if (parsePierce(action) > 1) return targets;
const subject = action.subject();
const isActorSubject = subject && subject.isActor && subject.isActor();
// Атакующий ближним боем должен сам быть в своей передней линии.
// Игрока здесь не режем — ему покажем сообщение на подтверждении.
// Врагов (ИИ) отсекаем сразу, чтобы они не выбирали такую атаку.
if (subject && !meleeAttackerCanReach(action) && !isActorSubject) {
return [];
}
// Игрок сам решает, куда бить: цели не фильтруем,
// а на подтверждении покажем «Слишком далеко!».
if (isActorSubject && action.isForOpponent()) {
return targets;
}
const unit = action.isForOpponent()
? action.opponentsUnit() : action.friendsUnit();
const front = getFrontLivingLineOfUnit(unit);
if (front < 0) return targets;
const filtered = targets.filter(t => getLineOfBattler(t) === front);
return filtered.length ? filtered : targets;
}
const _targetsForOpponents = Game_Action.prototype.targetsForOpponents;
Game_Action.prototype.targetsForOpponents = function() {
return filterByFrontLine(this, _targetsForOpponents.call(this));
};
const _targetsForFriends = Game_Action.prototype.targetsForFriends;
Game_Action.prototype.targetsForFriends = function() {
return filterByFrontLine(this, _targetsForFriends.call(this));
};
// ============================================================
// AoE / PIERCE
// ============================================================
function expandAoe(action, targets) {
if (!targets.length) return targets;
const note = itemNote(action);
const pierce = parsePierce(action);
const shapes = parseAoeShapes(action);
const hasPierce = pierce > 1 && /<pierceLines:\d+>/i.test(note);
if (!hasPierce && !shapes.length) return targets;
const unit = action.isForOpponent()
? action.opponentsUnit() : action.friendsUnit();
const cells = cellsOfUnit(unit);
if (!cells.length) return targets;
const result = new Set(targets);
if (hasPierce) {
const lines = [...new Set(cells.map(c => c.line))].sort((a, b) => a - b);
const activeLines = lines.slice(0, pierce);
for (const c of cells) if (activeLines.includes(c.line)) result.add(c.member);
}
for (const t of targets) {
const tcell = cells.find(c => c.member === t);
if (!tcell) continue;
for (const shape of shapes) {
if (shape.key === "line") {
const group = cells.filter(c => c.line === tcell.line);
group.sort((a, b) =>
Math.abs(a.memberIdx - tcell.memberIdx) -
Math.abs(b.memberIdx - tcell.memberIdx));
group.slice(0, shape.n).forEach(c => result.add(c.member));
} else if (shape.key === "column") {
const group = cells.filter(c => c.memberIdx === tcell.memberIdx);
group.sort((a, b) => a.line - b.line);
group.slice(0, shape.n).forEach(c => result.add(c.member));
} else if (shape.key === "cross") {
const cross = cells.filter(c =>
c.line === tcell.line || c.memberIdx === tcell.memberIdx);
cross.sort((a, b) => {
const da = Math.abs(a.memberIdx - tcell.memberIdx) +
Math.abs(a.line - tcell.line);
const db = Math.abs(b.memberIdx - tcell.memberIdx) +
Math.abs(b.line - tcell.line);
return da - db;
});
cross.slice(0, shape.n).forEach(c => result.add(c.member));
}
}
}
return [...result];
}
const _makeTargets = Game_Action.prototype.makeTargets;
Game_Action.prototype.makeTargets = function() {
let targets = _makeTargets.call(this);
if (this._forcing || !this.subject()) return targets;
if (!targets.length) return targets;
return expandAoe(this, targets);
};
// ============================================================
// ПРИКРЫТИЕ ТЫЛА (все эффекты действия)
// ============================================================
function findCoverGuard(target, action) {
if (!target || !target.isActor()) return null;
if (getLineOfBattler(target) <= 0) return null;
// Перехватываем только враждебные действия по цели.
// Дружеские баффы/хилки на тыл проходят как есть.
if (action && action.isForOpponent && !action.isForOpponent()) return null;
for (const m of $gameParty.allMembers()) {
if (m === target) continue;
if (m.hp <= 0) continue;
if (getLineOfBattler(m) !== 0) continue;
if (m.states().some(s => s && /<coverBack>/i.test(s.note || ""))) return m;
}
return null;
}
// Хук на штатный механизм замещения. Он вызывается ДО apply()
// и предназначен ровно для подмены цели. Сработает для любых
// эффектов: HP/MP-урон, состояния, баффы, common events и т.д.
const _BM_applySubstitute = BattleManager.applySubstitute;
BattleManager.applySubstitute = function(target) {
// Сначала даём отработать встроенному <Substitute>-трейту.
target = _BM_applySubstitute.call(this, target);
// Затем — наш state-based <coverBack>.
const guard = findCoverGuard(target, this._action);
if (guard && guard.hp > 0 && guard !== target) {
if (this._logWindow && this._logWindow.displaySubstitute) {
this._logWindow.displaySubstitute(guard, target);
}
return guard;
}
return target;
};
// ============================================================
// СПРАЙТ АКТЁРА (сцена построения)
// ============================================================
function Sprite_FormationActor() { this.initialize(...arguments); }
Sprite_FormationActor.prototype = Object.create(Sprite.prototype);
Sprite_FormationActor.prototype.constructor = Sprite_FormationActor;
Sprite_FormationActor.prototype.initialize = function(actor, faceLeft) {
Sprite.prototype.initialize.call(this);
this._actor = actor || null;
this._faceLeft = !!faceLeft;
this.anchor.x = 0.5;
this.anchor.y = 1.0;
this.z = 10;
this.loadBitmap();
};
Sprite_FormationActor.prototype.setActor = function(actor) {
if (this._actor === actor) return;
this._actor = actor || null;
this.loadBitmap();
};
Sprite_FormationActor.prototype.loadBitmap = function() {
if (!this._actor || !this._actor.battlerName()) {
this.bitmap = null; this.visible = false; return;
}
this.visible = true;
this.bitmap = ImageManager.loadSvActor(this._actor.battlerName());
this.scale.x = this._faceLeft ? 1 : -1;
this.bitmap.addLoadListener(() => this.updateFrame());
this.updateFrame();
};
Sprite_FormationActor.prototype.updateFrame = function() {
if (!this._actor || !this.bitmap || !this.bitmap.width) return;
const pw = Math.floor(this.bitmap.width / 9);
const ph = Math.floor(this.bitmap.height / 6);
if (!pw || !ph) return;
this.setFrame(0, 1 * ph, pw, ph);
};
// ============================================================
// СПРАЙТ «ИМЯ + HP»
// ============================================================
function Sprite_SlotInfo() { this.initialize(...arguments); }
Sprite_SlotInfo.prototype = Object.create(Sprite.prototype);
Sprite_SlotInfo.prototype.constructor = Sprite_SlotInfo;
Sprite_SlotInfo.prototype.initialize = function() {
Sprite.prototype.initialize.call(this);
this._actor = null;
const L = LAYOUT.infoLabel;
this._bitmap = new Bitmap(L.w, L.h);
this.bitmap = this._bitmap;
this.anchor.x = 0.5;
this.anchor.y = 1.0;
this.z = 15;
this.visible = false;
};
Sprite_SlotInfo.prototype.setActor = function(actor) {
this._actor = actor || null;
this.visible = !!actor;
this.refresh();
};
Sprite_SlotInfo.prototype.refresh = function() {
const b = this._bitmap; b.clear();
if (!this._actor) return;
const L = LAYOUT.infoLabel;
const w = b.width;
b.fontSize = L.nameFontSize;
b.outlineColor = "rgba(0,0,0,0.9)"; b.outlineWidth = 4;
b.textColor = "#ffffff";
b.drawText(this._actor.name(), 0, 0, w, 20, "center");
const hp = Math.max(0, this._actor.hp);
const mhp = Math.max(1, this._actor.mhp);
const ratio = Math.max(0, Math.min(1, hp / mhp));
const margin = L.hpBarMargin;
const bw = w - margin * 2;
const bh = L.hpBarH;
const bx = margin;
const by = L.h - bh - L.hpBarBottomGap;
b.fillRect(bx - 1, by - 1, bw + 2, bh + 2, "rgba(0,0,0,0.85)");
b.fillRect(bx, by, bw, bh, "rgba(40,40,40,0.9)");
const color = ratio > 0.5 ? "#3ad13a" : ratio > 0.25 ? "#e0c020" : "#e03030";
b.fillRect(bx, by, Math.floor(bw * ratio), bh, color);
b.fontSize = L.hpFontSize;
b.outlineColor = "rgba(0,0,0,0.95)"; b.outlineWidth = 3;
b.textColor = "#ffffff";
b.drawText(`${hp}/${mhp}`, 0, by - 4, w, 16, "center");
};
// ============================================================
// ЛЕВАЯ ПАНЕЛЬ «ВЫБОР ЛИНИИ»
// ============================================================
function Window_FormationLines() { this.initialize(...arguments); }
Window_FormationLines.prototype = Object.create(Window_Base.prototype);
Window_FormationLines.prototype.constructor = Window_FormationLines;
Window_FormationLines.prototype.initialize = function(rect) {
Window_Base.prototype.initialize.call(this, rect);
this._currentLine = 0;
this.refresh();
};
Window_FormationLines.prototype.setCurrentLine = function(line) {
if (this._currentLine === line) return;
this._currentLine = line;
this.refresh();
};
Window_FormationLines.prototype.refresh = function() {
this.contents.clear();
const names = ["Первая линия", "Вторая", "Третья", "Четвёртая", "Пятая", "Шестая"];
const lh = LAYOUT.linePanel.rowH;
for (let i = 0; i < P.partyLines; i++) {
const label = i === 0 ? "Фронт" : (names[i] || `Линия ${i + 1}`);
this.changeTextColor(i === this._currentLine ? "#ffe66a" : "#ffffff");
this.contents.outlineColor = "rgba(0,0,0,0.9)";
this.contents.outlineWidth = 4;
this.drawText(label, 0, i * lh, this.contents.width, lh, "left");
}
};
// ============================================================
// СЦЕНА ПОСТРОЕНИЯ
// ============================================================
function Scene_FormationGrid() { this.initialize(...arguments); }
Scene_FormationGrid.prototype = Object.create(Scene_MenuBase.prototype);
Scene_FormationGrid.prototype.constructor = Scene_FormationGrid;
Scene_FormationGrid.prototype.create = function() {
Scene_MenuBase.prototype.create.call(this);
this._slotCount = P.partySize * P.partyLines;
this._cursorSlot = 0;
this._pickedSlot = -1;
this._slots = this.buildInitialSlots();
this.createGridLayer();
this.createLabelsLayer();
this.createActorSprites();
this.createInfoSprites();
this.createCursorSprite();
this.createHelpWindow();
this.createLineWindow();
this.refreshActors();
this.updateCursorPosition();
};
Scene_FormationGrid.prototype.buildInitialSlots = function() {
const slots = new Array(this._slotCount).fill(null);
const map = $gameParty._formationSlots || {};
const members = $gameParty.members();
for (const a of members) {
const key = String(a.actorId());
if (Object.prototype.hasOwnProperty.call(map, key)) {
const s = Number(map[key]);
if (s >= 0 && s < this._slotCount && !slots[s]) slots[s] = a;
}
}
let free = 0;
for (const a of members) {
if (slots.indexOf(a) >= 0) continue;
while (free < this._slotCount && slots[free]) free++;
if (free >= this._slotCount) break;
slots[free++] = a;
}
return slots;
};
Scene_FormationGrid.prototype.slotPos = function(slot) {
const G = LAYOUT.grid;
const member = slot % P.partySize;
const line = Math.floor(slot / P.partySize);
const dir = P.partyMirror ? -1 : 1;
return {
x: G.originX + line * G.stepX * dir,
y: G.originY - member * G.stepY
};
};
Scene_FormationGrid.prototype.helpWindowRect = function() {
const H = LAYOUT.helpWindow;
return new Rectangle(H.x, H.y, H.w, H.h);
};
Scene_FormationGrid.prototype.createLineWindow = function() {
const L = LAYOUT.linePanel;
const rect = new Rectangle(L.x, L.y, L.w, L.h);
this._lineWindow = new Window_FormationLines(rect);
this.addChild(this._lineWindow);
};
Scene_FormationGrid.prototype.createGridLayer = function() {
const G = LAYOUT.grid;
const ip = G.innerPadding;
const cw = G.cellW;
const ch = G.cellH;
let maxX = Graphics.boxWidth;
let maxY = Graphics.boxHeight;
for (let i = 0; i < this._slotCount; i++) {
const p = this.slotPos(i);
maxX = Math.max(maxX, p.x + cw);
maxY = Math.max(maxY, p.y + ch);
}
const bmp = new Bitmap(maxX, maxY);
bmp.paintOpacity = 60;
for (let i = 0; i < this._slotCount; i++) {
const p = this.slotPos(i);
bmp.fillRect(
Math.round(p.x - cw / 2 + ip),
Math.round(p.y - ch + ip),
cw - ip * 2, ch - ip * 2, "white");
}
bmp.paintOpacity = 30;
for (let m = 0; m < P.partySize; m++) {
const p = this.slotPos(m);
bmp.fillRect(
Math.round(p.x - cw / 2 + ip),
Math.round(p.y - ch + ip),
cw - ip * 2, ch - ip * 2, "#ffe66a");
}
this._gridSprite = new Sprite(bmp);
this._gridSprite.z = 1;
this.addChild(this._gridSprite);
};
Scene_FormationGrid.prototype.createLabelsLayer = function() {
const F = LAYOUT.frontLabel;
const LL = LAYOUT.lineLabel;
const G = LAYOUT.grid;
let maxX = Graphics.boxWidth;
let maxY = Graphics.boxHeight;
for (let i = 0; i < this._slotCount; i++) {
const p = this.slotPos(i);
maxX = Math.max(maxX, p.x + G.cellW / 2 + LL.w / 2 + 20);
maxY = Math.max(maxY, p.y + G.cellH + 20);
}
maxX = Math.max(maxX, F.x + F.charW + 20);
maxY = Math.max(maxY, F.y + F.charH * 6 + 20);
const bmp = new Bitmap(maxX, maxY);
bmp.outlineColor = "rgba(0,0,0,0.9)";
bmp.outlineWidth = 4;
const text = "ФРОНТ";
bmp.fontSize = F.fontSize;
bmp.textColor = "#ffe66a";
for (let i = 0; i < text.length; i++) {
bmp.drawText(text[i], F.x, F.y + i * F.charH, F.charW, F.charH, "center");
}
bmp.fontSize = LL.fontSize;
const names = ["Первая линия", "Вторая", "Третья", "Четвёртая", "Пятая", "Шестая"];
for (let line = 0; line < P.partyLines; line++) {
const topSlot = line * P.partySize + (P.partySize - 1);
const p = this.slotPos(topSlot);
const label = names[line] || `Линия ${line + 1}`;
bmp.textColor = line === 0 ? "#ffe66a" : "#9ad1ff";
bmp.drawText(label, p.x - LL.w / 2, p.y - G.cellH - LL.offsetY, LL.w, LL.h, "center");
}
this._labelsSprite = new Sprite(bmp);
this._labelsSprite.z = 5;
this.addChild(this._labelsSprite);
};
Scene_FormationGrid.prototype.createActorSprites = function() {
this._actorSprites = [];
for (let i = 0; i < this._slotCount; i++) {
const spr = new Sprite_FormationActor(null, P.partyFaceLeft);
const p = this.slotPos(i);
spr.x = p.x; spr.y = p.y; spr.visible = false;
this.addChild(spr);
this._actorSprites.push(spr);
}
};
Scene_FormationGrid.prototype.createInfoSprites = function() {
this._infoSprites = [];
for (let i = 0; i < this._slotCount; i++) {
const info = new Sprite_SlotInfo();
const p = this.slotPos(i);
info.x = p.x;
info.y = p.y - LAYOUT.infoLabel.offsetY;
this.addChild(info);
this._infoSprites.push(info);
}
};
Scene_FormationGrid.prototype.refreshActors = function() {
for (let i = 0; i < this._slotCount; i++) {
const spr = this._actorSprites[i];
const info = this._infoSprites[i];
const actor = this._slots[i];
const picked = (i === this._pickedSlot);
if (actor) {
spr.setActor(actor); spr.visible = true;
spr.opacity = picked ? 160 : 255;
info.setActor(actor);
info.opacity = picked ? 140 : 255;
} else {
spr.visible = false;
info.visible = false;
}
}
};
Scene_FormationGrid.prototype.createCursorSprite = function() {
const C = LAYOUT.cursor;
const G = LAYOUT.grid;
const cw = G.cellW + C.extra;
const ch = G.cellH + C.extra;
const th = C.thickness;
const bmp = new Bitmap(cw, ch);
bmp.fillRect(0, 0, cw, th, "rgba(255,230,90,0.95)");
bmp.fillRect(0, ch - th, cw, th, "rgba(255,230,90,0.95)");
bmp.fillRect(0, 0, th, ch, "rgba(255,230,90,0.95)");
bmp.fillRect(cw - th, 0, th, ch, "rgba(255,230,90,0.95)");
this._cursorSprite = new Sprite(bmp);
this._cursorSprite.anchor.x = 0.5;
this._cursorSprite.anchor.y = 0.5;
this._cursorSprite.z = 20;
this.addChild(this._cursorSprite);
};
Scene_FormationGrid.prototype.updateCursorPosition = function() {
const p = this.slotPos(this._cursorSlot);
const G = LAYOUT.grid;
this._cursorSprite.x = p.x;
this._cursorSprite.y = p.y - G.cellH / 2;
if (this._lineWindow) {
const line = Math.floor(this._cursorSlot / P.partySize);
this._lineWindow.setCurrentLine(line);
}
};
Scene_FormationGrid.prototype.createHelpWindow = function() {
const rect = this.helpWindowRect();
this._helpWindow = new Window_Help(rect);
this._helpWindow.setText(this.defaultHelpText());
this.addChild(this._helpWindow);
};
Scene_FormationGrid.prototype.defaultHelpText = function() {
return "Стрелки: перемещение по сетке OK: взять/поставить (обмен) Esc: выход";
};
Scene_FormationGrid.prototype.update = function() {
Scene_MenuBase.prototype.update.call(this);
if (this._cursorSprite) this.processInput();
};
Scene_FormationGrid.prototype.processInput = function() {
if (Input.isRepeated("up")) this.moveCursor(0, -1);
if (Input.isRepeated("down")) this.moveCursor(0, 1);
if (Input.isRepeated("left")) this.moveCursor(-1, 0);
if (Input.isRepeated("right")) this.moveCursor( 1, 0);
if (Input.isTriggered("ok")) this.onOk();
if (Input.isTriggered("cancel")) this.onCancel();
};
Scene_FormationGrid.prototype.moveCursor = function(dx, dy) {
let line = Math.floor(this._cursorSlot / P.partySize);
let member = this._cursorSlot % P.partySize;
const lineDelta = P.partyMirror ? -dx : dx;
line = (line + lineDelta + P.partyLines) % P.partyLines;
member = (member - dy + P.partySize) % P.partySize;
this._cursorSlot = line * P.partySize + member;
this.updateCursorPosition();
SoundManager.playCursor();
};
Scene_FormationGrid.prototype.onOk = function() {
if (this._pickedSlot < 0) {
if (this._slots[this._cursorSlot]) {
this._pickedSlot = this._cursorSlot;
SoundManager.playOk();
this.refreshActors();
this._helpWindow.setText("OK: поставить / обменять Esc: отмена");
}
} else {
const a = this._pickedSlot, b = this._cursorSlot;
const tmp = this._slots[a];
this._slots[a] = this._slots[b];
this._slots[b] = tmp;
this._pickedSlot = -1;
SoundManager.playOk();
this.refreshActors();
this._helpWindow.setText(this.defaultHelpText());
}
};
Scene_FormationGrid.prototype.onCancel = function() {
if (this._pickedSlot >= 0) {
this._pickedSlot = -1;
this.refreshActors();
this._helpWindow.setText(this.defaultHelpText());
SoundManager.playCancel();
} else {
this.commitFormation();
SoundManager.playCancel();
this.popScene();
}
};
Scene_FormationGrid.prototype.commitFormation = function() {
const map = $gameParty._formationSlots || ($gameParty._formationSlots = {});
for (const k in map) delete map[k];
const ids = [];
for (let i = 0; i < this._slotCount; i++) {
const a = this._slots[i];
if (a) {
map[String(a.actorId())] = i;
ids.push(a.actorId());
}
}
for (const a of $gameParty.allMembers()) {
const key = String(a.actorId());
if (!Object.prototype.hasOwnProperty.call(map, key)) ids.push(a.actorId());
}
$gameParty._actors = ids;
};
Scene_Menu.prototype.commandFormation = function() {
if ($gameParty.size() >= 1) SceneManager.push(Scene_FormationGrid);
};
Window_MenuCommand.prototype.addFormationCommand = function() {
if (this.needsCommand("formation")) {
this.addCommand(TextManager.formation, "formation", $gameParty.size() >= 1);
}
};
PluginManager.registerCommand(pluginName, "openFormation", () => {
if (SceneManager._scene instanceof Scene_Battle) return;
SceneManager.push(Scene_FormationGrid);
});
// ============================================================
// «СЛИШКОМ ДАЛЕКО!» — модальное сообщение, ждёт клика
// ============================================================
const TOO_FAR_TEXT = "Слишком далеко! Цель в недоступной линии.";
// Аккуратно достаём текущий action.
function currentTargetingAction() {
if (typeof BattleManager.inputtingAction === "function") {
const a = BattleManager.inputtingAction();
if (a) return a;
}
if (BattleManager._inputtingAction) return BattleManager._inputtingAction;
if (BattleManager._action) return BattleManager._action;
const actor = BattleManager.actor && BattleManager.actor();
if (actor && actor.currentAction) {
const a = actor.currentAction();
if (a) return a;
}
return null;
}
// Показываем сообщение и временно деактивируем окно цели,
// чтобы то же OK не пролетело в него.
Scene_Battle.prototype.showTooFarMessage = function(text) {
const active = this._enemyWindow && this._enemyWindow.active ? this._enemyWindow
: this._actorWindow && this._actorWindow.active ? this._actorWindow
: null;
if (active) {
this._tooFarRestore = { win: active, index: active.index() };
active.deactivate();
}
SoundManager.playBuzzer();
$gameMessage.add(text || TOO_FAR_TEXT);
};
// Возвращаем фокус окну после того, как игрок кликнул по сообщению.
const _Scene_Battle_update = Scene_Battle.prototype.update;
Scene_Battle.prototype.update = function() {
_Scene_Battle_update.call(this);
if (this._tooFarRestore && !$gameMessage.isBusy()) {
const r = this._tooFarRestore;
this._tooFarRestore = null;
if (r.win && r.win.visible) {
r.win.activate();
r.win.select(Math.min(r.index, r.win.maxItems() - 1));
}
}
};
// Перехват OK в окне врагов.
if (typeof Window_BattleEnemy !== "undefined") {
const _WinEnemy_processOk = Window_BattleEnemy.prototype.processOk;
Window_BattleEnemy.prototype.processOk = function() {
if ($gameMessage.isBusy()) return;
const action = currentTargetingAction();
const enemy = (typeof this.enemy === "function") ? this.enemy() : null;
const reason = meleeBlockReason(action, enemy);
if (reason) {
SceneManager._scene.showTooFarMessage(reason);
return;
}
_WinEnemy_processOk.call(this);
};
}
// Симметрично для окна союзников.
if (typeof Window_BattleActor !== "undefined") {
const _WinActor_processOk = Window_BattleActor.prototype.processOk;
Window_BattleActor.prototype.processOk = function() {
if ($gameMessage.isBusy()) return;
const action = currentTargetingAction();
const actor = (typeof this.actor === "function") ? this.actor() : null;
const reason = meleeBlockReason(action, actor);
if (reason) {
SceneManager._scene.showTooFarMessage(reason);
return;
}
_WinActor_processOk.call(this);
};
}
// ============================================================
// ЭКСПОРТ
// ============================================================
window.BattleFormationGrid = {
config: P,
layout: LAYOUT,
scene: Scene_FormationGrid
};
})();
Code:
/*:
* @target MZ
* @plugindesc v3.0 Formation Reinforcements: волны + роли melee/ranged + респавн с нуля + диагностика
* @author Custom
*
* @help
* ============================================================
* Battle Formation Reinforcements v3.0
* ============================================================
* Требует BattleFormationGrid.js (v5.7+).
* Совместим с CustomEnemySV.js (SVSheet / Weapon / SimpleSheet).
*
* --- ЧТО ДЕЛАТЬ, ЕСЛИ НЕ РАБОТАЕТ ---
* 1) Открой консоль (F12) → начни бой.
* 2) Ищи строку: [BFG] BattleFormationReinforcements v3.0 loaded ...
* Если её нет — плагин не загружен (порядок в списке / имя файла / отключён).
* 3) Ищи строку: [BFG] setup: cfg={...}
* Там видно, что именно распарсилось из note.
* 4) Каждый ход пишет: [BFG] tick: turn=... alive=.../cap empty=[...] want=... n=...
* Если там interval=0 — тег не распознан, поставь интервал явно.
*
* --- ЛОГИКА ВОЛН ---
* - Каждая волна = <BFG_count:N> (или меньше, если не влезает).
* - <BFG_interval:N> — каждые N ходов. Дефолт: 1 (волны идут по ходам).
* <BFG_interval:0> — только экстренный спавн при полной пустоте арены.
* - Если волна не влезает целиком — спавним ЧАСТЬ, остаток добьётся позже.
*
* --- ПРИОРИТЕТ ЯЧЕЕК (в Note врага) ---
* <cellPriority:N> / <cellLine:N> — предпочитаемая линия (0-индексация).
* <cellSlot:N> — конкретная ячейка.
*
* --- РОЛИ (в Note врага) ---
* <cellRole:melee> / <cellMelee> — ближник.
* <cellRole:ranged> / <cellRanged> — дальник.
* Без роли — враг занимает линию с наименьшим числом живых.
*
* --- НАСТРОЙКА В NOTE ОТРЯДА (или Комментарии события битвы) ---
* <BFG_maxEnemies:N> макс. врагов ОДНОВРЕМЕННО (cap = min(N, cells))
* <BFG_delay:N> первое подкрепление через N ходов (деф. 1)
* <BFG_interval:N> каждые N ходов (деф. 1; 0 = только аварийный)
* <BFG_count:N> сколько за раз (деф. 1)
* <BFG_maxReinforcements:N> лимит всего подкреплений (0 = ∞)
* <BFG_se:Name> SE-файл (из audio/se/)
* <BFG_message:Text> сообщение при подкреплении
* <BFG_pool:1,3,5> пул ID врагов (иначе состав отряда)
* <BFG_noAutoFill> отключить автозаполнение
* <BFG_noReinforce> отключить подкрепления
* <BFG_noReposition> отключить шагание melee/ranged
* ============================================================
*
* @command spawn
* @text Немедленно вызвать подкрепление
* @arg count
* @text Сколько
* @type number
* @min 1
* @default 1
*
* @command resetTimer
* @text Сбросить таймер подкреплений
*
* @param enabled
* @text Плагин включён
* @type boolean
* @default true
*
* @param autoFill
* @text Автозаполнение ячеек при старте
* @type boolean
* @default true
*
* @param seVolume
* @text Громкость SE подкреплений
* @type number
* @min 0
* @max 100
* @default 80
*
* @param debug
* @text Логи в консоль (F12)
* @type boolean
* @default true
*/
(() => {
"use strict";
const pluginName = "BattleFormationReinforcements";
const BFG = window.BattleFormationGrid;
if (!BFG || !BFG.config) {
console.error(`[${pluginName}] Требуется BattleFormationGrid.js (v5.7+). ` +
`Проверь порядок плагинов: BFG должен стоять ВЫШЕ подкреплений.`);
return;
}
const raw = PluginManager.parameters(pluginName);
const toBool = (v, def) => (v === undefined ? def : String(v) !== "false");
const toNum = (v, def) => { const n = Number(v); return isNaN(n) ? def : n; };
const P = {
enabled: toBool(raw.enabled, true),
autoFill: toBool(raw.autoFill, true),
seVolume: Math.max(0, Math.min(100, Math.floor(toNum(raw.seVolume, 80)))),
debug: toBool(raw.debug, true)
};
const log = (...a) => { if (P.debug) console.log("%c[BFG]", "color:#8cf;font-weight:bold", ...a); };
const warn = (...a) => { if (P.debug) console.warn("%c[BFG]", "color:#fc8;font-weight:bold", ...a); };
const MAX_CELLS = () => BFG.config.enemySize * BFG.config.enemyLines;
const CELL_SIZE = () => BFG.config.enemySize;
const CELL_LINES = () => BFG.config.enemyLines;
// Громкая загрузочная строка — если её нет в консоли, плагин не поднялся.
log(`BattleFormationReinforcements v3.0 loaded. ` +
`Поле: ${CELL_SIZE()}x${CELL_LINES()}=${MAX_CELLS()} ячеек. ` +
`enabled=${P.enabled} autoFill=${P.autoFill} debug=${P.debug}`);
// ============================================================
// NOTE ВРАГА
// ============================================================
function getEnemyNote(enemyId) {
const e = $dataEnemies[enemyId];
return (e && e.note) || "";
}
function getEnemyPreferredLine(enemyId) {
const note = getEnemyNote(enemyId);
const m = /<cellPriority[:\s]\s*(\d+)\s*>/i.exec(note)
|| /<cellLine[:\s]\s*(\d+)\s*>/i.exec(note);
if (!m) return -1;
const line = Math.max(0, Number(m[1]));
return line < CELL_LINES() ? line : -1;
}
function getEnemyPreferredSlot(enemyId) {
const note = getEnemyNote(enemyId);
const m = /<cellSlot[:\s]\s*(\d+)\s*>/i.exec(note);
if (!m) return -1;
const slot = Math.max(0, Number(m[1]));
return slot < MAX_CELLS() ? slot : -1;
}
function getEnemyRole(enemyId) {
const note = getEnemyNote(enemyId);
if (/<cellRole[:\s]\s*(melee|ближник)\s*>/i.test(note) || /<cellMelee>/i.test(note)) return "melee";
if (/<cellRole[:\s]\s*(ranged|дальник|range)\s*>/i.test(note) || /<cellRanged>/i.test(note)) return "ranged";
return null;
}
// ============================================================
// ЧТЕНИЕ NOTE ОТРЯДА
// ============================================================
function parseTroopConfig(troop) {
let note = (troop && troop.note) || "";
if (troop && troop.pages) {
for (const page of troop.pages) {
if (page.list) {
for (const cmd of page.list) {
if (cmd.code === 108 || cmd.code === 408) {
note += " " + cmd.parameters[0];
}
}
}
}
}
const num = (re, def) => {
const m = re.exec(note);
if (!m) return def;
const n = Number(m[1]);
return isNaN(n) ? def : n;
};
const has = (re) => re.test(note);
// ВАЖНО: interval по умолчанию = 1 (волны идут по ходам).
// Если нужно «только аварийный спавн» — задай <BFG_interval:0> явно.
const cfg = {
maxEnemies: Math.max(0, Math.floor(num(/<BFG[_:]maxEnemies[:\s]\s*(\d+)\s*>/i, 0))),
delay: Math.max(0, Math.floor(num(/<BFG[_:]delay[:\s]\s*(\d+)\s*>/i, 1))),
interval: Math.max(0, Math.floor(num(/<BFG[_:]interval[:\s]\s*(\d+)\s*>/i, 1))),
count: Math.max(1, Math.floor(num(/<BFG[_:]count[:\s]\s*(\d+)\s*>/i, 1))),
maxReinforcements: Math.max(0, Math.floor(num(/<BFG[_:]maxReinforcements[:\s]\s*(\d+)\s*>/i, 0))),
se: "",
message: "",
pool: [],
noAutoFill: false,
noReinforce: false,
noReposition: false
};
const seM = /<BFG[_:]se[:\s]\s*([^>\s]+)\s*>/i.exec(note);
if (seM) cfg.se = seM[1];
const msgM = /<BFG[_:]message[:\s]\s*([^>]+?)\s*>/i.exec(note);
if (msgM) cfg.message = msgM[1].trim();
const poolM = /<BFG[_:]pool[:\s]\s*([\d,\s]+)\s*>/i.exec(note);
if (poolM) {
cfg.pool = poolM[1].split(",")
.map(s => Number(s.trim()))
.filter(n => n > 0);
}
if (has(/<BFG[_:]noAutoFill>/i)) cfg.noAutoFill = true;
if (has(/<BFG[_:]noReinforce>/i)) cfg.noReinforce = true;
if (has(/<BFG[_:]noReposition>/i)) cfg.noReposition = true;
log(`setup: note="${note.trim()}"`);
log(`setup: cfg=`, JSON.parse(JSON.stringify(cfg)));
const cells = MAX_CELLS();
if (cfg.maxEnemies > cells) {
warn(`<BFG_maxEnemies:${cfg.maxEnemies}> > числа ячеек (${cells}). ` +
`Фактический cap = ${cells}.`);
}
if (cfg.interval <= 0 && !cfg.noReinforce) {
warn(`<BFG_interval> = 0 — волны по ходам отключены, работает только аварийный спавн.`);
}
if (cfg.noReinforce) {
warn(`<BFG_noReinforce> — подкрепления полностью отключены для этого боя.`);
}
return cfg;
}
function totalCapOf(cfg) {
const cells = MAX_CELLS();
if (cfg && cfg.maxEnemies > 0) return Math.min(cfg.maxEnemies, cells);
return cells;
}
// ============================================================
// СОСТОЯНИЕ
// ============================================================
function ensureState() {
const t = $gameTroop;
if (!t) return null;
if (!t._reinfState) {
t._reinfState = {
pool: [],
spawned: 0,
cfg: null,
firstSpawnDone: false,
lastSpawnTurn: 0,
_lastTick: -1
};
}
return t._reinfState;
}
function ensureCfg() {
const t = $gameTroop;
if (!t) return null;
let st = t._reinfState;
if (!st || !st.cfg) {
st = ensureState();
if (!st) return null;
if (!st.cfg) {
const troop = $dataTroops[t._troopId];
if (troop) {
st.cfg = parseTroopConfig(troop);
if (!st.pool || !st.pool.length) {
const ids = t._enemies.map(e => e.enemyId());
st.pool = ids.length ? ids : [1];
}
log(`cfg восстановлен по _troopId=${t._troopId}`);
}
}
}
return st;
}
// ============================================================
// РАСПРЕДЕЛЕНИЕ ПО ЯЧЕЙКАМ (setup)
// ============================================================
function assignEnemiesToSlots(ids, maxCells, size) {
const result = new Array(maxCells).fill(0);
const used = new Set();
const priority = [];
const normal = [];
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const slot = getEnemyPreferredSlot(id);
const line = getEnemyPreferredLine(id);
if (slot >= 0) priority.push({ id, slot, line: -1, order: i });
else if (line >= 0) priority.push({ id, slot: -1, line, order: i });
else normal.push({ id, order: i });
}
for (const p of priority) {
if (p.slot >= 0 && !used.has(p.slot)) {
result[p.slot] = p.id;
used.add(p.slot);
continue;
}
if (p.line >= 0) {
const start = p.line * size;
let placed = false;
for (let m = 0; m < size; m++) {
const s = start + m;
if (s < maxCells && !used.has(s)) {
result[s] = p.id;
used.add(s);
placed = true;
break;
}
}
if (placed) continue;
}
normal.push(p);
}
normal.sort((a, b) => a.order - b.order);
for (const n of normal) {
for (let s = 0; s < maxCells; s++) {
if (!used.has(s)) {
result[s] = n.id;
used.add(s);
break;
}
}
}
return result;
}
// ============================================================
// SETUP
// ============================================================
const _Game_Troop_setup = Game_Troop.prototype.setup;
Game_Troop.prototype.setup = function(troopId) {
_Game_Troop_setup.call(this, troopId);
this._reinfState = null;
if (!P.enabled) return;
const troop = $dataTroops[troopId];
const cfg = parseTroopConfig(troop);
const st = ensureState();
if (!st) return;
st.cfg = cfg;
if (cfg.pool && cfg.pool.length) {
st.pool = cfg.pool.slice();
} else {
const ids = this._enemies.map(e => e.enemyId());
st.pool = ids.length ? ids : [1];
}
const maxCells = MAX_CELLS();
const size = CELL_SIZE();
const cap = totalCapOf(cfg);
const sourceIds = this._enemies.map(e => e.enemyId());
if (!sourceIds.length) sourceIds.push(st.pool[0] || 1);
const autoFill = P.autoFill && !cfg.noAutoFill;
const fillList = sourceIds.slice();
if (autoFill) {
while (fillList.length < cap) {
fillList.push(st.pool[(fillList.length - sourceIds.length) % st.pool.length]);
}
}
const slotToId = assignEnemiesToSlots(fillList, maxCells, size);
this._enemies = [];
for (let s = 0; s < maxCells; s++) {
const id = slotToId[s];
const enemy = new Game_Enemy(id > 0 ? id : st.pool[0], 0, 0);
enemy._formationSlotIndex = s;
if (id > 0) {
enemy._reinforcementPending = false;
} else {
enemy._reinforcementPending = true;
enemy.setHp(0);
enemy.hide();
}
this._enemies.push(enemy);
}
const liveAtStart = this._enemies.filter(e => !e._reinforcementPending).length;
log(`setup: autoFill=${autoFill} cap=${cap} cells=${maxCells} ` +
`заполнено при старте=${liveAtStart} ` +
`(из них «живых» врагов: ${this._enemies.filter(e => !e._reinforcementPending).length}, ` +
`пустых слотов: ${maxCells - liveAtStart})`);
};
const _Game_Enemy_index = Game_Enemy.prototype.index;
Game_Enemy.prototype.index = function() {
if (this._formationSlotIndex != null) return this._formationSlotIndex;
return _Game_Enemy_index.call(this);
};
// ============================================================
// СПРАЙТЫ
// ============================================================
function destroyEnemySprite(sp) {
if (!sp) return;
for (const key of ["_shadowSprite", "_weaponSprite"]) {
const sib = sp[key];
if (sib) {
if (sib.parent) sib.parent.removeChild(sib);
if (typeof sib.destroy === "function") sib.destroy();
sp[key] = null;
}
}
if (sp.parent) sp.parent.removeChild(sp);
if (typeof sp.destroy === "function") sp.destroy();
}
function recreateEnemySprite(enemy) {
const scene = SceneManager._scene;
if (!(scene instanceof Scene_Battle)) return;
const ss = scene._spriteset;
if (!ss || !ss._enemySprites) return;
const idx = ss._enemySprites.findIndex(sp => sp._enemy === enemy);
if (idx < 0) {
const sp = new Sprite_Enemy(enemy);
ss._enemySprites.push(sp);
if (ss._battleField) ss._battleField.addChild(sp);
} else {
const old = ss._enemySprites[idx];
const parent = old.parent;
destroyEnemySprite(old);
const sp = new Sprite_Enemy(enemy);
ss._enemySprites[idx] = sp;
const targetParent = parent || ss._battleField;
if (targetParent) targetParent.addChild(sp);
}
if (typeof ss.updateEnemySprites === "function") {
ss.updateEnemySprites();
}
}
function reviveEnemyInSlot(enemy, newEnemyId) {
if (newEnemyId && newEnemyId !== enemy.enemyId()) {
enemy._enemyId = newEnemyId;
}
enemy._hidden = false;
enemy._reinforcementPending = false;
enemy.clearStates();
enemy.clearBuffs();
enemy.clearActions();
enemy.setHp(enemy.mhp);
enemy.setMp(enemy.mmp);
enemy.refresh();
recreateEnemySprite(enemy);
}
function findEmptySlots() {
const max = MAX_CELLS();
const enemies = $gameTroop._enemies;
const res = [];
for (let i = 0; i < max; i++) {
const e = enemies[i];
if (!e) { res.push(i); continue; }
if (e._reinforcementPending) { res.push(i); continue; }
if (e.isDead() || e.hp <= 0 || e.isHidden()) { res.push(i); continue; }
}
return res;
}
// ============================================================
// ВЫБОР СЛОТА
// ============================================================
function pickSlotForEnemy(enemyId, empty, size) {
const prefSlot = getEnemyPreferredSlot(enemyId);
if (prefSlot >= 0 && empty.includes(prefSlot)) return prefSlot;
const prefLine = getEnemyPreferredLine(enemyId);
if (prefLine >= 0) {
const inLine = empty.find(s => Math.floor(s / size) === prefLine);
if (inLine !== undefined) return inLine;
let bestSlot = -1, bestDist = Infinity;
for (const s of empty) {
const d = Math.abs(Math.floor(s / size) - prefLine);
if (d < bestDist) { bestDist = d; bestSlot = s; }
}
if (bestSlot >= 0) return bestSlot;
}
const role = getEnemyRole(enemyId);
if (role === "melee") {
let best = -1, bestLine = Infinity;
for (const s of empty) {
const L = Math.floor(s / size);
if (L < bestLine) { bestLine = L; best = s; }
}
if (best >= 0) return best;
}
if (role === "ranged") {
let best = -1, bestLine = -Infinity;
for (const s of empty) {
const L = Math.floor(s / size);
if (L > bestLine) { bestLine = L; best = s; }
}
if (best >= 0) return best;
}
const lines = CELL_LINES();
const occupied = new Array(lines).fill(0);
const enemies = $gameTroop._enemies;
const maxCells = MAX_CELLS();
for (let s = 0; s < maxCells; s++) {
const e = enemies[s];
if (e && !e._reinforcementPending && e.isAlive()) {
occupied[Math.floor(s / size)]++;
}
}
let best = -1, bestCount = Infinity;
for (const s of empty) {
const L = Math.floor(s / size);
if (occupied[L] < bestCount) { bestCount = occupied[L]; best = s; }
}
if (best >= 0) return best;
return empty[0];
}
// ============================================================
// ПЕРЕМЕЩЕНИЕ
// ============================================================
function moveEnemyToSlot(enemy, newSlot) {
const oldSlot = enemy._formationSlotIndex;
if (oldSlot === newSlot) return false;
const enemies = $gameTroop._enemies;
const target = enemies[newSlot];
if (target && !target._reinforcementPending && target.isAlive()) return false;
let placeholder;
if (target && target._reinforcementPending) {
placeholder = target;
placeholder._formationSlotIndex = oldSlot;
} else {
placeholder = new Game_Enemy(enemy.enemyId(), 0, 0);
placeholder._formationSlotIndex = oldSlot;
placeholder._reinforcementPending = true;
placeholder.setHp(0);
placeholder.hide();
}
enemy._formationSlotIndex = newSlot;
enemies[newSlot] = enemy;
enemies[oldSlot] = placeholder;
recreateEnemySprite(enemy);
recreateEnemySprite(placeholder);
return true;
}
function repositionEnemies() {
const st = $gameTroop._reinfState;
if (!st || !st.cfg) return;
if (st.cfg.noReposition) return;
const size = CELL_SIZE();
const lines = CELL_LINES();
const maxCells = MAX_CELLS();
const enemies = $gameTroop._enemies;
const aliveByLine = [];
for (let L = 0; L < lines; L++) aliveByLine[L] = 0;
const list = [];
for (let s = 0; s < maxCells; s++) {
const e = enemies[s];
if (e && !e._reinforcementPending && e.isAlive()) {
list.push(e);
aliveByLine[Math.floor(s / size)]++;
}
}
const emptySet = new Set(findEmptySlots());
for (const e of list) {
const role = getEnemyRole(e.enemyId());
if (!role) continue;
const curSlot = e._formationSlotIndex;
const curLine = Math.floor(curSlot / size);
if (role === "melee") {
if (curLine <= 0) continue;
const targetLine = curLine - 1;
const start = targetLine * size;
let found = -1;
for (let k = 0; k < size; k++) {
const s = start + k;
if (emptySet.has(s)) { found = s; break; }
}
if (found >= 0 && moveEnemyToSlot(e, found)) {
emptySet.delete(found); emptySet.add(curSlot);
aliveByLine[curLine]--; aliveByLine[targetLine]++;
}
} else if (role === "ranged") {
if (curLine >= lines - 1) continue;
let allEmptyAhead = true;
for (let L = 0; L < curLine; L++) {
if (aliveByLine[L] > 0) { allEmptyAhead = false; break; }
}
if (!allEmptyAhead) continue;
const targetLine = curLine + 1;
const start = targetLine * size;
let found = -1;
for (let k = 0; k < size; k++) {
const s = start + k;
if (emptySet.has(s)) { found = s; break; }
}
if (found >= 0 && moveEnemyToSlot(e, found)) {
emptySet.delete(found); emptySet.add(curSlot);
aliveByLine[curLine]--; aliveByLine[targetLine]++;
}
}
}
}
// ============================================================
// СПАВН
// ============================================================
function doReinforce(count, isEmergency) {
const st = ensureCfg();
if (!st || !st.cfg) { warn("doReinforce: нет cfg."); return 0; }
const cfg = st.cfg;
if (cfg.noReinforce) { log("doReinforce: noReinforce."); return 0; }
if (!isEmergency && cfg.interval <= 0) { log("doReinforce: interval=0, skip."); return 0; }
const size = CELL_SIZE();
const cap = totalCapOf(cfg);
const aliveNow = $gameTroop._enemies.filter(e =>
e && !e._reinforcementPending && e.isAlive()
).length;
const roomByCap = Math.max(0, cap - aliveNow);
const empty = findEmptySlots();
const baseWant = Math.max(1, Math.floor(count || cfg.count));
const budgetLeft = cfg.maxReinforcements > 0
? Math.max(0, cfg.maxReinforcements - st.spawned)
: Infinity;
log(`doReinforce: turn=${$gameTroop.turnCount()} ` +
`alive=${aliveNow}/${cap} room=${roomByCap} empty=[${empty.join(",")}] ` +
`want=${baseWant} budgetLeft=${budgetLeft} emergency=${!!isEmergency}`);
if (roomByCap <= 0) { log(" → cap достигнут (все слоты до cap заняты)."); return 0; }
if (!empty.length) { log(" → нет пустых ячеек физически."); return 0; }
if (budgetLeft <= 0) { log(" → бюджет исчерпан."); return 0; }
const want = Math.min(baseWant, budgetLeft);
if (want <= 0) return 0;
const canFit = Math.min(roomByCap, empty.length);
const n = Math.min(want, canFit);
if (n <= 0) return 0;
log(` → Спавним ${n} (canFit=${canFit}, want=${want}).`);
const pool = (st.pool && st.pool.length) ? st.pool : [1];
const enemies = $gameTroop._enemies;
let spawned = 0;
for (let k = 0; k < n; k++) {
if (!empty.length) break;
const newId = pool[(st.spawned + k) % pool.length];
const slot = pickSlotForEnemy(newId, empty, size);
empty.splice(empty.indexOf(slot), 1);
let enemy = enemies[slot];
if (!enemy) {
enemy = new Game_Enemy(newId, 0, 0);
enemy._formationSlotIndex = slot;
enemies[slot] = enemy;
}
reviveEnemyInSlot(enemy, newId);
spawned++;
}
if (spawned > 0) {
st.spawned += spawned;
if (cfg.se) AudioManager.playSe({ name: cfg.se, volume: P.seVolume, pitch: 100, pan: 0 });
if (cfg.message) $gameMessage.add(cfg.message);
}
return spawned;
}
// ============================================================
// ТИК ХОДА (общий для двух триггеров, с защитой от дублей)
// ============================================================
function onTurnTick() {
if (!P.enabled) return;
const st = ensureCfg();
if (!st || !st.cfg) { warn("tick: cfg отсутствует."); return; }
const cfg = st.cfg;
const turn = $gameTroop.turnCount();
if (st._lastTick === turn) return; // уже обрабатывали этот ход
st._lastTick = turn;
log(`tick: turn=${turn} first=${st.firstSpawnDone} last=${st.lastSpawnTurn} ` +
`interval=${cfg.interval} delay=${cfg.delay} count=${cfg.count} ` +
`spawned=${st.spawned}`);
if (cfg.noReinforce) { log(" → noReinforce."); return; }
if (cfg.interval <= 0) { log(" → interval=0, только аварийный спавн."); return; }
if ($gameParty.isAllDead()) { log(" → party dead."); return; }
if (BattleManager._phase === "battleEnd") { log(" → battleEnd."); return; }
if (!st.firstSpawnDone) {
if (turn >= cfg.delay) {
const spawned = doReinforce(cfg.count);
if (spawned > 0) {
st.firstSpawnDone = true;
st.lastSpawnTurn = turn;
}
}
} else {
if (turn - st.lastSpawnTurn >= cfg.interval) {
const spawned = doReinforce(cfg.count);
if (spawned > 0) {
st.lastSpawnTurn = turn;
}
}
}
repositionEnemies();
}
// --- Триггер 1: increaseTurn (стандартный MZ) ---
if (typeof Game_Troop.prototype.increaseTurn === "function") {
const _increaseTurn = Game_Troop.prototype.increaseTurn;
Game_Troop.prototype.increaseTurn = function() {
_increaseTurn.call(this);
onTurnTick();
};
log("Триггер: Game_Troop.increaseTurn — ОК");
} else {
warn("Game_Troop.increaseTurn не найден — включён только fallback на startTurn.");
}
// --- Триггер 2: BattleManager.startTurn (fallback) ---
if (typeof BattleManager.startTurn === "function") {
const _startTurn = BattleManager.startTurn;
BattleManager.startTurn = function() {
_startTurn.call(this);
onTurnTick();
};
}
// ============================================================
// isAllDead — экстренный спавн
// ============================================================
const _Game_Troop_isAllDead = Game_Troop.prototype.isAllDead;
Game_Troop.prototype.isAllDead = function() {
if (!_Game_Troop_isAllDead.call(this)) return false;
if (!P.enabled) return true;
const st = ensureCfg();
const cfg = st && st.cfg;
if (!cfg) return true;
if (cfg.noReinforce) return true;
if (cfg.interval <= 0) return true;
if (cfg.maxReinforcements > 0 && st.spawned >= cfg.maxReinforcements) return true;
if (!st.pool || !st.pool.length) return true;
log("isAllDead: арена пуста, пробуем emergency-спавн.");
const spawned = doReinforce(cfg.count, true);
if (spawned > 0) {
st.lastSpawnTurn = this.turnCount();
return false;
}
return true;
};
// ============================================================
// СКРЫТИЕ ОЖИДАЮЩИХ
// ============================================================
const _Spriteset_Battle_update = Spriteset_Battle.prototype.update;
Spriteset_Battle.prototype.update = function() {
_Spriteset_Battle_update.call(this);
if (!this._enemySprites) return;
for (const sprite of this._enemySprites) {
const enemy = sprite._enemy;
if (!enemy || !enemy._reinforcementPending) continue;
if (sprite.visible || sprite.opacity > 0) {
sprite.visible = false;
sprite.opacity = 0;
}
for (const key of ["_shadowSprite", "_weaponSprite"]) {
const sib = sprite[key];
if (sib && (sib.visible || sib.opacity > 0)) {
sib.visible = false;
sib.opacity = 0;
}
}
}
};
// ============================================================
// КОМАНДЫ
// ============================================================
PluginManager.registerCommand(pluginName, "spawn", args => {
const n = Math.max(1, Number(args.count) || 1);
doReinforce(n, true);
});
PluginManager.registerCommand(pluginName, "resetTimer", () => {
const st = ensureState();
if (st) { st.firstSpawnDone = false; st.lastSpawnTurn = 0; st._lastTick = -1; }
});
// ============================================================
// ЭКСПОРТ
// ============================================================
window.BattleFormationReinforcements = {
config: P,
maxCells: MAX_CELLS,
totalCap: totalCapOf,
spawnNow: doReinforce,
findEmptySlots,
reviveEnemyInSlot,
recreateEnemySprite,
destroyEnemySprite,
moveEnemyToSlot,
repositionEnemies,
getEnemyPreferredLine,
getEnemyPreferredSlot,
getEnemyRole,
pickSlotForEnemy
};
})();
Code:
/*:
* @target MZ
* @plugindesc Кастомные SV-анимации врагов + тень + оружие.
* @author AI Assistant
* @help
* ПАПКИ:
* 1. Спрайты врагов: img/sv_enemies/
* 2. Оружие: img/system/Weapons1.png ...
*
* СТРУКТУРА SV-ЛИСТА ВРАГА (9 колонок × 6 строк):
* Колонки 0,1,2 — idle (ряд 0)
* Колонки 3,4,5 — анимация действия в ряду <AttackRow>
*
* СТРУКТУРА ЛИСТА ОРУЖИЯ (6 колонок × 6 строк):
* 2 блока по 3 кадра в каждой строке = 12 видов оружия (индексы 0..11).
* Чётные индексы (0,2,4,...) — левый блок (колонки 0,1,2).
* Нечётные индексы (1,3,5,...) — правый блок (колонки 3,4,5).
* Строка = Math.floor(index / 2).
*
* ТЕГИ:
* <SVSheet> — файл берётся из Battle Graphic врага
* <SVSheet: filename> — файл берётся из img/sv_enemies/filename.png
* <AttackRow: N> — ряд атаки (0=укол, 1=замах, 2=выстрел, 5=магия)
* <Weapon: ID> — оружие по ID из базы данных
* <Weapon: file,index> — вручную: файл из img/system/ и индекс 0..11
* <SimpleSheet: i>
* <SimpleSheet: i,row>
*/
(() => {
// ============ ПОЗИЦИОНИРОВАНИЕ СПРАЙТА ВРАГА ============
const SV_OFFSET_X = 0;
const SV_OFFSET_Y = 5;
// ============ ПОЗИЦИОНИРОВАНИЕ ОРУЖИЯ ============
const WEAPON_OFFSET_X = -20;
const WEAPON_OFFSET_Y = -40;
const WEAPON_SCALE = 1.0;
const WEAPON_SHOW_ALWAYS = false;
// ============ СКОРОСТЬ АНИМАЦИЙ ============
const SPEED_IDLE = 16;
const SPEED_ATTACK = 22;
// ============ СТРУКТУРА ЛИСТА ОРУЖИЯ ============
const WEAPON_TOTAL_COLS = 6;
const WEAPON_TOTAL_ROWS = 6;
// ============================================================
// ХУКИ BattleManager — флаг "выполняет действие"
// ============================================================
const _startAction = BattleManager.startAction;
BattleManager.startAction = function() {
_startAction.call(this);
if (this._subject) this._subject._performingAction = true;
};
const _endAction = BattleManager.endAction;
BattleManager.endAction = function() {
if (this._subject) this._subject._performingAction = false;
_endAction.call(this);
};
// ============================================================
// INIT
// ============================================================
const _initMembers = Sprite_Enemy.prototype.initMembers;
Sprite_Enemy.prototype.initMembers = function() {
_initMembers.call(this);
this._isSVSheet = false;
this._isSimpleSheet = false;
this._weaponSprite = null;
this._shadowSprite = null;
this._weaponFile = null;
this._weaponIndex = 0;
this._simpleIndex = 1;
this._simpleRow = 1;
this._attackRow = 1;
this._attackStartFrame = null;
this._svFileName = null;
this._cw = 0;
this._ch = 0;
this._ww = 0;
this._wh = 0;
};
// ============================================================
// setBattler
// ============================================================
const _setBattler = Sprite_Enemy.prototype.setBattler;
Sprite_Enemy.prototype.setBattler = function(battler) {
if (this._shadowSprite) {
if (this._shadowSprite.parent)
this._shadowSprite.parent.removeChild(this._shadowSprite);
this._shadowSprite = null;
}
if (this._weaponSprite) {
if (this._weaponSprite.parent)
this._weaponSprite.parent.removeChild(this._weaponSprite);
this._weaponSprite = null;
}
_setBattler.call(this, battler);
this._isSVSheet = false;
this._isSimpleSheet = false;
this._weaponFile = null;
this._weaponIndex = 0;
this._simpleIndex = 1;
this._simpleRow = 1;
this._attackRow = 1;
this._attackStartFrame = null;
this._svFileName = null;
this._cw = 0;
this._ch = 0;
this._ww = 0;
this._wh = 0;
if (battler && battler.isEnemy()) {
const enemy = battler.enemy();
if (enemy && enemy.meta) {
// <SVSheet> — имя из Battle Graphic моба
// <SVSheet: filename> — имя из тега
if (enemy.meta.SVSheet !== undefined) {
this._isSVSheet = true;
this.scale.x = -1; // враг смотрит влево
const raw = String(enemy.meta.SVSheet).trim();
if (raw.length > 0) this._svFileName = raw;
}
// <AttackRow: N>
if (enemy.meta.AttackRow !== undefined) {
const n = Number(String(enemy.meta.AttackRow).trim());
if (!isNaN(n) && n >= 0 && n <= 5) this._attackRow = n;
}
// <SimpleSheet: i> или <SimpleSheet: i,row>
if (enemy.meta.SimpleSheet !== undefined) {
this._isSimpleSheet = true;
const args = String(enemy.meta.SimpleSheet).split(',');
if (args[0] && !isNaN(Number(args[0].trim())))
this._simpleIndex = Number(args[0].trim());
if (args[1] && !isNaN(Number(args[1].trim())))
this._simpleRow = Number(args[1].trim());
}
// <Weapon: ID> или <Weapon: file,index>
if (enemy.meta.Weapon && this._isSVSheet) {
const args = enemy.meta.Weapon.split(',');
if (args.length === 1 && !isNaN(Number(args[0].trim()))) {
const wid = Number(args[0].trim());
const weapon = $dataWeapons[wid];
if (weapon) {
this._weaponFile = weapon.svWeapon || 'Weapons1';
this._weaponIndex = Number(weapon.svWeaponIndex) || 0;
}
} else if (args.length >= 2) {
this._weaponFile = args[0].trim();
this._weaponIndex = Number(args[1].trim()) || 0;
}
}
}
}
this.createCustomSprites();
};
// ============================================================
// Создание тени и оружия
// ============================================================
Sprite_Enemy.prototype.createCustomSprites = function() {
// Тень
if (this._isSVSheet && !this._shadowSprite) {
const SW = 70, SH = 20;
this._shadowSprite = new Sprite(new Bitmap(SW, SH));
this._shadowSprite.anchor.x = 0.5;
this._shadowSprite.anchor.y = 0.5;
const ctx = this._shadowSprite.bitmap.context;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.beginPath();
ctx.ellipse(SW/2, SH/2, SW/2 - 2, SH/2 - 2, 0, 0, Math.PI * 2);
ctx.fill();
this._shadowSprite.bitmap.baseTexture.update();
} else if (!this._isSVSheet && this._shadowSprite) {
if (this._shadowSprite.parent)
this._shadowSprite.parent.removeChild(this._shadowSprite);
this._shadowSprite = null;
}
// Оружие (sibling, за спрайтом)
if (this._isSVSheet && this._weaponFile && !this._weaponSprite) {
const bmp = ImageManager.loadBitmap('img/system/', this._weaponFile, 0, true);
this._weaponSprite = new Sprite(bmp);
this._weaponSprite.anchor.x = 0.5;
this._weaponSprite.anchor.y = 0.5;
this._weaponSprite.visible = false;
} else if ((!this._isSVSheet || !this._weaponFile) && this._weaponSprite) {
if (this._weaponSprite.parent)
this._weaponSprite.parent.removeChild(this._weaponSprite);
this._weaponSprite = null;
}
};
// ============================================================
// updateBitmap
// Имя файла: из тега <SVSheet: name>, иначе из Battle Graphic моба
// ============================================================
const _updateBitmap = Sprite_Enemy.prototype.updateBitmap;
Sprite_Enemy.prototype.updateBitmap = function() {
if (this._battler && this._battler.isEnemy()
&& (this._isSVSheet || this._isSimpleSheet)) {
const name = this._svFileName || this._battler.battlerName();
if (this._bitmapName !== name) {
this._bitmapName = name;
this.bitmap = ImageManager.loadBitmap('img/sv_enemies/', name, 0, true);
this._cw = 0;
this._ch = 0;
}
} else {
_updateBitmap.call(this);
}
};
// ============================================================
// updateFrame
// ============================================================
const _updateFrame = Sprite_Enemy.prototype.updateFrame;
Sprite_Enemy.prototype.updateFrame = function() {
if (this._isSVSheet) {
this.updateSVFrame();
} else if (this._isSimpleSheet) {
this.updateSimpleFrame();
} else {
_updateFrame.call(this);
}
};
// ============================================================
// updatePosition — сдвиг спрайта
// ============================================================
const _updatePosition = Sprite_Enemy.prototype.updatePosition;
Sprite_Enemy.prototype.updatePosition = function() {
_updatePosition.call(this);
if (this._isSVSheet) {
this.x += SV_OFFSET_X;
this.y += SV_OFFSET_Y;
}
};
// ============================================================
// update — тень и оружие как siblings
// Порядок слоёв: [ ..., тень, оружие, спрайт ]
// ============================================================
const _update = Sprite_Enemy.prototype.update;
Sprite_Enemy.prototype.update = function() {
_update.call(this);
if (!this._isSVSheet || !this.parent) return;
const parent = this.parent;
// --- Тень ---
if (this._shadowSprite) {
if (this._shadowSprite.parent !== parent) {
if (this._shadowSprite.parent)
this._shadowSprite.parent.removeChild(this._shadowSprite);
const idx = parent.getChildIndex(this);
if (idx >= 0) parent.addChildAt(this._shadowSprite, idx);
}
this._shadowSprite.x = this.x - SV_OFFSET_X;
this._shadowSprite.y = this.y - SV_OFFSET_Y;
this._shadowSprite.visible = this.visible;
this._shadowSprite.opacity = this.opacity;
}
// --- Оружие ---
if (this._weaponSprite && this._weaponFile) {
if (this._weaponSprite.parent !== parent) {
if (this._weaponSprite.parent)
this._weaponSprite.parent.removeChild(this._weaponSprite);
const idx = parent.getChildIndex(this);
if (idx >= 0) parent.addChildAt(this._weaponSprite, idx);
}
const isPerforming = !!(this._battler && this._battler._performingAction);
this._weaponSprite.visible =
this.visible && (WEAPON_SHOW_ALWAYS || isPerforming);
let ox = WEAPON_OFFSET_X;
const flipped = this.scale.x < 0;
if (flipped) ox = -ox;
this._weaponSprite.x = this.x + ox;
this._weaponSprite.y = this.y + WEAPON_OFFSET_Y;
this._weaponSprite.opacity = this.opacity;
this._weaponSprite.scale.x = WEAPON_SCALE * (flipped ? -1 : 1);
this._weaponSprite.scale.y = WEAPON_SCALE;
}
};
// ============================================================
// SV-АНИМАЦИЯ ВРАГА
// ============================================================
Sprite_Enemy.prototype.updateSVFrame = function() {
if (!this.bitmap || !this.bitmap.isReady()) return;
if (this._cw === 0 && this.bitmap.width > 0) {
this._cw = Math.floor(this.bitmap.width / 9);
this._ch = Math.floor(this.bitmap.height / 6);
}
if (this._cw === 0 || this._ch === 0) return;
const battler = this._battler;
if (!battler) return;
const isPerforming = !!battler._performingAction;
// --- Атака ---
if (isPerforming && this._attackRow >= 0) {
if (this._attackStartFrame === null) {
this._attackStartFrame = Graphics.frameCount;
}
const elapsed = Graphics.frameCount - this._attackStartFrame;
let phase = Math.floor(elapsed / SPEED_ATTACK);
if (phase > 2) phase = 2;
const col = 3 + phase;
const row = this._attackRow;
this.setFrame(col * this._cw, row * this._ch, this._cw, this._ch);
this.updateWeaponFrame(phase);
return;
}
// --- Idle ---
this._attackStartFrame = null;
this.updateWeaponFrame(-1);
const phase = Math.floor(Graphics.frameCount / SPEED_IDLE) % 3;
this.setFrame(phase * this._cw, 0, this._cw, this._ch);
};
// ============================================================
// Кадр оружия
// phase: 0,1,2 — фаза анимации; -1 — покой
// ============================================================
Sprite_Enemy.prototype.updateWeaponFrame = function(phase) {
if (!this._weaponSprite || !this._weaponSprite.bitmap) return;
const bmp = this._weaponSprite.bitmap;
if (!bmp.isReady() || !bmp.width) return;
if (this._ww === 0) {
this._ww = Math.floor(bmp.width / WEAPON_TOTAL_COLS);
this._wh = Math.floor(bmp.height / WEAPON_TOTAL_ROWS);
}
if (this._ww === 0 || this._wh === 0) return;
const idx = this._weaponIndex;
const row = Math.floor(idx / 2);
const half = idx % 2;
const p = (phase >= 0 && phase <= 2) ? phase : 0;
const col = half * 3 + p;
this._weaponSprite.setFrame(
col * this._ww,
row * this._wh,
this._ww,
this._wh
);
};
// ============================================================
// SimpleSheet
// ============================================================
Sprite_Enemy.prototype.updateSimpleFrame = function() {
if (!this.bitmap || !this.bitmap.isReady()) return;
if (this._cw === 0 && this.bitmap.width > 0) {
this._cw = Math.floor(this.bitmap.width / 12);
this._ch = Math.floor(this.bitmap.height / 2);
}
if (this._cw === 0) return;
const pattern = Math.floor(Graphics.frameCount / SPEED_IDLE) % 3;
const charIdx = this._simpleIndex - 1;
const row = this._simpleRow - 1;
this.setFrame(
(charIdx * 3 + pattern) * this._cw,
row * this._ch,
this._cw,
this._ch
);
};
})();