Skip to content

Commit

Permalink
Display parsed costumes 👔. (#22)
Browse files Browse the repository at this point in the history
* [WIP] Add a page to view the costumes.
* Partial fix for costume parsing (#21)
* Partial fix for costume parsing
* A few refinements
* Minor refactoring.
* Set the number of frames.
* Fix sprites rendering.
* Improve display.
* Bit of cleaning.
* Add support for different costume sets.

---------

Co-authored-by: Gamaiel Zavala <[email protected]>
  • Loading branch information
gmarty and gzip authored Jun 15, 2024
1 parent eaf99e5 commit d8db4e6
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 10 deletions.
29 changes: 29 additions & 0 deletions src/components/CostumesList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ColumnListHeader from './ColumnListHeader';
import ColumnListItem from './ColumnListItem';

const CostumesList = ({ costumeSets, currentSetId, currentId }) => {
return (
<>
{costumeSets.map((costumeSet, costumeSetId) => (
<div key={costumeSetId}>
<ColumnListHeader>Costume set {costumeSetId}</ColumnListHeader>
{costumeSet.sprdesc.map((unused, id) => {
const selected = costumeSetId === currentSetId && id === currentId;
const path = `/costumes/${costumeSetId}/${id}`;
const label = `Costume ${id}`;

return (
<ColumnListItem
key={id}
path={selected ? null : path}>
{label}
</ColumnListItem>
);
})}
</div>
))}
</>
);
};

export default CostumesList;
1 change: 1 addition & 0 deletions src/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
147 changes: 147 additions & 0 deletions src/containers/CostumeCanvasContainer.js
Original file line number Diff line number Diff line change
@@ -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 (
<canvas
ref={canvasRef}
width={width}
height={height}
className={clsx(
'rounded',
isComputing ? 'opacity-0' : 'opacity-100 transition-opacity',
)}
style={{ width: width * zoom, height: height * zoom }}
/>
);
};

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;
93 changes: 93 additions & 0 deletions src/containers/CostumesContainer.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<PrimaryColumn>
<CostumesList
costumeSets={sprdesc}
currentSetId={currentSetId}
currentId={currentId}
/>
</PrimaryColumn>

<Main>
<MainHeader title={`Costume ${currentId}`}>
<ResourceMetadata metadata={costume.metadata} />
</MainHeader>
<div className="flex flex-row flex-wrap gap-4">
{Array(frameNum)
.fill()
.map((unused, frame) => (
<CostumeCanvasContainer
key={frame}
id={costumeId}
frame={frame}
gfx={costumegfx[currentSetId]}
sprdesc={sprdesc[currentSetId].sprdesc}
sproffs={sproffs[currentSetId].sproffs}
sprlens={sprlens[currentSetId].sprlens}
sprdata={sprdata[currentSetId].sprdata}
sprpals={sprpals[currentSetId]}
zoom={2}
/>
))}
</div>
</Main>
</>
);
};

export default CostumesContainer;
1 change: 1 addition & 0 deletions src/containers/GfxContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const GfxContainer = ({ titlegfx, costumegfx, roomgfx }) => {
currentId={isRoomGfx ? currentId : null}
/>
</PrimaryColumn>

<Main>
<MainHeader
title={
Expand Down
1 change: 1 addition & 0 deletions src/containers/PrepositionsContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const PrepositionsContainer = ({ preps, lang }) => {
lang={lang}
/>
</PrimaryColumn>

<Main>
<MainHeader title="Prepositions">
<ResourceMetadata metadata={preps.metadata} />
Expand Down
45 changes: 37 additions & 8 deletions src/containers/ResourceExplorer.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,6 +18,24 @@ const ResourceExplorer = () => {

return (
<Routes>
<Route
path="/titles"
element={
<TitlesContainer
rooms={resources.rooms}
titles={resources.titles}
/>
}>
<Route
path=":id"
element={
<TitlesContainer
rooms={resources.rooms}
titles={resources.titles}
/>
}
/>
</Route>
<Route
path="/rooms"
element={
Expand All @@ -40,19 +59,29 @@ const ResourceExplorer = () => {
/>
</Route>
<Route
path="/titles"
path="/costumes"
element={
<TitlesContainer
rooms={resources.rooms}
titles={resources.titles}
<CostumesContainer
costumegfx={resources.costumegfx}
costumes={resources.costumes}
sprpals={resources.sprpals}
sprdesc={resources.sprdesc}
sprlens={resources.sprlens}
sproffs={resources.sproffs}
sprdata={resources.sprdata}
/>
}>
<Route
path=":id"
path=":setId/:id"
element={
<TitlesContainer
rooms={resources.rooms}
titles={resources.titles}
<CostumesContainer
costumegfx={resources.costumegfx}
costumes={resources.costumes}
sprpals={resources.sprpals}
sprdesc={resources.sprdesc}
sprlens={resources.sprlens}
sproffs={resources.sproffs}
sprdata={resources.sprdata}
/>
}
/>
Expand Down
1 change: 1 addition & 0 deletions src/containers/ScriptContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const ScriptContainer = ({ scripts }) => {
currentId={currentId}
/>
</PrimaryColumn>

<Main>
{!script ? (
<h1>Scripts</h1>
Expand Down
1 change: 1 addition & 0 deletions src/containers/TitlesContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const TitlesContainer = ({ rooms, titles }) => {
titles={titles}
/>
</PrimaryColumn>

<Main>
{!title ? (
<h1>Titles</h1>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/parser/costumes/parseSprdesc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit d8db4e6

Please sign in to comment.