Skip to content

Commit 645706d

Browse files
authored
Merge pull request #1 from hasanharman/codex/tile-only-assets-flexible-index
feat: migrate to tile-only assets with flexible tile indexing
1 parent f1318fe commit 645706d

539 files changed

Lines changed: 4089 additions & 149 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,40 @@ pnpm dev
7777
3. Right-click to erase tiles, and click-drag to paint quickly.
7878
4. Save/load maps locally or export your map as PNG.
7979

80+
## Tile Assets
81+
82+
- Runtime source: `public/tiles/<realm>/r<row>-c<col>.png`
83+
- Single tile sprite size: `130x230` pixels
84+
- Isometric placement footprint on canvas: `128x64` pixels
85+
- Optional index file: `public/tiles/manifest.json`
86+
87+
### Character Overlays
88+
89+
- Runtime source: `public/characters/<character-realm>/<character-id>.svg|png`
90+
- Character sprite canvas should also be `130x230` pixels (same as tile sprite canvas)
91+
- Recommended character figure footprint inside that canvas: around `44x88` pixels to `56x104` pixels
92+
- Anchor character feet near the tile anchor baseline (roughly the same visual base line as terrain sprites)
93+
- Characters render on top of terrain and occupy one tile cell
94+
- Included sample set: `public/characters/hobbits/hobbit-1.png` to `hobbit-8.png`
95+
96+
### Add new tile items (flexible rows/cols)
97+
98+
You can add any number of items per row. The app no longer assumes fixed `0..11` columns.
99+
100+
1. Add tile PNG files with this naming pattern:
101+
- `public/tiles/<realm>/r<row>-c<col>.png`
102+
- Examples: `r0-c12.png`, `r0-c19.png`, `r1-c5.png`
103+
2. Register each tile in the picker config at `lib/tiles.ts`:
104+
- Use the correct group `row` and tile `col`.
105+
- You do not need to pad groups with `"Empty"` placeholders.
106+
- You can add new groups with new `row` indices when needed.
107+
3. Restart the app (or refresh) and the tile will render in picker + canvas.
108+
109+
Notes:
110+
111+
- Runtime rendering supports variable row/col values and falls back to `r0-c0` if a specific tile image is missing.
112+
- Collection validation now accepts tile coordinates as non-negative integers (`row >= 0`, `col >= 0`).
113+
80114
## Collections
81115

82116
Shared maps are available at `/collections`.

app/page.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { Suspense } from "react";
22
import IsoCanvas from "@/components/iso-canvas";
3-
import TilePicker from "@/components/tile-picker";
3+
import AssetPicker from "@/components/asset-picker";
44
import Toolbar from "@/components/toolbar";
55
import CollectionLoader from "@/components/collection-loader";
66

