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.

Quick startInstall, check, first plugin — 5 minutes TutorialBuild a quest log step by step — 20 minutes PitfallsThe mistakes everybody makes once — read before shipping API referenceEvery module and function, searchable

1Install

  1. Copy MF_Core.js to js/plugins/ of your project.
  2. Open Tools → Plugin Manager, add MF_Core and move it to the top of the list, above every other MF_* plugin.
  3. 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:

PitfallIn JavaScript strings the backslash must be doubled: "\\C[3]". In the event editor's Show Text you type it once: \C[3]. Details

Tutorial: 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.

StepModuleWhat you learn
1MF.CoreDeclaring dependencies and versions
2MF.SchemaValidated plugin parameters with defaults
3MF.SaveState inside the save file — no DataManager patching
4MF.I18nTranslations with plural forms
5MF.TextA custom text code for messages
6MF.UI, MF.LayoutA window sized in % and "auto", centered by an anchor
7MF.TweenSlide-in animation
8MF.Input, MF.HookA 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.

PitfallStore plain data (objects, arrays, strings, numbers) — not class instances or sprites. Details

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].

PitfallCode names are letters only, and V N P G C I PX PY FS are taken by RPG Maker. Details

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);
};
PitfallMF.UI.Window calls refresh() automatically only after a resize. Call it yourself after creation and whenever the data changes. Details

7The 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).

PitfallDurations are in frames, not milliseconds. Tweens pause when the game window loses focus and are cancelled on scene change. Details

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.

PitfallRPG Maker already uses Q, W, X, Z, arrows, Space, Enter, Esc, Shift… bind refuses to steal them and prints a warning — that is why the default hotkey is J. Details

9Try it

  1. Add MF_QuestLog below MF_Core in the Plugin Manager.
  2. Create an event with:
    • Plugin Command → MF_QuestLog → Start quest, id slimes, title Defeat 3 slimes
    • Show Text: New quest: \QUEST[slimes]!
  3. Playtest, talk to the event, press J. Save, reload — the quest is still there.
You have used 8 modules of MF_Core. Everything else in the reference works the same way: small, independent building blocks.

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

Next steps

If you want to…Read
react to switches, variables, items without codeMF.Condition
play a cut-scene: move, wait, fade, talkMF.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 barMF.Assets
let players change settings that persistMF.Config
build an in-game editor (drag, undo, save)MF.Document, MF.Input.draggable, MF.Layout
load your own JSON data filesMF.Data
change your data format later without breaking savesMF.Save (version, migrate), MF.Migration