Skip to content

Commit

Permalink
Better unit testing, add items, download .ter files
Browse files Browse the repository at this point in the history
  • Loading branch information
LachlanBWWright committed Dec 15, 2024
1 parent 46c23e7 commit 8fd04ad
Show file tree
Hide file tree
Showing 17 changed files with 1,030 additions and 74 deletions.
721 changes: 720 additions & 1 deletion frontend/package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/jest": "^29.5.14",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand All @@ -35,11 +36,13 @@
"eslint-plugin-react-refresh": "^0.4.9",
"gh-pages": "^6.2.0",
"globals": "^15.9.0",
"jsdom": "^25.0.1",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1",
"vitest": "^2.1.8"
"vitest": "^2.1.8",
"vitest-canvas-mock": "^0.3.3"
}
}
1 change: 1 addition & 0 deletions frontend/src/data/items/itemAtoms.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { atom } from "jotai";

export const SelectedItem = atom<number | undefined>(undefined);
export const ClickToAddItem = atom<number | undefined>(undefined);
3 changes: 3 additions & 0 deletions frontend/src/data/tiles/tileAtoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { atom } from "jotai";

export const SelectedTile = atom<number>(0);
42 changes: 39 additions & 3 deletions frontend/src/editor/EditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { Stage } from "react-konva";
import { Updater, useImmer } from "use-immer";

