//============================================================================= // MPP_EncounterEffect.js //============================================================================= // Copyright (c) 2021 Mokusei Penguin // Released under the MIT license // http://opensource.org/licenses/mit-license.php //============================================================================= /*: * @target MZ * @plugindesc Change the effect at the time of encounter to a special effect. * @author Mokusei Penguin * @url * * @help [version 4.0.0] * This plugin is for RPG Maker MZ. * * ▼ Plugin command * - In the item to enter a numerical value, * select the text and write 'v[N]' to refer to the variable N. * * 〇 setType * - The effect type will be changed to the specified number * only once next. * * 〇 setCharacter * - The effect will be executed around the specified character * only once next. * - This setting is valid only for effect type 1. * * 〇 setColor * - The effect will change to the specified color only once next. * * ▼ Effect type * -1 : No SE & effects * 0 : Default. * 1 : Breaks around the character. * 2 : The screen splits from the left and scatters to the left. * 3 : The entire screen scatters to the front. * 4 : The screen splits from the left and scatters to the right. * 5 : Divide into squares and expand to the front. * * ▼ Other * - In effect type 1, if the main character is not specified, * [Player] will be the center for random encounters, * and [This Event] will be the center for * the event command [Battle Processing]. * * ================================ * Mail : wood_penguin@yahoo.co.jp (@ is half-width) * Blog : http://woodpenguin.blog.fc2.com/ * License : MIT license * * @command setType * @desc Change the effect type only in the next battle. * @arg type * @desc * @type number * @min -1 * @max 5 * @default 1 * * * @command setCharacter * @desc Specify the main character for the next effect. * This setting is valid only for effect type 1. * @arg character * @desc -1:Player, 0:This event, 1-:Event with specified ID * @type number * @min -1 * @max 999 * @default 0 * * @command setColor * @desc Specifies the color of the next effect. * @arg color * @desc R,G,B * @default 255,255,255 * * * @param Effect Type * @desc * @type number * @min 0 * @max 5 * @default 1 * * @param Effect Color * @desc R,G,B * @default 255,255,255 * */ /*:ja * @target MZ * @plugindesc エンカウント時の演出を特殊なエフェクトに変更します。 * @author 木星ペンギン * @url * * @help [version 4.0.0] * このプラグインはRPGツクールMZ用です。 * * ▼ プラグインコマンド * - 数値を入力する項目で、テキストを選択して v[N] と記述することで * 変数N番を参照します。 * * 〇 エフェクトタイプ変更 * - 次の1回のみ、エフェクトタイプが指定した番号に変更されます。 * * 〇 中心キャラ指定 * - 次の1回のみ、エフェクトが指定したキャラクターを中心に実行されます。 * - この設定はエフェクトタイプ1のみ有効です。 * * 〇 エフェクト色指定 * - 次の1回のみ、エフェクトが指定した色に変更されます。 * * ▼ エフェクトタイプ * -1: SE & エフェクトなし * 0 : デフォルト * 1 : キャラクターを中心に割れる * 2 : 画面が左から割れて左に散る * 3 : 画面全体が前面に飛び散る * 4 : 画面が左から割れて右に散る * 5 : 四角く区切り、手前に拡大する * * ▼ その他 * - エフェクトタイプ1にて、中心となるキャラクターを指定していない場合、 * ランダムエンカウントでは[プレイヤー]、 * イベントコマンドの【戦闘の処理】では[このイベント]が中心となります。 * * ================================ * Mail : wood_penguin@yahoo.co.jp (@は半角) * Blog : http://woodpenguin.blog.fc2.com/ * License : MIT license * * @command setType * @text エフェクトタイプ変更 * @desc 次に行う戦闘のみエフェクトタイプを変更します。 * @arg type * @text タイプ * @desc エフェクトタイプ * @type number * @min -1 * @max 5 * @default 1 * * @command setCharacter * @text 中心キャラ指定 * @desc 次に行うエフェクトで中心となるキャラクターを指定します。 * この設定はエフェクトタイプ1でのみ有効です。 * @arg character * @text キャラクター * @desc 中心となるキャラクター * -1:プレイヤー, 0:このイベント, 1~:指定したIDのイベント * @type number * @min -1 * @max 999 * @default 0 * * @command setColor * @text エフェクト色指定 * @desc 次に行うエフェクトの色を指定します。 * @arg color * @text 色(RGB) * @desc * @default 255,255,255 * * * @param Effect Type * @text エフェクトタイプ * @desc 画面割れエフェクトのタイプ * @type number * @min 0 * @max 5 * @default 1 * * @param Effect Color * @text エフェクト色 * @desc エフェクトのデフォルト色 * (r,g,bで指定) * @default 255,255,255 * * */ (() => { 'use strict'; const pluginName = 'MPP_EncounterEffect'; // Plugin Parameters const parameters = PluginManager.parameters(pluginName); const param_EffectType = Number(parameters['Effect Type'] || 1); const param_DefaultColor = parameters['Effect Color'].split(',').map(Number); const _importedPlugin = (...names) => { return names.some(name => PluginManager._scripts.includes(name)); }; const pluginOptions = [ 'MPP_EncounterEffect_Op1', 'MPP_EncounterEffect_Op2' ]; // JsExtensions 代替 const MathExt = (() => { // Number.prototype.clamp と違い、下限優先 const clamp = (x, min, max) => Math.max(Math.min(x, max), min); const mod = (x, n) => ((x % n) + n) % n; const randomInt = (x) => Math.floor(x * Math.random()); return { clamp, mod, randomInt }; })(); /** * 指定された二つの配列の内容が等しい場合はtrueを返します。 * @param {array} ary1 * @param {array} ary2 * @returns {boolean} */ const array_equals = (ary1, ary2) => { return JSON.stringify(ary1) === JSON.stringify(ary2); } // 各タイプのパラメータ const baseTypeParameters = [ null, // Type 1 { 'Shape Type': 'square', 'Center': 'character', 'Break Direction': 'none', 'Break Duration': 45, 'Interval Duration': 45, 'Scatter Duration': 0, 'Move Duration': 60, 'Split Radial': 8, 'Radial Random Rate': 90, 'Circle Radius': 96, 'Circle Increase Rate': 150, 'Circle Random Rate': 40 }, // Type 2 { 'Shape Type': 'random', 'Break Direction': 'left', 'Break Duration': 30, 'Interval Duration': 45, 'Scatter Direction': 'left', 'Scatter Duration': 24, 'Move Duration': 60, 'Split X': 7, 'X Random Rate': 80, 'Split Y': 5, 'Y Random Rate': 80 }, // Type 3 { 'Shape Type': 'triangle', 'Break Direction': 'inside', 'Break Duration': 35, 'Interval Duration': 40, 'Scatter Direction': 'inside', 'Scatter Duration': 16, 'Move Duration': 100, 'Split X': 8, 'X Random Rate': 80, 'Split Y': 6, 'Y Random Rate': 80, 'Jump': true }, // Type 4 { 'Shape Type': 'random', 'Break Direction': 'left', 'Break Duration': 30, 'Interval Duration': 45, 'Scatter Direction': 'right', 'Scatter Duration': 6, 'Move Duration': 45, 'Split X': 9, 'X Random Rate': 70, 'Split Y': 7, 'Y Random Rate': 70 }, // Type 5 { 'Shape Type': 'square', 'Break Direction': 'outside', 'Break Duration': 30, 'Interval Duration': 45, 'Scatter Direction': 'left', 'Scatter Duration': 50, 'Move Duration': 80, 'Split X': 15, 'X Random Rate': 0, 'Split Y': 11, 'Y Random Rate': 0 }, ]; //------------------------------------------------------------------------- // EncounterEffect function EncounterEffect() { throw new Error('This is a static class'); } if (_importedPlugin(...pluginOptions)) { window.EncounterEffect = EncounterEffect; } EncounterEffect.centerPoint = new Point(); EncounterEffect.windowBounds = null; EncounterEffect.clear = function() { this._snapTexture = null; this._opacity = 0; this._fragments = null; this._type = param_EffectType; this._character = null; this._color = param_DefaultColor; this._angle = 0; }; EncounterEffect.destroy = function() { if (this._snapTexture) { this._snapTexture.destroy(); this._snapTexture = null; } }; EncounterEffect.params = function() { return this._type >= 0 ? baseTypeParameters[this._type] : null; }; EncounterEffect.type = function() { return this._type; }; EncounterEffect.setup = function(snapTexture) { this._snapTexture = snapTexture; this._opacity = 255; this._opacityDelay = 0; this._opacityDuration = 0; this._params = this.params(); this.setupWindowBounds(); this.setupCenterPoint(); this.createFragments(); this.sortFragment(this.currentParam('Break Direction')); this.setupAngle(); }; EncounterEffect.setupWindowBounds = function() { const { width, height } = Graphics; this.windowBounds = { minX: 0, maxX: width, minY: 0, maxY: height }; }; EncounterEffect.setupCenterPoint = function() { if (this.currentParam('Center') === 'character') { const { minX, maxX, minY, maxY } = this.windowBounds; const character = this.centerCharacter(); const charX = character.screenX(); const charY = character.screenY(); const cx = Math.round(MathExt.clamp(charX, minX, maxX)); const cy = Math.round(MathExt.clamp(charY - 24, minY, maxY)); this.centerPoint.set(cx, cy); } else { const cx = Graphics.width / 2; const cy = Graphics.height / 2; this.centerPoint.set(cx, cy); } }; EncounterEffect.setupAngle = function() { this._angle = 0; this._targetAngle = 0; this._angleEasing = new Easing(); if (this._type === 5) { const duration = this.effectSpeed(); this._targetAngle = -3; this._angleEasing.start('Slow end', duration); } }; EncounterEffect.snapTexture = function() { return this._snapTexture; }; EncounterEffect.setType = function(type) { this._type = type; }; EncounterEffect.opacity = function() { return this._opacity; }; EncounterEffect.setColor = function(...color) { this._color = color; }; EncounterEffect.color = function() { return this._color; }; EncounterEffect.setCharacter = function(character) { this._character = character; }; EncounterEffect.centerCharacter = function() { return ( this._character || $gameMap.getInterpreterCharacter(0) || $gamePlayer ); }; EncounterEffect.createFragments = function() { this._fragments = []; switch (this._type) { case 1: this.createFragmentsT1(); break; case 2: case 3: case 4: case 5: this.createFragmentsT2(); break; } }; EncounterEffect.createFragmentsT1 = function() { const { minX, maxX, minY, maxY } = this.windowBounds; const allPoints = this.createAllPointsT1(); for (let points of this.generaterPointsT1(allPoints)) { points = this.cutProtrusionX(points, minX, false); points = this.cutProtrusionX(points, maxX, true); points = this.cutProtrusionY(points, minY, false); points = this.cutProtrusionY(points, maxY, true); points = this.removeOuterPoint(points); if (points.length >= 3) { this.addFragment(this.uniqPoints(points)); } } }; EncounterEffect.generaterPointsT1 = function*(allPoints) { const square = this.currentParam('Shape Type', 'square') === 'square'; const max = allPoints[0] ? allPoints[0].length : 1; for (const [i, outPoints] of allPoints.entries()) { const inPoints = i > 0 ? allPoints[i - 1] : null; for (const [j, point2] of outPoints.entries()) { const point3 = outPoints[(j + 1) % max]; if (inPoints) { const point4 = inPoints[(j + 1) % max]; const point1 = inPoints[j]; if (square) { yield [point1, point2, point3, point4]; } else { yield [point1, point2, point4]; yield [point2, point3, point4]; } } else { yield [this.centerPoint, point2, point3]; } } } }; EncounterEffect.cutProtrusionX = function(points, x, cutAbove) { const size = points.length; for (const p1 of points) { const i = points.indexOf(p1); const p2 = points[(i + 1) % points.length]; const p3 = this.verticalIntersectionPoint(p1, p2, x); if (p3) { points.splice(i + 1, 0, p3); } } if (size < points.length) { return points.filter(p => cutAbove ? p.x <= x : p.x >= x); } return points; }; EncounterEffect.verticalIntersectionPoint = function(point1, point2, x) { const x1 = point1.x; const y1 = point1.y; const x2 = point2.x; const y2 = point2.y; if ((x1 < x && x2 > x) || (x1 > x && x2 < x)) { const y = y1 + Math.round((y2 - y1) * (x - x1) / (x2 - x1)); return new Point(x, y); } return null; }; EncounterEffect.cutProtrusionY = function(points, y, cutAbove) { const size = points.length; for (const p1 of points) { const i = points.indexOf(p1); const p2 = points[(i + 1) % points.length]; const p3 = this.horizontalIntersectionPoint(p1, p2, y); if (p3) { points.splice(i + 1, 0, p3); } } if (size < points.length) { return points.filter(p => cutAbove ? p.y <= y : p.y >= y); } return points; }; EncounterEffect.horizontalIntersectionPoint = function(point1, point2, y) { const x1 = point1.x; const y1 = point1.y; const x2 = point2.x; const y2 = point2.y; if ((y1 < y && y2 > y) || (y1 > y && y2 < y)) { const x = x1 + Math.round((x2 - x1) * (y - y1) / (y2 - y1)); return new Point(x, y); } return null; }; EncounterEffect.removeOuterPoint = function(points) { const { minX, maxX, minY, maxY } = this.windowBounds; return points.filter( ({ x, y }) => x >= minX && x <= maxX && y >= minY && y <= maxY ); }; EncounterEffect.uniqPoints = function(points) { return points.filter( (p1, i) => points.findIndex(p2 => p2.equals(p1)) === i ); }; EncounterEffect.createAllPointsT1 = function() { const { 'Shape Type': sType = 'square', 'Split Radial': splitX = 8, 'Circle Radius': baseRadius = 96, 'Circle Increase Rate': circleRate = 150 } = this._params; const maxRadius = this.maxRadius(); const allPoints = []; let startAngle = 360 * Math.random(); let innerR = 0; let currentR = baseRadius; for (let i = 0; i < 8; i++) { if (sType === 'triangle') startAngle += 180 / splitX; if (i === 7) innerR = 1000000; allPoints.push(this.createRoundPoints(startAngle, innerR, currentR)); if (innerR > maxRadius) break; innerR += currentR; currentR *= circleRate / 100; } return allPoints; }; EncounterEffect.createRoundPoints = function(startAngle, innerR, currentR) { const { 'Split Radial': splitX = 8, 'Radial Random Rate': rRate = 90, 'Circle Random Rate': cRate = 40 } = this._params; const baseAngle = 360 / splitX; const randomAngle = (n) => { return (startAngle + baseAngle * n + baseAngle * rRate * Math.random() / 100) % 360; }; const randomRadius = () => { return innerR + currentR * (1 - cRate * (0.75 - Math.random()) / 100); }; return [...Array(splitX).keys()].map( i => this.calculatePoint(randomAngle(i), randomRadius()) ); }; EncounterEffect.calculatePoint = function(angle, radius) { const { x, y } = this.centerPoint; const sx = Math.round(radius * Math.cos(angle * Math.PI / 180)); const sy = Math.round(radius * Math.sin(angle * Math.PI / 180)); return new Point(x + sx, y + sy); }; EncounterEffect.maxRadius = function() { const { width, height } = Graphics; const { x, y } = this.centerPoint; const deltaX = x < width / 2 ? width - x : x; const deltaY = y < height / 2 ? height - y : y; return Math.hypot(deltaX, deltaY); }; EncounterEffect.createFragmentsT2 = function() { switch (this.currentParam('Shape Type', 'square')) { case 'square': this.createFragmentsT2Square(); break; case 'triangle': this.createFragmentsT2Triangle(); break; case 'random': this.createFragmentsT2Random(); break; } }; EncounterEffect.createFragmentsT2Square = function() { const allPoints = this.createAllPointsT2Square(); for (const points of this.generaterPointsT2(allPoints)) { this.addFragment(points); } }; EncounterEffect.generaterPointsT2 = function*(allPoints) { const { 'Shape Type': sType = 'square', 'Split X': splitX = 12, 'Split Y': splitY = 9 } = this._params; for (let i = 0; i < splitX; i++) { const leftPoints = allPoints[i]; const rightPoints = allPoints[i + 1]; for (let j = 0; j < splitY; j++) { const point1 = leftPoints[j]; const point2 = rightPoints[j]; const point3 = rightPoints[j + 1]; const point4 = leftPoints[j + 1]; if (sType === 'triangle') { yield [point1, point2, point4]; yield [point2, point3, point4]; } else { yield [point1, point2, point3, point4]; } } } }; EncounterEffect.createAllPointsT2Square = function() { const { 'Split X': splitX = 12, 'Split Y': splitY = 9, 'X Random Rate': xRate = 80, 'Y Random Rate': yRate = 80 } = this._params; const { minX, maxX, minY, maxY } = this.windowBounds; const width = Graphics.width / Math.max(splitX - xRate / 100, 1); const height = Graphics.height / Math.max(splitY - yRate / 100, 1); const pointX = (i) => { if (i === 0) { return minX; } else if (i === splitX) { return maxX; } return this.randomValue(width, i, xRate); }; const pointY = (j) => { if (j === 0) { return minY; } else if (j === splitY) { return maxY; } return this.randomValue(height, j, yRate); }; return [...Array(splitX + 1).keys()].map(i => { return [...Array(splitY + 1).keys()].map(j => { return new Point(pointX(i), pointY(j)); }); }); }; EncounterEffect.randomValue = function(value, index, rate) { return Math.round(value * (index - rate * Math.random() / 100)); }; EncounterEffect.createFragmentsT2Triangle = function() { const allPoints = this.createAllPointsT2Triangle(); for (const points of this.generaterPointsT2(allPoints)) { this.addFragment(points); } }; EncounterEffect.createAllPointsT2Triangle = function() { const { 'Split X': splitX = 12, 'Split Y': splitY = 9, 'X Random Rate': xRate = 80, 'Y Random Rate': yRate = 80 } = this._params; const { minX, maxX, minY, maxY } = this.windowBounds; const width = Graphics.width / Math.max(splitX - xRate / 100, 1); const height = Graphics.height / Math.max(splitY - 0.5, 1); const bottomY = height / 2 * yRate / 100; const pointX = (i) =>{ if (i === 0) { return minX; } else if (i === splitX) { return maxX; } return this.randomValue(width, i, xRate); }; const pointY = (i, j) => { if (j === 0) { return minY; } else if (j === splitY) { return maxY; } if (i % 2 === 1 && j === 1) { return this.randomValue(height / 2, j, yRate); } const py = bottomY - (i % 2 === 0 ? 0 : height / 2); return py + this.randomValue(height, j, yRate); }; return [...Array(splitX + 1).keys()].map(i => { return [...Array(splitY + 1).keys()].map(j => { return new Point(pointX(i), pointY(i, j)); }); }); }; EncounterEffect.createFragmentsT2Random = function() { const allPolygons = this.createPolygonsT2Random(); const sidePolygon = (i, j, pos) => { const si = i + (pos === 0 || pos === 1 ? -1 : 1); const subPolygons = allPolygons[si]; if (!subPolygons) { return null; } const sj = Math.floor(j / 2) * 2; const polygon2 = subPolygons[sj]; if (Math.floor(pos / 2) === Math.floor(polygon2.pos / 2)) { return subPolygons[sj + 1]; } return polygon2; }; for (const [i, mainPolygons] of allPolygons.entries()) { for (const [j, polygon1] of mainPolygons.entries()) { if (polygon1.state === 'none') { const d = MathExt.randomInt(3); let polygon2; if (d === 0) { // 上 polygon2 = mainPolygons[j - 1]; } else if (d === 1) { // 下 polygon2 = mainPolygons[j + 1]; } else { // 横 polygon2 = sidePolygon(i, j, polygon1.pos); } if (polygon2 && polygon2.state === 'none') { this.joinPolygon(polygon1, polygon2, d); } } } } for (const polygon of allPolygons.flat()) { if (polygon.state === 'none' || polygon.state === 'received') { this.addFragment(polygon.points); } } }; /** * * @returns {array} 二次元配列。 * pos: 分割した際の位置。0=左上, 1=左下, 2=右上, 3=右下。 */ EncounterEffect.createPolygonsT2Random = function() { const allPoints = this.createAllPointsT2Square(); const polygons = []; for (const [p1, p2, p3, p4] of this.generaterPointsT2(allPoints)) { if (Math.random() < 0.5) { polygons.push( { points:[p4, p1, p2], pos:0, state:'none' }, { points:[p4, p2, p3], pos:3, state:'none' } ); } else { polygons.push( { points:[p1, p2, p3], pos:2, state:'none' }, { points:[p4, p1, p3], pos:1, state:'none' } ); } } const splitY = this.currentParam('Split Y', 9); const allPolygons = []; while (polygons.length > 0) { allPolygons.push(polygons.splice(0, splitY * 2)); } return allPolygons; }; EncounterEffect.joinPolygon = function(polygon1, polygon2, d) { const pos1 = polygon1.pos; const pos2 = polygon2.pos; let start = 0, index = 0; if (d === 0) { start = pos1 === 0 || pos1 === 1 ? 2 : 1; index = 1; } else if (d === 1) { start = 0; index = pos1 === 0 ? 2 : pos1 === 2 ? 0 : pos2 === 0 ? 0 : 2; } else { start = pos1 === 0 || pos1 === 1 ? 1 : 2; index = pos2 === 0 || pos2 === 1 ? 2 : 0; } polygon1.points.splice(start, 0, polygon2.points[index]); polygon1.state = 'received'; polygon2.state = 'joined'; }; EncounterEffect.addFragment = function(points) { const fragment = new Encounter_Fragment(points); if (fragment.isValid()) { fragment.setup(); this._fragments.push(fragment); } }; EncounterEffect.sortFragment = function(direction) { const formula = this.compareFormula(direction); if (formula) { this._fragments.sort((a, b) => formula(a) - formula(b)); } }; EncounterEffect.compareFormula = function(direction) { const { x, y } = this.centerPoint; switch (direction) { case 'left': return (f) => { return f._x; }; case 'center': return (f) => { return Math.abs(x - f._x); }; case 'right': return (f) => { return -f._x; }; case 'inside': return (f) => { return Math.abs(x - f._x) + Math.abs(y - f._y) * y / x; }; case 'outside': return (f) => { return -Math.abs(x - f._x) - Math.abs(y - f._y) * y / x; }; } return null; }; EncounterEffect.breakFragments = function(start, end, duration) { for (let i = start; i < end; i++) { this._fragments[i].onBreak(duration, i); } }; EncounterEffect.onBattleStart = function() { const { 'Move Duration': duration = 60, 'Scatter Direction': direction = 'none', 'Scatter Duration': delay = 0 } = this._params; this.sortFragment(direction); this.startFragments(duration, delay); this.startOpacity(duration); if (this._type === 5) { this._targetAngle = -4; this._angleEasing.start('Straight', duration); } }; EncounterEffect.startFragments = function(duration, delay) { const maxItems = this.maxItems(); for (const [i, fragment] of this._fragments.entries()) { fragment.onBattleStart(duration, delay); fragment.setDelay(Math.floor(delay * i / maxItems)); } }; EncounterEffect.startOpacity = function(duration) { if (this._type === 5) { this._opacityDuration = 1; this._opacityDelay = duration - 1; } else { this._opacityDelay = Math.floor(duration / 2); this._opacityDuration = duration - this._opacityDelay; } }; EncounterEffect.update = function() { if (this.isRunning()) { this._fragments.forEach(fragment => fragment.update()); this.updateOpacity(); this.updateAngle(); if (this._opacity === 0) { this.destroy(); this.clear(); } } }; EncounterEffect.updateOpacity = function() { if (this._opacityDuration > 0 && this.updateOpacityDelay()) { const d = this._opacityDuration--; this._opacity *= (d - 1) / d; } }; EncounterEffect.updateOpacityDelay = function() { if (this._opacityDelay > 0) { this._opacityDelay--; return false; } return true; }; EncounterEffect.updateAngle = function() { const easing = this._angleEasing; if (easing.isMoving()) { this._angle = easing.apply(this._angle, this._targetAngle); easing.update(); } }; EncounterEffect.maxItems = function() { return this._fragments ? this._fragments.length : 0; }; EncounterEffect.fragments = function() { return this._fragments || []; }; EncounterEffect.isValid = function() { return !!baseTypeParameters[this._type]; }; EncounterEffect.isRunning = function() { return !!this._fragments; }; EncounterEffect.isReady = function() { return !this.isRunning() || this._opacityDuration < 45; }; EncounterEffect.breakDuration = function() { return this.currentParam('Break Duration', 45); }; EncounterEffect.effectSpeed = function() { const d = this.currentParam('Interval Duration', 45); return this.breakDuration() + d + 2; }; EncounterEffect.fadeSpeed = function() { return this.currentParam('Move Duration', 60); }; EncounterEffect.currentParam = function(name, def = null) { return this._params ? this._params[name] || def : def; }; EncounterEffect.rotation = function() { return this._angle * Math.PI / 180; }; //------------------------------------------------------------------------- // SceneManager SceneManager.snapTexture = function() { const stage = this._scene const width = Graphics.width; const height = Graphics.height; const renderTexture = PIXI.RenderTexture.create(width, height); if (stage) { const renderer = Graphics.app.renderer; renderer.render(stage, renderTexture); stage.worldTransform.identity(); } return renderTexture; }; //------------------------------------------------------------------------- // SoundManager const _SoundManager_playBattleStart = SoundManager.playBattleStart; SoundManager.playBattleStart = function() { if (EncounterEffect.type() >= 0) { _SoundManager_playBattleStart.apply(this, arguments); } }; //------------------------------------------------------------------------- // Encounter_Fragment class Encounter_Fragment { constructor(points) { this._points = points; this.initRect(points); this.initMembers(); } initMembers() { const rect = this._rect; this._x = rect.x + Math.floor(rect.width / 2); this._y = rect.y + Math.floor(rect.height / 2); this._targetX = 0; this._targetY = 0; this._jumpHeight = 0; this._scale = 1; this._targetScale = 1; this._moveEasing = new Easing(); this._opacity = 255; this._opacityDuration = 0; this._delay = 0; this._flash = false; this._break = false; this._slideX = 0; this._slideY = 0; this.visible = true; } initRect(points) { const allX = points.map( point => point.x ); const allY = points.map( point => point.y ); const x = Math.min(...allX); const y = Math.min(...allY); const width = Math.max(...allX) - x; const height = Math.max(...allY) - y; this._rect = new Rectangle(x, y, width, height); } isValid() { return this._rect.width > 4 && this._rect.height > 4; } setDelay(delay) { this._delay = delay; } clearFlash() { this._flash = false; } isFlashRequested() { return this._flash; } rect() { return this._rect; } isBreak() { return this._break; } lineWidth() { return 4; } flashOpacity() { return 255; } breakRate() { return 1; } slideRate() { return 1; } rotationRate() { return 1; } screenX() { return this._x; } screenY() { return this._y - this._jumpHeight; } flashColor() { const color = [...EncounterEffect.color()]; color[3] = this.flashOpacity(); return color; } points() { return this._points; } setup() { this.setupRotation(); } setupRotation() { this._speed = 0; this._pace = 0; this._rotationX = 0; this._rotationY = 0; this._rotationZ = 0; const rate = this.rotationRate(); if (Math.random() < 0.5) { this._rotationSpeedX = (Math.random() + 0.5) * 2 * rate; this._rotationSpeedY = 0; } else { this._rotationSpeedX = 0; this._rotationSpeedY = (Math.random() + 0.5) * 2 * rate; } this._rotationSpeedZ = (Math.random() - 0.5) * 3 * rate; } textureMatrix() { const tx = this._break ? this._slideX : 0; const ty = this._break ? this._slideY : 0; return new PIXI.Matrix(1, 0, 0, 1, tx, ty); } onBreak(duration, index) { this.startEncounter(duration, index); this._flash = true; this._break = true; } startEncounter(duration, index) { switch (EncounterEffect.type()) { case 1: this.setupMoveT1(duration); this.setupSlideT1() break; case 2: case 3: case 4: this.setupMoveT2(); this.setupSlideT2(); break; case 5: this.setupMoveT5(duration); break; } } setupMoveT1(duration) { const breakRate = this.breakRate(); this._moveEasing.start('Slow end', duration); this._speed = this.hypotSpeed(1.5) * breakRate; this._pace = -1; this.setMoveT1(breakRate / 100, breakRate / 100); } setupMoveT2() { const breakRate = this.breakRate(); this._speed = 0.125 * breakRate; this._pace = -1; } setupSlideT1() { const slideRate = this.slideRate(); const { x, y } = EncounterEffect.centerPoint; const radian = Math.atan2(this._y - y, this._x - x); this._slideX = 20 * Math.cos(radian) * slideRate; this._slideY = 20 * Math.sin(radian) * slideRate; } setupSlideT2() { const slideRate = this.slideRate(); const { x, y, width, height } = this._rect; const mx = (MathExt.randomInt(21) - 10) * slideRate; const my = (MathExt.randomInt(21) - 10) * slideRate; const minX = x + width - Graphics.width; const minY = y + height - Graphics.height; this._slideX = MathExt.clamp(mx, minX, x); this._slideY = MathExt.clamp(my, minY, y); } setupMoveT5(duration) { this._moveEasing.start('Slow end', duration); this._speed = 0; this._pace = 0; this.setMoveAway('outside', 1.1); } update() { if (this.visible && this.updateDelay()) { this.updateMove(); this.updateOpacity(); this.updateRotation(); } } updateDelay() { if (this._delay > 0) { this._delay--; return false; } return true; } updateMove() { const easing = this._moveEasing; if (easing.isMoving()) { this._x = easing.apply(this._x, this._targetX); this._y = easing.apply(this._y, this._targetY); this._scale = easing.apply(this._scale, this._targetScale); this.visible = this._scale < 8; if (EncounterEffect.currentParam('Jump')) { const d = easing._duration; const wd = easing._wholeDuration; const m = 4 * (d - 1) / wd; this._jumpHeight = (2 * 2 - Math.pow(m - 2, 2)) * 96; } easing.update(); } } updateOpacity() { if (this._opacityDuration > 0) { const d = this._opacityDuration--; this._opacity *= (d - 1) / d; } } updateRotation() { if (this._speed > 0) { this._speed *= 1 + 0.04 * this._pace; this._rotationX += this._rotationSpeedX * this._speed; this._rotationY += this._rotationSpeedY * this._speed; this._rotationZ += this._rotationSpeedZ * this._speed; } } onBattleStart(duration, delay) { switch (EncounterEffect.type()) { case 1: this.onBattleStartT1(duration); break; case 2: this.onBattleStartT2(Math.max(duration - delay, 1)); break; case 3: this.onBattleStartT3(duration); break; case 4: this.onBattleStartT4(duration); break; case 5: this.onBattleStartT5(Math.max(duration - delay, 1)); break; } } onBattleStartT1(duration) { this._moveEasing.start('Slow end', duration); this._speed = this.hypotSpeed(40); this._pace = -1; this.setMoveT1(0.8 + Math.random() / 2, 0.8 + Math.random() / 2); } hypotSpeed(baseSpeed) { const { x, y } = EncounterEffect.centerPoint; return baseSpeed / Math.sqrt(Math.hypot(this._x - x, this._y - y)); } setMoveT1(rateX, rateY) { const { minX, maxX, minY, maxY } = EncounterEffect.windowBounds; const { x: cx, y: cy } = EncounterEffect.centerPoint; const sx = this._x - cx; const sy = this._y - cy; let ox = sx; let oy = sy; if (ox < 0) { oy *= (minX - cx) / ox; ox = minX - cx; } else if (ox > 0) { oy *= (maxX - cx) / ox; ox = maxX - cx; } else { oy = oy < cy ? minY - cy : maxY - cy; } if (oy < minY - cy) { ox *= (minY - cy) / oy; oy = minY - cy; } else if (oy > maxY - cy) { ox *= (maxY - cy) / oy; oy = maxY - cy; } this._targetX = this._x + (ox - sx) * rateX; this._targetY = this._y + (oy - sy) * rateY; } onBattleStartT2(duration) { this._moveEasing.start('Slow start', duration); this._speed = 1; this._pace = 1; const dir = EncounterEffect.currentParam('Scatter Direction'); this.setMoveT2(dir); } setMoveT2(direction) { const { x: cx, y: cy } = EncounterEffect.centerPoint; switch (direction) { case 'left': this._targetX = this._x - Graphics.width; this._targetY = this._y + (this._y - cy); break; case 'right': this._targetX = this._x + Graphics.width; this._targetY = this._y + (this._y - cy); break; case 'outside': { const radian = Math.atan2(this._y - cy, this._x - cx); const d = (cx + cy) / 2; this._targetX = this._x + d * Math.cos(radian); this._targetY = this._y + d * Math.sin(radian); break; } } } onBattleStartT3(duration) { this._moveEasing.start('Slow end', duration); this._speed = 2; this._pace = 0; this.setMoveT3(); } setMoveT3() { const { x: cx, y: cy } = EncounterEffect.centerPoint; const sx = this._x - cx; const sy = this._y - cy; const rate = 3; this._targetX = (sx + 80 * (Math.random() - 0.5)) * rate + cx; this._targetY = (sy + 32 * (Math.random() - 0.5)) * rate + cy * 3; const delta = Math.abs(sx) + Math.abs(sy); this._targetScale = delta < 128 && Math.random() < 0.4 ? 12 : 1; this._targetScale += 2.5 * Math.random(); } onBattleStartT4(duration) { this._moveEasing.start('Slow end', duration); this._speed = 2; this._pace = -1; const dir = EncounterEffect.currentParam('Scatter Direction'); this.setMoveAway(dir, 2); } onBattleStartT5(duration) { this._moveEasing.start('Straight', duration); this._speed = 0; this._pace = 0; this.setMoveAway('outside', 2.25); this._targetScale = 1.75; this._opacityDuration = duration; } setMoveAway(direction, rate) { let { x, y } = EncounterEffect.centerPoint; switch (direction) { case 'left': x = Graphics.width; break; case 'right': x = 0; break; } this._targetX = x + (this._x - x) * rate; this._targetY = y + (this._y - y) * rate; } opacity() { return this._opacity; } scaleX() { return Math.cos(this._rotationX * Math.PI / 180) * this._scale; } scaleY() { return Math.cos(this._rotationY * Math.PI / 180) * this._scale; } rotation() { return this._rotationZ * Math.PI / 180; } } if (_importedPlugin(...pluginOptions)) { window.Encounter_Fragment = Encounter_Fragment; } //------------------------------------------------------------------------- // Easing class Easing { constructor(type = '', duration = 0) { this.setType(type); this.setDuration(duration); } start(type, duration) { this.setType(type); this.setDuration(duration); } setType(type) { this._type = type || 'Slow end'; } setDuration(duration) { this._duration = duration; this._wholeDuration = duration; } clear() { this._duration = 0; } isMoving() { return this._duration > 0; } update() { if (this._duration > 0) { this._duration--; } } apply(current, target) { const d = this._duration; const wd = this._wholeDuration; const lt = this.calc((wd - d) / wd); const t = this.calc((wd - d + 1) / wd); const start = (current - target * lt) / (1 - lt); return start + (target - start) * t; } calc(t) { switch (this._type) { case 'Slow start': return this.easeIn(t); case 'Slow end': return this.easeOut(t); case 'Slow start and end': return this.easeInOut(t); default: return t; } } easeIn(t) { return Math.pow(t, 2); } easeOut(t) { return 1 - Math.pow(1 - t, 2); } easeInOut(t) { if (t < 0.5) { return this.easeIn(t * 2) / 2; } else { return this.easeOut(t * 2 - 1) / 2 + 0.5; } } }; //------------------------------------------------------------------------- // PluginManager PluginManager.registerCommand(pluginName, 'setType', args => { EncounterEffect.setType(PluginManager.mppValue(args.type)); }); PluginManager.registerCommand(pluginName, 'setCharacter', function(args) { const eId = PluginManager.mppValue(args.character); EncounterEffect.setCharacter(this.character(eId)); }); PluginManager.registerCommand(pluginName, 'setColor', args => { const color = args.color.split(','); const r = PluginManager.mppValue(color[0]); const g = PluginManager.mppValue(color[1]); const b = PluginManager.mppValue(color[2]); EncounterEffect.setColor(r, g, b); }); PluginManager.mppValue = function(value) { const match = /^V\[(\d+)\]$/i.exec(value); return match ? $gameVariables.value(+match[1]) : +value; }; //------------------------------------------------------------------------- // Game_Map Game_Map.prototype.getInterpreterCharacter = function(param) { const interpreter = this._interpreter; return interpreter.isRunning() ? interpreter.character(param) : null; }; //------------------------------------------------------------------------- // Graphics_Fragment class Graphics_Fragment extends PIXI.Graphics { constructor(fragment) { super(); this._fragment = fragment; this._blendColor = [0, 0, 0, 0]; this._break = fragment.isBreak(); this.clearFlash(); this.initPivot(); this.refresh(); this.update(); } clearFlash() { this._flashColor = [0, 0, 0, 0]; this._flashDuration = 0; } initPivot() { const rect = this._fragment.rect(); const x = rect.x + Math.floor(rect.width / 2); const y = rect.y + Math.floor(rect.height / 2); this.pivot.set(x, y); } refreshIfNeeded() { if (!this._break && this._fragment.isBreak()) { this._break = true; this.refresh(); } } refresh() { this.clear(); const options = { texture: EncounterEffect.snapTexture(), matrix: this._fragment.textureMatrix() }; this.beginTextureFill(options); this.drawPolygon(); this.endFill(); const lw = this._fragment.lineWidth() / 2; if (this._break && lw > 0) { const c = EncounterEffect.color(); const color = c[0] * 256 * 256 + c[1] * 256 + c[2]; this.lineStyle(lw + 0.5, color, 0.25, 0.3); this.drawPolygon(); this.lineStyle(lw, color, 0.5, 0.3); this.drawPolygon(); } } drawPolygon() { for (const [i, point] of this._fragment.points().entries()) { i === 0 ? this.moveTo(point.x, point.y) : this.lineTo(point.x, point.y); } this.closePath(); } update() { this.refreshIfNeeded(); this.updatePosition(); this.updateOpacity(); this.updateFlash(); this.updateVisibility(); } updatePosition() { const fragment = this._fragment; this.x = fragment.screenX(); this.y = fragment.screenY(); this.scale.x = fragment.scaleX(); this.scale.y = fragment.scaleY(); this.rotation = fragment.rotation(); } updateOpacity() { this.alpha = this._fragment.opacity() / 255; } updateFlash() { this.setupFlash(); if (this._flashDuration > 0) { const d = this._flashDuration--; this._flashColor[3] *= (d - 1) / d; if (this._flashDuration % 8 === 0) { this.setBlendColor(this._flashColor); } } } setupFlash() { if (this._fragment.isFlashRequested()) { this._flashDuration = 8 * (Utils.isMobileDevice() ? 2 : 3) + 1; this._flashColor = this._fragment.flashColor(); this._fragment.clearFlash(); } } setBlendColor(color) { if (!(color instanceof Array)) { throw new Error("Argument must be an array"); } if (!array_equals(this._blendColor, color)) { this._blendColor = [...color]; this._updateColorFilter(); } } _createColorFilter() { this._colorFilter = new ColorFilter(); if (!this.filters) { this.filters = []; } this.filters.push(this._colorFilter); } _removeColorFilter() { this.filters = null; this._colorFilter = null; } _updateColorFilter() { if (this._colorFilter && this._blendColor[3] === 0) { this._removeColorFilter(); return; } if (!this._colorFilter) { this._createColorFilter(); } this._colorFilter.setBlendColor(this._blendColor); } updateVisibility() { this.visible = this._fragment.visible; } } //------------------------------------------------------------------------- // Sprite_EncounterEffect // // スプライトクラスではないですが、 // RPGツクールのSpriteクラスのような扱いをするという意味でのクラス名です。 class Sprite_EncounterEffect extends PIXI.Container { constructor() { super(); this.createBlackBack(); this.createFragments(); this._fadeDelay = 0; this._fadeDuration = 0; this.initPosition(); this.updateRotation(); } destroy() { const options = { children: true }; super.destroy(options); } createBlackBack() { const p = 40; const width = Graphics.width + p * 2; const height = Graphics.height + p * 2; this._backGraphics = new PIXI.Graphics(); this._backGraphics.beginFill(0); this._backGraphics.drawRect(-p, -p, width, height); this._backGraphics.endFill(); this.addChild(this._backGraphics); } createFragments() { for (const fragment of EncounterEffect.fragments()) { this.addChild(new Graphics_Fragment(fragment)); } } initPosition() { const { x, y } = EncounterEffect.centerPoint; this.x = x; this.y = y; this.pivot.set(x, y); } startFadeIn(duration) { const rate = 3; this._fadeDelay = Math.floor(duration / rate); this._fadeDuration = duration - this._fadeDelay; } update() { this.updateChildren(); this.updateRotation(); this.updateOpacity(); this.updateFadeIn(); } updateChildren() { for (const child of this.children) { if (child.update) { child.update(); } } } updateRotation() { this.rotation = EncounterEffect.rotation(); } updateOpacity() { this.alpha = EncounterEffect.opacity() / 255; } updateFadeIn() { if (this._fadeDuration > 0 && this.updateFadeDelay()) { const d = this._fadeDuration; this._backGraphics.alpha -= this._backGraphics.alpha / d; this._fadeDuration--; } } updateFadeDelay() { if (this._fadeDelay > 0) { this._fadeDelay--; return false; } return true; } isSpritesetVisible() { return this.alpha < 1 || this._backGraphics.alpha < 1; } } //------------------------------------------------------------------------- // Sprite_Actor const _Sprite_Actor_updateMove = Sprite_Actor.prototype.updateMove; Sprite_Actor.prototype.updateMove = function() { if (EncounterEffect.isReady()) { _Sprite_Actor_updateMove.apply(this, arguments); } }; //------------------------------------------------------------------------- // Spriteset_Map Spriteset_Map.prototype.temporaryHideDestination = function() { this._destinationSprite.visible = false; }; //------------------------------------------------------------------------- // Scene_Boot const _Scene_Boot_create = Scene_Boot.prototype.create; Scene_Boot.prototype.create = function() { _Scene_Boot_create.apply(this, arguments); EncounterEffect.clear(); }; //------------------------------------------------------------------------- // Scene_Base Scene_Base.prototype.createEncEffSprite = function() { this._encEffSprite = new Sprite_EncounterEffect(); this.addChild(this._encEffSprite); }; Scene_Base.prototype.destroyEncEffSprite = function() { this.removeChild(this._encEffSprite); this._encEffSprite.destroy(); delete this._encEffSprite; }; Scene_Base.prototype.clearFade = function() { this._fadeSign = 0; this._fadeDuration = 0; this._fadeWhite = 0; this._fadeOpacity = 0; this.updateColorFilter(); }; //------------------------------------------------------------------------- // Scene_Map const _Scene_Map_startEncounterEffect = Scene_Map.prototype.startEncounterEffect; Scene_Map.prototype.startEncounterEffect = function() { if (EncounterEffect.isValid()) { this.snapForBattleBackground(); this.snapEncounterTexture(); _Scene_Map_startEncounterEffect.apply(this, arguments); this.createEncEffSprite(); this._spriteset.visible = false; return; } else if (EncounterEffect.type() < 0) { EncounterEffect.clear(); this.startFadeOut(this.fadeSpeed(), false); } else { EncounterEffect.clear(); _Scene_Map_startEncounterEffect.apply(this, arguments); } }; const _Scene_Map_updateEncounterEffect = Scene_Map.prototype.updateEncounterEffect Scene_Map.prototype.updateEncounterEffect = function() { if (EncounterEffect.isRunning()) { this.updateMppEncounterEffect(); } else { _Scene_Map_updateEncounterEffect.apply(this, arguments); } }; const _Scene_Map_encounterEffectSpeed = Scene_Map.prototype.encounterEffectSpeed; Scene_Map.prototype.encounterEffectSpeed = function() { if (EncounterEffect.isRunning()) { return EncounterEffect.effectSpeed(); } else { return _Scene_Map_encounterEffectSpeed.apply(this, arguments); } }; Scene_Map.prototype.snapEncounterTexture = function() { this._windowLayer.visible = false; this._spriteset.temporaryHideDestination(); EncounterEffect.setup(SceneManager.snapTexture()); }; Scene_Map.prototype.updateMppEncounterEffect = function() { EncounterEffect.update(); if (this._encounterEffectDuration > 0) { this._encounterEffectDuration--; const speed = this.encounterEffectSpeed(); const n = speed - this._encounterEffectDuration; const breakEnd = EncounterEffect.breakDuration(); if (n > 2 && n <= breakEnd + 2) { const maxItems = EncounterEffect.maxItems(); const start = Math.floor(maxItems * (n - 3) / breakEnd); const end = Math.floor(maxItems * (n - 2) / breakEnd); EncounterEffect.breakFragments(start, end, speed); } if (n === Math.floor(speed / 4)) { BattleManager.playBattleBgm(); } } }; //------------------------------------------------------------------------- // Scene_Battle const _Scene_Battle_start = Scene_Battle.prototype.start; Scene_Battle.prototype.start = function() { _Scene_Battle_start.apply(this, arguments); this.startEncounterEffect(); }; const _Scene_Battle_update = Scene_Battle.prototype.update; Scene_Battle.prototype.update = function() { _Scene_Battle_update.apply(this, arguments); this.updateEncounterEffect(); }; Scene_Battle.prototype.startEncounterEffect = function() { if (EncounterEffect.isRunning()) { this.clearFade(); EncounterEffect.onBattleStart(); this.createEncEffSprite(); this._encEffSprite.startFadeIn(EncounterEffect.fadeSpeed()); } }; Scene_Battle.prototype.updateEncounterEffect = function() { if (EncounterEffect.isRunning()) { EncounterEffect.update(); this._spriteset.visible = this._encEffSprite.isSpritesetVisible(); if (!EncounterEffect.isRunning()) { this.destroyEncEffSprite(); } } }; })();