import { GameController } from "./game"; export type SpriteSheetType = "normal" | "autoTile" | "single"; export interface SpriteSheetData { src: string; sheetType: SpriteSheetType; characters: string[]; chipPos?: number; defaultChipPos?: number; } export interface SpriteSheet extends SpriteSheetData { image: HTMLImageElement; } export interface MapChip { x: number; y: number; originX: number; originY: number; character: string; } export class MapEditor { controller: GameController; spriteSheets: SpriteSheet[] = []; selectedChip: MapChip | null = null; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; chipSize: number; chips: Array = []; sheetX: number = 0; sheetY: number = 0; initialized: boolean = false; constructor( spriteSheetData: SpriteSheetData[], chipSize: number, controller: GameController, canvas: HTMLCanvasElement ) { this.controller = controller; this.canvas = canvas; this.ctx = this.canvas.getContext("2d")!; this.chipSize = chipSize; let loadedImageCount = 0; spriteSheetData.forEach((data, index) => { const spriteSheetImage = new Image(); spriteSheetImage.src = data.src; const spriteSheet = { src: data.src, sheetType: data.sheetType, characters: data.characters, image: spriteSheetImage, chipPos: data.chipPos, defaultChipPos: data.defaultChipPos, }; this.spriteSheets.push(spriteSheet); spriteSheetImage.onload = () => { if (!["autoTile", "single"].includes(spriteSheet.sheetType)) { const chipCount = (spriteSheetImage.width / chipSize) * (spriteSheetImage.height / chipSize); if (chipCount !== data.characters.length) { throw new Error( `読み込んだチップの数 (${chipCount}) と文字列配列の要素数 (${data.characters.length}) が一致しませんでした。(シート:${spriteSheet.src})` ); } } loadedImageCount++; if (loadedImageCount === spriteSheetData.length) { this.resizeCanvas(); this.drawAllChips(); } }; }); window.addEventListener("resize", () => this.resizeCanvas()); if (this.canvas.parentElement) { const resizeObserver = new ResizeObserver((entries) => { for (let entry of entries) { if (entry.target === this.canvas.parentElement) { this.resizeCanvas(); } } }); resizeObserver.observe(this.canvas.parentElement); } this.canvas.addEventListener("click", (e) => { this.selectChip(e); }); this.controller.addContentLoadEvent(() => { this.initialized = true; }); this.loadSavedSelectedChip(); this.drawAllChips(); } resizeCanvas() { this.canvas.width = this.canvas.parentElement?.clientWidth || window.innerWidth; this.drawAllChips(); } drawAllChips() { this.sheetX = 0; this.sheetY = 0; this.chips = []; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.spriteSheets.forEach((spriteSheet) => { this.drawChips(spriteSheet); }); if (this.selectedChip) { this.searchAndSelectChipByCharacter(this.selectedChip.character); } } drawChips(spriteSheet: SpriteSheet) { let charIndex = 0; //autoTileは仮仕様で最初のチップしか読み込まない const limit = spriteSheet.sheetType === "autoTile" ? 1 : Infinity; for (let sy = 0; sy < spriteSheet.image.height && charIndex < limit; sy += this.chipSize) { for (let sx = 0; sx < spriteSheet.image.width && charIndex < limit; sx += this.chipSize) { if ( spriteSheet.characters[charIndex] === "" || (spriteSheet.sheetType === "single" && spriteSheet.chipPos !== charIndex) ) { charIndex++; continue; } let character = spriteSheet.characters[charIndex]; if (spriteSheet.sheetType === "single" && spriteSheet.chipPos === charIndex) { character = spriteSheet.characters[0]; } this.ctx.drawImage( spriteSheet.image, sx, sy, this.chipSize, this.chipSize, this.sheetX, this.sheetY, this.chipSize, this.chipSize ); // 初期フォントサイズをセット let fontSize = 12; this.ctx.font = `${fontSize}px Arial`; let textMetrics = this.ctx.measureText(character); // 文字列がchipSizeを超えていると、フォントサイズを小さくする while (textMetrics.width > this.chipSize && fontSize > 0) { fontSize -= 0.5; this.ctx.font = `${fontSize}px Arial`; textMetrics = this.ctx.measureText(character); } this.ctx.strokeStyle = "black"; this.ctx.lineWidth = 2; this.ctx.strokeText(character, this.sheetX + fontSize / 5, this.sheetY + fontSize); this.ctx.fillStyle = "white"; this.ctx.fillText(character, this.sheetX + fontSize / 5, this.sheetY + fontSize); const chip = { x: this.sheetX, y: this.sheetY, originX: sx, originY: sy, character: character, }; this.chips.push(chip); if (this.selectedChip == null && spriteSheet.defaultChipPos === charIndex) { this.selectedChip = chip; } this.sheetX += this.chipSize; if (this.sheetX + this.chipSize > this.canvas.width) { this.sheetX = 0; this.sheetY += this.chipSize; } charIndex++; } } } searchAndSelectChipByCharacter(character: string) { let isSelected = false; // 末尾の "?" を削除 const selectedCharacter = character.endsWith("?") ? character.slice(0, -1) : character; for (let i = 0; i < this.chips.length; i++) { let chip = this.chips[i]; //同じ文字のチップがあった場合に処理 if (chip.character === selectedCharacter) { //選択中チップ上書き this.selectedChip = chip; this.chipSelect(); this.drawSelectionBox(chip.x, chip.y); isSelected = true; break; } } if (!isSelected) { this.selectedChip = null; } } selectChip(e: MouseEvent) { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const offsetX = Math.floor(x / this.chipSize) * this.chipSize; const offsetY = Math.floor(y / this.chipSize) * this.chipSize; const chip = this.chips.find((chip) => chip.x === offsetX && chip.y === offsetY); if (chip) { this.selectedChip = chip; this.drawAllChips(); this.saveSelectedChipToLocalStorage(); } } getSelectedChip(): MapChip | null { return this.selectedChip; } chipSelect() { if (this.initialized) { if (this.selectedChip) { this.controller.sendMessageEvent({ type: "editFloorChip", image: this.selectedChip.character }); } } else { this.controller.addContentLoadEvent(() => { if (this.selectedChip) { this.controller.sendMessageEvent({ type: "editFloorChip", image: this.selectedChip.character }); } }); } } drawSelectionBox(offsetX: number, offsetY: number) { this.ctx.strokeStyle = "black"; this.ctx.lineWidth = 6; this.ctx.strokeRect(offsetX, offsetY, this.chipSize, this.chipSize); this.ctx.strokeStyle = "red"; this.ctx.lineWidth = 2; this.ctx.strokeRect(offsetX, offsetY, this.chipSize, this.chipSize); } saveSelectedChipToLocalStorage() { if (this.selectedChip) { localStorage.setItem("selectedChip", JSON.stringify(this.selectedChip)); } } loadSavedSelectedChip() { const savedSelectedChip = localStorage.getItem("selectedChip"); if (savedSelectedChip) { this.selectedChip = JSON.parse(savedSelectedChip); } } }