import { FenceMenu } from "./subviews/fences/FenceMenu";
import { useSetAtom } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import { SelectedFence } from "../data/fences/fenceAtoms";
import { Items } from "./subviews/Items";
import { ItemMenu } from "./subviews/items/ItemMenu";
import { Splines } from "./subviews/Splines";
import { SplineMenu } from "./subviews/splines/SplineMenu";
import { SelectedItem } from "../data/items/itemAtoms";
import { ClickToAddItem, SelectedItem } from "../data/items/itemAtoms";
import { SelectedSpline } from "../data/splines/splineAtoms";
import { WaterBodies } from "./subviews/WaterBodies";
import { WaterMenu } from "./subviews/water/WaterMenu";
Expand All @@ -35,10 +35,12 @@ export function EditorView({
data,
setData,
mapImages,
setMapImages,
}: {
data: ottoMaticLevel;
setData: Updater<ottoMaticLevel>;
mapImages: HTMLCanvasElement[];
setMapImages: (newCanvases: HTMLCanvasElement[]) => void;
}) {
const [view, setView] = useState<View>(View.fences);
const [stage, setStage] = useImmer({
Expand All @@ -50,6 +52,7 @@ export function EditorView({
const setSelectedItem = useSetAtom(SelectedItem);
const setSelectedSpline = useSetAtom(SelectedSpline);
const setSelectedWaterBody = useSetAtom(SelectedWaterBody);
const clickToAddItem = useAtomValue(ClickToAddItem);
console.log(data);
const zoomIn = () =>
setStage((stage) => {
Expand Down Expand Up @@ -109,7 +112,14 @@ export function EditorView({
{view === View.water && <WaterMenu data={data} setData={setData} />}
{view === View.items && <ItemMenu data={data} setData={setData} />}
{view === View.splines && <SplineMenu data={data} setData={setData} />}
{view === View.tiles && <TileMenu />}
{view === View.tiles && (
<TileMenu
data={data}
setData={setData}
mapImages={mapImages}
setMapImages={setMapImages}
/>
)}
{view === View.topology && <TopologyMenu />}
</div>
<Stage
Expand All @@ -120,6 +130,32 @@ export function EditorView({
x={stage.x}
y={stage.y}
draggable={true}
onClick={(e) => {
console.log("TEST");
if (clickToAddItem === undefined) return;
const stage = e.target.getStage();

const pos = stage?.getRelativePointerPosition();
if (!pos) return;
const x = Math.round(pos.x);
const z = Math.round(pos.y);
console.log("SETDATA", x, z);
console.log(data.Itms[1000].obj);
console.log(data.Itms[1000].obj.length);

setData((data) => {
data.Itms[1000].obj.push({
x: x,
z: z,
type: clickToAddItem,
flags: 0,
p0: 0,
p1: 0,
p2: 0,
p3: 0,
});
});
}}
onDblClick={() => {
setSelectedFence(undefined);
setSelectedItem(undefined);
Expand Down
25 changes: 17 additions & 8 deletions frontend/src/editor/MapPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ottoPreprocessor, {
newJsonProcess,
} from "../data/preprocessors/ottoPreprocessor";
import { lzssCompress } from "../utils/lzss";
import { imageDataToSixteenBit } from "../utils/imageConverter";

export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) {
const [data, setData] = useImmer<ottoMaticLevel | null>(null);
Expand All @@ -24,9 +25,6 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) {
const [mapImages, setMapImages] = useState<HTMLCanvasElement[] | undefined>(
undefined,
);
const [mapImagesData, setMapImagesData] = useState<ArrayBuffer[] | undefined>(
undefined,
);
const [processed, setProcessed] = useState(false);
useEffect(() => {
const loadMap = async () => {
Expand Down Expand Up @@ -72,21 +70,30 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) {
downloadLink.click();

//Download Images
if (!mapImagesData) return;
if (!mapImages) return;

//TODO: Hardcoded values that will break for other games / if actual compression is implemented
const imageSize =
OTTO_SUPERTILE_TEXMAP_SIZE * OTTO_SUPERTILE_TEXMAP_SIZE * 2;
const compressedImageSize = imageSize + Math.ceil(imageSize / 8);
const imageDownloadBuffer = new DataView(
new ArrayBuffer(mapImagesData.length * (4 + compressedImageSize)),
new ArrayBuffer(mapImages.length * (4 + compressedImageSize)),
);
for (let i = 0; i < mapImagesData.length; i++) {
for (let i = 0; i < mapImages.length; i++) {
const pos = i * (compressedImageSize + 4);
//New dataview
//Output file has 32-bit size headers before each image, image is size^2 2-byte pixels
imageDownloadBuffer.setInt32(pos, compressedImageSize);
const decompressed = lzssCompress(new DataView(mapImagesData[i]));
const canvasCtx = mapImages[i].getContext("2d");
if (!canvasCtx) throw new Error("Could not get canvas context");
const decompressed = lzssCompress(
imageDataToSixteenBit(
canvasCtx.getImageData(0, 0, mapImages[i].width, mapImages[i].height)
.data,
),
);

//const decompressed = lzssCompress(new DataView(mapImagesData[i]));
for (let j = 0; j < decompressed.byteLength; j++) {
imageDownloadBuffer.setUint8(pos + 4 + j, decompressed.getUint8(j));
}
Expand All @@ -108,7 +115,6 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) {
setMapFile={setMapFile}
setMapImagesFile={setMapImagesFile}
setMapImages={setMapImages}
setMapImagesData={setMapImagesData}
/>
);
return (
Expand All @@ -118,6 +124,8 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) {
onClick={() => {
setMapFile(undefined);
setData(null);
setMapImages(undefined);
setMapImagesFile(undefined);
}}
>
←New Map
Expand All @@ -140,6 +148,7 @@ export function MapPrompt({ pyodide }: { pyodide: PyodideInterface }) {
data={data}
setData={setData as Updater<ottoMaticLevel>}
mapImages={mapImages}
setMapImages={setMapImages}
/>
) : (
<p>Loading...</p>
Expand Down
43 changes: 9 additions & 34 deletions frontend/src/editor/UploadPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Button } from "../components/Button";
import { FileUpload } from "../components/FileUpload";
import { lzssDecompress } from "../utils/lzss";
import { OTTO_SUPERTILE_TEXMAP_SIZE } from "../python/structSpecs/ottoMaticInterface";
import { sixteenBitToImageData } from "../utils/imageConverter";

//import level1Url from "./assets/ottoMatic/terrain/EarthFarm.ter.rsrc?url";

Expand All @@ -10,13 +11,11 @@ export function UploadPrompt({
setMapFile,
setMapImagesFile,
setMapImages,
setMapImagesData,
}: {
mapFile: File | undefined;
setMapFile: (file: File) => void;
setMapImagesFile: (file: File) => void;
setMapImages: (images: HTMLCanvasElement[]) => void;
setMapImagesData: (images: ArrayBuffer[]) => void;
}) {
const useFile = async (url: string) => {
const rsrcName = url + ".rsrc"; //.ter to .ter.rsrc
Expand All @@ -32,11 +31,10 @@ export function UploadPrompt({
const imgFile = new File([img], url.split("/").pop() ?? "");
const imgBuffer = await imgFile.arrayBuffer();
const imgDataView = new DataView(imgBuffer);
const [mapImages, mapImagesData] = loadMapImages(imgDataView);
const mapImages = loadMapImages(imgDataView);

setMapImagesFile(imgFile);
setMapImages(mapImages);
setMapImagesData(mapImagesData);
};

return (
Expand Down Expand Up @@ -66,10 +64,9 @@ export function UploadPrompt({
//Uses Big Endian by default - Which is what Otto uses
const dataView = new DataView(buffer);

const [mapImages, mapImagesData] = loadMapImages(dataView);
const mapImages = loadMapImages(dataView);
setMapImagesFile(mapImagesFile);
setMapImages(mapImages);
setMapImagesData(mapImagesData);
}}
/>
</div>
Expand Down Expand Up @@ -123,9 +120,7 @@ export function UploadPrompt({
);
}

function loadMapImages(
dataView: DataView,
): [HTMLCanvasElement[], ArrayBuffer[]] {
function loadMapImages(dataView: DataView): HTMLCanvasElement[] {
let offset = 0;
let numSupertiles = 0;

Expand Down Expand Up @@ -164,35 +159,15 @@ function loadMapImages(
const imageData = imgCtx?.getImageData(
0,
0,
OTTO_SUPERTILE_TEXMAP_SIZE,
OTTO_SUPERTILE_TEXMAP_SIZE,
{
//colorSpace: "srgb",
},
imgCanvas.width,
imgCanvas.height,
);

if (!imageData) {
throw new Error("Could not create image data");
}
for (
let i = 0;
i < OTTO_SUPERTILE_TEXMAP_SIZE * OTTO_SUPERTILE_TEXMAP_SIZE;
i++
) {
const data = decompressedBuffer.getUint16(i * 2);

//A RRRRR GGGGG BBBBB
//R - 0111 1100 0000 0000
//G - 0000 0011 1110 0000
//B - 0000 0000 0001 1111
const r = ((data & 0x7c00) >> 10) * 8;
const g = ((data & 0x03e0) >> 5) * 8;
const b = (data & 0x001f) * 8;
imageData.data[4 * i] = Math.floor(r); //(data >> 8) & 0xff;
imageData.data[4 * i + 1] = Math.floor(g); //(data >> 16) & 0xff;
imageData.data[4 * i + 2] = Math.floor(b); /* & 0x80 */ //data & 0xff;
imageData.data[4 * i + 3] = data & 0x8000 ? 255 : 255; //0xff;
}

sixteenBitToImageData(decompressedBuffer, imageData);

if (!imgCtx) {
throw new Error("Bad data!");
Expand All @@ -204,5 +179,5 @@ function loadMapImages(
imgCanvas;
mapImages.push(imgCanvas);
}
return [mapImages, mapImagesData];
return mapImages;
}
3 changes: 2 additions & 1 deletion frontend/src/editor/subviews/Items.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ottoMaticLevel } from "../../python/structSpecs/ottoMaticInterface";
import { Layer } from "react-konva";
import { Layer, Rect } from "react-konva";
import { Updater } from "use-immer";
import { Item } from "./items/Item";

Expand All @@ -14,6 +14,7 @@ export function Items({

return (
<Layer>
<Rect />
{data.Itms[1000].obj.map((_, itemIdx) => (
<Item key={itemIdx} data={data} setData={setData} itemIdx={itemIdx} />
))}
Expand Down
55 changes: 39 additions & 16 deletions frontend/src/editor/subviews/Tiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
OTTO_SUPERTILE_TEXMAP_SIZE,
ottoMaticLevel,
} from "../../python/structSpecs/ottoMaticInterface";
import { Layer, Image } from "react-konva";
import { useMemo } from "react";
import { Layer, Image, Rect } from "react-konva";
import { Fragment, useMemo } from "react";
import { SelectedTile } from "../../data/tiles/tileAtoms";
import { useAtom } from "jotai";

export function Tiles({
data,
Expand All @@ -13,7 +15,8 @@ export function Tiles({
data: ottoMaticLevel;
mapImages: HTMLCanvasElement[];
}) {
//if (!data.Itms) return <></>;
//if (!data.Itms) return <></>;\
const [selectedTile, setSelectedTile] = useAtom(SelectedTile);
const header = data.Hedr[1000].obj;
const supertilesWide = header.mapWidth / OTTO_SUPERTILE_SIZE;
const superTileGrid = data.STgd[1000].obj;
Expand All @@ -29,19 +32,39 @@ export function Tiles({

return (
<Layer>
{imageGrid.map((img, i) => (
<Image
key={i}
image={img}
x={
(i * OTTO_SUPERTILE_TEXMAP_SIZE) %
(OTTO_SUPERTILE_TEXMAP_SIZE * supertilesWide)
}
y={Math.floor(i / supertilesWide) * OTTO_SUPERTILE_TEXMAP_SIZE}
width={OTTO_SUPERTILE_TEXMAP_SIZE}
height={OTTO_SUPERTILE_TEXMAP_SIZE}
/>
))}
{imageGrid.map((img, i) => {
const isSelected = selectedTile === i;

return (
<Fragment key={i}>
<Image
image={superTileGrid[i].superTileId === 0 ? undefined : img}
onClick={() => setSelectedTile(i)}
x={
(i * OTTO_SUPERTILE_TEXMAP_SIZE) %
(OTTO_SUPERTILE_TEXMAP_SIZE * supertilesWide)
}
y={Math.floor(i / supertilesWide) * OTTO_SUPERTILE_TEXMAP_SIZE}
width={OTTO_SUPERTILE_TEXMAP_SIZE}
height={OTTO_SUPERTILE_TEXMAP_SIZE}
fill={isSelected ? "red" : ""}
/>
{isSelected && (
<Rect
onClick={() => setSelectedTile(i)}
x={
(i * OTTO_SUPERTILE_TEXMAP_SIZE) %
(OTTO_SUPERTILE_TEXMAP_SIZE * supertilesWide)
}
y={Math.floor(i / supertilesWide) * OTTO_SUPERTILE_TEXMAP_SIZE}
width={OTTO_SUPERTILE_TEXMAP_SIZE}
height={OTTO_SUPERTILE_TEXMAP_SIZE}
stroke="red"
/>
)}
</Fragment>
);
})}
</Layer>
);
}
Loading

0 comments on commit 8fd04ad

Please sign in to comment.