diff --git a/src/components/CostumesList.js b/src/components/CostumesList.js
new file mode 100644
index 0000000..11b852b
--- /dev/null
+++ b/src/components/CostumesList.js
@@ -0,0 +1,29 @@
+import ColumnListHeader from './ColumnListHeader';
+import ColumnListItem from './ColumnListItem';
+
+const CostumesList = ({ costumeSets, currentSetId, currentId }) => {
+ return (
+ <>
+ {costumeSets.map((costumeSet, costumeSetId) => (
+
+ Costume set {costumeSetId}
+ {costumeSet.sprdesc.map((unused, id) => {
+ const selected = costumeSetId === currentSetId && id === currentId;
+ const path = `/costumes/${costumeSetId}/${id}`;
+ const label = `Costume ${id}`;
+
+ return (
+
+ {label}
+
+ );
+ })}
+
+ ))}
+ >
+ );
+};
+
+export default CostumesList;
diff --git a/src/components/Header.js b/src/components/Header.js
index cad8dd5..76fd111 100644
--- a/src/components/Header.js
+++ b/src/components/Header.js
@@ -17,6 +17,7 @@ import meteor from '../assets/meteor.png';
const navigation = [
{ name: 'Screens', href: '/rooms/1' },
+ { name: 'Costumes', href: '/costumes/0/0' },
{ name: 'Gfx', href: '/roomgfx/0' },
{ name: 'Scripts', href: '/scripts/1' },
{ name: 'Prepositions', href: '/preps' },
diff --git a/src/containers/CostumeCanvasContainer.js b/src/containers/CostumeCanvasContainer.js
new file mode 100644
index 0000000..4479437
--- /dev/null
+++ b/src/containers/CostumeCanvasContainer.js
@@ -0,0 +1,147 @@
+import { useRef, useState, useEffect } from 'react';
+import { clsx } from 'clsx';
+import { getPalette } from '../lib/paletteUtils';
+
+// Display a costume on a canvas.
+
+// prettier-ignore
+const darkpalette = [
+ 0x2d, 0x1d, 0x2d, 0x3d,
+ 0x2d, 0x1d, 0x2d, 0x3d,
+ 0x2d, 0x1d, 0x2d, 0x3d,
+ 0x2d, 0x1d, 0x2d, 0x3d,
+];
+
+const CostumeCanvasContainer = ({
+ id,
+ frame,
+ gfx,
+ sprdesc,
+ sproffs,
+ sprlens,
+ sprdata,
+ sprpals,
+ zoom = 1,
+}) => {
+ const canvasRef = useRef(null);
+ const [isComputing, setIsComputing] = useState(true);
+
+ const desc = sprdesc[id];
+ // this was 3 bytes per sprite in the data but has been parsed down to 1 byte
+ const offset = sproffs[desc + frame] / 3;
+ const spritesNum = sprlens[desc + frame];
+ const palette = sprpals.palette;
+
+ // Compute the bounding box.
+ let left = 239;
+ let right = 0;
+ let top = 239;
+ let bottom = 0;
+ for (let i = 0; i < spritesNum; i++) {
+ const { x, y } = sprdata[offset + i];
+
+ left = Math.min(left, x);
+ right = Math.max(right, x + 8);
+ top = Math.min(top, y);
+ bottom = Math.max(bottom, y + 8);
+ }
+
+ const width = right - left;
+ const height = bottom - top;
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ const ctx = canvas.getContext('2d');
+
+ setTimeout(() => {
+ draw(
+ ctx,
+ gfx.gfx,
+ sprdata,
+ offset,
+ spritesNum,
+ palette,
+ left,
+ top,
+ width,
+ height,
+ );
+ setIsComputing(false);
+ });
+ }, [
+ frame,
+ gfx,
+ sprdata,
+ offset,
+ spritesNum,
+ palette,
+ left,
+ top,
+ width,
+ height,
+ ]);
+
+ return (
+
+ );
+};
+
+const draw = (
+ ctx,
+ gfx,
+ sprdata,
+ offset,
+ spritesNum,
+ palette,
+ left,
+ top,
+ width,
+ height,
+) => {
+ // Clear the canvas.
+ ctx.fillStyle = 'lightgrey';
+ ctx.fillRect(0, 0, width, height);
+
+ for (let i = 0; i < spritesNum; i++) {
+ const { x, y, tile, flip, paletteId } = sprdata[offset + i];
+
+ const pal = getPalette([
+ palette[paletteId],
+ palette[paletteId + 1],
+ palette[paletteId + 2],
+ palette[paletteId + 3],
+ ]);
+
+ for (let j = 0; j < 8; j++) {
+ const n1 = gfx[tile * 2 * 8 + j];
+ const n2 = gfx[(tile * 2 + 1) * 8 + j];
+ for (let k = 0; k < 8; k++) {
+ const mask = 1 << k;
+ const val = (n1 & mask ? 1 : 0) | ((n2 & mask ? 1 : 0) << 1);
+
+ // Skip the transparent palette colour.
+ if (val === 0) {
+ continue;
+ }
+
+ ctx.fillStyle = pal[val];
+ if (flip) {
+ ctx.fillRect(k + x - left, j + y - top, 1, 1);
+ } else {
+ ctx.fillRect(7 - k + x - left, j + y - top, 1, 1);
+ }
+ }
+ }
+ }
+};
+
+export default CostumeCanvasContainer;
diff --git a/src/containers/CostumesContainer.js b/src/containers/CostumesContainer.js
new file mode 100644
index 0000000..e3eb355
--- /dev/null
+++ b/src/containers/CostumesContainer.js
@@ -0,0 +1,93 @@
+import { useParams } from 'react-router-dom';
+import PrimaryColumn from '../components/PrimaryColumn';
+import CostumesList from '../components/CostumesList';
+import Main from '../components/Main';
+import MainHeader from '../components/MainHeader';
+import ResourceMetadata from '../components/ResourceMetadata';
+import CostumeCanvasContainer from './CostumeCanvasContainer';
+
+// @todo Parse it from 3DAED-3DB05 instead of hardcoding.
+// prettier-ignore
+const costumeIdLookupTable = [
+ 0x00, 0x03, 0x01, 0x06, 0x08,
+ 0x02, 0x00, 0x07, 0x0c, 0x04,
+ 0x09, 0x0a, 0x12, 0x0b, 0x14,
+ 0x0d, 0x11, 0x0f, 0x0e, 0x10,
+ 0x17, 0x00, 0x01, 0x05, 0x16,
+];
+
+const CostumesContainer = ({
+ costumegfx,
+ costumes,
+ sprpals,
+ sprdesc,
+ sproffs,
+ sprlens,
+ sprdata,
+}) => {
+ const { setId, id } = useParams();
+
+ const currentSetId =
+ typeof setId === 'undefined' ? null : parseInt(setId, 10);
+ const currentId = typeof id === 'undefined' ? null : parseInt(id, 10);
+ const costume =
+ costumes.find(({ metadata }) => metadata.id === currentId) || null;
+ const costumeId =
+ currentSetId === 0 ? costumeIdLookupTable[currentId] : currentId;
+
+ const getFramesNumbersFromCostumeId = (costumeId = 0) => {
+ if (costumeId === sprdesc[currentSetId].sprdesc.length - 1) {
+ // @todo Find a better way than hardcoding it.
+ return currentSetId === 0 ? 2 : 1;
+ }
+
+ return (
+ sprdesc[currentSetId].sprdesc[costumeId + 1] -
+ sprdesc[currentSetId].sprdesc[costumeId]
+ );
+ };
+
+ const frameNum = getFramesNumbersFromCostumeId(costumeId);
+
+ if (!costume) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {Array(frameNum)
+ .fill()
+ .map((unused, frame) => (
+
+ ))}
+
+
+ >
+ );
+};
+
+export default CostumesContainer;
diff --git a/src/containers/GfxContainer.js b/src/containers/GfxContainer.js
index 4661410..957dd98 100644
--- a/src/containers/GfxContainer.js
+++ b/src/containers/GfxContainer.js
@@ -41,6 +41,7 @@ const GfxContainer = ({ titlegfx, costumegfx, roomgfx }) => {
currentId={isRoomGfx ? currentId : null}
/>
+
{
lang={lang}
/>
+
diff --git a/src/containers/ResourceExplorer.js b/src/containers/ResourceExplorer.js
index 00cd0cf..2c3a17e 100644
--- a/src/containers/ResourceExplorer.js
+++ b/src/containers/ResourceExplorer.js
@@ -1,6 +1,7 @@
import { Routes, Route } from 'react-router-dom';
import { useRom } from '../contexts/RomContext';
import RoomsContainer from './RoomsContainer';
+import CostumesContainer from './CostumesContainer';
import GfxContainer from './GfxContainer';
import PrepositionsContainer from './PrepositionsContainer';
import RomMapContainer from './RomMapContainer';
@@ -17,6 +18,24 @@ const ResourceExplorer = () => {
return (
+
+ }>
+
+ }
+ />
+
{
/>
}>
}
/>
diff --git a/src/containers/ScriptContainer.js b/src/containers/ScriptContainer.js
index 1080e00..d7ac84f 100644
--- a/src/containers/ScriptContainer.js
+++ b/src/containers/ScriptContainer.js
@@ -19,6 +19,7 @@ const ScriptContainer = ({ scripts }) => {
currentId={currentId}
/>
+
{!script ? (
Scripts
diff --git a/src/containers/TitlesContainer.js b/src/containers/TitlesContainer.js
index ca83732..e391634 100644
--- a/src/containers/TitlesContainer.js
+++ b/src/containers/TitlesContainer.js
@@ -64,6 +64,7 @@ const TitlesContainer = ({ rooms, titles }) => {
titles={titles}
/>
+
{!title ? (
Titles
diff --git a/src/lib/parser/costumes/parseSprdesc.js b/src/lib/parser/costumes/parseSprdesc.js
index 6e4af57..f7013b8 100644
--- a/src/lib/parser/costumes/parseSprdesc.js
+++ b/src/lib/parser/costumes/parseSprdesc.js
@@ -13,8 +13,8 @@ const parseSprdesc = (arrayBuffer, i = 0, offset = 0) => {
sprdesc.push(parser.getUint16());
}
- // For some reason, the last element is a Uint8, probably unused.
- sprdesc.push(parser.getUint8());
+ // For some reason, the last element is an unused Uint8.
+ parser.getUint8();
const map = {
type: 'sprdesc',