7-
8-
97
export default function Home() {
108
return (
119
<main className="flex h-dvh flex-col overflow-hidden">
@@ -14,7 +12,7 @@ export default function Home() {
1412
</Suspense>
1513
<Toolbar />
1614
<IsoCanvas />
17-
<TilePicker />
15+
<AssetPicker />
1816
</main>
1917
);
2018
}

collections/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ Each tile in the map matrix is one of:
3131

3232
Ranges:
3333

34-
- `row`: `0..5`
35-
- `col`: `0..11`
34+
- `row`: integer `>= 0`
35+
- `col`: integer `>= 0`
3636
- `realm`: only for mixed mode, one of `shire`, `gondor`, `mordor`, `lothlorien`, `rohan`, `moria`, `rivendell`
3737

3838
## Validate locally

collections/schema/map.schema.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,17 @@
6767
"minItems": 2,
6868
"maxItems": 2,
6969
"prefixItems": [
70-
{ "type": "integer", "minimum": 0, "maximum": 5 },
71-
{ "type": "integer", "minimum": 0, "maximum": 11 }
70+
{ "type": "integer", "minimum": 0 },
71+
{ "type": "integer", "minimum": 0 }
7272
]
7373
},
7474
{
7575
"type": "array",
7676
"minItems": 3,
7777
"maxItems": 3,
7878
"prefixItems": [
79-
{ "type": "integer", "minimum": 0, "maximum": 5 },
80-
{ "type": "integer", "minimum": 0, "maximum": 11 },
79+
{ "type": "integer", "minimum": 0 },
80+
{ "type": "integer", "minimum": 0 },
8181
{
8282
"type": "string",
8383
"enum": [

components/asset-picker.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use client";
2+
3+
import TilePicker from "@/components/tile-picker";
4+
import CharacterPicker from "@/components/character-picker";
5+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6+
import { HugeiconsIcon } from "@hugeicons/react";
7+
import { Castle02Icon, UniversalAccessIcon } from "@hugeicons/core-free-icons";
8+
9+
export default function AssetPicker() {
10+
return (
11+
<div className="shrink-0 border-t bg-background">
12+
<Tabs defaultValue="buildings" className="gap-0">
13+
<div className="border-b px-2 py-2 sm:px-3">
14+
<TabsList>
15+
<TabsTrigger value="buildings">
16+
<HugeiconsIcon icon={Castle02Icon} />{" "}
17+
<span className="font-light text-xs">Buildings</span>
18+
</TabsTrigger>
19+
<TabsTrigger value="characters">
20+
<HugeiconsIcon icon={UniversalAccessIcon} />{" "}
21+
<span className="font-light text-xs">Characters</span>
22+
</TabsTrigger>
23+
</TabsList>
24+
</div>
25+
<TabsContent value="buildings" className="h-48">
26+
<TilePicker />
27+
</TabsContent>
28+
<TabsContent value="characters" className="h-48">
29+
<CharacterPicker />
30+
</TabsContent>
31+
</Tabs>
32+
</div>
33+
);
34+
}

components/character-picker.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use client";
2+
3+
import { useMemo } from "react";
4+
import { useMapStore } from "@/lib/store";
5+
import {
6+
CHARACTERS,
7+
CHARACTER_REALMS,
8+
getCharacterPath,
9+
} from "@/lib/characters";
10+
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
11+
import { cn } from "@/lib/utils";
12+
import {
13+
Tooltip,
14+
TooltipContent,
15+
TooltipProvider,
16+
TooltipTrigger,
17+
} from "@/components/ui/tooltip";
18+
import { MIXED_TEXTURE_PLACE_ID, TEXTURE_PLACES } from "@/lib/textures";
19+
20+
export default function CharacterPicker() {
21+
const { activeCharacterTool, setActiveCharacterTool, location } = useMapStore();
22+
const locationLabel =
23+
location === MIXED_TEXTURE_PLACE_ID
24+
? "Mixed"
25+
: (TEXTURE_PLACES.find((place) => place.id === location)?.label ?? "Shire");
26+
27+
const groupedCharacters = useMemo(
28+
() =>
29+
CHARACTER_REALMS.map((realm) => ({
30+
...realm,
31+
characters: CHARACTERS.filter((character) => character.realm === realm.id),
32+
})).filter((realm) => realm.characters.length > 0),
33+
[],
34+
);
35+
36+
return (
37+
<ScrollArea className="h-full w-full bg-background/80">
38+
<div className="flex flex-col gap-2 p-2 sm:p-3">
39+
<span className="px-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground whitespace-nowrap">
40+
{locationLabel}
41+
</span>
42+
<div className="flex gap-3 sm:gap-4">
43+
<TooltipProvider delayDuration={200}>
44+
{groupedCharacters.map((realm) => (
45+
<div key={realm.id} className="flex flex-col gap-1">
46+
<span className="px-1 text-[11px] font-semibold text-muted-foreground whitespace-nowrap sm:text-xs">
47+
{realm.label}
48+
</span>
49+
<div className="flex gap-1">
50+
{realm.characters.map((character) => {
51+
const isActive = activeCharacterTool === character.id;
52+
return (
53+
<Tooltip key={character.id}>
54+
<TooltipTrigger asChild>
55+
<button
56+
className={cn(
57+
"block h-[96px] w-[54px] shrink-0 overflow-hidden rounded-md border-2 transition-colors sm:h-[115px] sm:w-[65px]",
58+
isActive
59+
? "border-primary ring-2 ring-primary/30"
60+
: "border-transparent hover:border-muted-foreground/30",
61+
)}
62+
style={{
63+
backgroundImage: `url('${getCharacterPath(character.id)}')`,
64+
backgroundRepeat: "no-repeat",
65+
backgroundPosition: "center",
66+
backgroundSize: "cover",
67+
}}
68+
onClick={() => setActiveCharacterTool(character.id)}
69+
/>
70+
</TooltipTrigger>
71+
<TooltipContent side="top">
72+
<p>{character.label}</p>
73+
</TooltipContent>
74+
</Tooltip>
75+
);
76+
})}
77+
</div>
78+
</div>
79+
))}
80+
</TooltipProvider>
81+
</div>
82+
</div>
83+
<ScrollBar orientation="horizontal" />
84+
</ScrollArea>
85+
);
86+
}

components/collection-loader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ const isTile = (value: unknown): value is [number, number, string?] => {
2828
return false;
2929
}
3030
const [row, col, realm] = value;
31-
if (!Number.isInteger(row) || row < 0 || row > 5) return false;
32-
if (!Number.isInteger(col) || col < 0 || col > 11) return false;
31+
if (!Number.isInteger(row) || row < 0) return false;
32+
if (!Number.isInteger(col) || col < 0) return false;
3333
if (realm !== undefined && (typeof realm !== "string" || !REALMS.has(realm))) {
3434
return false;
3535
}

0 commit comments

Comments
 (0)