Skip to content

Commit b8a7dad

Browse files
committed
wiki skins
1 parent 7d3791a commit b8a7dad

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import SkillCalculator from './pages/Calculators/SkillCalculator';
3232
import TreeCalculator from './pages/Calculators/TreeCalculator';
3333
import Verify from './pages/Verify';
3434
import ForgeWiki from './pages/ForgeWiki';
35+
import SkinsPage from './pages/Skins';
3536

3637
function App() {
3738
return (
@@ -70,6 +71,7 @@ function App() {
7071
<Route path="calculators/skills" element={<SkillCalculator />} />
7172
<Route path="calculators/tree" element={<TreeCalculator />} />
7273
<Route path="wiki/forge" element={<ForgeWiki />} />
74+
<Route path="skins" element={<SkinsPage />} />
7375
<Route path="*" element={<Home />} />
7476
</Route>
7577
</Routes>

src/components/Layout/Sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
4444
title: 'Wiki',
4545
items: [
4646
{ name: 'Items', path: '/items', icon: Shirt }, // Shirt as placeholder for Items/Chest
47+
{ name: 'Skins', path: '/skins', icon: Shirt },
4748
{ name: 'Pets', path: '/pets', icon: Cat },
4849
{ name: 'Mounts', path: '/mounts', icon: Star },
4950
{ name: 'Skills', path: '/skills', icon: Star },

src/pages/Skins.tsx

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { useMemo } from 'react';
2+
import { useGameData } from '../hooks/useGameData';
3+
import { Card } from '../components/UI/Card';
4+
import { GameIcon } from '../components/UI/GameIcon';
5+
6+
interface SkinStat {
7+
StatNode: {
8+
UniqueStat: {
9+
StatType: string;
10+
StatNature: string;
11+
};
12+
};
13+
MinValue: number;
14+
MaxValue: number;
15+
}
16+
17+
interface SkinEntry {
18+
SkinId: {
19+
Type: string;
20+
Idx: number;
21+
};
22+
PossibleStats: SkinStat[];
23+
SetId: string;
24+
MaxStatCount: number;
25+
}
26+
27+
// 4x4 Grid -> 16 slots.
28+
// Rows 1-2 are used = 8 icons.
29+
// Assuming sequential mapping based on the list order.
30+
const SPRITE_COLS = 4;
31+
const SPRITE_ROWS = 4;
32+
33+
export default function Skins() {
34+
const { data: skinsData, loading } = useGameData<Record<string, SkinEntry>>('SkinsLibrary.json');
35+
36+
const skins = useMemo(() => {
37+
if (!skinsData) return [];
38+
// Parse the weird keys like "{'Type': 'Helmet', 'Idx': 0}" to get structured data if needed,
39+
// but the value already contains SkinId.
40+
// We just need to sort them to ensure consistent mapping to the sprite.
41+
// Let's sort by Type then Idx, or just trusting the order?
42+
// Better to sort by Idx/Type to match logical groupings.
43+
// However, the sprite sheet order determines the mapping.
44+
// Based on common sprite generation, it's usually sequential.
45+
46+
// Let's try to parse the key order or just use the values.
47+
// The values have Type/Idx.
48+
// Let's sort by SetId (if valid) then Type/Idx?
49+
// Actually, looking at the JSON:
50+
// Helmet 0, Armour 0 (Santa)
51+
// Helmet 1, Armour 1 (Ski)
52+
// ...
53+
// It looks like pairs.
54+
55+
return Object.values(skinsData).sort((a, b) => {
56+
// Helper to get visual order for IDs based on user feedback (Sprite layout)
57+
// Sprite appears to be: Santa(0) -> Snowman(2) -> Ski(1) -> Others
58+
const getVisualOrder = (idx: number) => {
59+
if (idx === 0) return 0;
60+
if (idx === 2) return 1;
61+
if (idx === 1) return 2;
62+
return 10 + idx; // Others come after
63+
};
64+
65+
const orderA = getVisualOrder(a.SkinId.Idx);
66+
const orderB = getVisualOrder(b.SkinId.Idx);
67+
68+
if (orderA !== orderB) return orderA - orderB;
69+
70+
// Helper for Type order: Helmet before Armour (to match sprite Helmet -> Armour)
71+
// Helmet starts with 'H', Armour with 'A'.
72+
// We want Helmet first.
73+
const isHelmetA = a.SkinId.Type === 'Helmet';
74+
const isHelmetB = b.SkinId.Type === 'Helmet';
75+
76+
if (isHelmetA && !isHelmetB) return -1;
77+
if (!isHelmetA && isHelmetB) return 1;
78+
79+
return 0;
80+
});
81+
}, [skinsData]);
82+
83+
if (loading) {
84+
return <div className="p-8 text-center text-text-muted">Loading Skins...</div>;
85+
}
86+
87+
return (
88+
<div className="space-y-6 animate-fade-in">
89+
<div className="flex flex-col gap-2">
90+
<h1 className="text-3xl font-bold bg-gradient-to-r from-accent-primary to-accent-secondary bg-clip-text text-transparent">
91+
Skins Wiki
92+
</h1>
93+
<p className="text-text-secondary">
94+
Discover all available character skins and their potential stats.
95+
</p>
96+
</div>
97+
98+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
99+
{skins.map((skin, index) => {
100+
// Calculate sprite position
101+
const col = index % SPRITE_COLS;
102+
const row = Math.floor(index / SPRITE_COLS);
103+
const bgX = (col * 100) / (SPRITE_COLS - 1); // Percentage for CSS
104+
const bgY = (row * 100) / (SPRITE_ROWS - 1);
105+
106+
return (
107+
<Card key={`${skin.SkinId.Type}-${skin.SkinId.Idx}`} className="flex flex-col gap-4 overflow-hidden group">
108+
<div className="flex items-center gap-4">
109+
{/* Icon Container with Sprite */}
110+
<div
111+
className="w-16 h-16 rounded-lg border-2 border-border shadow-inner shrink-0 bg-bg-secondary"
112+
style={{
113+
backgroundImage: 'url(./Texture2D/SkinsUiIcons.png)',
114+
backgroundSize: '400% 400%', // 4 columns = 400%
115+
backgroundPosition: `${bgX}% ${bgY}%`,
116+
imageRendering: 'pixelated'
117+
}}
118+
/>
119+
<div className="flex flex-col">
120+
<span className="font-bold text-lg group-hover:text-accent-primary transition-colors">
121+
{skin.SkinId.Type}
122+
</span>
123+
<span className="text-xs text-text-muted bg-bg-input px-2 py-0.5 rounded-full w-fit">
124+
ID: {skin.SkinId.Idx}
125+
</span>
126+
{skin.SetId && (
127+
<span className="text-xs text-accent-tertiary mt-1">
128+
{skin.SetId}
129+
</span>
130+
)}
131+
</div>
132+
</div>
133+
134+
<div className="space-y-2 bg-bg-secondary/30 p-3 rounded-md">
135+
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
136+
Possible Stats (Max {skin.MaxStatCount})
137+
</div>
138+
{skin.PossibleStats.map((stat, i) => (
139+
<div key={i} className="flex items-center justify-between text-sm">
140+
<div className="flex items-center gap-1.5 text-text-secondary">
141+
<GameIcon name="swords" className="w-3 h-3 text-accent-primary" />
142+
<span>
143+
{stat.StatNode.UniqueStat.StatType}
144+
<span className="opacity-50 ml-1 text-xs">
145+
({stat.StatNode.UniqueStat.StatNature})
146+
</span>
147+
</span>
148+
</div>
149+
<span className="text-green-400 font-mono text-xs">
150+
{(stat.MinValue * 100).toFixed(0)}% - {(stat.MaxValue * 100).toFixed(0)}%
151+
</span>
152+
</div>
153+
))}
154+
</div>
155+
</Card>
156+
);
157+
})}
158+
</div>
159+
</div>
160+
);
161+
}

0 commit comments

Comments
 (0)