rpgmaker-mz-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +425 -0
  3. package/dist/events/commandBuilders.d.ts +405 -0
  4. package/dist/events/commandBuilders.d.ts.map +1 -0
  5. package/dist/events/commandBuilders.js +554 -0
  6. package/dist/events/commandBuilders.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +105 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/registry.d.ts +81 -0
  12. package/dist/registry.d.ts.map +1 -0
  13. package/dist/registry.js +49 -0
  14. package/dist/registry.js.map +1 -0
  15. package/dist/tiles/autotile.d.ts +65 -0
  16. package/dist/tiles/autotile.d.ts.map +1 -0
  17. package/dist/tiles/autotile.js +201 -0
  18. package/dist/tiles/autotile.js.map +1 -0
  19. package/dist/tiles/catalog/dungeon.d.ts +16 -0
  20. package/dist/tiles/catalog/dungeon.d.ts.map +1 -0
  21. package/dist/tiles/catalog/dungeon.js +765 -0
  22. package/dist/tiles/catalog/dungeon.js.map +1 -0
  23. package/dist/tiles/catalog/index.d.ts +78 -0
  24. package/dist/tiles/catalog/index.d.ts.map +1 -0
  25. package/dist/tiles/catalog/index.js +115 -0
  26. package/dist/tiles/catalog/index.js.map +1 -0
  27. package/dist/tiles/catalog/inside.d.ts +16 -0
  28. package/dist/tiles/catalog/inside.d.ts.map +1 -0
  29. package/dist/tiles/catalog/inside.js +765 -0
  30. package/dist/tiles/catalog/inside.js.map +1 -0
  31. package/dist/tiles/catalog/outside.d.ts +16 -0
  32. package/dist/tiles/catalog/outside.d.ts.map +1 -0
  33. package/dist/tiles/catalog/outside.js +799 -0
  34. package/dist/tiles/catalog/outside.js.map +1 -0
  35. package/dist/tiles/catalog/overworld.d.ts +16 -0
  36. package/dist/tiles/catalog/overworld.d.ts.map +1 -0
  37. package/dist/tiles/catalog/overworld.js +585 -0
  38. package/dist/tiles/catalog/overworld.js.map +1 -0
  39. package/dist/tiles/catalog/sf.d.ts +16 -0
  40. package/dist/tiles/catalog/sf.d.ts.map +1 -0
  41. package/dist/tiles/catalog/sf.js +1313 -0
  42. package/dist/tiles/catalog/sf.js.map +1 -0
  43. package/dist/tiles/paint.d.ts +20 -0
  44. package/dist/tiles/paint.d.ts.map +1 -0
  45. package/dist/tiles/paint.js +65 -0
  46. package/dist/tiles/paint.js.map +1 -0
  47. package/dist/tiles/png.d.ts +11 -0
  48. package/dist/tiles/png.d.ts.map +1 -0
  49. package/dist/tiles/png.js +180 -0
  50. package/dist/tiles/png.js.map +1 -0
  51. package/dist/tiles/tileCodec.d.ts +95 -0
  52. package/dist/tiles/tileCodec.d.ts.map +1 -0
  53. package/dist/tiles/tileCodec.js +186 -0
  54. package/dist/tiles/tileCodec.js.map +1 -0
  55. package/dist/tiles/tileFlags.d.ts +98 -0
  56. package/dist/tiles/tileFlags.d.ts.map +1 -0
  57. package/dist/tiles/tileFlags.js +129 -0
  58. package/dist/tiles/tileFlags.js.map +1 -0
  59. package/dist/tiles/tilegeom.d.ts +29 -0
  60. package/dist/tiles/tilegeom.d.ts.map +1 -0
  61. package/dist/tiles/tilegeom.js +111 -0
  62. package/dist/tiles/tilegeom.js.map +1 -0
  63. package/dist/tiles/transparency.d.ts +22 -0
  64. package/dist/tiles/transparency.d.ts.map +1 -0
  65. package/dist/tiles/transparency.js +59 -0
  66. package/dist/tiles/transparency.js.map +1 -0
  67. package/dist/tools/actorTools.d.ts +48 -0
  68. package/dist/tools/actorTools.d.ts.map +1 -0
  69. package/dist/tools/actorTools.js +154 -0
  70. package/dist/tools/actorTools.js.map +1 -0
  71. package/dist/tools/allTools.d.ts +8 -0
  72. package/dist/tools/allTools.d.ts.map +1 -0
  73. package/dist/tools/allTools.js +56 -0
  74. package/dist/tools/allTools.js.map +1 -0
  75. package/dist/tools/assetTools.d.ts +113 -0
  76. package/dist/tools/assetTools.d.ts.map +1 -0
  77. package/dist/tools/assetTools.js +97 -0
  78. package/dist/tools/assetTools.js.map +1 -0
  79. package/dist/tools/batchTools.d.ts +3 -0
  80. package/dist/tools/batchTools.d.ts.map +1 -0
  81. package/dist/tools/batchTools.js +138 -0
  82. package/dist/tools/batchTools.js.map +1 -0
  83. package/dist/tools/battleTools.d.ts +72 -0
  84. package/dist/tools/battleTools.d.ts.map +1 -0
  85. package/dist/tools/battleTools.js +322 -0
  86. package/dist/tools/battleTools.js.map +1 -0
  87. package/dist/tools/catalogTools.d.ts +3 -0
  88. package/dist/tools/catalogTools.d.ts.map +1 -0
  89. package/dist/tools/catalogTools.js +134 -0
  90. package/dist/tools/catalogTools.js.map +1 -0
  91. package/dist/tools/classTools.d.ts +85 -0
  92. package/dist/tools/classTools.d.ts.map +1 -0
  93. package/dist/tools/classTools.js +279 -0
  94. package/dist/tools/classTools.js.map +1 -0
  95. package/dist/tools/commonEventTools.d.ts +32 -0
  96. package/dist/tools/commonEventTools.d.ts.map +1 -0
  97. package/dist/tools/commonEventTools.js +150 -0
  98. package/dist/tools/commonEventTools.js.map +1 -0
  99. package/dist/tools/eventCommandTools.d.ts +37 -0
  100. package/dist/tools/eventCommandTools.d.ts.map +1 -0
  101. package/dist/tools/eventCommandTools.js +893 -0
  102. package/dist/tools/eventCommandTools.js.map +1 -0
  103. package/dist/tools/eventPageTools.d.ts +112 -0
  104. package/dist/tools/eventPageTools.d.ts.map +1 -0
  105. package/dist/tools/eventPageTools.js +597 -0
  106. package/dist/tools/eventPageTools.js.map +1 -0
  107. package/dist/tools/itemTools.d.ts +82 -0
  108. package/dist/tools/itemTools.d.ts.map +1 -0
  109. package/dist/tools/itemTools.js +380 -0
  110. package/dist/tools/itemTools.js.map +1 -0
  111. package/dist/tools/listTools.d.ts +58 -0
  112. package/dist/tools/listTools.d.ts.map +1 -0
  113. package/dist/tools/listTools.js +99 -0
  114. package/dist/tools/listTools.js.map +1 -0
  115. package/dist/tools/mapTools.d.ts +270 -0
  116. package/dist/tools/mapTools.d.ts.map +1 -0
  117. package/dist/tools/mapTools.js +1066 -0
  118. package/dist/tools/mapTools.js.map +1 -0
  119. package/dist/tools/moveTools.d.ts +43 -0
  120. package/dist/tools/moveTools.d.ts.map +1 -0
  121. package/dist/tools/moveTools.js +240 -0
  122. package/dist/tools/moveTools.js.map +1 -0
  123. package/dist/tools/objectTools.d.ts +12 -0
  124. package/dist/tools/objectTools.d.ts.map +1 -0
  125. package/dist/tools/objectTools.js +221 -0
  126. package/dist/tools/objectTools.js.map +1 -0
  127. package/dist/tools/paintTools.d.ts +3 -0
  128. package/dist/tools/paintTools.d.ts.map +1 -0
  129. package/dist/tools/paintTools.js +134 -0
  130. package/dist/tools/paintTools.js.map +1 -0
  131. package/dist/tools/pluginScanTools.d.ts +22 -0
  132. package/dist/tools/pluginScanTools.d.ts.map +1 -0
  133. package/dist/tools/pluginScanTools.js +119 -0
  134. package/dist/tools/pluginScanTools.js.map +1 -0
  135. package/dist/tools/pluginTools.d.ts +11 -0
  136. package/dist/tools/pluginTools.d.ts.map +1 -0
  137. package/dist/tools/pluginTools.js +63 -0
  138. package/dist/tools/pluginTools.js.map +1 -0
  139. package/dist/tools/projectTools.d.ts +11 -0
  140. package/dist/tools/projectTools.d.ts.map +1 -0
  141. package/dist/tools/projectTools.js +74 -0
  142. package/dist/tools/projectTools.js.map +1 -0
  143. package/dist/tools/skillTools.d.ts +83 -0
  144. package/dist/tools/skillTools.d.ts.map +1 -0
  145. package/dist/tools/skillTools.js +369 -0
  146. package/dist/tools/skillTools.js.map +1 -0
  147. package/dist/tools/stateTools.d.ts +28 -0
  148. package/dist/tools/stateTools.d.ts.map +1 -0
  149. package/dist/tools/stateTools.js +161 -0
  150. package/dist/tools/stateTools.js.map +1 -0
  151. package/dist/tools/systemTools.d.ts +86 -0
  152. package/dist/tools/systemTools.d.ts.map +1 -0
  153. package/dist/tools/systemTools.js +342 -0
  154. package/dist/tools/systemTools.js.map +1 -0
  155. package/dist/tools/tileTools.d.ts +9 -0
  156. package/dist/tools/tileTools.d.ts.map +1 -0
  157. package/dist/tools/tileTools.js +34 -0
  158. package/dist/tools/tileTools.js.map +1 -0
  159. package/dist/tools/tileTransparency.d.ts +40 -0
  160. package/dist/tools/tileTransparency.d.ts.map +1 -0
  161. package/dist/tools/tileTransparency.js +156 -0
  162. package/dist/tools/tileTransparency.js.map +1 -0
  163. package/dist/tools/tilesetTools.d.ts +23 -0
  164. package/dist/tools/tilesetTools.d.ts.map +1 -0
  165. package/dist/tools/tilesetTools.js +209 -0
  166. package/dist/tools/tilesetTools.js.map +1 -0
  167. package/dist/tools/validationTools.d.ts +50 -0
  168. package/dist/tools/validationTools.d.ts.map +1 -0
  169. package/dist/tools/validationTools.js +229 -0
  170. package/dist/tools/validationTools.js.map +1 -0
  171. package/dist/utils/commit.d.ts +62 -0
  172. package/dist/utils/commit.d.ts.map +1 -0
  173. package/dist/utils/commit.js +119 -0
  174. package/dist/utils/commit.js.map +1 -0
  175. package/dist/utils/fileHandler.d.ts +49 -0
  176. package/dist/utils/fileHandler.d.ts.map +1 -0
  177. package/dist/utils/fileHandler.js +137 -0
  178. package/dist/utils/fileHandler.js.map +1 -0
  179. package/dist/utils/records.d.ts +8 -0
  180. package/dist/utils/records.d.ts.map +1 -0
  181. package/dist/utils/records.js +10 -0
  182. package/dist/utils/records.js.map +1 -0
  183. package/dist/utils/types.d.ts +465 -0
  184. package/dist/utils/types.d.ts.map +1 -0
  185. package/dist/utils/types.js +5 -0
  186. package/dist/utils/types.js.map +1 -0
  187. package/dist/validation/assets.d.ts +53 -0
  188. package/dist/validation/assets.d.ts.map +1 -0
  189. package/dist/validation/assets.js +198 -0
  190. package/dist/validation/assets.js.map +1 -0
  191. package/dist/validation/createRefs.d.ts +37 -0
  192. package/dist/validation/createRefs.d.ts.map +1 -0
  193. package/dist/validation/createRefs.js +89 -0
  194. package/dist/validation/createRefs.js.map +1 -0
  195. package/dist/validation/eventCommands.d.ts +70 -0
  196. package/dist/validation/eventCommands.d.ts.map +1 -0
  197. package/dist/validation/eventCommands.js +273 -0
  198. package/dist/validation/eventCommands.js.map +1 -0
  199. package/dist/validation/gate.d.ts +56 -0
  200. package/dist/validation/gate.d.ts.map +1 -0
  201. package/dist/validation/gate.js +50 -0
  202. package/dist/validation/gate.js.map +1 -0
  203. package/dist/validation/moveCommands.d.ts +31 -0
  204. package/dist/validation/moveCommands.d.ts.map +1 -0
  205. package/dist/validation/moveCommands.js +123 -0
  206. package/dist/validation/moveCommands.js.map +1 -0
  207. package/dist/validation/pluginCommands.d.ts +83 -0
  208. package/dist/validation/pluginCommands.d.ts.map +1 -0
  209. package/dist/validation/pluginCommands.js +134 -0
  210. package/dist/validation/pluginCommands.js.map +1 -0
  211. package/dist/validation/pluginManifest.d.ts +67 -0
  212. package/dist/validation/pluginManifest.d.ts.map +1 -0
  213. package/dist/validation/pluginManifest.js +199 -0
  214. package/dist/validation/pluginManifest.js.map +1 -0
  215. package/dist/validation/references.d.ts +57 -0
  216. package/dist/validation/references.d.ts.map +1 -0
  217. package/dist/validation/references.js +314 -0
  218. package/dist/validation/references.js.map +1 -0
  219. package/package.json +67 -0
