|
- import {NextPage} from 'next';
- import NextImage from 'next/image';
- import styles from '../styles/pages/index.module.css';
- import {GenerateMapForm} from '../components/GenerateMapForm';
- import getFormValues from '@theoryofnekomata/formxtra';
- import {
- ChangeEventHandler,
- FormEventHandler,
- MouseEventHandler,
- UIEventHandler,
- useEffect,
- useRef,
- useState,
- } from 'react';
- import {MapType, WaterType, WorldData} from '../utils/types';
- import WORLD_BACKGROUND from '../assets/gfx/world_background.png';
- import WORLD_FOREGROUND from '../assets/gfx/world_foreground.png';
- import {DropdownSelect} from '../components/DropdownSelect';
- import {ActionButton} from '../components/ActionButton';
-
- const IMAGE_URLS_INDEX = [
- MapType.BIOME,
- MapType.ELEVATION,
- MapType.WATER_MASK,
- MapType.BATHYMETRY,
- MapType.REAL_COLOR,
- ]
-
- const IMAGE_STACKING = [
- MapType.WATER_MASK,
- MapType.BATHYMETRY,
- MapType.ELEVATION,
- MapType.REAL_COLOR,
- MapType.BIOME,
- ]
-
- const loadImage = async (src: string, processTransparency = false) => {
- return new Promise<HTMLImageElement>((resolve, reject) => {
- const originalImage = new Image()
- originalImage.addEventListener('load', (e) => {
- const theOriginalImage = e.target as HTMLImageElement
-
- if (!processTransparency) {
- resolve(theOriginalImage)
- return
- }
-
- const tempCanvas = window.document.createElement('canvas')
- tempCanvas.width = theOriginalImage.width
- tempCanvas.height = theOriginalImage.height
- const context = tempCanvas.getContext('2d')
-
- if (!context) {
- reject()
- return
- }
-
- context.drawImage(e.target as HTMLImageElement, 0, 0)
- const oldImageData = context.getImageData(0, 0, theOriginalImage.width, theOriginalImage.height)
- const newImageData = context.createImageData(oldImageData)
- for (let y = 0; y < oldImageData.height; y += 1) {
- for (let x = 0; x < oldImageData.width; x += 1) {
- const i = ((y * oldImageData.width) + x) * 4;
- if (oldImageData.data[i] === 255 && oldImageData.data[i + 1] === 0 && oldImageData.data[i + 2] === 255) {
- newImageData.data[i] = 0
- newImageData.data[i + 1] = 0
- newImageData.data[i + 2] = 0
- newImageData.data[i + 3] = 0
- continue;
- }
-
- newImageData.data[i] = oldImageData.data[i]
- newImageData.data[i + 1] = oldImageData.data[i + 1]
- newImageData.data[i + 2] = oldImageData.data[i + 2]
- newImageData.data[i + 3] = oldImageData.data[i + 3]
- }
- }
- context.clearRect(0, 0, theOriginalImage.width, theOriginalImage.height)
- context.putImageData(newImageData, 0, 0)
-
- const modifiedImage = new Image()
- modifiedImage.addEventListener('load', (e2) => {
- resolve(e2.target as HTMLImageElement)
- })
-
- modifiedImage.addEventListener('error', () => {
- reject()
- })
-
- modifiedImage.src = tempCanvas.toDataURL()
- })
-
- originalImage.addEventListener('error', () => {
- reject()
- })
-
- originalImage.src = src
- })
- }
-
- type Data = {
- worldData: WorldData,
- baseDataImageUrls: string[],
- }
-
- const drawWater = (context: CanvasRenderingContext2D, backgroundImage: HTMLImageElement, spriteWaterLayer: number[][], x: number, y: number) => {
- const dx = x * 16;
- const dy = y * 16;
-
- // TODO animate water
- switch (spriteWaterLayer[y][x]) {
- case WaterType.LIGHT:
- context.drawImage(backgroundImage, 20 * 16, 0, 16, 16, dx, dy, 16, 16);
- return;
- case WaterType.LAVA:
- context.drawImage(backgroundImage, 24 * 16, 0, 16, 16, dx, dy, 16, 16);
- return;
- default:
- break;
- }
- context.drawImage(backgroundImage, 0, 0, 16, 16, dx, dy, 16, 16);
- }
-
- const drawLand = (context: CanvasRenderingContext2D, backgroundImage: HTMLImageElement, spriteBackgroundLayer: number[][], x: number, y: number) => {
- const tileIndex = spriteBackgroundLayer[y][x] % 60
- const tileVariant = Math.floor(spriteBackgroundLayer[y][x] / 60)
- const dx = x * 16;
- const dy = y * 16;
- let tx = 4 * tileVariant;
- let ty = 1;
-
- switch (tileIndex) {
- case 0:
- return;
- case 1:
- tx += 1;
- break;
- default:
- if (tileIndex < 30) {
- tx += Math.floor((tileIndex - 2) / 14);
- ty = (tileIndex - 2) % 14 + 2;
- } else if (tileIndex < 60) {
- tx += Math.floor(tileIndex / 15);
- ty = tileIndex % 15 + 1;
- }
- break;
- }
-
- context.drawImage(backgroundImage, tx * 16, ty * 16, 16, 16, dx, dy, 16, 16);
- }
-
- const drawObjects = (context: CanvasRenderingContext2D, foregroundImage: HTMLImageElement, spriteBackgroundLayer: number[][], x: number, y: number) => {
- const dx = x * 16;
- const dy = y * 16;
- let tx: number;
- let ty: number;
- if (spriteBackgroundLayer[y][x] < 900) {
- tx = (spriteBackgroundLayer[y][x] - 700) % 12
- ty = Math.floor((spriteBackgroundLayer[y][x] - 700) / 12)
- } else {
- tx = 13
- ty = spriteBackgroundLayer[y][x] - 900
- // fixme animated sprites in second column
- }
- context.drawImage(foregroundImage, tx * 16, ty * 16, 16, 16, dx, dy, 16, 16);
- }
-
- const drawWorld = async (canvas: HTMLCanvasElement, data: Data) => {
- const context = canvas.getContext('2d')
- if (!context) {
- return
- }
-
- context.clearRect(0, 0, canvas.width, canvas.height)
- const backgroundImage = await loadImage(WORLD_BACKGROUND.src, true)
- const foregroundImage = await loadImage(WORLD_FOREGROUND.src, true)
-
- for (let y = 0; y < data.worldData.height; y += 1) {
- for (let x = 0; x < data.worldData.width; x += 1) {
- drawWater(context, backgroundImage, data.worldData.spriteWaterLayer, x, y);
- drawLand(context, backgroundImage, data.worldData.spriteBackgroundLayer, x, y);
- drawObjects(context, foregroundImage, data.worldData.spriteForegroundLayer, x, y);
- }
- }
- }
-
- const createSuperMarioWarWorldFile = (worldData: WorldData) => {
- return `#Version
- ${worldData.version}
-
- #Music Category
- ${worldData.musicCategory}
-
- #Width
- ${worldData.width}
-
- #Height
- ${worldData.height}
-
- #Sprite Water Layer
- ${worldData.spriteWaterLayer.map(s => s.join(',')).join('\n')}
-
- #Sprite Background Layer
- ${worldData.spriteBackgroundLayer.map(s => s.join(',')).join('\n')}
-
- #Sprite Foreground Layer
- ${worldData.spriteForegroundLayer.map(s => s.join(',')).join('\n')}
-
- #Connections
- ${new Array(worldData.height).fill(new Array(worldData.width).fill('0').join(',')).join('\n')}
-
- #Tile Types (Stages, Doors, Start Tiles)
- ${new Array(worldData.height).fill(new Array(worldData.width).fill('0').join(',')).join('\n')}
-
- #Vehicle Boundaries
- ${new Array(worldData.height).fill(new Array(worldData.width).fill('0').join(',')).join('\n')}
-
- #Stages
- #Stage Type 0,Map,Mode,Goal,Points,Bonus List(Max 10),Name,End World, then mode settings (see sample tour file for details)
- #Stage Type 1,Bonus House Name,Sequential/Random Order,Display Text,Powerup List(Max 5)
- 0
-
- #Warps
- #location 1 x, y, location 2 x, y
- 0
-
- #Vehicles
- #Sprite,Stage Type, Start Column, Start Row, Min Moves, Max Moves, Sprite Paces, Sprite Direction, Boundary
- 0
-
- #Initial Items
- 0
- `
- }
-
- const IndexPage: NextPage = () => {
- const [data, setData] = useState<Data>()
- const [previewMapOpacity, setPreviewMapOpacity] = useState<Record<MapType, number>>(() => {
- return IMAGE_URLS_INDEX.reduce(
- (theValue, mapType) => {
- return {
- ...theValue,
- [mapType]: 1,
- }
- },
- {} as Record<MapType, number>
- );
- })
- const canvasRef = useRef<HTMLCanvasElement>(null)
- const baseMapScrollRef = useRef<HTMLDivElement>(null)
- const worldScrollRef = useRef<HTMLDivElement>(null)
- const scrollRef = useRef(false)
- const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
- e.preventDefault()
- const form = e.target as HTMLFormElement
- const values = getFormValues(form)
- const url = new URL('/api/generate', 'http://localhost:3000');
- const search = new URLSearchParams(values)
- url.search = search.toString()
- const response = await fetch(url.toString(), {
- method: 'GET',
- headers: {
- 'Accept': 'application/json',
- },
- })
- const responseData = await response.json()
- if (response.ok) {
- setData(responseData)
- }
- }
-
- const saveScreenshot: MouseEventHandler = async (e) => {
- e.preventDefault()
- if (!canvasRef.current) {
- return;
- }
- canvasRef.current.toBlob((blob) => {
- const url = URL.createObjectURL(blob as Blob)
- const a = window.document.createElement('a')
- a.href = url
- a.download = 'world.png';
- a.click()
- }, 'image/png');
- }
-
- const saveWorld: MouseEventHandler = async (e) => {
- e.preventDefault()
- if (!data) {
- return;
- }
-
- const blob = new Blob([
- createSuperMarioWarWorldFile(data.worldData)
- ], {
- type: 'text/plain',
- })
- const url = URL.createObjectURL(blob)
- const a = window.document.createElement('a')
- a.href = url
- a.download = 'world.txt';
- a.click()
- }
-
- const changePreviewMapOpacity: ChangeEventHandler<HTMLInputElement> = (e) => {
- const { currentTarget } = e
- const { value, name } = currentTarget
- setPreviewMapOpacity(oldMapOpacity => ({
- ...oldMapOpacity,
- [name as MapType]: value,
- }));
- }
-
- const handleBaseMapScroll: UIEventHandler<HTMLDivElement> = (e) => {
- if (!worldScrollRef.current) {
- return;
- }
-
- if (scrollRef.current) {
- scrollRef.current = false;
- return;
- }
-
- scrollRef.current = true;
- const target = e.currentTarget
- if (target.scrollHeight > target.clientHeight) {
- worldScrollRef.current.scrollTop = Math.floor(target.scrollTop / (target.scrollHeight - target.clientHeight) * (worldScrollRef.current.scrollHeight - worldScrollRef.current.clientHeight));
- }
- if (target.scrollWidth > target.clientWidth) {
- worldScrollRef.current.scrollLeft = Math.floor(target.scrollLeft / (target.scrollWidth - target.clientWidth) * (worldScrollRef.current.scrollWidth - worldScrollRef.current.clientWidth));
- }
- }
-
- const handleWorldScroll: UIEventHandler<HTMLDivElement> = (e) => {
- if (!baseMapScrollRef.current) {
- return;
- }
-
- if (scrollRef.current) {
- scrollRef.current = false;
- return;
- }
-
- scrollRef.current = true;
- const target = e.currentTarget
- if (target.scrollHeight > target.clientHeight) {
- baseMapScrollRef.current.scrollTop = Math.floor(target.scrollTop / (target.scrollHeight - target.clientHeight) * (baseMapScrollRef.current.scrollHeight - baseMapScrollRef.current.clientHeight));
- }
- if (target.scrollWidth > target.clientWidth) {
- baseMapScrollRef.current.scrollLeft = Math.floor(target.scrollLeft / (target.scrollWidth - target.clientWidth) * (baseMapScrollRef.current.scrollWidth - baseMapScrollRef.current.clientWidth));
- }
- }
-
- useEffect(() => {
- if (!data) {
- return
- }
-
- if (!canvasRef.current) {
- return
- }
-
- void drawWorld(canvasRef.current, data);
- }, [data, canvasRef])
-
- return (
- <div
- className={styles.base}
- >
- <div
- className={styles.ui}
- >
- <form
- className={styles.form}
- onSubmit={handleSubmit}
- >
- <GenerateMapForm />
- </form>
- </div>
- <div
- className={styles.map}
- >
- {
- data
- && (
- <>
- <div
- className={styles.mapScroll}
- onScroll={handleBaseMapScroll}
- ref={baseMapScrollRef}
- >
- {
- IMAGE_STACKING.map((v: MapType) => (
- <img
- key={v}
- src={data.baseDataImageUrls[IMAGE_URLS_INDEX.indexOf(v)]}
- alt={v}
- style={{
- opacity: previewMapOpacity[v],
- }}
- className={styles.baseMapCanvas}
- />
- ))
- }
- </div>
- <div
- className={styles.mapForm}
- >
- {
- IMAGE_STACKING.map((v: MapType) => (
- <div
- key={v}
- >
- <label>
- <span
- className={styles.fieldLabel}
- >
- {v}
- </span>
- <input
- type="range"
- min={0}
- max={1}
- step={0.01}
- defaultValue={1}
- name={v}
- onChange={changePreviewMapOpacity}
- />
- </label>
- </div>
- ))
- }
- </div>
- </>
- )
- }
- </div>
- <div
- className={styles.map}
- >
- {
- data
- && (
- <>
- <div
- className={styles.mapScroll}
- onScroll={handleWorldScroll}
- ref={worldScrollRef}
- >
- <canvas
- width={data.worldData.width * 16}
- height={data.worldData.height * 16}
- ref={canvasRef}
- className={styles.mapCanvas}
- />
- </div>
- <div
- className={styles.mapForm}
- >
- <ActionButton
- onClick={saveWorld}
- >
- Save World
- </ActionButton>
- {' '}
- <ActionButton
- onClick={saveScreenshot}
- >
- Save Screenshot
- </ActionButton>
- </div>
- </>
- )
- }
- </div>
- </div>
- )
- }
-
- export default IndexPage;
|