Getting Started
Five minutes to a working setup, then one complete example that touches the modules you will use most: plugin registration, parameters, save data, translations, text codes, a window with layout, a tween and a hotkey.
1Install
- Copy
MF_Core.jstojs/plugins/of your project. - Open Tools → Plugin Manager, add MF_Core and move it to the top of the list, above every other
MF_*plugin. - Leave the parameters at their defaults for now.
2Check it works
Start a playtest and press F8 to open the developer console. Type:
MF.Core.list()
// → [{ name: "MF_Core", version: "1.2.0" }]
MF.Units.resolve("100%-240", Graphics.boxWidth)
// → 576 (with the default 816 px screen)
MF.I18n.locale()
// → "en" / "ru-RU" / … — taken from the database or the system
If MF is undefined, the plugin is not enabled or failed to load — the console shows why.
3First plugin
Create js/plugins/MF_Hello.js, add it in the Plugin Manager below MF_Core, and start a playtest:
/*:
* @target MZ
* @plugindesc [v0.1.0] Hello, MF_Core.
* @base MF_Core
* @orderAfter MF_Core
* @help Requires: MF_Core 1.0.0+ (written against 1.0.0)
*/
(() => {
"use strict";
MF.Core.register("MF_Hello", "0.1.0", { requires: { MF_Core: "1.0.0" } });
let greeted = false;
MF.Hook.after(Scene_Map.prototype, "start", function() {
if (greeted) return; // start() runs every time you return from a menu
greeted = true;
MF.Text.show("Hello from \\C[3]MF_Core\\C[0]!\\W[30] Nice to meet you.");
}, "MF_Hello");
})();
A message appears on the map with a colored word and a half-second pause (\W[30] = 30 frames).
Three things happened:
MF.Core.register(…, { requires })— if MF_Core is missing or too old, the player sees a clear error instead of a random crash.@base/@orderAftermake the Plugin Manager warn even earlier.MF.Hook.after— extendsScene_Map.startwithout copying engine code.MF.Text.show— shows a message and returns a Promise;\W[n]is one of the extra text codes.
"\\C[3]". In the event editor's Show Text you type it once: \C[3]. DetailsTutorial: a quest log
We build MF_QuestLog.js: event commands start and complete quests, the state is stored in the save file, a hotkey on the map opens a centered window listing the quests, and a text code prints a quest's title inside messages. About 120 lines in total.
| Step | Module | What you learn |
|---|---|---|
| 1 | MF.Core | Declaring dependencies and versions |
| 2 | MF.Schema | Validated plugin parameters with defaults |
| 3 | MF.Save | State inside the save file — no DataManager patching |
| 4 | MF.I18n | Translations with plural forms |
| 5 | MF.Text | A custom text code for messages |
| 6 | MF.UI, MF.Layout | A window sized in % and "auto", centered by an anchor |
| 7 | MF.Tween | Slide-in animation |
| 8 | MF.Input, MF.Hook | A hotkey that doesn't clash with RPG Maker's keys |
Each step shows only the new code. The full source is at the end.
1Skeleton
/*:
* @target MZ
* @plugindesc [v0.1.0] Simple quest log (MF_Core tutorial).
* @author You
* @base MF_Core
* @orderAfter MF_Core
*
* @help MF_QuestLog.js
* Requires: MF_Core 1.0.0+ (written against 1.0.0)
*/
(() => {
"use strict";
const PLUGIN_NAME = "MF_QuestLog";
if (!window.MF || !MF.Core) throw new Error(`${PLUGIN_NAME} requires MF_Core.`);
MF.Core.register(PLUGIN_NAME, "0.1.0", { requires: { MF_Core: "1.0.0" } });
// … the next steps go here
})();
Write down the MF_Core version you develop against — in code and in @help. When MF_Core changes later, you will know immediately which plugins need a look. See Versioning policy.
2Parameters
Add a parameter to the header:
* @param hotkey
* @text Hotkey
* @desc Key that opens the quest log on the map (a-z, 0-9).
* @default j
…and read it with a schema:
const params = MF.Core.parameters(PLUGIN_NAME, {
type: "object",
properties: {
hotkey: { type: "string", pattern: "^[a-z0-9]$", default: "j" }
}
});
A wrong value (e.g. "ctrl") produces one readable console warning listing the field, instead of a crash three scenes later. Numbers and booleans from the Plugin Manager arrive as strings — the schema converts them.
3Save data and plugin commands
The quest list must survive saving and loading. MF.Save stores it inside the save file:
MF.Save.register(PLUGIN_NAME, {
version: 1,
default: () => ({ quests: {} }) // id → { title, done }
});
const state = () => MF.Save.get(PLUGIN_NAME);
Add two plugin commands to the header…
* @command start
* @text Start quest
* @arg id
* @text Quest ID
* @arg title
* @text Title
*
* @command complete
* @text Complete quest
* @arg id
* @text Quest ID
…and implement them:
PluginManager.registerCommand(PLUGIN_NAME, "start", args => {
state().quests[args.id] = { title: args.title, done: false };
});
PluginManager.registerCommand(PLUGIN_NAME, "complete", args => {
const quest = state().quests[args.id];
if (quest) quest.done = true;
});
New game → empty list. Save → the list is written. Load → it is back. Old saves made before the plugin was installed simply get the default.
4Translations
MF.I18n.register(PLUGIN_NAME, {
en: {
empty: "No quests yet.",
done: "done",
count: { one: "{count} active quest", other: "{count} active quests" }
},
ru: {
empty: "Заданий пока нет.",
done: "выполнено",
count: {
one: "{count} активное задание",
few: "{count} активных задания",
many: "{count} активных заданий",
other: "{count} активного задания"
}
}
});
const t = MF.I18n.translator(PLUGIN_NAME);
t("count", { count: 3 }); // "3 active quests" / "3 активных задания"
The language comes from the database locale (or the player's saved choice). Missing keys fall back to English, then to the key itself — with a one-time warning.
5A text code for messages
MF.Text.registerMacro("QUEST", id => {
const quest = state().quests[id];
return quest ? `\\C[2]${quest.title}\\C[0]` : "???";
});
Now any Show Text can say New quest: \QUEST[slimes]! and the title appears in color. A macro's result may contain other codes — here \C[2].
6The window
MF.UI.Window is a normal Window_Base that understands a layout: sizes in %, "auto" height in rows, and an anchor.
function Window_QuestList() {
this.initialize(...arguments);
}
Window_QuestList.prototype = Object.create(MF.UI.Window.prototype);
Window_QuestList.prototype.constructor = Window_QuestList;
Window_QuestList.prototype.initialize = function(spec) {
MF.UI.Window.prototype.initialize.call(this, spec);
this.refresh(); // draw once after creation
};
Window_QuestList.prototype.quests = function() {
return Object.values(state().quests);
};
// height: "auto" asks for this many rows: a header line + one per quest
Window_QuestList.prototype.layoutRows = function() {
return Math.max(2, this.quests().length + 1);
};
Window_QuestList.prototype.refresh = function() {
MF.UI.Window.prototype.refresh.call(this); // clears the contents
const quests = this.quests();
const active = quests.filter(q => !q.done).length;
this.drawText(t("count", { count: active }), 0, 0, this.innerWidth, "center");
if (quests.length === 0) {
this.drawText(t("empty"), 0, this.lineHeight(), this.innerWidth, "center");
return;
}
quests.forEach((quest, i) => {
const y = this.lineHeight() * (i + 1);
this.changePaintOpacity(!quest.done);
this.drawText(quest.title, 0, y, this.innerWidth);
if (quest.done) this.drawText(t("done"), 0, y, this.innerWidth, "right");
});
this.changePaintOpacity(true);
};
MF.UI.Window calls refresh() automatically only after a resize. Call it yourself after creation and whenever the data changes. Details7The scene and a slide-in animation
function Scene_QuestLog() {
this.initialize(...arguments);
}
Scene_QuestLog.prototype = Object.create(Scene_MenuBase.prototype);
Scene_QuestLog.prototype.constructor = Scene_QuestLog;
Scene_QuestLog.prototype.create = function() {
Scene_MenuBase.prototype.create.call(this);
this._questWindow = new Window_QuestList({
anchor: "center",
width: "60%", maxWidth: 520,
height: "auto", maxHeight: "90%"
});
this.addWindow(this._questWindow);
};
Scene_QuestLog.prototype.start = function() {
Scene_MenuBase.prototype.start.call(this);
const win = this._questWindow;
MF.Tween.from(win, { y: win.y - 60, contentsOpacity: 0 }, { duration: 15, easing: "easeOutCubic" });
};
Scene_QuestLog.prototype.update = function() {
Scene_MenuBase.prototype.update.call(this);
if (Input.isTriggered("cancel") || Input.isTriggered("questLog") || TouchInput.isCancelled()) {
SoundManager.playCancel();
this.popScene();
}
};
The window is centered at 60 % of the screen width (never wider than 520 px), as tall as its rows, and slides in over 15 frames (¼ second).
8The hotkey
MF.Input.bind("questLog", { keys: [params.hotkey] });
MF.Hook.after(Scene_Map.prototype, "update", function() {
if (Input.isTriggered("questLog") && $gamePlayer.canMove() && !SceneManager.isSceneChanging()) {
SceneManager.push(Scene_QuestLog);
}
}, PLUGIN_NAME);
$gamePlayer.canMove() is false while an event or a message is running, so the log never opens in the middle of a cut-scene.
bind refuses to steal them and prints a warning — that is why the default hotkey is J. Details9Try it
- Add
MF_QuestLogbelowMF_Corein the Plugin Manager. - Create an event with:
- Plugin Command → MF_QuestLog → Start quest, id
slimes, titleDefeat 3 slimes - Show Text:
New quest: \QUEST[slimes]!
- Plugin Command → MF_QuestLog → Start quest, id
- Playtest, talk to the event, press J. Save, reload — the quest is still there.
Full source
MF_QuestLog.js (click to expand)
/*:
* @target MZ
* @plugindesc [v0.1.0] Simple quest log (MF_Core tutorial).
* @author You
* @base MF_Core
* @orderAfter MF_Core
*
* @param hotkey
* @text Hotkey
* @desc Key that opens the quest log on the map (a-z, 0-9).
* @default j
*
* @command start
* @text Start quest
* @arg id
* @text Quest ID
* @arg title
* @text Title
*
* @command complete
* @text Complete quest
* @arg id
* @text Quest ID
*
* @help MF_QuestLog.js
* Requires: MF_Core 1.0.0+ (written against 1.0.0)
*
* Text code: \QUEST[id] — title of a quest.
*/
(() => {
"use strict";
const PLUGIN_NAME = "MF_QuestLog";
if (!window.MF || !MF.Core) throw new Error(`${PLUGIN_NAME} requires MF_Core.`);
MF.Core.register(PLUGIN_NAME, "0.1.0", { requires: { MF_Core: "1.0.0" } });
//--- Parameters -----------------------------------------------------------
const params = MF.Core.parameters(PLUGIN_NAME, {
type: "object",
properties: {
hotkey: { type: "string", pattern: "^[a-z0-9]$", default: "j" }
}
});
//--- Save data and commands -------------------------------------------------
MF.Save.register(PLUGIN_NAME, {
version: 1,
default: () => ({ quests: {} })
});
const state = () => MF.Save.get(PLUGIN_NAME);
PluginManager.registerCommand(PLUGIN_NAME, "start", args => {
state().quests[args.id] = { title: args.title, done: false };
});
PluginManager.registerCommand(PLUGIN_NAME, "complete", args => {
const quest = state().quests[args.id];
if (quest) quest.done = true;
});
//--- Translations -------------------------------------------------------------
MF.I18n.register(PLUGIN_NAME, {
en: {
empty: "No quests yet.",
done: "done",
count: { one: "{count} active quest", other: "{count} active quests" }
},
ru: {
empty: "Заданий пока нет.",
done: "выполнено",
count: {
one: "{count} активное задание",
few: "{count} активных задания",
many: "{count} активных заданий",
other: "{count} активного задания"
}
}
});
const t = MF.I18n.translator(PLUGIN_NAME);
//--- Text code ------------------------------------------------------------------
MF.Text.registerMacro("QUEST", id => {
const quest = state().quests[id];
return quest ? `\\C[2]${quest.title}\\C[0]` : "???";
});
//--- Window -----------------------------------------------------------------------
function Window_QuestList() {
this.initialize(...arguments);
}
Window_QuestList.prototype = Object.create(MF.UI.Window.prototype);
Window_QuestList.prototype.constructor = Window_QuestList;
Window_QuestList.prototype.initialize = function(spec) {
MF.UI.Window.prototype.initialize.call(this, spec);
this.refresh();
};
Window_QuestList.prototype.quests = function() {
return Object.values(state().quests);
};
Window_QuestList.prototype.layoutRows = function() {
return Math.max(2, this.quests().length + 1);
};
Window_QuestList.prototype.refresh = function() {
MF.UI.Window.prototype.refresh.call(this);
const quests = this.quests();
const active = quests.filter(q => !q.done).length;
this.drawText(t("count", { count: active }), 0, 0, this.innerWidth, "center");
if (quests.length === 0) {
this.drawText(t("empty"), 0, this.lineHeight(), this.innerWidth, "center");
return;
}
quests.forEach((quest, i) => {
const y = this.lineHeight() * (i + 1);
this.changePaintOpacity(!quest.done);
this.drawText(quest.title, 0, y, this.innerWidth);
if (quest.done) this.drawText(t("done"), 0, y, this.innerWidth, "right");
});
this.changePaintOpacity(true);
};
//--- Scene ------------------------------------------------------------------------
function Scene_QuestLog() {
this.initialize(...arguments);
}
Scene_QuestLog.prototype = Object.create(Scene_MenuBase.prototype);
Scene_QuestLog.prototype.constructor = Scene_QuestLog;
Scene_QuestLog.prototype.create = function() {
Scene_MenuBase.prototype.create.call(this);
this._questWindow = new Window_QuestList({
anchor: "center",
width: "60%", maxWidth: 520,
height: "auto", maxHeight: "90%"
});
this.addWindow(this._questWindow);
};
Scene_QuestLog.prototype.start = function() {
Scene_MenuBase.prototype.start.call(this);
const win = this._questWindow;
MF.Tween.from(win, { y: win.y - 60, contentsOpacity: 0 }, { duration: 15, easing: "easeOutCubic" });
};
Scene_QuestLog.prototype.update = function() {
Scene_MenuBase.prototype.update.call(this);
if (Input.isTriggered("cancel") || Input.isTriggered("questLog") || TouchInput.isCancelled()) {
SoundManager.playCancel();
this.popScene();
}
};
//--- Hotkey ---------------------------------------------------------------------
MF.Input.bind("questLog", { keys: [params.hotkey] });
MF.Hook.after(Scene_Map.prototype, "update", function() {
if (Input.isTriggered("questLog") && $gamePlayer.canMove() && !SceneManager.isSceneChanging()) {
SceneManager.push(Scene_QuestLog);
}
}, PLUGIN_NAME);
})();
Checklist for every MF_* plugin
@base MF_Coreand@orderAfter MF_Corein the header.MF.Core.register(name, version, { requires: { MF_Core: "x.y.z" } })and the same version in@help.- Parameters read through a schema.
- State in
MF.Save(per save) orMF.Config(global settings) — never in module-level variables that are lost on load. - Engine methods extended with
MF.Hook, not copied. - Backslashes doubled in JavaScript strings.
- Skimmed the Pitfalls list.
Next steps
| If you want to… | Read |
|---|---|
| react to switches, variables, items without code | MF.Condition |
| play a cut-scene: move, wait, fade, talk | MF.Tween, MF.Timer, MF.Queue, MF.Text.show / choice |
| add text effects (sound, face change, speed) | MF.Text |
| preload images and sounds with a loading bar | MF.Assets |
| let players change settings that persist | MF.Config |
| build an in-game editor (drag, undo, save) | MF.Document, MF.Input.draggable, MF.Layout |
| load your own JSON data files | MF.Data |
| change your data format later without breaking saves | MF.Save (version, migrate), MF.Migration |