Rpgmaker authoring
Skill Redseb/rpgmaker-mz-mcp/.claude/skills/rpgmaker-authoring
How to build a coherent, playable RPG Maker MZ game with the rpgmaker-mz MCP server — the authoring judgment the tools don't enforce. Use when creating or editing a game with the mcp__rpgmaker-mz__* tools: painting maps, placing NPCs/events, wiring transfers/shops/chests/battles, or setting up a custom cast. Covers tile layering (transparent overlays need a base), the solid-landmark + facing-transfer entrance idiom, giving interactables graphics, verifying asset names, the Hit-Rate and Add-Skill-Type traits, the self-switch one-shot pattern, and always playing the build (validators prove structure, not that it looks/plays right). Not for bootstrapping a custom tileset catalog — that's the tileset-catalog skill.From its SKILL.md
npx -y skills add Redseb/rpgmaker-mz-mcp --skill rpgmaker-authoringAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.3 KB, as published. Nobody here has run it
Authoring a playable RPG Maker MZ game
The mcp__rpgmaker-mz__* tools let you build a game, but a build that succeeds and
passes validate_project/validate_references/validate_event can still be a badly
built game — invisible NPCs, terrain with black gaps, entrances you walk into,
battlers that fail to load. This skill is the authoring judgment the tools don't
enforce, distilled from real playtests. Read it before/while building a game.
Rule 0 — validators prove structure, not that it looks/plays right
validate_* check command shape and id integrity. They will not catch: a
transparent tile with nothing beneath it, an NPC with no sprite, a battlerName that
doesn't exist, a transfer placed on the wrong tile, a class that always misses. The
only way to know the game is good is to open it in the RPG Maker editor and play it.
Treat "validators clean" as necessary, never sufficient.
The flip side: the event-writing tools refuse a write whose result is structurally
broken (wrong parameter count for a command code, a list missing its { code: 0 }
terminator, a choice/conditional/loop block left unclosed or missing a When branch, an
action-button event stranded on an impassable tile) — the tool errors
and nothing is saved. That error is a real bug in what you were about to write: fix
the input. force: true writes it anyway and exists for the rare case where the
validator is wrong (an exotic plugin, a deliberate experiment) — reaching for it to
make an error go away just puts the broken data on disk, which is what the refusal
saved you from. Advisory findings (unknown command code, unknown asset name, long text
line) still come back as warnings on a successful write — heed those too.
When you finish a chunk, say what still needs an in-editor look.
Tile layering — transparent overlays need a base (the #1 map mistake)
Each map cell has 6 stacked z-layers in the flat data array
(index = (layer*height + y)*width + x):
| layer | role |
|---|---|
| 0 | lower ground A (opaque base terrain) |
| 1 | lower ground B (overlays) |
| 2 | upper tile A (objects, drawn over the player) |
| 3 | upper tile B |
| 4 | shadow pen |
| 5 | region id |
The tilemap renders bottom-up (0→1→2→3) with per-pixel alpha — upper layers composite over lower ones, and transparent pixels reveal what's beneath.
Many autotiles are overlays with transparent/feathered edges — Overworld forest, roads, mountains, and similar decorative terrain elsewhere. If you paint one directly on layer 0 (the paint default), it replaces the base and its transparent pixels show the black empty background, not ground.
Do this:
- Fill the opaque base terrain (grass, plain dirt, floor) on layer 0:
fill_area(mapId, x, y, w, h, grassId)(layer defaults to 0) - Paint transparent overlays on layer 1, leaving the base intact beneath:
fill_area(mapId, x, y, w, h, forestId, layer: 1)— roads, forest, mountains. - Multi-tile B/C objects (houses, fountains) go on layer 2 via
place_object(its default), so they draw over the ground and the player can walk behind them.
Autotiling still works per-layer: forest on layer 1 borders against forest on layer 1 only (the base grass on layer 0 doesn't affect its shape), so a clump still feathers its edges — and now grass shows through the feathering. Passability resolves upper-first, so the layer-1 overlay decides the cell with the base as fallback.
Heuristic: opaque, full-cell ground → layer 0. Anything decorative that should "sit on" the ground (forest/road/mountain/most named-feature autotiles) → layer 1 over a base. When unsure whether a tile is transparent, assume it is and layer it — or check the sheet's alpha.
Painting maps
- Find tilesets first. There's no tileset-list tool yet — read the project's
data/Tilesets.json: each entry'sid/nameandtilesetNames[9][A1,A2,A3,A4,A5,B,C,D,E](empty string = unused sheet). You need thetilesetIdfor every paint/catalog/flag call. - Painting an existing map? Check ITS
tilesetIdfirst (get_map/check_passabilityreport it). Tile ids are position-based and valid under any tileset, so painting ids you looked up in tileset 3's catalog onto a map still set to tileset 1 succeeds silently and renders the wrong graphics with the wrong passability. The default/pre-existing MAP001 — and everycreate_mapcall that omitstilesetId— is tileset 1 (Overworld). Fix withupdate_map(mapId, { tilesetId })(safe: it doesn't touch tile data), then re-verify passability. - Get tile ids with
find_tile(tilesetId, query)(case-insensitive substring, not synonyms — "water" won't match "Sea"; browse names withget_tile_catalog(tilesetId, sheet)first) orget_tile_catalog. On a custom sheet cataloged by the tileset-catalog skill,searchDescriptions: truealso matches the drafted visual description, which is often more searchable than the name. Autotile entries return the kind's base id — paint it and the shape is computed from neighbours. - Don't resize an existing map.
update_mapchangingwidth/heightdoes not resize the tiledataarray — it corrupts the map. Create the map at the size you need withcreate_map(name, width, height, tilesetId, parentId)instead. - Empty cells are impassable. A cell with no ground tile on any layer resolves to
blocked (the engine's fall-through). Always lay a base ground layer before relying
on a cell being walkable; verify with
check_passability(mapId, x, y).
Entrances, doors & passability — the idiom
Landmarks the player should not walk through (buildings, dungeon mouths, trees, signs) should be solid, and the transfer should trigger from the tile the player faces them from — not from walking onto the landmark.
- Make the landmark solid: paint/
place_objectit, then if the tile isn't already blocked,set_tile_flags(tilesetId, tileId, { passage: { down:false,... } }, applyToAutotileKind)— or check withget_tile_flags/check_passability. - Action-button events only fire from facing when priority is
same. With prioritybelow(the blank-page default), an action-button event fires only when the player is standing on its tile — so on an impassable tile (a!Dooron a wall, an invisible trigger on a solid landmark) it can never fire at all. Any event the player activates by facing it — doors, entrance triggers on solid tiles, signs — needspriorityType: 1/priority: 'same'. (create_npcdefaults tosame;create_map_eventpages default tobelow— set it explicitly.) The write tools refuse this combination on an impassable tile rather than placing a dead event; fix the priority, don'tforcepast it. - Two valid transfer idioms:
- Action-button, facing the landmark: put a
trigger: action_button, prioritysameevent on the solid landmark tile (or the wall the door sits on); the player faces it and presses to enter. Best for buildings/dungeon entrances/doors. - Player-touch doormat: an invisible
trigger: player_touch, prioritybelowtransfer event on a walkable door/threshold tile the player steps onto. Fine for interior exits and map-edge gaps.
- Action-button, facing the landmark: put a
- Avoid a
player_touchtransfer sitting on a decorative marker tile you can walk onto — it works but reads wrong (you walk into the building to leave). - Build the transfer with
build_transfer_player(mapId, x, y, direction, fade)→insert_event_commands(or pass in acreate_map_eventpagelist).
Events & NPCs
- Give interactable NPCs a graphic.
create_npcwith nocharacterName(and no tile graphic) makes an invisible NPC — almost always a mistake for a talker. PasscharacterName(a basename fromlist_assets('characters')) +characterIndex. Invisible is only correct for pure trigger/controller events (auto-run logic, doormat transfers). - MZ does not word-wrap message text. A Show Text line (401) longer than the
window is silently cut off at the right edge. Break the text into short lines
yourself: keep each line to ~55 characters (or ~38 when a face graphic is
shown — the face eats a third of the window), max 4 lines per message box; start
a new Show Text block for longer dialogue. Escape codes (
\C[n],\N[n], …) don't count toward the display width. - One-shot events (chests, defeated bosses, one-time cutscenes) — two-page
self-switch pattern:
- Page 1: does the thing (give item / battle / dialogue), then
build_control_switch(scope:'self_switch', name:'A', value:'on')at the end. - Page 2:
conditions: { selfSwitchValid: true, selfSwitchCh: 'A' }, showing the "done" state (opened chest graphic, or empty for a removed boss).create_map_eventdeep-merges pageconditions, so you only pass the two fields.
- Page 1: does the thing (give item / battle / dialogue), then
- Never pick a switch/variable/common-event ID by hand — ask
next_free_id. These are one global namespace, and reusing an ID some other part of the game already owns fails silently: no crash, no validator hit, just a door that turns out to be already open hours into a playtest.next_free_id(type, count?)hands back IDs nothing has claimed;list_allocated_ids(type)shows what's taken, and with anidshows every place that one is used before you touch it. Then name what you take withset_switch_name/set_variable_name— an unnamed switch is a claim nobody wrote down, and naming it is what makes it visible later. (Self switches are exempt: they're scoped to their own event and can't collide.) - Composition loop: the
build_*tools return{ command }or{ commands }; concatenate them into a pagelist(forcreate_map_event/create_npc.commands) or splice withinsert_event_commands(mapId, eventId, pageIndex, commands).call_common_eventreturns{ command }like thebuild_*tools, so it composes the same way. - Cutscene choreography interleaves
insert_event_commands(dialogue chunks) withset_movement_route(movement) on the same page — both append before the terminator, so build the page in play order. Forcustom/raw routes, the move-route codes you actually need (full table:KNOWN_MOVE_COMMANDSinsrc/validation/moveCommands.ts): 1–4 move down/left/right/up, 12/13 step forward/back, 14 jump[dx,dy], 15 wait[frames], 16–19 turn down/left/right/up, 25/26 turn toward/away from player, 29/30 speed/frequency[n], 37/38 through on/off, 39/40 transparent on/off, 41 change image['SheetName', index], 44 play SE. A wrong code isn't refused (plugins add codes) — it's a warning plus silently-weird choreography, so double-check codes, and setskippable: trueon forced routes so a blocked step can't soft-lock the scene. An off-screen actor entering mid-scene: give the event a graphic-lessthrough: truepage and open its route with41 ['SheetName', index]+38(image on, through off), rather than pre-placing a visible sprite. - Common-event bodies and troop pages take the same insert path, via
target:insert_event_commands(target: "common_event", commonEventId, commands)orinsert_event_commands(target: "troop_page", troopId, pageIndex, commands). (targetdefaults to"map_event", which is why the map form above can omit it.) Building the wholelistup front and passing it tocreate_common_event/create_troopalso works — append a{ code: 0, indent: 0, parameters: [] }terminator, or the write is refused as unterminated.
Database & combat correctness
- Hit Rate trait or everything misses. A custom class and a custom enemy
that should land physical attacks need an xparam-HIT trait —
{ code: 22, dataId: 0, value: 0.95 }— passed intraits. The default templates shiptraits: [], and HIT sums from 0 with no baseline, so a trait-free battler always misses physical actions. (create_class/create_actor/create_enemydescriptions carry this NOTE.) - Add Skill Type trait or learned skills never show. A class's
learningsonly teach the skill — the actor can't see or use it unless the class also has an Add Skill Type trait for that skill'sstypeId:{ code: 41, dataId: <stypeId>, value: 1 }(default DB: 1 = Magic, 2 = Special). Without it there is no Magic/Special command in battle or the menu at all — the symptom is "my characters have no skills" even thoughlearningsare correct. Check each learned skill'sstypeIdinSkills.jsonand cover every distinct one with a code-41 trait (a class mixing Magic + Special skills needs both). - Verify every asset name. A wrong filename is a silent blank sprite and a
runtime load error (
Failed to load img/enemies/Foo.png). Check names againstlist_assets(...)before wiring:battlerName→enemies,characterName→characters,faceName→faces, audio →bgm/bgs/me/se. Several tools warn on unknown names (warn-by-default) — heed the warnings, don't ignore them. - Double-check references the create tools don't validate. Some create tools throw
on a bad id (
add_class_learningskillId,create_troopenemyId,call_common_event), but others silently accept a dangling id (create_state_skillstateId,create_enemyactions/dropItems, skill/itemeffectsdataIds). After building, runvalidate_referencesto catch dangling ids across the whole project. - Set the starting party and position.
set_party({ partyMembers: [...] })(actor ids; note the arg name differs fromget_party's bare array) andupdate_starting_position(mapId, x, y). A game with no party or a start on a blocked tile is broken.
Ship checklist
validate_project(command shape),validate_references(id integrity),validate_eventon the complex events.- Headless smoke test (agent-runnable, no editor needed): serve the project dir
(
python3 -m http.server 8123 -d <project>),npm i playwright-corein a scratch dir, and launch an already-cached Playwright Chromium (~/Library/Caches/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-mac-arm64/chrome-headless-shell, args--use-gl=angle --enable-unsafe-swiftshader) — no browser download. Keyboard events do NOT reach RMMZ headless; drive the engine inpage.evaluateinstead:DataManager.setupNewGame(); SceneManager.goto(Scene_Map)to start,$gamePlayer.reserveTransfer(mapId,x,y,dir,0)to jump maps (it is deferred while a message window is open),$gameMap.event(id).start()to fire an event,Input.virtualClick('ok')to advance text. Collectconsole/pageerroroutput and screenshot each map/cutscene/battle, then look at the images — this catches the rendering/asset problems validators can't (wrong tileset, invisible NPCs, missing battlers). - Open it in the editor and play: every NPC visible? every transfer lands on the right tile, facing the right way? battlers render? attacks actually hit? terrain solid where it should be, no black gaps in forest/roads? entrances feel like entrances? The smoke test doesn't judge audio choice, pacing, or feel — a human pass still matters.
- Report what you verified in-engine vs. what still needs a human look.