import { zip, unzip } from "fflate"; import { qsThrowIfNotExists, getFlagsDataStorageKey, insertAlert, translateErrorMessage } from "../utils"; import { FloorData, GameController, ObjectDataMap, minWidth, minHeight } from "./game"; import * as bootstrap from "bootstrap"; import { Flag, getFlagsByStorage, getFlagsCSVByStorage, initFlagEditor } from "./flagEditor"; import { getMessagesByStorage, getMessagesCSVByStorage, initMessageEditor, getMessageStorageKey, Messages, } from "./messageEditer"; import * as encoding from "encoding-japanese"; export interface webAssetData { id: string; url: string; type: string; width: number; height: number; } export type PreviewMode = "original" | "scale" | "extend"; export function initMapEditorPage(controller: GameController) { const projectSelect = qsThrowIfNotExists(document, "#projectSelect"); const floorSelect = qsThrowIfNotExists(document, "#floorSelect"); const scrollButton = qsThrowIfNotExists(document, "#scroll"); const editFloorButton = qsThrowIfNotExists(document, "#editFloor"); const setObjectButton = qsThrowIfNotExists(document, "#setObject"); const previewButton = qsThrowIfNotExists(document, "#preview"); const editorViewButton = qsThrowIfNotExists(document, "#editorView"); const gameViewButton = qsThrowIfNotExists(document, "#gameView"); const zipButton = qsThrowIfNotExists(document, "#zip"); const loadZipButton = qsThrowIfNotExists(document, "#loadZip"); const gameFocusButton = qsThrowIfNotExists(document, "#gameFocus"); const floorWidthInput = qsThrowIfNotExists(document, "#floorWidth"); const floorHeightInput = qsThrowIfNotExists(document, "#floorHeight"); const container = qsThrowIfNotExists(document, "#gameContainer"); const defaultHeight = 15; const defaultWidth = 15; let gameInitialized = false; let startedAge: number; let startedDate: number; const getFloorSize = () => { const result = getCurrentMapData(); if (!result || !result.floors || !getPreviousSelectedFloor(result.floors)) return { height: 15, width: 15 }; const selectedFloor = getPreviousSelectedFloor(result.floors); if (selectedFloor.length === 0 || !Array.isArray(selectedFloor[0])) { return { height: 15, width: 15 }; } const floorHeight = selectedFloor.length; const floorWidth = selectedFloor[0].length; return { height: floorHeight, width: floorWidth }; }; const getPreviousSelectedFloor = (floors: FloorData[]) => { return floors[Number(previousFloor) - 1]; }; const setFloorSizeInput = () => { const { height, width } = getFloorSize(); floorWidthInput.value = width.toString(); floorHeightInput.value = height.toString(); }; const getProjectsStorageKey = () => { return "projects"; }; const loadProjects = () => { const projectsJson = window.localStorage.getItem(getProjectsStorageKey()); return projectsJson ? JSON.parse(projectsJson) : ["プロジェクト"]; }; const saveProjects = (projects: string[]) => { window.localStorage.setItem(getProjectsStorageKey(), JSON.stringify(projects)); loadProjectOptions(); }; const addProject = (projectName: string) => { const projects = loadProjects(); if (projects.includes(projectName)) { alert("同名のプロジェクトが既に存在します"); return false; } projects.push(projectName); saveProjects(projects); return true; }; const selectProject = (projectName: string) => { for (let i = 0; i < projectSelect.options.length; i++) { if (projectSelect.options[i].value === projectName) { projectSelect.selectedIndex = i; break; } } }; const removeProject = (projectName: string) => { const projects = loadProjects(); const index = projects.indexOf(projectName); if (index === -1) { alert("指定された名前のプロジェクトが存在しません"); return; } projects.splice(index, 1); saveProjects(projects); }; const loadProjectOptions = () => { const projects = loadProjects(); projectSelect.innerHTML = ""; for (let project of projects) { const option = document.createElement("option"); option.value = option.text = project; projectSelect.appendChild(option); } const removeProjectButton = qsThrowIfNotExists(document, "#removeProjectButton"); if (projects.length === 1) { removeProjectButton.disabled = true; } else { removeProjectButton.disabled = false; } }; const getSelectedProjectStorageKey = () => { return `selectedProject`; }; const loadSelectedProject = () => { return window.localStorage.getItem(getSelectedProjectStorageKey()); }; const getSelectedProjectValue = () => { return projectSelect.options[projectSelect.selectedIndex].value; }; const saveSelectedProject = () => { const projectValue = getSelectedProjectValue(); window.localStorage.setItem(getSelectedProjectStorageKey(), projectValue); }; const setSelectedProject = (value: string) => { const options = Array.from(projectSelect.options); const matchingOption = options.find((option) => option.value === value); if (matchingOption) { matchingOption.selected = true; } }; const loadAndSetSelectedProject = () => { const savedProjectValue = loadSelectedProject(); if (savedProjectValue) { setSelectedProject(savedProjectValue); } }; const getFloorStorageKey = (project: string) => { return `project${project}-floorLevel`; }; const loadFloorLevelByProject = (project: string) => { const floorLevel = window.localStorage.getItem(getFloorStorageKey(project)); return floorLevel ? parseInt(floorLevel) : null; }; const loadFloorLevel = () => { const project = getSelectedProjectValue(); return loadFloorLevelByProject(project); }; const saveFloorLevel = (level?: string) => { const project = getSelectedProjectValue(); const floorLevel = level ?? floorSelect.options.length; window.localStorage.setItem(getFloorStorageKey(project), floorLevel.toString()); }; const createFloorOptopn = () => { const option = document.createElement("option"); option.value = (floorSelect.options.length + 1).toString(); option.text = "フロア " + (floorSelect.options.length + 1).toString(); return option; }; const createEmptyfloorCharacters = (height: number, width: number) => { let result: string[][] = []; for (let i = 0; i < height; i++) { let row = []; for (let j = 0; j < width; j++) { row.push("0"); } result.push(row); } return result; }; const updateMapSize = async () => { const height = parseInt(floorHeightInput.value, 10); const width = parseInt(floorWidthInput.value, 10); const result = await controller.getEditorState(); const existingFloor = getPreviousSelectedFloor(result.floors); const objects = result.objects; const updatedFloor = existingFloor.map((floor) => { const floorWidth = floor.length; if (floorWidth > width) { return floor.slice(0, width); } else if (floorWidth < width) { const newChips = new Array(width - floorWidth); for (let i = 0; i < newChips.length; i++) { newChips[i] = 0; } return [...floor, ...newChips]; } else { return floor; } }); const floorsLength = updatedFloor.length; if (floorsLength < height) { const newRows = new Array(height - floorsLength); for (let i = 0; i < newRows.length; i++) { newRows[i] = new Array(width); for (let j = 0; j < width; j++) { newRows[i][j] = 0; } } updatedFloor.push(...newRows); } else if (floorsLength > height) { updatedFloor.length = height; } const floorsForUpdate = result.floors; floorsForUpdate[Number(floorSelect.value) - 1] = updatedFloor; controller.sendMessageEvent({ type: "map", floors: floorsForUpdate, objects: objects, messages: result.message, flags: result.flags, }); //変更後の内容で保存 savePreviousMapDataToLocalStorage(updatedFloor, objects); return updatedFloor; }; const resetFloorLevelDisplay = () => { const savedFloorLevel = loadFloorLevel(); while (floorSelect.options.length > 1) { floorSelect.remove(floorSelect.options.length - 1); } if (savedFloorLevel !== null) { for (let i = 1; i < savedFloorLevel; i++) { const option = createFloorOptopn(); floorSelect.appendChild(option); } } previousFloor = "1"; if (gameInitialized) { controller.sendMessageEvent({ type: "editWarp", floor: 1, x: 0, y: 0 }); } }; const getMapFloorDataStorageKey = (project: string, floor: string) => { return `project${project}-floor${floor}`; }; const getMapObjectDataStorageKey = (project: string) => { return `project${project}-object`; }; const savePreviousMapData = async () => { const { floors, objects } = await controller.getEditorState(); savePreviousMapDataToLocalStorage(getPreviousSelectedFloor(floors), objects); }; const savePreviousAllMapDataToLocalStorage = (floors: FloorData[], objects: ObjectDataMap) => { floors.forEach((floor, i) => { window.localStorage.setItem(getMapFloorDataStorageKey(previousProject, `${i + 1}`), JSON.stringify(floor)); }); window.localStorage.setItem(getMapObjectDataStorageKey(previousProject), JSON.stringify(objects)); }; const savePreviousMapDataToLocalStorage = (floors: FloorData, objects: ObjectDataMap) => { window.localStorage.setItem(getMapFloorDataStorageKey(previousProject, previousFloor), JSON.stringify(floors)); window.localStorage.setItem(getMapObjectDataStorageKey(previousProject), JSON.stringify(objects)); }; const getCurrentMapData = () => { const project = getSelectedProjectValue(); const floorCount = floorSelect.options.length; let allFloors: FloorData[] = []; for (let floor = 1; floor <= floorCount; floor++) { const floorKey = getMapFloorDataStorageKey(project, floor.toString()); const floorDataString = window.localStorage.getItem(floorKey); if (floorDataString != null) { const floorData = JSON.parse(floorDataString); allFloors.push(floorData); } else { allFloors.push(createEmptyfloorCharacters(defaultHeight, defaultWidth)); } } const objectKey = getMapObjectDataStorageKey(project); const objectDataString = window.localStorage.getItem(objectKey); const objects: ObjectDataMap = objectDataString ? JSON.parse(objectDataString) : Object.fromEntries(Array.from({ length: floorCount }, (value, i) => [`${i + 1}`, []])); const flags = getFlagsByStorage(project); const messages = getMessagesByStorage(project); return { floors: allFloors, objects, flags, messages }; }; const setCurrentMapData = async () => { const mapData = getCurrentMapData(); if (mapData) { sendMapData(mapData); } else { //仮エラー処理 alert("マップデータを正常に送信できませんでした。"); } }; const sendMapData = (mapData: { floors: FloorData[]; objects: ObjectDataMap; flags: Flag[] | undefined; messages: Messages | undefined; }) => { controller.sendMessageEvent({ type: "map", ...mapData }); }; const defaultMode = "scale"; const getPreviewModeStorageKey = () => { return `previewMode`; }; const loadPreviewMode = () => { return window.localStorage.getItem(getPreviewModeStorageKey()) ?? defaultMode; }; const savePreviewMode = (mode: PreviewMode) => { window.localStorage.setItem(getPreviewModeStorageKey(), mode); }; const initPreviewModeModal = () => { const previewMode = qsThrowIfNotExists(document, "#previewMode"); const previewModeSelect = qsThrowIfNotExists(document, "#previewModeSelect"); const previewModeModal = qsThrowIfNotExists(document, "#previewModeModal"); const previewModeForm = qsThrowIfNotExists(previewModeModal, "#previewModeForm"); const modal = new bootstrap.Modal(previewModeModal); const savedPreviewMode = loadPreviewMode(); for (let i = 0; i < previewModeSelect.options.length; i++) { if (previewModeSelect.options[i].value === savedPreviewMode) { previewModeSelect.selectedIndex = i; break; } } previewMode.addEventListener("click", () => { modal.show(); }); previewModeForm.addEventListener("submit", async (event) => { event.preventDefault(); const mode = previewModeSelect.options[previewModeSelect.selectedIndex].value as PreviewMode; savePreviewMode(mode); resizePreview(); modal.hide(); }); }; const resizePreview = () => { const gameContainer = qsThrowIfNotExists(document, "#gameContainer"); const previewMode = loadPreviewMode(); let width = previewMode === "original" ? defaultWidth : gameContainer.clientWidth; let height = previewMode === "original" ? defaultHeight : gameContainer.clientHeight; const gameWidth = previewMode === "extend" ? width : minWidth; const gameHeight = previewMode === "extend" ? height : minHeight; const setGameWidth = gameWidth < minWidth ? minWidth : gameWidth; const setGameHeight = gameHeight < minHeight ? minHeight : gameHeight; if (previewMode !== "scale") { width = width < minWidth ? minWidth : width; height = height < minHeight ? minHeight : height; } // 2回動かさないと正常に変化しない // TODO 原因究明及び可能なら修正 controller.setGameSize(width, height, setGameWidth, setGameHeight); controller.setGameSize(width, height, setGameWidth, setGameHeight); setCurrentMapData(); }; const initUpdateSizeModal = () => { const updateSizeButton = qsThrowIfNotExists(document, "#updateSizeButton"); const updateSizeModal = qsThrowIfNotExists(document, "#updateSizeModal"); const modalWidthInput = qsThrowIfNotExists(updateSizeModal, 'input[name="width"]'); const modalHeightInput = qsThrowIfNotExists(updateSizeModal, 'input[name="height"]'); const updateSizeForm = qsThrowIfNotExists(updateSizeModal, "#updateSizeForm"); const modal = new bootstrap.Modal(updateSizeModal); updateSizeButton.addEventListener("click", () => { modalWidthInput.value = floorWidthInput.value; modalHeightInput.value = floorHeightInput.value; modal.show(); }); updateSizeForm.addEventListener("submit", async (event) => { event.preventDefault(); floorWidthInput.value = modalWidthInput.value; floorHeightInput.value = modalHeightInput.value; await updateMapSize(); modal.hide(); }); }; const initEditFloorCompositionModal = () => { const editFloorCompositionBtn = qsThrowIfNotExists(document, "#editFloorComposition"); const floorSelect = qsThrowIfNotExists(document, "#floorSelect"); const dragList = qsThrowIfNotExists(document, ".drag-list"); const editFloorCompositionModal = qsThrowIfNotExists(document, "#editFloorCompositionModal"); const editFloorCompositionForm = qsThrowIfNotExists( editFloorCompositionModal, "#editFloorCompositionForm" ); const modal = new bootstrap.Modal(editFloorCompositionModal); const createFloorListElement = (newFloorNumber: number) => { const newListItem = document.createElement("li"); newListItem.id = `dragFloorItem${newFloorNumber}`; newListItem.dataset.floorNumber = `${newFloorNumber}`; newListItem.dataset.floorState = `normal`; newListItem.classList.add("d-flex"); newListItem.draggable = true; return newListItem; }; const initDragElement = (dragElm: HTMLElement) => { dragElm.ondragstart = (event: DragEvent) => { if (event.dataTransfer && event.target instanceof HTMLElement) { event.dataTransfer.setData("text/plain", event.target.id); } }; dragElm.ondragover = (event: DragEvent) => { event.preventDefault(); let rect = dragElm.getBoundingClientRect(); if (event.clientY && event.clientY - rect.top < dragElm.clientHeight / 2) { dragElm.classList.add("drag-border-top"); dragElm.classList.remove("drag-border-bottom"); } else { dragElm.classList.add("drag-border-bottom"); dragElm.classList.remove("drag-border-top"); } }; dragElm.ondragleave = () => { dragElm.classList.remove("drag-border-top", "drag-border-bottom"); }; dragElm.ondrop = (event: DragEvent) => { if (event.dataTransfer) { event.preventDefault(); const id = event.dataTransfer.getData("text/plain"); const targetElm = qsThrowIfNotExists(editFloorCompositionModal, `#${id}`); if (event.clientY) { let rect = dragElm.getBoundingClientRect(); if (event.clientY - rect.top < dragElm.clientHeight / 2) { dragElm.parentElement?.insertBefore(targetElm, dragElm); } else { dragElm.parentElement?.insertBefore(targetElm, dragElm.nextSibling); } dragElm.classList.remove("drag-border-top", "drag-border-bottom"); } } }; }; editFloorCompositionBtn.addEventListener("click", () => { //リストを空にする while (dragList.firstChild) { dragList.removeChild(dragList.firstChild); } for (let i = 1; i <= floorSelect.length; i++) { const listItem = createFloorListElement(i); const floorText = floorSelect.options[i - 1].innerText; //フロアの名前をリスト要素に追加 const textSpan = document.createElement("span"); textSpan.textContent = floorText; listItem.appendChild(textSpan); //フロアを削除するボタンの作成 const removeButton = document.createElement("button"); removeButton.className = "btn btn-danger btn-sm ms-auto"; removeButton.type = "button"; removeButton.textContent = "-"; //フロアの削除をキャンセルするボタンの作成 const cancelButton = document.createElement("button"); cancelButton.classList.add("btn", "btn-secondary", "btn-sm", "ms-auto", "d-none"); cancelButton.type = "button"; cancelButton.innerText = "キャンセル"; removeButton.addEventListener("click", () => { textSpan.textContent = "-" + floorText; listItem.dataset.floorState = `remove`; listItem.classList.add("text-secondary"); cancelButton.classList.remove("d-none"); removeButton.classList.add("d-none"); }); cancelButton.addEventListener("click", () => { textSpan.textContent = floorText; listItem.dataset.floorState = `normal`; listItem.classList.remove("text-secondary"); removeButton.classList.remove("d-none"); cancelButton.classList.add("d-none"); }); listItem.appendChild(cancelButton); listItem.appendChild(removeButton); dragList.appendChild(listItem); } //ドラッグアンドドロップ処理 editFloorCompositionModal.querySelectorAll(".drag-list li").forEach((dragElm) => { initDragElement(dragElm); }); editFloorCompositionModal.querySelectorAll(".drag-list li *").forEach((child) => { child.ondragstart = (event) => { child.parentElement?.ondragstart?.(event); }; child.ondragover = (event) => { child.parentElement?.ondragover?.(event); }; }); modal.show(); }); //フロア追加 const addButton = qsThrowIfNotExists(editFloorCompositionModal, "#addFloorButton"); addButton.addEventListener("click", () => { let maxFloor = 0; editFloorCompositionModal.querySelectorAll(".drag-list li").forEach((item) => { const number = parseInt(item.innerText.match(/\d+/)?.[0] || "0", 10); if (number > maxFloor) maxFloor = number; }); const newFloorNumber = maxFloor + 1; const newListItem = createFloorListElement(newFloorNumber); newListItem.dataset.floorState = `add`; initDragElement(newListItem); const cancelButton = document.createElement("button"); cancelButton.classList.add("btn", "btn-secondary", "btn-sm", "ms-auto"); cancelButton.innerText = "キャンセル"; cancelButton.addEventListener("click", (e) => { cancelButton.parentElement?.remove(); }); newListItem.appendChild(document.createTextNode(`+フロア ${newFloorNumber} `)); newListItem.appendChild(cancelButton); dragList.appendChild(newListItem); }); editFloorCompositionForm.addEventListener("submit", async (event) => { event.preventDefault(); const currentMapData = getCurrentMapData(); const newFloors: FloorData[] = []; const newObjects: { [key: string]: any[] } = {}; dragList.querySelectorAll("li").forEach((listItem) => { const floorState = listItem.dataset.floorState; const floorNumber = parseInt(listItem.dataset.floorNumber || "0", 10); switch (floorState) { case "normal": newFloors.push(currentMapData.floors[floorNumber - 1]); newObjects[`${newFloors.length}`] = currentMapData.objects[`${floorNumber}`] ?? []; break; case "add": newFloors.push(createEmptyfloorCharacters(defaultHeight, defaultWidth)); newObjects[`${newFloors.length}`] = []; break; } }); if (newFloors.length === 0) { alert("全てのフロアが削除されてしまっている為、確定できません。"); return; } const newMapData = { floors: newFloors, objects: newObjects, flags: currentMapData.flags, messages: currentMapData.messages, }; //ゴミデータを残さないように削除処理であぶれたデータを消す if (newFloors.length < currentMapData.floors.length) { for (let i = newFloors.length + 1; i <= currentMapData.floors.length; i++) window.localStorage.removeItem(getMapFloorDataStorageKey(previousProject, i.toString())); } saveFloorLevel(newFloors.length.toString()); resetFloorLevelDisplay(); sendMapData(newMapData); savePreviousAllMapDataToLocalStorage(newFloors, newObjects); modal.hide(); }); }; const initAddProjectModal = () => { const addProjectModalElem = qsThrowIfNotExists(document, "#addProjectModal"); const addProjectButton = qsThrowIfNotExists(document, "#addProjectButton"); const projectNameInput = qsThrowIfNotExists(addProjectModalElem, `input[name="name"]`); const addProjectForm = qsThrowIfNotExists(addProjectModalElem, "#addProjectForm"); const modal = new bootstrap.Modal(addProjectModalElem); addProjectButton.addEventListener("click", () => { projectNameInput.value = ""; modal.show(); }); addProjectForm.addEventListener("submit", (event) => { event.preventDefault(); if (addProject(projectNameInput.value)) { modal.hide(); } }); }; const initRemoveProjectModal = () => { const removeProjectModalElem = qsThrowIfNotExists(document, "#removeProjectModal"); const removeProjectButton = qsThrowIfNotExists(document, "#removeProjectButton"); const projectNameInput = qsThrowIfNotExists(removeProjectModalElem, `input[name="name"]`); const removeProjectForm = qsThrowIfNotExists(removeProjectModalElem, "#removeProjectForm"); const modal = new bootstrap.Modal(removeProjectModalElem); removeProjectButton.addEventListener("click", () => { projectNameInput.value = getSelectedProjectValue(); modal.show(); }); removeProjectForm.addEventListener("submit", (event) => { event.preventDefault(); const projectName = getSelectedProjectValue(); for (let i = 1; i <= floorSelect.options.length; i++) { window.localStorage.removeItem(getMapFloorDataStorageKey(projectName, i.toString())); } window.localStorage.removeItem(getMapObjectDataStorageKey(projectName)); window.localStorage.removeItem(getFloorStorageKey(projectName)); removeProject(projectName); //選択中のプロジェクトが削除された場合に備えて読み込み直す previousProject = getSelectedProjectValue(); saveSelectedProject(); setCurrentMapData(); setFloorSizeInput(); modal.hide(); }); }; const initEditObjectModal = async () => { const editObjectModal = qsThrowIfNotExists(document, "#objectEditModal"); const modalNoInput = qsThrowIfNotExists(editObjectModal, 'input[name="no"]'); const modalXInput = qsThrowIfNotExists(editObjectModal, 'input[name="x"]'); const modalYInput = qsThrowIfNotExists(editObjectModal, 'input[name="y"]'); const modalWallInput = qsThrowIfNotExists(editObjectModal, 'input[name="wall"]'); const modalEventInput = qsThrowIfNotExists(editObjectModal, 'input[name="event"]'); const modalVarsInput = qsThrowIfNotExists(editObjectModal, 'input[name="vars"]'); const modalImageInput = qsThrowIfNotExists(editObjectModal, 'input[name="image"]'); const modalHideCheckbox = qsThrowIfNotExists(editObjectModal, 'input[name="hide"]'); const modalSubmitButton = qsThrowIfNotExists(editObjectModal, 'button[type="submit"]'); const deleteButton = qsThrowIfNotExists(editObjectModal, "#deleteButton"); const editObjectForm = qsThrowIfNotExists(editObjectModal, "#objectEditForm"); const webAssetSelect = qsThrowIfNotExists(editObjectModal, "#webAssetSelect"); const modal = new bootstrap.Modal(editObjectModal); //jsonデータを取得して画像の選択肢にする const response = await fetch( "https://firebasestorage.googleapis.com/v0/b/scrap-conan-online.appspot.com/o/packages%2Feditor%2Fmasterdata.json?alt=media" ); const webAssets: webAssetData[] = await response.json(); const defaultOption = document.createElement("option"); defaultOption.value = ""; defaultOption.text = "未選択"; webAssetSelect.innerHTML = ""; webAssetSelect.add(defaultOption); webAssets.forEach((item: { id: string }) => { const option = document.createElement("option"); option.value = item.id; option.text = item.id; webAssetSelect.add(option); }); const webAssetSelectChangeEvent = () => { const objectChipCanvas = qsThrowIfNotExists(editObjectModal, "#objectChipCanvas"); if (webAssetSelect.options[webAssetSelect.selectedIndex].value === "") { objectChipCanvas.classList.remove("pe-none"); objectChipCanvas.classList.remove("opacity-50"); if (modalImageInput.value === "") { modalSubmitButton.disabled = true; } } else { objectChipCanvas.classList.add("pe-none"); objectChipCanvas.classList.add("opacity-50"); modalSubmitButton.disabled = false; } }; webAssetSelect.addEventListener("change", webAssetSelectChangeEvent); controller.addEventListener("posObject", async (event: Event) => { if (controller.objectChipSelector) { const customEvent = event as CustomEvent; const data = customEvent.detail; if (data.object) { modalNoInput.value = data.object.no; modalXInput.value = data.object.x; modalYInput.value = data.object.y; modalWallInput.value = data.object.wall; modalEventInput.value = data.object.event; modalVarsInput.value = data.object.vars; modalHideCheckbox.checked = data.object.disabled === true; let isWebAsset = false; for (let i = 0; i < webAssetSelect.options.length; i++) { if (webAssetSelect.options[i].value === data.object.image) { webAssetSelect.selectedIndex = i; isWebAsset = true; break; } } //未選択状態化 if (!isWebAsset) { webAssetSelect.selectedIndex = 0; } webAssetSelectChangeEvent(); controller.objectChipSelector.searchAndSelectChipByCharacter(data.object.image); deleteButton.classList.remove("d-none"); } else { modalNoInput.value = ""; modalXInput.value = data.x; modalYInput.value = data.y; modalWallInput.value = ""; modalEventInput.value = ""; modalVarsInput.value = ""; modalHideCheckbox.checked = false; deleteButton.classList.add("d-none"); //画像等の選択状態をリセットする処理。必要に応じてコメント解除 /* webAssetSelect.selectedIndex = 0; webAssetSelectChangeEvent(); modalImageInput.value = ""; controller.objectChipSelector.searchAndSelectChipByCharacter(""); modalSubmitButton.disabled = true; */ } modal.show(); } }); controller.addEventListener("gameStartedAge", async (gameStartedAgeEvemt: Event) => { if (!(gameStartedAgeEvemt instanceof CustomEvent)) { return; } startedAge = gameStartedAgeEvemt.detail.age; startedDate = gameStartedAgeEvemt.detail.date; }); controller.addEventListener("media", async (mediaEvent: Event) => { if (!(mediaEvent instanceof CustomEvent)) { return; } if (startedAge == null || startedDate == null) return; const { type, age } = mediaEvent.detail; const sendedAt = (age - startedAge) * (1000 / 30) + startedDate; const outputTableBody = qsThrowIfNotExists(document, "#outputTableBody"); const newRow = document.createElement("tr"); const dateObj = new Date(sendedAt); const formattedDate = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart( 2, "0" )}-${String(dateObj.getDate()).padStart(2, "0")} ${String(dateObj.getHours()).padStart(2, "0")}:${String( dateObj.getMinutes() ).padStart(2, "0")}:${String(dateObj.getSeconds()).padStart(2, "0")}`; const rowData = [ `${outputTableBody.rows.length + 1}`, formattedDate, type, JSON.stringify(mediaEvent.detail), ]; rowData.forEach((data) => { const cell = newRow.insertCell(); cell.textContent = data; }); outputTableBody.prepend(newRow); if (outputTableBody.rows.length > 100) { outputTableBody.deleteRow(-1); } }); editObjectForm.addEventListener("submit", async (event) => { event.preventDefault(); let image = modalImageInput.value; image = webAssetSelect.options[webAssetSelect.selectedIndex].value !== "" ? webAssetSelect.options[webAssetSelect.selectedIndex].value : image; if (modalHideCheckbox.checked) { image += "?"; } const type = modalNoInput.value === "" ? "editObject" : "editObjectNo"; if (type === "editObject") { controller.sendMessageEvent({ type: type, floor: floorSelect.value, x: parseInt(modalXInput.value), y: parseInt(modalYInput.value), image: image, event: modalEventInput.value, vars: modalVarsInput.value, wall: modalWallInput.value, }); } else { controller.sendMessageEvent({ type: type, no: modalNoInput.value, floor: floorSelect.value, x: parseInt(modalXInput.value), y: parseInt(modalYInput.value), image: image, event: modalEventInput.value, vars: modalVarsInput.value, wall: modalWallInput.value, }); } modalSubmitButton.disabled = true; modal.hide(); }); deleteButton.addEventListener("click", async () => { controller.sendMessageEvent({ type: "deleteObject", floor: floorSelect.value, x: parseInt(modalXInput.value), y: parseInt(modalYInput.value), }); modal.hide(); }); // vars-optionsの値を全て読み込み、変わらない値としてスクリプト側で持っておく const allOptions = Array.from( editObjectModal.querySelectorAll("#vars-options option") as NodeListOf ); const displayVarsOptionsByEvent = () => { const eventValue = modalEventInput.value; const varsOptionsList = qsThrowIfNotExists(editObjectModal, "#vars-options"); varsOptionsList.innerHTML = ""; const relevantOptions = allOptions.filter((option) => option.dataset.event === eventValue); relevantOptions.forEach((option) => { varsOptionsList.appendChild(option.cloneNode(true)); }); }; displayVarsOptionsByEvent(); modalEventInput.addEventListener("change", displayVarsOptionsByEvent); }; const initResetModal = () => { const resetModalElem = qsThrowIfNotExists(document, "#resetModal"); const resetForm = qsThrowIfNotExists(resetModalElem, "#resetForm"); const modal = new bootstrap.Modal(resetModalElem); const resetButton = document.querySelector("#resetModal .btn-primary") as HTMLButtonElement; container.addEventListener("keydown", (e) => { if (e.code === "Escape") { modal.show(); } }); resetForm.addEventListener("submit", (event) => { event.preventDefault(); controller.doReset(); modal.hide(); }); resetModalElem.addEventListener("shown.bs.modal", () => { resetButton.focus(); }); resetModalElem.addEventListener("hidden.bs.modal", () => { controller.focusInputHandler(); }); }; const initFloorJumpModal = () => { const jumpModalElem = qsThrowIfNotExists(document, "#jumpModal"); const jumpForm = qsThrowIfNotExists(jumpModalElem, "#jumpForm"); const modal = new bootstrap.Modal(jumpModalElem); const jumpFloorInput = document.getElementById("jumpFloorInput") as HTMLInputElement; container.addEventListener("keydown", (e) => { if (e.code === "Digit0") { modal.show(); } }); jumpForm.addEventListener("keydown", (event) => { if (event.key === "Enter") { const jumpButton = document.querySelector("#jumpModal .btn-primary") as HTMLButtonElement; jumpButton.click(); } }); jumpForm.addEventListener("submit", (event) => { const jumpFloor = parseInt(jumpFloorInput.value, 10); event.preventDefault(); controller.doReset(jumpFloor); modal.hide(); }); jumpModalElem.addEventListener("shown.bs.modal", () => { jumpFloorInput.focus(); }); jumpModalElem.addEventListener("hidden.bs.modal", () => { controller.focusInputHandler(); }); }; // プロジェクトのロード loadProjectOptions(); loadAndSetSelectedProject(); let previousProject = projectSelect.value; let previousFloor = floorSelect.value; resetFloorLevelDisplay(); initEditFloorCompositionModal(); initAddProjectModal(); initRemoveProjectModal(); initUpdateSizeModal(); initEditObjectModal(); initPreviewModeModal(); initResetModal(); initFloorJumpModal(); initFlagEditor({ onFlagChanged: () => { setCurrentMapData(); // フラグが変更されたら呼び出し、更新を反映する }, }); initMessageEditor({ onMessageChanged: () => { setCurrentMapData(); // メッセージが変更されたら呼び出し、更新を反映する }, }); //ゲーム読み込み時イベント controller.addContentLoadEvent(async () => { await setCurrentMapData(); setFloorSizeInput(); resizePreview(); controller.sendMessageEvent({ type: "editorProp", target: "object", prop: "opacity", value: 0.5 }); gameInitialized = true; }); controller.addEventListener("edited", () => { savePreviousMapData(); }); controller.addEventListener("error", (errorEvent) => { savePreviousMapData(); if (errorEvent instanceof CustomEvent) { const errorModalElement = qsThrowIfNotExists(document, "#errorModal"); const errorMessageElement = qsThrowIfNotExists(errorModalElement, ".error-message"); const errorModal = new bootstrap.Modal(errorModalElement); let errorMessage = `エラー:${errorEvent.detail.message}`; errorMessage = translateErrorMessage(errorMessage); errorMessageElement.innerHTML = `${errorMessage}`; insertAlert(document.body, `${errorMessage}`); errorModal.show(); } }); window.addEventListener("resize", () => { resizePreview(); }); projectSelect.addEventListener("change", async () => { await savePreviousMapData(); resetFloorLevelDisplay(); saveSelectedProject(); previousProject = projectSelect.value; await setCurrentMapData(); setFloorSizeInput(); }); const changeFloorEventWithoutSave = () => { previousFloor = floorSelect.value; controller.sendMessageEvent({ type: "editWarp", floor: floorSelect.value, x: 0, y: 0 }); setFloorSizeInput(); }; floorSelect.addEventListener("change", async () => { await savePreviousMapData(); changeFloorEventWithoutSave(); }); const modeButtons = [scrollButton, editFloorButton, setObjectButton, previewButton]; const viewButtons = [editorViewButton, gameViewButton]; function changeButtonState(buttons: HTMLButtonElement[], activeButton: HTMLButtonElement) { buttons.forEach((button) => { if (button === activeButton) { button.classList.remove("btn-secondary"); button.classList.add("btn-primary"); } else { button.classList.remove("btn-primary"); button.classList.add("btn-secondary"); } }); } const setDisabledInPreview = (): void => { const selectors = [ "#projectColumn button", "#projectColumn select", "#targetColumn button", "#targetColumn select", ]; const elements = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)) ); const isPreviewMode = previewButton.classList.contains("btn-primary"); elements.forEach((elem) => { elem.disabled = isPreviewMode; }); }; scrollButton.addEventListener("click", function () { controller.changeEditMode("scroll"); changeButtonState(modeButtons, scrollButton); setDisabledInPreview(); controller.sendMessageEvent({ type: "editorProp", target: "object", prop: "opacity", value: 0.5 }); }); editFloorButton.addEventListener("click", function () { changeButtonState(modeButtons, editFloorButton); setDisabledInPreview(); controller.changeEditMode("editFloor"); controller.sendMessageEvent({ type: "editorProp", target: "object", prop: "opacity", value: 0.5 }); }); setObjectButton.addEventListener("click", function () { controller.changeEditMode("raiseEvent"); changeButtonState(modeButtons, setObjectButton); setDisabledInPreview(); controller.sendMessageEvent({ type: "editorProp", target: "object", prop: "opacity", value: 1 }); }); previewButton.addEventListener("click", function () { controller.changeEditMode("preview", Number(floorSelect.value)); changeButtonState(modeButtons, previewButton); setDisabledInPreview(); //一時的にdisabled化、プレビュー待機時間中に切り替えを防ぐ modeButtons.forEach((button) => { button.disabled = true; }); // 1秒後にdisabledを解除 setTimeout(() => { modeButtons.forEach((button) => { button.disabled = false; }); }, 1000); controller.sendMessageEvent({ type: "editorProp", target: "object", prop: "opacity", value: 0.5 }); controller.focusInputHandler(); }); editorViewButton.addEventListener("click", function () { changeButtonState(viewButtons, editorViewButton); controller.sendMessageEvent({ type: "editorProp", target: "all", prop: "image", value: 0 }); }); gameViewButton.addEventListener("click", function () { changeButtonState(viewButtons, gameViewButton); controller.sendMessageEvent({ type: "editorProp", target: "all", prop: "image", value: 1 }); }); const showRewriteProjectModal = () => { return new Promise((resolve) => { const modalElem = qsThrowIfNotExists(document, "#rewriteProjectModal"); const modal = new bootstrap.Modal(modalElem, { keyboard: false, }); modal.show(); const okButton = qsThrowIfNotExists(modalElem, "#okRewriteProjectButton"); const cancelButton = qsThrowIfNotExists(modalElem, "#cancelRewriteProjectButton"); okButton.addEventListener("click", () => resolve(true)); cancelButton.addEventListener("click", () => resolve(false)); }); }; zipButton.addEventListener("click", async function () { const projectName = projectSelect.options[projectSelect.selectedIndex].value; const { floors, objects } = getCurrentMapData(); const fileContents: Record = {}; for (let i = 0; i < floors.length; i++) { const floorString = floors[i].map((row: string[]) => row.join("\t")).join("\n"); const floorFile = new TextEncoder().encode(floorString); fileContents[`${projectName}/floors/floor${i + 1}.txt`] = floorFile; } const flagsCsv = getFlagsCSVByStorage(projectSelect.value); if (flagsCsv != null) { const flagsFile = new TextEncoder().encode(flagsCsv); fileContents[`${projectName}/flags/flags.txt`] = flagsFile; } for (let i = 1; i <= Math.ceil(Object.keys(objects).length / 5); i++) { let objectStrings: string[] = []; for (let j = (i - 1) * 5 + 1; j <= i * 5; j++) { if (objects[j.toString()]) { objects[j.toString()].forEach((object) => { const objectData = [ object.no, j, object.x, object.y, `${j}:${object.x}:${object.y}`, object.image, object.event ?? "", object.wall ?? "", object.vars ?? "", ]; const objectString = objectData.join("\t"); objectStrings.push(objectString); }); } } const objectFile = new TextEncoder().encode(objectStrings.join("\n")); fileContents[`${projectName}/objects/objects${(i - 1) * 5 + 1}-${i * 5}.txt`] = objectFile; } const messagesCsv = getMessagesCSVByStorage(projectName); if (messagesCsv != null) { const objectFile = new TextEncoder().encode(messagesCsv); fileContents[`${projectName}/messages/messages.csv`] = objectFile; } const zippedContent: Uint8Array = await new Promise((resolve, reject) => { zip(fileContents, (err, data) => { if (err) { reject(err); } resolve(data); }); }); const blob = new Blob([zippedContent], { type: "application/zip" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${projectName}.zip`; a.click(); URL.revokeObjectURL(url); }); loadZipButton.addEventListener("click", function () { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = ".zip"; fileInput.onchange = function () { if (fileInput.files && fileInput.files.length > 0) { const file = fileInput.files[0]; const reader = new FileReader(); reader.onload = function () { if (reader.result) { const buffer = new Uint8Array(reader.result as ArrayBuffer); unzip(buffer, async function (err, data) { if (err) { console.error("zipからの読み込みに失敗しました", err); } else { const folderNames = Object.keys(data) .map((key) => { console.log(encoding.detect(key)); if (encoding.detect(key) === "SJIS") { const uint8array = encoding.stringToCode(key); const converted = encoding.convert(uint8array, { from: "SJIS", to: "UNICODE", }); return encoding.codeToString(converted); } else { return key; } }) .map((key) => key.split("/")[0]); if (folderNames.length === 0) { console.error("zipファイルの中にフォルダが存在しません"); return; } const projectName = folderNames[0]; const projects = loadProjects(); const index = projects.indexOf(projectName); if (index >= 0) { //プロジェクトが存在する場合は確認 const overwrite = await showRewriteProjectModal(); if (!overwrite) { return; } } else { //プロジェクトが存在しない場合は追加 addProject(projectName); } const decoder = new TextDecoder("utf-8"); const floors: FloorData[] = []; const objects: ObjectDataMap = {}; for (let i = 0; data[`${projectName}/floors/floor${i + 1}.txt`]; i++) { const floorFile = data[`${projectName}/floors/floor${i + 1}.txt`]; const floorString = decoder.decode(floorFile); const floor = floorString.split("\n").map((row) => row.split("\t")); floors.push(floor); window.localStorage.setItem( getMapFloorDataStorageKey(projectName, `${i + 1}`), JSON.stringify(floor) ); if (!objects[i + 1]) { objects[i + 1] = []; } } //上書きする場合、前のマップデータのフロア数が今回のものより大きい場合は、データが残らないよう超過分を削除 const beforeFloorLevel = loadFloorLevelByProject(projectName); if (beforeFloorLevel) { for (let i = floors.length + 1; i <= floorSelect.options.length; i++) { window.localStorage.removeItem( getMapFloorDataStorageKey(projectName, i.toString()) ); } } for ( let i = 1; data[`${projectName}/objects/objects${(i - 1) * 5 + 1}-${i * 5}.txt`]; i++ ) { const objectFile = data[`${projectName}/objects/objects${(i - 1) * 5 + 1}-${i * 5}.txt`]; const objectStrings = decoder.decode(objectFile).split("\n"); objectStrings.forEach((objectString) => { if (objectString != "") { const [no, floor, x, y, , image, event, wall, vars] = objectString.split("\t"); if (!objects[floor]) { objects[floor] = []; } objects[floor].push({ no: parseInt(no), x: parseInt(x), y: parseInt(y), image, event: event, wall: wall, vars: vars, }); } }); } window.localStorage.setItem( getMapObjectDataStorageKey(projectName), JSON.stringify(objects) ); window.localStorage.setItem(getFloorStorageKey(projectName), floors.length.toString()); selectProject(projectName); saveSelectedProject(); previousProject = projectName; resetFloorLevelDisplay(); await setCurrentMapData(); setFloorSizeInput(); const messagesFile = data[`${projectName}/messages/messages.csv`]; if (messagesFile == null) { console.warn("messages.csvが存在しません"); } else { const messagesString = decoder.decode(messagesFile); const messagesData = messagesString.replace(/(\n)(\d)/g, ", $2").split(", "); const messageLocalStorage: string[] = []; messagesData.map((message) => { const rows = message.split("\t"); for (let i = 0; i < 3; i++) { if (i === 2) { rows[i] = rows[i] .split('""') .map((e) => e.replace(/"/g, "")) .join('"'); } } const seri: string = rows[0]; const id: string = rows[1]; const messages: string = rows[2]; const messagesStrings = `${seri}\t${id}\t${messages}`; messageLocalStorage.push(messagesStrings); }); window.localStorage.setItem( getMessageStorageKey(projectName), messageLocalStorage.join(", ") ); } const flagsFile = data[`${projectName}/flags/flags.txt`]; if (flagsFile == null) { console.warn("flags.txtが存在しません"); } else { const flagsString = decoder.decode(flagsFile); window.localStorage.setItem(getFlagsDataStorageKey(projectName), flagsString); } } }); } }; reader.readAsArrayBuffer(file); } }; fileInput.click(); }); gameFocusButton.addEventListener("click", () => { controller.focusInputHandler(); }); } export function setObjectModalImageCharacter(character: string) { const editObjectModal = qsThrowIfNotExists(document, "#objectEditModal"); const modalImageInput = qsThrowIfNotExists(editObjectModal, 'input[name="image"]'); modalImageInput.value = character; const modalSubmitButton = qsThrowIfNotExists(editObjectModal, 'button[type="submit"]'); modalSubmitButton.disabled = false; }