//============================================================================= // RPG Maker MZ - Recruit Member // recruit_member.js //============================================================================= /*: * @target MZ * @plugindesc Shows a polished recruitment presentation when an actor is recruited. * @author Gunther Peeters (Elvium) * * @param defaultFanfare * @text Default Fanfare * @type file * @dir audio/me * @desc ME played when a hero is recruited. Use none for no fanfare. * @default none * * @param textOptions * @text Text Options * * @param textColor * @parent textOptions * @text Text Color * @type string * @desc Hex text color for the recruitment lines. * @default #ffffff * * @param background * @parent textOptions * @text Background * @type select * @option Window * @value window * @option Dim * @value dim * @option Transparent * @value transparent * @default window * * @param line1 * @parent textOptions * @text Line 1 * @type string * @desc Supports {name} and {id}. * @default New hero recruited! * * @param line2 * @parent textOptions * @text Line 2 * @type string * @desc Supports {name} and {id}. * @default {name} * * @param line3 * @parent textOptions * @text Line 3 * @type string * @desc Supports {name} and {id}. Leave empty to hide this line. * @default has joined your group. * * @param showBox * @text Show Box * * @param alignHorizontal * @parent showBox * @text Align Horizontal * @type select * @option Left * @value left * @option Center * @value center * @option Right * @value right * @default center * * @param alignVertical * @parent showBox * @text Align Vertical * @type select * @option Top * @value top * @option Middle * @value middle * @option Bottom * @value bottom * @default middle * * @param fadeBackground * @parent showBox * @text Fade Background * @type boolean * @on YES * @off NO * @default true * * @param showTime * @parent showBox * @text Show Time * @type number * @min 1 * @max 300 * @desc Minimum display time in frames. Capped at 300. * @default 120 * * @param allowSkip * @parent showBox * @text Allow Skip * @type boolean * @on YES * @off NO * @default true * * @param animation * @text Animation * * @param showAnimation * @parent animation * @text Show Animation * @type select * @option None * @value none * @option Static * @value static * @option Jump * @value jump * @option Spin * @value spin * @option Both * @value both * @default jump * * @param animationPosition * @parent animation * @text Position * @type select * @option Top * @value top * @option Bottom * @value bottom * @default top * * @command recruitSettings * @text Recruit Settings * @desc One-shot overrides for the next Recruit Member command. * * @arg defaultFanfare * @text Default Fanfare * @type file * @dir audio/me * @desc ME played when a hero is recruited. Use none to keep silent. * @default * * @arg textColor * @text Text Color * @type string * @desc Hex text color for the recruitment lines. * @default * * @arg background * @text Background * @type select * @option Window * @value window * @option Dim * @value dim * @option Transparent * @value transparent * @default * * @arg line1 * @text Line 1 * @type string * @desc Supports {name} and {id}. * @default * * @arg line2 * @text Line 2 * @type string * @desc Supports {name} and {id}. * @default * * @arg line3 * @text Line 3 * @type string * @desc Supports {name} and {id}. Leave empty to hide this line. * @default * * @arg alignHorizontal * @text Align Horizontal * @type select * @option Left * @value left * @option Center * @value center * @option Right * @value right * @default * * @arg alignVertical * @text Align Vertical * @type select * @option Top * @value top * @option Middle * @value middle * @option Bottom * @value bottom * @default * * @arg fadeBackground * @text Fade Background * @type boolean * @on YES * @off NO * @default * * @arg showTime * @text Show Time * @type number * @min 1 * @max 300 * @desc Minimum display time in frames. Capped at 300. * @default * * @arg allowSkip * @text Allow Skip * @type boolean * @on YES * @off NO * @default * * @arg showAnimation * @text Show Animation * @type select * @option None * @value none * @option Static * @value static * @option Jump * @value jump * @option Spin * @value spin * @option Both * @value both * @default * * @arg animationPosition * @text Position * @type select * @option Top * @value top * @option Bottom * @value bottom * @default * * @command recruitMember * @text Recruit Member * @desc Recruit an actor and show the recruitment presentation. * * @arg recruitId * @text Recruit Member * @type actor * @desc Actor to recruit. * @default 1 * * @help * ============================================================================ * Recruit Member * ============================================================================ * * This plugin only handles actor recruitment and the recruitment presentation. * Party management remains outside this plugin. * * Flow: * - Recruit Settings stores one-shot overrides. * - Recruit Member adds the actor to the RPG Maker party. * - A recruitment presentation is shown using the actor database name/sprite. * - Call any party-management plugin commands separately in your event if needed. * * Placeholders: * - {name} = actor name * - {id} = actor ID * * Fanfare: * - Uses ME. * - Use none for no fanfare. * - Skipping the presentation does not stop the ME. * * Fade Background: * - ON adds a temporary dark overlay behind the recruit display. * - OFF leaves the scene visible, including visual effects behind the display. * * ============================================================================ */ (() => { "use strict"; const pluginName = "recruit_member"; const parameters = PluginManager.parameters(pluginName); function boolValue(value, fallback) { if (value === undefined || value === null || value === "") { return fallback; } return String(value) === "true"; } function numberValue(value, fallback) { const number = Number(value); return Number.isFinite(number) ? number : fallback; } function clampShowTime(value) { return Math.max(1, Math.min(300, Math.round(numberValue(value, 120)))); } function choiceValue(value, choices, fallback) { value = String(value || fallback).toLowerCase(); return choices.includes(value) ? value : fallback; } function hexColor(value, fallback) { value = String(value || "").trim(); return /^#[0-9a-f]{6}$/i.test(value) ? value : fallback; } function fanfareName(value) { value = String(value || "").trim(); return value.toLowerCase() === "none" ? "" : value; } function parseActorId(value) { value = String(value || "0").replace(/\\v\[(\d+)\]/gi, (_, n) => { return $gameVariables.value(Number(n)); }); return Number(value || 0); } function factoryDefaultSettings() { return { defaultFanfare: "none", textColor: "#ffffff", background: "window", line1: "New hero recruited!", line2: "{name}", line3: "has joined your group.", alignHorizontal: "center", alignVertical: "middle", fadeBackground: true, showTime: 120, allowSkip: true, showAnimation: "jump", animationPosition: "top" }; } function defaultSettings() { const factory = factoryDefaultSettings(); return { defaultFanfare: fanfareName(parameters["defaultFanfare"] || factory.defaultFanfare), textColor: hexColor(parameters["textColor"], factory.textColor), background: choiceValue(parameters["background"], ["window", "dim", "transparent"], factory.background), line1: String(parameters["line1"] || factory.line1), line2: String(parameters["line2"] || factory.line2), line3: String(parameters["line3"] || factory.line3), alignHorizontal: choiceValue(parameters["alignHorizontal"], ["left", "center", "right"], factory.alignHorizontal), alignVertical: choiceValue(parameters["alignVertical"], ["top", "middle", "bottom"], factory.alignVertical), fadeBackground: boolValue(parameters["fadeBackground"], factory.fadeBackground), showTime: clampShowTime(parameters["showTime"] || factory.showTime), allowSkip: boolValue(parameters["allowSkip"], factory.allowSkip), showAnimation: choiceValue(parameters["showAnimation"], ["none", "static", "jump", "spin", "both"], factory.showAnimation), animationPosition: choiceValue(parameters["animationPosition"], ["top", "bottom"], factory.animationPosition) }; } function commandArg(args, name, defaults) { if (args[name] === undefined || args[name] === "") { return defaults[name]; } return args[name]; } function settingsFromArgs(args) { const defaults = defaultSettings(); return { defaultFanfare: fanfareName(commandArg(args, "defaultFanfare", defaults) || "none"), textColor: hexColor(commandArg(args, "textColor", defaults), defaults.textColor), background: choiceValue(commandArg(args, "background", defaults), ["window", "dim", "transparent"], defaults.background), line1: String(commandArg(args, "line1", defaults)), line2: String(commandArg(args, "line2", defaults)), line3: String(commandArg(args, "line3", defaults)), alignHorizontal: choiceValue(commandArg(args, "alignHorizontal", defaults), ["left", "center", "right"], defaults.alignHorizontal), alignVertical: choiceValue(commandArg(args, "alignVertical", defaults), ["top", "middle", "bottom"], defaults.alignVertical), fadeBackground: boolValue(commandArg(args, "fadeBackground", defaults), defaults.fadeBackground), showTime: clampShowTime(commandArg(args, "showTime", defaults)), allowSkip: boolValue(commandArg(args, "allowSkip", defaults), defaults.allowSkip), showAnimation: choiceValue(commandArg(args, "showAnimation", defaults), ["none", "static", "jump", "spin", "both"], defaults.showAnimation), animationPosition: choiceValue(commandArg(args, "animationPosition", defaults), ["top", "bottom"], defaults.animationPosition) }; } function recruitText(line, actor) { return String(line || "") .replace(/\{name\}/gi, actor.name()) .replace(/\{id\}/gi, String(actor.actorId())); } function playRecruitFanfare(settings) { if (!settings.defaultFanfare) { return; } AudioManager.playMe({ name: settings.defaultFanfare, volume: 90, pitch: 100, pan: 0 }); } const _Game_Temp_initialize = Game_Temp.prototype.initialize; Game_Temp.prototype.initialize = function() { _Game_Temp_initialize.call(this); this._recruitMemberPresentation = null; }; Game_Temp.prototype.startRecruitMemberPresentation = function(actorId, settings) { this._recruitMemberPresentation = { actorId: actorId, settings: settings, active: true }; }; Game_Temp.prototype.recruitMemberPresentation = function() { return this._recruitMemberPresentation; }; Game_Temp.prototype.clearRecruitMemberPresentation = function() { this._recruitMemberPresentation = null; }; const _Game_Interpreter_updateWaitMode = Game_Interpreter.prototype.updateWaitMode; Game_Interpreter.prototype.updateWaitMode = function() { if (this._waitMode === "recruitMemberPresentation") { const presentation = $gameTemp.recruitMemberPresentation(); if (presentation && presentation.active) { return true; } this._waitMode = ""; return false; } return _Game_Interpreter_updateWaitMode.call(this); }; class Sprite_RecruitActor extends Sprite { initialize(actor) { super.initialize(); this._actor = actor; this._directionIndex = 0; this.anchor.set(0.5, 1); this.loadBitmap(); } loadBitmap() { this.bitmap = ImageManager.loadCharacter(this._actor.characterName()); this.bitmap.addLoadListener(this.updateFrame.bind(this)); } setDirectionIndex(index) { if (this._directionIndex !== index) { this._directionIndex = index; this.updateFrame(); } } updateFrame() { const bitmap = this.bitmap; if (!bitmap || !bitmap.isReady()) { return; } const big = ImageManager.isBigCharacter(this._actor.characterName()); const pw = bitmap.width / (big ? 3 : 12); const ph = bitmap.height / (big ? 4 : 8); const index = big ? 0 : this._actor.characterIndex(); const blockX = big ? 0 : index % 4 * 3; const blockY = big ? 0 : Math.floor(index / 4) * 4; const directionRows = [0, 1, 3, 2]; const sx = (blockX + 1) * pw; const sy = (blockY + directionRows[this._directionIndex]) * ph; this.setFrame(sx, sy, pw, ph); this.scale.set(2, 2); } } class Window_RecruitMember extends Window_Base { initialize(rect, actor, settings) { super.initialize(rect); this._actor = actor; this._settings = settings; this.openness = 0; this.refresh(); } applyRecruitBackground(type) { if (type === "dim") { this.setBackgroundType(1); } else if (type === "transparent") { this.setBackgroundType(2); } else { this.setBackgroundType(0); } } refresh() { const lines = [this._settings.line1, this._settings.line2, this._settings.line3] .map(line => recruitText(line, this._actor)) .filter(line => line.length > 0); this.contents.clear(); this.contents.fontBold = true; this.changeTextColor(this._settings.textColor); const lineHeight = this.lineHeight(); const totalHeight = lines.length * lineHeight; let y = Math.max(0, Math.floor((this.innerHeight - totalHeight) / 2)); for (const line of lines) { this.drawText(line, 0, y, this.innerWidth, "center"); y += lineHeight; } this.resetFontSettings(); } } class Sprite_RecruitMemberPresentation extends Sprite { initialize(presentation) { super.initialize(); this._presentation = presentation; this._actor = $gameActors.actor(presentation.actorId); this._settings = presentation.settings; this._duration = this._settings.showTime; this._closing = false; this._finished = false; this._age = 0; this.createBackgroundFade(); this.createWindow(); this.createActorSprite(); } createBackgroundFade() { this._fadeSprite = new Sprite(new Bitmap(Graphics.boxWidth, Graphics.boxHeight)); this._fadeSprite.bitmap.fillAll("black"); this._fadeSprite.opacity = 0; this.addChild(this._fadeSprite); } createWindow() { const width = Math.min(560, Graphics.boxWidth - 48); const height = 156; const x = this.alignedX(width); const y = this.alignedY(height); const rect = new Rectangle(x, y, width, height); this._window = new Window_RecruitMember(rect, this._actor, this._settings); this._window.applyRecruitBackground(this._settings.background); this.addChild(this._window); } createActorSprite() { if (this._settings.showAnimation === "none") { return; } this._actorSprite = new Sprite_RecruitActor(this._actor); this._actorSprite.x = this._window.x + this._window.width / 2; if (this._settings.animationPosition === "bottom") { this._actorSprite.y = this._window.y + this._window.height + 48; } else { this._actorSprite.y = this._window.y - 8; } this._actorSpriteBaseY = this._actorSprite.y; this.addChild(this._actorSprite); } alignedX(width) { if (this._settings.alignHorizontal === "left") { return 24; } if (this._settings.alignHorizontal === "right") { return Graphics.boxWidth - width - 24; } return Math.floor((Graphics.boxWidth - width) / 2); } alignedY(height) { if (this._settings.alignVertical === "top") { return 64; } if (this._settings.alignVertical === "bottom") { return Graphics.boxHeight - height - 64; } return Math.floor((Graphics.boxHeight - height) / 2); } update() { super.update(); if (this._finished) { return; } this._age++; if (!this._closing) { this.updateOpening(); } this.updateActorAnimation(); if (!this._closing && this.canClose()) { this.startClosing(); } if (this._closing) { this.updateClosing(); } } updateOpening() { if (this._window.openness < 255) { this._window.open(); } if (this._settings.fadeBackground) { this._fadeSprite.opacity = Math.min(160, this._fadeSprite.opacity + 16); } } updateActorAnimation() { if (!this._actorSprite) { return; } const mode = this._settings.showAnimation; const jump = mode === "jump" || mode === "both"; const spin = mode === "spin" || mode === "both"; this._actorSprite.y = this._actorSpriteBaseY + (jump ? Math.sin(this._age / 8) * 10 : 0); this._actorSprite.setDirectionIndex(spin ? Math.floor(this._age / 10) % 4 : 0); } canClose() { const timeReady = this._age >= this._duration; const skipReady = this._settings.allowSkip && this._age >= 12 && (Input.isTriggered("ok") || Input.isTriggered("cancel") || TouchInput.isTriggered()); return timeReady || skipReady; } startClosing() { this._closing = true; this._window.close(); } updateClosing() { if (this._settings.fadeBackground) { this._fadeSprite.opacity = Math.max(0, this._fadeSprite.opacity - 16); } this.opacity = Math.max(0, this.opacity - 24); if (this.opacity <= 0) { this.finish(); } } finish() { if (this._finished) { return; } this._finished = true; this._presentation.active = false; $gameTemp._recruitMemberSettingsOverride = null; $gameTemp.clearRecruitMemberPresentation(); if (this.parent) { this.parent.removeChild(this); } } } const _Scene_Base_update = Scene_Base.prototype.update; Scene_Base.prototype.update = function() { _Scene_Base_update.call(this); this.updateRecruitMemberPresentation(); }; Scene_Base.prototype.updateRecruitMemberPresentation = function() { if (this._recruitMemberPresentationSprite) { if (!this._recruitMemberPresentationSprite.parent) { this._recruitMemberPresentationSprite = null; } return; } const presentation = $gameTemp.recruitMemberPresentation(); if (presentation && presentation.active) { this._recruitMemberPresentationSprite = new Sprite_RecruitMemberPresentation(presentation); this.addChild(this._recruitMemberPresentationSprite); } }; PluginManager.registerCommand(pluginName, "recruitSettings", args => { $gameTemp._recruitMemberSettingsOverride = settingsFromArgs(args); }); PluginManager.registerCommand(pluginName, "recruitMember", function(args) { const actorId = parseActorId(args.recruitId); const actorData = $dataActors[actorId]; if (!actorData) { return; } const settings = $gameTemp._recruitMemberSettingsOverride || defaultSettings(); $gameParty.addActor(actorId); $gamePlayer.refresh(); playRecruitFanfare(settings); $gameTemp.startRecruitMemberPresentation(actorId, settings); this.setWaitMode("recruitMemberPresentation"); }); })();