@@ -0,0 +1,1066 @@
1
+ import { z } from 'zod';
2
+ import { readJsonFile, readJsonArraySoft, getMapPath, getDataPath, fileExists, } from '../utils/fileHandler.js';
3
+ import { commitChange, commitDelete } from '../utils/commit.js';
4
+ import { validateEvent } from '../validation/eventCommands.js';
5
+ import { writeGate } from '../validation/gate.js';
6
+ import { spliceIntoList } from '../events/commandBuilders.js';
7
+ import { layeredPassability } from '../tiles/tileFlags.js';
8
+ import { refExists } from '../validation/references.js';
9
+ /** Dimensions the RPG Maker MZ editor defaults to when creating a new map. */
10
+ const DEFAULT_MAP_WIDTH = 17;
11
+ const DEFAULT_MAP_HEIGHT = 13;
12
+ /**
13
+ * The pre-commit gate every event-writing tool installs: validate the event that
14
+ * is *about* to be written, refuse the write outright if it is structurally
15
+ * malformed (unless `force`), and keep any advisory findings for the response.
16
+ *
17
+ * Reachability is folded in here rather than checked by the caller afterwards
18
+ * because it, too, is block-worthy — an action-button page that can never fire
19
+ * is a dead event, not a style note.
20
+ */
21
+ export function eventWriteGate(projectPath, mapId, force, extra = []) {
22
+ return writeGate(force, `event on map ${mapId}`, async (event) => [
23
+ ...extra,
24
+ ...validateEvent(event).warnings,
25
+ ...(await actionButtonReachabilityWarnings(projectPath, mapId, event)),
26
+ ]);
27
+ }
28
+ /**
29
+ * Trim a freshly-created event down to a confirmation summary for a tool
30
+ * response. The full event echoes every default field of every page (hundreds
31
+ * of tokens even for a bare NPC), yet the caller already knows what it sent and
32
+ * can re-read specifics with get_map_event — so return just id/name/position and
33
+ * a page count. Validation warnings are still computed from the *full* event by
34
+ * the caller before the event is summarized.
35
+ */
36
+ export function summarizeCreatedEvent(event) {
37
+ return {
38
+ id: event.id,
39
+ name: event.name,
40
+ x: event.x,
41
+ y: event.y,
42
+ pageCount: event.pages.length,
43
+ };
44
+ }
45
+ /**
46
+ * Flag an event with an action-button page drawn with priority "below
47
+ * characters" (0) while the event's tile is impassable. Such a page fires only
48
+ * when the player STANDS ON the tile — impossible on a blocked cell — so the
49
+ * event (a !Door on a wall, an entrance trigger on a solid landmark) can never
50
+ * fire at all. Priority "same as characters" (1) triggers from facing, which is
51
+ * what these events want.
52
+ *
53
+ * Structural (`severity: 'error'`): the event is dead on arrival, which is
54
+ * essentially never intended, so the write is refused unless forced. Still fails
55
+ * soft on any *read* problem (e.g. a bare fixture with no tileset) — an
56
+ * unanswerable check must never block.
57
+ */
58
+ export async function actionButtonReachabilityWarnings(projectPath, mapId, event) {
59
+ const affected = (event.pages ?? []).some((p) => p && p.trigger === 0 && p.priorityType === 0 && (p.list?.length ?? 0) > 1);
60
+ if (!affected)
61
+ return [];
62
+ try {
63
+ const map = await getMap(projectPath, mapId);
64
+ const tilesets = await readJsonFile(getDataPath(projectPath, 'Tilesets.json'));
65
+ const tileset = tilesets.find((t) => t && t.id === map.tilesetId);
66
+ if (!tileset)
67
+ return [];
68
+ const stackFlags = [];
69
+ for (let z = 3; z >= 0; z--) {
70
+ const tileId = map.data[tileIndex(map.width, map.height, event.x, event.y, z)] || 0;
71
+ stackFlags.push(tileset.flags[tileId] ?? 0);
72
+ }
73
+ const passable = layeredPassability(stackFlags);
74
+ if (passable.down || passable.left || passable.right || passable.up)
75
+ return [];
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ return [
81
+ {
82
+ path: 'pages',
83
+ severity: 'error',
84
+ message: `action-button page with priority "below characters" sits on an impassable tile (${event.x}, ${event.y}) — it only fires when the player stands on it, which is impossible there, so it can never trigger; use priorityType 1 (same as characters) so it fires from facing (doors, entrances, signs)`,
85
+ },
86
+ ];
87
+ }
88
+ /**
89
+ * Get map data by ID
90
+ */
91
+ export async function getMap(projectPath, mapId) {
92
+ const mapPath = getMapPath(projectPath, mapId);
93
+ try {
94
+ return await readJsonFile(mapPath);
95
+ }
96
+ catch (error) {
97
+ if (error.code === 'ENOENT' || /ENOENT/.test(String(error))) {
98
+ throw new Error(`Map ${mapId} does not exist`);
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+ /**
104
+ * Get all map info
105
+ */
106
+ export async function getMapInfos(projectPath) {
107
+ const mapInfosPath = getDataPath(projectPath, 'MapInfos.json');
108
+ return await readJsonFile(mapInfosPath);
109
+ }
110
+ /**
111
+ * Build a blank MapData mirroring what the RPG Maker MZ editor writes for a
112
+ * freshly-created map: every tile zeroed (unpainted) across all 6 layers and no
113
+ * events. Field order matches the editor's own output so round-tripped files
114
+ * stay tidy. Kept pure (no I/O) so the template shape can be unit-tested.
115
+ */
116
+ export function blankMapData(width, height, tilesetId) {
117
+ return {
118
+ autoplayBgm: false,
119
+ autoplayBgs: false,
120
+ battleback1Name: '',
121
+ battleback2Name: '',
122
+ bgm: { name: '', pan: 0, pitch: 100, volume: 90 },
123
+ bgs: { name: '', pan: 0, pitch: 100, volume: 90 },
124
+ disableDashing: false,
125
+ displayName: '',
126
+ encounterList: [],
127
+ encounterStep: 30,
128
+ height,
129
+ note: '',
130
+ parallaxLoopX: false,
131
+ parallaxLoopY: false,
132
+ parallaxName: '',
133
+ parallaxShow: true,
134
+ parallaxSx: 0,
135
+ parallaxSy: 0,
136
+ scrollType: 0,
137
+ specifyBattleback: false,
138
+ tilesetId,
139
+ width,
140
+ data: new Array(width * height * 6).fill(0),
141
+ events: [],
142
+ };
143
+ }
144
+ /**
145
+ * Create a new map: write a fresh `data/MapNNN.json` and register it in the map
146
+ * tree (`data/MapInfos.json`). Allocates the next unused map id, appends a tree
147
+ * entry, and lays down a blank (all-tiles-zeroed) map of the given size.
148
+ *
149
+ * Both files are written through the commit choke point, so a dry-run previews
150
+ * the new map file *and* the MapInfos entry together. Does not touch
151
+ * `System.json`: the editor's map tree is driven entirely by MapInfos, so a new
152
+ * entry is enough for the editor to see the map (`System.editMapId` only tracks
153
+ * which map is open on launch).
154
+ */
155
+ export async function createMap(projectPath, options) {
156
+ const width = options.width ?? DEFAULT_MAP_WIDTH;
157
+ const height = options.height ?? DEFAULT_MAP_HEIGHT;
158
+ const tilesetId = options.tilesetId ?? 1;
159
+ const parentId = options.parentId ?? 0;
160
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
161
+ throw new Error(`Map dimensions must be positive integers (got ${width}x${height})`);
162
+ }
163
+ const infos = await getMapInfos(projectPath);
164
+ // A non-root parent must point at an existing map, or the map is orphaned in
165
+ // the tree.
166
+ if (parentId !== 0 && !infos[parentId]) {
167
+ throw new Error(`parentId ${parentId} does not match any existing map`);
168
+ }
169
+ // A bad tilesetId renders as a black map at runtime; validate it against
170
+ // Tilesets.json (loaded fail-soft — skip the check if the file is missing).
171
+ const tilesets = await readJsonArraySoft(getDataPath(projectPath, 'Tilesets.json'));
172
+ if (tilesets.length > 0 && !tilesets[tilesetId]) {
173
+ throw new Error(`tilesetId ${tilesetId} does not match any existing tileset`);
174
+ }
175
+ // Allocate a fresh id: one past the highest existing map id. MapInfos is a
176
+ // 1-indexed array whose slot 0 is null.
177
+ const maxId = infos.reduce((max, info) => (info && info.id > max ? info.id : max), 0);
178
+ const newId = maxId + 1;
179
+ // Guard: never clobber an existing MapNNN.json. With correct id allocation the
180
+ // file should be genuinely unused; a collision means the tree and the files on
181
+ // disk are out of sync, which we surface rather than silently overwrite.
182
+ const mapPath = getMapPath(projectPath, newId);
183
+ if (await fileExists(mapPath)) {
184
+ throw new Error(`Map file for id ${newId} already exists though the id is unused in MapInfos — refusing to overwrite (${mapPath})`);
185
+ }
186
+ const maxOrder = infos.reduce((max, info) => (info && info.order > max ? info.order : max), 0);
187
+ const mapInfo = {
188
+ id: newId,
189
+ expanded: false,
190
+ name: options.name,
191
+ order: maxOrder + 1,
192
+ parentId,
193
+ scrollX: 0,
194
+ scrollY: 0,
195
+ };
196
+ infos[newId] = mapInfo;
197
+ const map = blankMapData(width, height, tilesetId);
198
+ // Write the map file first, then register it in the tree — both through the
199
+ // commit choke point so dry-run previews both writes.
200
+ await commitChange(mapPath, map);
201
+ await commitChange(getDataPath(projectPath, 'MapInfos.json'), infos);
202
+ return { mapId: newId, mapInfo, map };
203
+ }
204
+ /**
205
+ * Delete a map: remove its entry from the map tree (`data/MapInfos.json`) and
206
+ * delete its `data/MapNNN.json` file. Any child maps are **reparented** to the
207
+ * deleted map's own parent rather than deleted with it — so a whole town/dungeon
208
+ * sub-tree isn't wiped by removing one node. Both the tree rewrite and the file
209
+ * deletion go through the commit choke point, so a dry-run previews the tree
210
+ * change and the file removal together.
211
+ *
212
+ * Does not touch `System.json`; if the deleted map happens to be the editor's
213
+ * open/start map that reference is left dangling (the editor tolerates it), same
214
+ * hands-off stance `create_map` takes toward `System.json`.
215
+ */
216
+ export async function deleteMap(projectPath, mapId) {
217
+ const infos = await getMapInfos(projectPath);
218
+ const target = infos[mapId];
219
+ if (!target) {
220
+ throw new Error(`Map ${mapId} does not exist in the map tree`);
221
+ }
222
+ // Warn (don't block) on references that will dangle once the map is gone: the
223
+ // game start map and any direct Transfer Player (201) that targets it. Deletion
224
+ // still proceeds — this mirrors the documented hands-off stance, just surfaced.
225
+ const warnings = await danglingMapReferences(projectPath, mapId, infos);
226
+ // Reparent the deleted map's direct children onto its parent so they aren't
227
+ // orphaned (parentId pointing at a now-gone map).
228
+ const reparentedTo = target.parentId;
229
+ const reparentedChildren = [];
230
+ for (const info of infos) {
231
+ if (info && info.parentId === mapId) {
232
+ info.parentId = reparentedTo;
233
+ reparentedChildren.push(info.id);
234
+ }
235
+ }
236
+ // Drop the tree entry, preserving the 1-indexed array shape (slot -> null).
237
+ infos[mapId] = null;
238
+ // Rewrite the tree, then delete the map file — both via the commit choke
239
+ // point so dry-run previews both operations.
240
+ await commitChange(getDataPath(projectPath, 'MapInfos.json'), infos);
241
+ await commitDelete(getMapPath(projectPath, mapId));
242
+ return {
243
+ mapId,
244
+ reparentedTo,
245
+ reparentedChildren,
246
+ ...(warnings.length ? { warnings } : {}),
247
+ };
248
+ }
249
+ /**
250
+ * Collect references that would dangle after `mapId` is deleted: the game start
251
+ * map (`System.startMapId`), and any *direct* Transfer Player (201) command —
252
+ * across every other map's events, common events, and troop pages — that targets
253
+ * it. Variable-designated transfers can't be resolved statically, so they're
254
+ * skipped. Read-only; each file loads fail-soft so a partial project can't break
255
+ * the delete.
256
+ */
257
+ async function danglingMapReferences(projectPath, mapId, infos) {
258
+ const warnings = [];
259
+ try {
260
+ const sys = await readJsonFile(getDataPath(projectPath, 'System.json'));
261
+ if (sys.startMapId === mapId) {
262
+ warnings.push(`map ${mapId} is the game start map (System.startMapId) — update it after deleting`);
263
+ }
264
+ }
265
+ catch {
266
+ // System.json missing/unreadable — skip the start-map check.
267
+ }
268
+ const scanList = (list, where) => {
269
+ if (!Array.isArray(list))
270
+ return;
271
+ list.forEach((cmd, i) => {
272
+ if (!cmd || !Array.isArray(cmd.parameters))
273
+ return;
274
+ if (cmd.code === 201 && cmd.parameters[0] === 0 && cmd.parameters[1] === mapId) {
275
+ warnings.push(`Transfer Player at ${where} / command ${i} targets map ${mapId}`);
276
+ }
277
+ });
278
+ };
279
+ for (const info of infos) {
280
+ if (!info || info.id === mapId)
281
+ continue;
282
+ let map;
283
+ try {
284
+ map = await readJsonFile(getMapPath(projectPath, info.id));
285
+ }
286
+ catch {
287
+ continue;
288
+ }
289
+ for (const event of map.events ?? []) {
290
+ if (!event || !Array.isArray(event.pages))
291
+ continue;
292
+ event.pages.forEach((page, pi) => scanList(page?.list, `map ${info.id} / event ${event.id} / page ${pi}`));
293
+ }
294
+ }
295
+ const commonEvents = await readJsonArraySoft(getDataPath(projectPath, 'CommonEvents.json'));
296
+ commonEvents.forEach((ce) => {
297
+ if (ce)
298
+ scanList(ce.list, `common event ${ce.id}`);
299
+ });
300
+ const troops = await readJsonArraySoft(getDataPath(projectPath, 'Troops.json'));
301
+ troops.forEach((troop) => {
302
+ if (!troop || !Array.isArray(troop.pages))
303
+ return;
304
+ troop.pages.forEach((page, pi) => scanList(page?.list, `troop ${troop.id} / page ${pi}`));
305
+ });
306
+ return warnings;
307
+ }
308
+ /**
309
+ * Walk each map's parent chain to the root, throwing if a cycle is found (a map
310
+ * that is transitively its own ancestor). Catches self-parenting and longer
311
+ * loops that would otherwise make the editor's tree render infinitely.
312
+ */
313
+ function assertNoTreeCycles(infos) {
314
+ for (const start of infos) {
315
+ if (!start)
316
+ continue;
317
+ const seen = new Set([start.id]);
318
+ let parentId = start.parentId;
319
+ while (parentId !== 0) {
320
+ if (seen.has(parentId)) {
321
+ throw new Error(`Map tree cycle detected involving map ${start.id}`);
322
+ }
323
+ seen.add(parentId);
324
+ const parent = infos[parentId];
325
+ if (!parent)
326
+ break;
327
+ parentId = parent.parentId;
328
+ }
329
+ }
330
+ }
331
+ /**
332
+ * Edit the map tree (`data/MapInfos.json`) without touching any map's tiles or
333
+ * events: reparent (move under a different node / to the top level), reorder
334
+ * siblings, rename, or expand/collapse. Applies a batch of per-map updates, then
335
+ * validates the whole tree stays acyclic before committing. Every referenced map
336
+ * (and any non-zero `parentId`) must already exist.
337
+ */
338
+ export async function updateMapTree(projectPath, updates) {
339
+ const infos = await getMapInfos(projectPath);
340
+ // Validate every target exists before mutating anything, so a bad update in
341
+ // the batch can't leave a partially-applied tree.
342
+ for (const update of updates) {
343
+ if (!infos[update.mapId]) {
344
+ throw new Error(`Map ${update.mapId} does not exist in the map tree`);
345
+ }
346
+ if (update.parentId !== undefined) {
347
+ if (update.parentId === update.mapId) {
348
+ throw new Error(`Map ${update.mapId} cannot be its own parent`);
349
+ }
350
+ if (update.parentId !== 0 && !infos[update.parentId]) {
351
+ throw new Error(`parentId ${update.parentId} does not match any existing map`);
352
+ }
353
+ }
354
+ }
355
+ const updated = [];
356
+ for (const update of updates) {
357
+ const info = infos[update.mapId];
358
+ if (update.parentId !== undefined)
359
+ info.parentId = update.parentId;
360
+ if (update.order !== undefined)
361
+ info.order = update.order;
362
+ if (update.name !== undefined)
363
+ info.name = update.name;
364
+ if (update.expanded !== undefined)
365
+ info.expanded = update.expanded;
366
+ updated.push(info);
367
+ }
368
+ // A reparent can introduce a cycle (A under B, B under A); reject before write.
369
+ assertNoTreeCycles(infos);
370
+ await commitChange(getDataPath(projectPath, 'MapInfos.json'), infos);
371
+ return { updated };
372
+ }
373
+ /**
374
+ * Update map properties. Refuses a width/height change: those would desync the
375
+ * flat `data` tile array (sized `width*height*6`) without repadding it. Use
376
+ * {@link resizeMap} for that.
377
+ */
378
+ export async function updateMap(projectPath, mapId, updates) {
379
+ const map = await getMap(projectPath, mapId);
380
+ // A width/height change here would NOT resize the tile `data` array, silently
381
+ // desyncing the grid — the engine reads tiles by (layer*height + y)*width + x,
382
+ // so a mismatched width/height reads garbage or out of bounds. Reject it (so
383
+ // it can't half-apply) and point at resize_map, which repads every z-layer.
384
+ if ((updates.width !== undefined && updates.width !== map.width) ||
385
+ (updates.height !== undefined && updates.height !== map.height)) {
386
+ throw new Error('update_map cannot change a map width/height (it does not resize the tile data array). Use resize_map instead.');
387
+ }
388
+ // `data` and `events` bypass the dedicated tools' invariants: a wrong-length
389
+ // `data` array desyncs the grid (the exact thing this tool guards width/height
390
+ // against), and a raw `events` array skips event validation and id conventions.
391
+ if (updates.data !== undefined) {
392
+ throw new Error('update_map cannot set the tile data array. Use paint_tiles/fill_area/set_map_tile (or resize_map to change dimensions).');
393
+ }
394
+ if (updates.events !== undefined) {
395
+ throw new Error('update_map cannot set the events array. Use create_map_event/update_map_event/delete_map_event.');
396
+ }
397
+ const updatedMap = { ...map, ...updates };
398
+ const mapPath = getMapPath(projectPath, mapId);
399
+ await commitChange(mapPath, updatedMap);
400
+ return updatedMap;
401
+ }
402
+ /**
403
+ * Rebuild a map's flat tile `data` array for new dimensions, preserving each
404
+ * z-layer's tiles where the old and new grids overlap and zero-filling any
405
+ * newly-exposed cells (cropping when a dimension shrinks). RPG Maker MZ packs
406
+ * the 6 z-layers contiguously into one `width*height*6` array indexed by
407
+ * `(layer*height + y)*width + x`, so a plain width/height swap misaligns every
408
+ * layer — this copies cell-by-cell into a freshly-sized array. Pure (no I/O) so
409
+ * the repadding math can be unit-tested.
410
+ */
411
+ export function resizeMapData(oldData, oldWidth, oldHeight, newWidth, newHeight) {
412
+ const LAYERS = 6;
413
+ const newData = new Array(newWidth * newHeight * LAYERS).fill(0);
414
+ const copyWidth = Math.min(oldWidth, newWidth);
415
+ const copyHeight = Math.min(oldHeight, newHeight);
416
+ for (let layer = 0; layer < LAYERS; layer++) {
417
+ for (let y = 0; y < copyHeight; y++) {
418
+ for (let x = 0; x < copyWidth; x++) {
419
+ const oldIndex = (layer * oldHeight + y) * oldWidth + x;
420
+ const newIndex = (layer * newHeight + y) * newWidth + x;
421
+ newData[newIndex] = oldData[oldIndex];
422
+ }
423
+ }
424
+ }
425
+ return newData;
426
+ }
427
+ /**
428
+ * Resize an existing map to new width/height, repadding every z-layer of its
429
+ * tile `data` array (see {@link resizeMapData}) so the grid stays in sync — the
430
+ * safe path `update_map` refuses. Existing tiles are kept where the grids
431
+ * overlap; shrinking crops the excess. Warns (warn-by-default, never blocks)
432
+ * about any event now left outside the new bounds so the caller can move or
433
+ * remove it. Writes through the commit choke point (dry-run/diff aware).
434
+ */
435
+ export async function resizeMap(projectPath, mapId, width, height) {
436
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
437
+ throw new Error(`Map dimensions must be positive integers (got ${width}x${height})`);
438
+ }
439
+ const map = await getMap(projectPath, mapId);
440
+ const previousWidth = map.width;
441
+ const previousHeight = map.height;
442
+ map.data = resizeMapData(map.data, previousWidth, previousHeight, width, height);
443
+ map.width = width;
444
+ map.height = height;
445
+ // Shrinking can leave events outside the new grid. They aren't deleted (the
446
+ // caller may want to reposition them), just flagged.
447
+ const warnings = [];
448
+ for (const event of map.events) {
449
+ if (event && (event.x >= width || event.y >= height)) {
450
+ warnings.push(`Event ${event.id} "${event.name}" at (${event.x},${event.y}) is now outside the ${width}x${height} map bounds`);
451
+ }
452
+ }
453
+ // If this is the game's start map, shrinking can push the player's starting
454
+ // position off the grid — the party would spawn out of bounds. Read System.json
455
+ // fail-soft (a bare fixture may not have startX/Y); never throw over it.
456
+ try {
457
+ const sys = await readJsonFile(getDataPath(projectPath, 'System.json'));
458
+ if (sys.startMapId === mapId && ((sys.startX ?? 0) >= width || (sys.startY ?? 0) >= height)) {
459
+ warnings.push(`Start position (${sys.startX ?? 0},${sys.startY ?? 0}) on this start map is now outside the ${width}x${height} map bounds — update it with update_starting_position`);
460
+ }
461
+ }
462
+ catch {
463
+ // System.json missing/unreadable — skip the start-position check.
464
+ }
465
+ const mapPath = getMapPath(projectPath, mapId);
466
+ await commitChange(mapPath, map);
467
+ const result = { mapId, width, height, previousWidth, previousHeight };
468
+ return warnings.length > 0 ? { ...result, warnings } : result;
469
+ }
470
+ /**
471
+ * Set a map's random-encounter list (and optionally its `encounterStep`, the
472
+ * average number of steps between encounters). Replaces `encounterList` wholesale
473
+ * with the given troops — each entry is `{ troopId, weight?, regionSet? }` where
474
+ * `weight` biases the random pick (editor default 5) and `regionSet` restricts the
475
+ * encounter to those map region ids (empty = anywhere). Every `troopId` is
476
+ * validated to exist in Troops.json (throws otherwise, matching create_troop's
477
+ * enemy check), so a bad troop can't sail through the way it does via update_map's
478
+ * raw field-poking. Writes through the commit choke point (dry-run/diff aware).
479
+ */
480
+ export async function setEncounters(projectPath, mapId, encounters, encounterStep) {
481
+ const troops = await readJsonArraySoft(getDataPath(projectPath, 'Troops.json'));
482
+ const encounterList = encounters.map((e, i) => {
483
+ // Skip the check only when Troops.json couldn't be loaded at all (fail-soft),
484
+ // so a real project always validates the troopId.
485
+ if (troops.length > 0 && !refExists(troops, e.troopId)) {
486
+ throw new Error(`Encounter ${i} references troopId ${e.troopId}, which does not exist`);
487
+ }
488
+ return {
489
+ troopId: e.troopId,
490
+ weight: e.weight ?? 5,
491
+ regionSet: e.regionSet ?? [],
492
+ };
493
+ });
494
+ const map = await getMap(projectPath, mapId);
495
+ map.encounterList = encounterList;
496
+ if (encounterStep !== undefined)
497
+ map.encounterStep = encounterStep;
498
+ await commitChange(getMapPath(projectPath, mapId), map);
499
+ return { mapId, encounterList, encounterStep: map.encounterStep };
500
+ }
501
+ /**
502
+ * Get events from a specific map
503
+ */
504
+ export async function getMapEvents(projectPath, mapId) {
505
+ const map = await getMap(projectPath, mapId);
506
+ return map.events;
507
+ }
508
+ /**
509
+ * Get a specific event from a map
510
+ */
511
+ export async function getMapEvent(projectPath, mapId, eventId) {
512
+ const events = await getMapEvents(projectPath, mapId);
513
+ return events[eventId] || null;
514
+ }
515
+ /**
516
+ * Update a map event
517
+ */
518
+ export async function updateMapEvent(projectPath, mapId, eventId, updates, precommit) {
519
+ const map = await getMap(projectPath, mapId);
520
+ if (!map.events[eventId]) {
521
+ throw new Error(`Event ${eventId} not found on map ${mapId}`);
522
+ }
523
+ // Normalize any supplied pages onto a blank page (same as create_map_event):
524
+ // a partial page taken raw would be missing engine-required fields and crash
525
+ // on load (the class of bug Phase 6.1 fixed for actors).
526
+ const normalized = { ...updates };
527
+ if (updates.pages !== undefined) {
528
+ normalized.pages = updates.pages.map((page) => normalizeEventPage(page));
529
+ }
530
+ const nextX = normalized.x ?? map.events[eventId].x;
531
+ const nextY = normalized.y ?? map.events[eventId].y;
532
+ if (nextX < 0 || nextX >= map.width || nextY < 0 || nextY >= map.height) {
533
+ throw new Error(`Event position (${nextX}, ${nextY}) is out of map bounds`);
534
+ }
535
+ map.events[eventId] = { ...map.events[eventId], ...normalized, id: eventId };
536
+ await precommit?.(map.events[eventId]);
537
+ const mapPath = getMapPath(projectPath, mapId);
538
+ await commitChange(mapPath, map);
539
+ return map.events[eventId];
540
+ }
541
+ /**
542
+ * Build a blank event page mirroring what the RPG Maker MZ editor writes for a
543
+ * freshly-created event: no graphic, an empty (code-0-terminated) command list,
544
+ * action-button trigger, priority below characters, and default movement. Field
545
+ * values verified against the editor's own output. Pure so the template can be
546
+ * unit-tested and reused (by `create_npc` and `normalizeEventPage`).
547
+ */
548
+ export function blankEventPage() {
549
+ return {
550
+ conditions: {
551
+ actorId: 1,
552
+ actorValid: false,
553
+ itemId: 1,
554
+ itemValid: false,
555
+ selfSwitchCh: 'A',
556
+ selfSwitchValid: false,
557
+ switch1Id: 1,
558
+ switch1Valid: false,
559
+ switch2Id: 1,
560
+ switch2Valid: false,
561
+ variableId: 1,
562
+ variableValid: false,
563
+ variableValue: 0,
564
+ },
565
+ directionFix: false,
566
+ image: { characterName: '', characterIndex: 0, direction: 2, pattern: 0, tileId: 0 },
567
+ list: [{ code: 0, indent: 0, parameters: [] }],
568
+ moveFrequency: 3,
569
+ moveRoute: { list: [{ code: 0, parameters: [] }], repeat: true, skippable: false, wait: false },
570
+ moveSpeed: 3,
571
+ moveType: 0,
572
+ priorityType: 0,
573
+ stepAnime: false,
574
+ through: false,
575
+ trigger: 0,
576
+ walkAnime: true,
577
+ };
578
+ }
579
+ /**
580
+ * Merge a partial event page onto a blank page so a caller can specify only the
581
+ * fields that differ from the editor's "New Event" default. Top-level fields
582
+ * overwrite; the nested `image` and `conditions` objects deep-merge (so passing
583
+ * just `image.characterName` keeps the other graphic defaults). `list` and
584
+ * `moveRoute`, when omitted, fall back to the blank page's (a valid code-0
585
+ * terminated list / route). A page passed in full round-trips unchanged, so this
586
+ * is safe for callers like `create_npc` that already build a complete page.
587
+ */
588
+ export function normalizeEventPage(partial) {
589
+ const blank = blankEventPage();
590
+ return {
591
+ ...blank,
592
+ ...partial,
593
+ conditions: { ...blank.conditions, ...(partial.conditions ?? {}) },
594
+ image: { ...blank.image, ...(partial.image ?? {}) },
595
+ list: partial.list ?? blank.list,
596
+ moveRoute: partial.moveRoute ?? blank.moveRoute,
597
+ };
598
+ }
599
+ /**
600
+ * Create a new event on a map. Each supplied page is merged onto a blank page
601
+ * (see {@link normalizeEventPage}), so a caller can pass a partial page — only the
602
+ * fields that differ from the editor's "New Event" default — without needing the
603
+ * full RPG Maker page schema. An event with no pages gets one blank page.
604
+ */
605
+ export async function createMapEvent(projectPath, mapId, eventData, precommit) {
606
+ const map = await getMap(projectPath, mapId);
607
+ // Find the next available event ID
608
+ const maxId = map.events.reduce((max, event, index) => {
609
+ return event && index > max ? index : max;
610
+ }, 0);
611
+ const suppliedPages = eventData.pages ?? [];
612
+ const pages = suppliedPages.length ? suppliedPages.map(normalizeEventPage) : [blankEventPage()];
613
+ // Spread first so the computed id always wins, even if a caller passes one.
614
+ const newEvent = {
615
+ ...eventData,
616
+ pages,
617
+ id: maxId + 1,
618
+ };
619
+ map.events[maxId + 1] = newEvent;
620
+ await precommit?.(newEvent);
621
+ const mapPath = getMapPath(projectPath, mapId);
622
+ await commitChange(mapPath, map);
623
+ return newEvent;
624
+ }
625
+ /**
626
+ * Scan a map's remaining events for command parameters that reference the given
627
+ * (about-to-be-deleted) event id, so the caller can be warned about pages that
628
+ * will point at a now-gone event. Covers the commands whose parameter carries an
629
+ * event id: Set Event Location (203), Show Animation (212), Show Balloon (213),
630
+ * and forced Set Movement Route (205). For 212/213/205 the character id follows
631
+ * the -1 player / 0 this event / N event id convention, so only a value === the
632
+ * deleted id **and > 0** is a real reference. Pure (no I/O); warn-only.
633
+ */
634
+ export function eventReferenceWarnings(events, deletedId) {
635
+ const warnings = [];
636
+ for (const event of events) {
637
+ if (!event || event.id === deletedId || !Array.isArray(event.pages))
638
+ continue;
639
+ event.pages.forEach((page, pi) => {
640
+ const list = page?.list;
641
+ if (!Array.isArray(list))
642
+ return;
643
+ for (const cmd of list) {
644
+ if (!cmd || !Array.isArray(cmd.parameters))
645
+ continue;
646
+ // Set Event Location: parameters[0] is the moved event's id (>0).
647
+ // 212/213/205: parameters[0] is a characterId (-1 player/0 this event/N).
648
+ const target = cmd.code === 203
649
+ ? cmd.parameters[0]
650
+ : cmd.code === 212 || cmd.code === 213 || cmd.code === 205
651
+ ? cmd.parameters[0]
652
+ : undefined;
653
+ if (target === deletedId && deletedId > 0) {
654
+ const what = cmd.code === 203
655
+ ? 'Set Event Location'
656
+ : cmd.code === 212
657
+ ? 'Show Animation'
658
+ : cmd.code === 213
659
+ ? 'Show Balloon'
660
+ : 'Set Movement Route';
661
+ warnings.push(`Event ${event.id} "${event.name}" page ${pi} has a ${what} command (code ${cmd.code}) referencing deleted event ${deletedId}`);
662
+ }
663
+ }
664
+ });
665
+ }
666
+ return warnings;
667
+ }
668
+ /**
669
+ * Delete an event from a map. Warns (never blocks) when other events' pages still
670
+ * reference the deleted event id via Set Event Location / Show Animation / Show
671
+ * Balloon / forced Set Movement Route (see {@link eventReferenceWarnings}).
672
+ */
673
+ export async function deleteMapEvent(projectPath, mapId, eventId) {
674
+ const map = await getMap(projectPath, mapId);
675
+ // Mutators throw on a missing target (consistent with delete_map / update_map_event);
676
+ // getters are the ones that return null.
677
+ if (!map.events[eventId]) {
678
+ throw new Error(`Event ${eventId} not found on map ${mapId}`);
679
+ }
680
+ // Scan the other events BEFORE nulling the slot so the reference warnings see
681
+ // the full event set (they already skip the deleted id anyway).
682
+ const warnings = eventReferenceWarnings(map.events, eventId);
683
+ map.events[eventId] = null;
684
+ const mapPath = getMapPath(projectPath, mapId);
685
+ await commitChange(mapPath, map);
686
+ return warnings.length > 0 ? { success: true, warnings } : { success: true };
687
+ }
688
+ /**
689
+ * Search events by name
690
+ */
691
+ export async function searchMapEvents(projectPath, mapId, searchTerm) {
692
+ const events = await getMapEvents(projectPath, mapId);
693
+ const lowerSearchTerm = searchTerm.toLowerCase();
694
+ return events.filter((event) => event && event.name.toLowerCase().includes(lowerSearchTerm));
695
+ }
696
+ /**
697
+ * Add a command to an event page
698
+ */
699
+ export async function addEventCommand(projectPath, mapId, eventId, pageIndex, command, position, precommit) {
700
+ const map = await getMap(projectPath, mapId);
701
+ if (!map.events[eventId]) {
702
+ throw new Error(`Event ${eventId} not found on map ${mapId}`);
703
+ }
704
+ const event = map.events[eventId];
705
+ if (!event.pages[pageIndex]) {
706
+ throw new Error(`Page ${pageIndex} not found on event ${eventId}`);
707
+ }
708
+ // add_event_command is the single-command case of the shared splice path.
709
+ spliceIntoList(event.pages[pageIndex].list, [command], position);
710
+ await precommit?.(event);
711
+ const mapPath = getMapPath(projectPath, mapId);
712
+ await commitChange(mapPath, map);
713
+ return event;
714
+ }
715
+ /**
716
+ * Get map dimensions
717
+ */
718
+ export async function getMapDimensions(projectPath, mapId) {
719
+ const map = await getMap(projectPath, mapId);
720
+ return {
721
+ width: map.width,
722
+ height: map.height,
723
+ };
724
+ }
725
+ /**
726
+ * Read the raw tile ids in a rectangular window of one z-layer — a token-cheap
727
+ * windowed alternative to {@link getMap} for inspecting part of a big painted map.
728
+ * Returns `tiles` as a 2D array (rows top→bottom, each left→right). The rectangle
729
+ * must lie fully within the map bounds (throws otherwise). Uses the pure
730
+ * {@link tileIndex} helper for the flat-array index math. Read-only.
731
+ */
732
+ export async function getMapRegion(projectPath, mapId, x, y, width, height, layer = 0) {
733
+ const map = await getMap(projectPath, mapId);
734
+ if (x < 0 || y < 0 || x + width > map.width || y + height > map.height) {
735
+ throw new Error(`Region (${x}, ${y}) ${width}x${height} is out of bounds for the ${map.width}x${map.height} map`);
736
+ }
737
+ const tiles = [];
738
+ for (let row = 0; row < height; row++) {
739
+ const cols = [];
740
+ for (let col = 0; col < width; col++) {
741
+ cols.push(map.data[tileIndex(map.width, map.height, x + col, y + row, layer)]);
742
+ }
743
+ tiles.push(cols);
744
+ }
745
+ return { mapId, x, y, width, height, layer, tiles };
746
+ }
747
+ /**
748
+ * Set map tile at specific position
749
+ */
750
+ export async function setMapTile(projectPath, mapId, x, y, layer, tileId) {
751
+ const map = await getMap(projectPath, mapId);
752
+ if (x < 0 || x >= map.width || y < 0 || y >= map.height) {
753
+ throw new Error(`Position (${x}, ${y}) is out of map bounds`);
754
+ }
755
+ // RPG Maker MZ stores tiles in a 1D array with 6 layers
756
+ // Index = (layer * height + y) * width + x
757
+ const index = tileIndex(map.width, map.height, x, y, layer);
758
+ map.data[index] = tileId;
759
+ const mapPath = getMapPath(projectPath, mapId);
760
+ await commitChange(mapPath, map);
761
+ }
762
+ /**
763
+ * Compute the flat `data` array index for a tile at (x, y) on a z-layer.
764
+ *
765
+ * RPG Maker MZ stores map tiles in a single 1D array of `width * height * 6`
766
+ * entries (6 stacked layers: 2 lower, 2 upper, shadow pen, region ID). Kept as
767
+ * a pure function so the index math can be unit-tested without file I/O.
768
+ */
769
+ export function tileIndex(width, height, x, y, layer) {
770
+ return (layer * height + y) * width + x;
771
+ }
772
+ export const mapToolDefinitions = [
773
+ {
774
+ name: 'get_map',
775
+ description: 'Get map data by ID. The tile `data` array can be huge on a painted map (width*height*6 ints) and blow the MCP token limit, so pass includeData:false to omit it (you get dataTileCount instead) and read tiles with get_map_region when needed. includeData defaults to true for backward compatibility.',
776
+ inputSchema: {
777
+ mapId: z.number().int().positive().describe('The ID of the map to retrieve'),
778
+ includeData: z
779
+ .boolean()
780
+ .optional()
781
+ .describe('Include the full tile `data` array (default true). Pass false to omit it (returns dataTileCount) and avoid the token cost of a big painted map.'),
782
+ },
783
+ handler: async (ctx, args) => {
784
+ const map = await getMap(ctx.projectPath, args.mapId);
785
+ if (args.includeData === false) {
786
+ // Drop the large tile `data` array; report its length instead. Mirrors
787
+ // create_map/update_map's dataTileCount echo (P2-6) so clients can inspect
788
+ // a map's shape without paying for every tile (read tiles via get_map_region).
789
+ const { data, ...mapWithoutData } = map;
790
+ return { ...mapWithoutData, dataTileCount: data.length };
791
+ }
792
+ return map;
793
+ },
794
+ },
795
+ {
796
+ name: 'get_map_region',
797
+ description: 'Read the raw tile ids in a rectangular window of one map layer — a token-cheap alternative to get_map for inspecting part of a painted map. Returns `tiles` as a 2D array (rows top→bottom, each left→right) of raw engine tile ids. The rectangle must lie fully within the map bounds (throws otherwise). layer defaults to 0 (see set_map_tile for the z-layer meanings).',
798
+ inputSchema: {
799
+ mapId: z.number().int().positive().describe('The ID of the map'),
800
+ x: z.number().int().min(0).describe('Left edge of the window (tile column)'),
801
+ y: z.number().int().min(0).describe('Top edge of the window (tile row)'),
802
+ width: z.number().int().positive().describe('Window width in tiles'),
803
+ height: z.number().int().positive().describe('Window height in tiles'),
804
+ layer: z
805
+ .number()
806
+ .int()
807
+ .min(0)
808
+ .max(5)
809
+ .optional()
810
+ .describe('Z-layer 0-5 (0-1 lower, 2-3 upper, 4 shadow, 5 region); default 0'),
811
+ },
812
+ handler: (ctx, args) => getMapRegion(ctx.projectPath, args.mapId, args.x, args.y, args.width, args.height, args.layer),
813
+ },
814
+ {
815
+ name: 'get_map_infos',
816
+ description: 'Get information about all maps',
817
+ inputSchema: {},
818
+ handler: (ctx) => getMapInfos(ctx.projectPath),
819
+ },
820
+ {
821
+ name: 'create_map',
822
+ mutates: true,
823
+ description: 'Create a new blank map: writes a new data/MapNNN.json (all tiles unpainted) and registers it in the map tree (MapInfos.json). Allocates the next unused map id and returns it. Paint tiles afterward with paint_tiles/fill_area (autotile-aware) and add events with create_map_event/create_npc.',
824
+ inputSchema: {
825
+ name: z.string().describe('Map name shown in the editor map tree'),
826
+ width: z.number().int().positive().optional().describe('Width in tiles (default 17)'),
827
+ height: z.number().int().positive().optional().describe('Height in tiles (default 13)'),
828
+ parentId: z
829
+ .number()
830
+ .int()
831
+ .optional()
832
+ .describe('Parent map id in the tree; 0 (default) = top level'),
833
+ tilesetId: z.number().int().positive().optional().describe('Tileset id (default 1)'),
834
+ },
835
+ handler: async (ctx, args) => {
836
+ const { mapId, mapInfo, map } = await createMap(ctx.projectPath, {
837
+ name: args.name,
838
+ width: args.width,
839
+ height: args.height,
840
+ parentId: args.parentId,
841
+ tilesetId: args.tilesetId,
842
+ });
843
+ // Drop the large all-zero blank tile array from the response — a fresh map
844
+ // is unpainted by definition, so echoing ~w*h*6 zeros is pure bloat (P2-6).
845
+ // get_map returns the full `data` when it's actually needed.
846
+ const { data, ...mapWithoutData } = map;
847
+ return { mapId, mapInfo, map: { ...mapWithoutData, dataTileCount: data.length } };
848
+ },
849
+ },
850
+ {
851
+ name: 'delete_map',
852
+ mutates: true,
853
+ description: "Delete a map: remove its entry from the map tree (MapInfos.json) and delete its data/MapNNN.json file. The deleted map's direct children are reparented onto its parent (not deleted), so removing one node doesn't wipe a whole sub-tree. Does not touch System.json.",
854
+ inputSchema: {
855
+ mapId: z.number().int().positive().describe('The ID of the map to delete'),
856
+ },
857
+ handler: (ctx, args) => deleteMap(ctx.projectPath, args.mapId),
858
+ },
859
+ {
860
+ name: 'update_map_tree',
861
+ mutates: true,
862
+ description: 'Edit the map tree (MapInfos.json) only — reparent, reorder, rename, or expand/collapse maps without touching their tiles or events. Takes a batch of per-map updates; every referenced map (and any non-zero parentId) must exist, and the resulting tree must stay acyclic.',
863
+ inputSchema: {
864
+ updates: z
865
+ .array(z.object({
866
+ mapId: z.number().int().positive().describe('The map to update'),
867
+ parentId: z.number().int().optional().describe('New parent map id; 0 = top level'),
868
+ order: z.number().int().optional().describe('New sort order among siblings'),
869
+ name: z.string().optional().describe('New tree display name'),
870
+ expanded: z.boolean().optional().describe('Whether the node is expanded in the tree'),
871
+ }))
872
+ .describe('One or more per-map tree edits to apply together'),
873
+ },
874
+ handler: (ctx, args) => updateMapTree(ctx.projectPath, args.updates),
875
+ },
876
+ {
877
+ name: 'get_map_events',
878
+ description: 'Get all events from a specific map',
879
+ inputSchema: { mapId: z.number().int().positive().describe('The ID of the map') },
880
+ handler: (ctx, args) => getMapEvents(ctx.projectPath, args.mapId),
881
+ },
882
+ {
883
+ name: 'get_map_event',
884
+ description: 'Get a specific event from a map',
885
+ inputSchema: {
886
+ mapId: z.number().int().positive().describe('The ID of the map'),
887
+ eventId: z.number().int().positive().describe('The ID of the event'),
888
+ },
889
+ handler: (ctx, args) => getMapEvent(ctx.projectPath, args.mapId, args.eventId),
890
+ },
891
+ {
892
+ name: 'update_map_event',
893
+ mutates: true,
894
+ forceable: true,
895
+ description: "Update a map event's properties. Refuses the write (nothing is saved) if the resulting event is structurally invalid — pass force: true to override.",
896
+ inputSchema: {
897
+ mapId: z.number().int().positive().describe('The ID of the map'),
898
+ eventId: z.number().int().positive().describe('The ID of the event'),
899
+ updates: z
900
+ .record(z.string(), z.unknown())
901
+ .describe('Object containing event properties to update'),
902
+ },
903
+ handler: async (ctx, args) => {
904
+ const gate = eventWriteGate(ctx.projectPath, args.mapId, args.force);
905
+ const event = await updateMapEvent(ctx.projectPath, args.mapId, args.eventId, args.updates, gate.precommit);
906
+ return gate.respond({ event });
907
+ },
908
+ },
909
+ {
910
+ name: 'create_map_event',
911
+ mutates: true,
912
+ forceable: true,
913
+ description: 'Create a new event on a map. Each page is merged onto a blank "New Event" page (trigger 0 action-button, priority 0 below characters, no graphic, empty command list, standing move type), so you only supply the fields that differ — pass e.g. `{ image: { characterName: \'Actor1\', characterIndex: 0 }, trigger: 3, list: [...] }` and the rest is filled in. Nested `image`/`conditions` deep-merge; an omitted `list` becomes a valid empty (code-0-terminated) list. Omit `pages` entirely for a bare one-page event. For the common "talking NPC" case prefer create_npc. An action-button page meant to fire from facing (doors, entrances, signs) needs `priorityType: 1` — with the default 0 (below) it only fires when stood on, so on an impassable tile it can never trigger (this is refused, not written; pass force: true to override). A structurally invalid command list is refused the same way. Page fields: `image` { characterName, characterIndex, direction (2 down/4 left/6 right/8 up), pattern, tileId }, `trigger` (0 action-button/1 player-touch/2 event-touch/3 autorun/4 parallel), `priorityType` (0 below/1 same/2 above), `moveType` (0 fixed/1 random/2 approach/3 custom), `conditions`, `list`.',
914
+ inputSchema: {
915
+ mapId: z.number().int().positive().describe('The ID of the map'),
916
+ name: z.string().describe('Event name'),
917
+ x: z.number().int().min(0).describe('X tile position'),
918
+ y: z.number().int().min(0).describe('Y tile position'),
919
+ note: z.string().optional().describe('Event note field'),
920
+ pages: z
921
+ .array(z.record(z.string(), z.unknown()))
922
+ .optional()
923
+ .describe('Event pages; each is merged onto a blank page so you can pass only the differing fields. Omit for one blank page.'),
924
+ },
925
+ handler: async (ctx, args) => {
926
+ // dryRun/force are dispatcher arguments, not event fields — strip them so
927
+ // they can't leak into the event body via the spread below.
928
+ const { mapId, dryRun: _dryRun, force, ...eventData } = args;
929
+ const gate = eventWriteGate(ctx.projectPath, mapId, force);
930
+ const event = await createMapEvent(ctx.projectPath, mapId, eventData, gate.precommit);
931
+ // Return a compact summary, not the full event with every defaulted page
932
+ // field — a huge token cost on every authoring call (re-read via get_map_event).
933
+ return gate.respond({ event: summarizeCreatedEvent(event) });
934
+ },
935
+ },
936
+ {
937
+ name: 'search_map_events',
938
+ description: 'Search events on a map by name',
939
+ inputSchema: {
940
+ mapId: z.number().int().positive().describe('The ID of the map'),
941
+ searchTerm: z.string().describe('The search term to find events'),
942
+ },
943
+ handler: (ctx, args) => searchMapEvents(ctx.projectPath, args.mapId, args.searchTerm),
944
+ },
945
+ {
946
+ name: 'add_event_command',
947
+ mutates: true,
948
+ forceable: true,
949
+ description: 'Add a command to an event page. Refuses the write (nothing is saved) if the resulting page is structurally invalid — e.g. the command has the wrong parameter count for its code. Pass force: true to override.',
950
+ inputSchema: {
951
+ mapId: z.number().int().positive().describe('The ID of the map'),
952
+ eventId: z.number().int().positive().describe('The ID of the event'),
953
+ pageIndex: z.number().int().min(0).describe('Zero-based page index'),
954
+ command: z
955
+ .object({
956
+ code: z.number().int().describe('Event command code (see RPG Maker MZ documentation)'),
957
+ indent: z.number().int().min(0).optional().default(0).describe('Indentation level'),
958
+ parameters: z.array(z.unknown()).describe('Command parameters'),
959
+ })
960
+ .describe('The event command to insert'),
961
+ position: z
962
+ .number()
963
+ .int()
964
+ .min(0)
965
+ .optional()
966
+ .describe('Insertion index; defaults to end of the list'),
967
+ },
968
+ handler: async (ctx, args) => {
969
+ const gate = eventWriteGate(ctx.projectPath, args.mapId, args.force);
970
+ const event = await addEventCommand(ctx.projectPath, args.mapId, args.eventId, args.pageIndex, args.command, args.position, gate.precommit);
971
+ return gate.respond({ event });
972
+ },
973
+ },
974
+ {
975
+ name: 'update_map',
976
+ mutates: true,
977
+ description: "Update a map's top-level properties (name, display name, bgm, encounters, etc.). Does not repaint tiles. Cannot change width/height (that would desync the tile data array) — use resize_map for that.",
978
+ inputSchema: {
979
+ mapId: z.number().int().positive().describe('The ID of the map'),
980
+ updates: z.record(z.string(), z.unknown()).describe('Partial MapData properties to merge'),
981
+ },
982
+ handler: async (ctx, args) => {
983
+ const map = await updateMap(ctx.projectPath, args.mapId, args.updates);
984
+ // Drop the large tile `data` array (~w*h*6 ints) from the echo — update_map
985
+ // never edits tiles, so returning them is pure token bloat (on a painted
986
+ // 40x40 map it blew past the MCP token limit; the write still applied).
987
+ // Mirrors create_map (P2-6); get_map returns the full data when needed.
988
+ const { data, ...mapWithoutData } = map;
989
+ return { ...mapWithoutData, dataTileCount: data.length };
990
+ },
991
+ },
992
+ {
993
+ name: 'resize_map',
994
+ mutates: true,
995
+ description: "Resize a map to new width/height, safely repadding every z-layer of its tile data (existing tiles kept where the old and new grids overlap; new cells blank; shrinking crops). This is the ONLY safe way to change a map's dimensions — update_map refuses a width/height change because it would not resize the tile array. Warns about any event left outside the new bounds.",
996
+ inputSchema: {
997
+ mapId: z.number().int().positive().describe('The ID of the map to resize'),
998
+ width: z.number().int().positive().describe('New width in tiles'),
999
+ height: z.number().int().positive().describe('New height in tiles'),
1000
+ },
1001
+ handler: (ctx, args) => resizeMap(ctx.projectPath, args.mapId, args.width, args.height),
1002
+ },
1003
+ {
1004
+ name: 'set_encounters',
1005
+ mutates: true,
1006
+ description: "Set a map's random-encounter list (replaces it wholesale) and optionally its encounterStep (average steps between encounters). Each encounter is { troopId, weight?, regionSet? }: weight biases the random pick (default 5), regionSet restricts it to those map region ids (empty/omitted = anywhere). Every troopId is validated against Troops.json — a non-existent troop throws. Prefer this over update_map for encounters (it validates and hides the on-disk shape).",
1007
+ inputSchema: {
1008
+ mapId: z.number().int().positive().describe('The ID of the map'),
1009
+ encounters: z
1010
+ .array(z.object({
1011
+ troopId: z.number().int().describe('Troop id from Troops.json'),
1012
+ weight: z.number().int().optional().describe('Relative encounter weight (default 5)'),
1013
+ regionSet: z
1014
+ .array(z.number().int())
1015
+ .optional()
1016
+ .describe('Region ids this encounter is restricted to (empty = anywhere)'),
1017
+ }))
1018
+ .describe('The full encounter list to set (replaces any existing entries)'),
1019
+ encounterStep: z
1020
+ .number()
1021
+ .int()
1022
+ .positive()
1023
+ .optional()
1024
+ .describe('Average number of steps between encounters (unchanged if omitted)'),
1025
+ },
1026
+ handler: (ctx, args) => setEncounters(ctx.projectPath, args.mapId, args.encounters, args.encounterStep),
1027
+ },
1028
+ {
1029
+ name: 'get_map_dimensions',
1030
+ description: 'Get the width and height (in tiles) of a map',
1031
+ inputSchema: { mapId: z.number().int().positive().describe('The ID of the map') },
1032
+ handler: (ctx, args) => getMapDimensions(ctx.projectPath, args.mapId),
1033
+ },
1034
+ {
1035
+ name: 'set_map_tile',
1036
+ mutates: true,
1037
+ description: 'Set a single raw tile ID at (x, y) on a given z-layer (0-5). Note: tile IDs are raw engine integers; this is a low-level primitive without autotile/passability awareness.',
1038
+ inputSchema: {
1039
+ mapId: z.number().int().positive().describe('The ID of the map'),
1040
+ x: z.number().int().nonnegative().describe('X tile position'),
1041
+ y: z.number().int().nonnegative().describe('Y tile position'),
1042
+ layer: z
1043
+ .number()
1044
+ .int()
1045
+ .min(0)
1046
+ .max(5)
1047
+ .describe('Z-layer 0-5 (0-1 lower, 2-3 upper, 4 shadow, 5 region)'),
1048
+ tileId: z.number().int().nonnegative().describe('Raw tile ID'),
1049
+ },
1050
+ handler: async (ctx, args) => {
1051
+ await setMapTile(ctx.projectPath, args.mapId, args.x, args.y, args.layer, args.tileId);
1052
+ return { success: true };
1053
+ },
1054
+ },
1055
+ {
1056
+ name: 'delete_map_event',
1057
+ mutates: true,
1058
+ description: 'Delete an event from a map by ID',
1059
+ inputSchema: {
1060
+ mapId: z.number().int().positive().describe('The ID of the map'),
1061
+ eventId: z.number().int().positive().describe('The ID of the event'),
1062
+ },
1063
+ handler: (ctx, args) => deleteMapEvent(ctx.projectPath, args.mapId, args.eventId),
1064
+ },
1065
+ ];
1066
+ //# sourceMappingURL=mapTools.js.map