mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-23 08:32:51 +00:00
fix: audio on GNU/Linux
This commit is contained in:
@@ -13,10 +13,21 @@ export default function PckEditorView() {
|
|||||||
const [openedPath, setOpenedPath] = useState<string | null>(null);
|
const [openedPath, setOpenedPath] = useState<string | null>(null);
|
||||||
const [selectedAssetId, setSelectedAssetId] = useState<string | null>(null);
|
const [selectedAssetId, setSelectedAssetId] = useState<string | null>(null);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isEditingProperty, setIsEditingProperty] = useState<{ idx: number, key: string, val: string } | null>(null);
|
const [isEditingProperty, setIsEditingProperty] = useState<{
|
||||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
idx: number;
|
||||||
const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null);
|
key: string;
|
||||||
const [showTypeModal, setShowTypeModal] = useState<{ file: { name: string, data: Uint8Array } } | null>(null);
|
val: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [notification, setNotification] = useState<{
|
||||||
|
message: string;
|
||||||
|
type: "success" | "error";
|
||||||
|
} | null>(null);
|
||||||
|
const [showTypeModal, setShowTypeModal] = useState<{
|
||||||
|
file: { name: string; data: Uint8Array };
|
||||||
|
} | null>(null);
|
||||||
const [isChangingType, setIsChangingType] = useState(false);
|
const [isChangingType, setIsChangingType] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const replaceInputRef = useRef<HTMLInputElement>(null);
|
const replaceInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -32,8 +43,12 @@ export default function PckEditorView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const root: Record<string, TempNode> = {};
|
const root: Record<string, TempNode> = {};
|
||||||
pck.files.forEach(asset => {
|
pck.files.forEach((asset) => {
|
||||||
if (searchTerm && !asset.path.toLowerCase().includes(searchTerm.toLowerCase())) return;
|
if (
|
||||||
|
searchTerm &&
|
||||||
|
!asset.path.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
return;
|
||||||
const parts = asset.path.split("/");
|
const parts = asset.path.split("/");
|
||||||
let currentLevel = root;
|
let currentLevel = root;
|
||||||
let currentPath = "";
|
let currentPath = "";
|
||||||
@@ -46,7 +61,7 @@ export default function PckEditorView() {
|
|||||||
path: currentPath,
|
path: currentPath,
|
||||||
isFolder: !isLast,
|
isFolder: !isLast,
|
||||||
asset: isLast ? asset : undefined,
|
asset: isLast ? asset : undefined,
|
||||||
children: {}
|
children: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
currentLevel = currentLevel[part].children;
|
currentLevel = currentLevel[part].children;
|
||||||
@@ -60,9 +75,9 @@ export default function PckEditorView() {
|
|||||||
if (!a.isFolder && b.isFolder) return 1;
|
if (!a.isFolder && b.isFolder) return 1;
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
})
|
})
|
||||||
.map(node => ({
|
.map((node) => ({
|
||||||
...node,
|
...node,
|
||||||
children: convert(node.children)
|
children: convert(node.children),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,13 +85,24 @@ export default function PckEditorView() {
|
|||||||
}, [pck, searchTerm]);
|
}, [pck, searchTerm]);
|
||||||
|
|
||||||
const selectedAsset = useMemo(() => {
|
const selectedAsset = useMemo(() => {
|
||||||
return pck?.files.find(f => f.id === selectedAssetId) || null;
|
return pck?.files.find((f) => f.id === selectedAssetId) || null;
|
||||||
}, [pck, selectedAssetId]);
|
}, [pck, selectedAssetId]);
|
||||||
|
|
||||||
const [assetPreview, setAssetPreview] = useState<{ id: string, url: string } | null>(null);
|
const [assetPreview, setAssetPreview] = useState<{
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedAsset || ![PCKAssetType.SKIN, PCKAssetType.CAPE, PCKAssetType.TEXTURE, PCKAssetType.SKIN_DATA].includes(selectedAsset.type)) {
|
if (
|
||||||
|
!selectedAsset ||
|
||||||
|
![
|
||||||
|
PCKAssetType.SKIN,
|
||||||
|
PCKAssetType.CAPE,
|
||||||
|
PCKAssetType.TEXTURE,
|
||||||
|
PCKAssetType.SKIN_DATA,
|
||||||
|
].includes(selectedAsset.type)
|
||||||
|
) {
|
||||||
setAssetPreview(null);
|
setAssetPreview(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,7 +116,10 @@ export default function PckEditorView() {
|
|||||||
};
|
};
|
||||||
}, [selectedAsset]);
|
}, [selectedAsset]);
|
||||||
|
|
||||||
const assetPreviewUrl = (assetPreview && selectedAsset && assetPreview.id === selectedAsset.id) ? assetPreview.url : null;
|
const assetPreviewUrl =
|
||||||
|
assetPreview && selectedAsset && assetPreview.id === selectedAsset.id
|
||||||
|
? assetPreview.url
|
||||||
|
: null;
|
||||||
|
|
||||||
const toggleFolder = (path: string) => {
|
const toggleFolder = (path: string) => {
|
||||||
const next = new Set(expandedFolders);
|
const next = new Set(expandedFolders);
|
||||||
@@ -100,7 +129,7 @@ export default function PckEditorView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderTree = (nodes: any[], depth = 0) => {
|
const renderTree = (nodes: any[], depth = 0) => {
|
||||||
return nodes.map(node => {
|
return nodes.map((node) => {
|
||||||
const isExpanded = expandedFolders.has(node.path) || !!searchTerm;
|
const isExpanded = expandedFolders.has(node.path) || !!searchTerm;
|
||||||
const isSelected = selectedAssetId === node.asset?.id;
|
const isSelected = selectedAssetId === node.asset?.id;
|
||||||
return (
|
return (
|
||||||
@@ -115,14 +144,19 @@ export default function PckEditorView() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{ paddingLeft: `${depth * 16 + 12}px` }}
|
style={{ paddingLeft: `${depth * 16 + 12}px` }}
|
||||||
className={`flex items-center gap-2 p-2 cursor-pointer transition-all border-l-2 ${isSelected
|
className={`flex items-center gap-2 p-2 cursor-pointer transition-all border-l-2 ${
|
||||||
? "bg-[#FFFF55]/10 border-[#FFFF55] text-[#FFFF55]"
|
isSelected
|
||||||
: "border-transparent hover:bg-white/5 text-white"
|
? "bg-[#FFFF55]/10 border-[#FFFF55] text-[#FFFF55]"
|
||||||
} ${node.isFolder ? "font-bold" : ""}`}
|
: "border-transparent hover:bg-white/5 text-white"
|
||||||
|
} ${node.isFolder ? "font-bold" : ""}`}
|
||||||
>
|
>
|
||||||
{node.isFolder ? (
|
{node.isFolder ? (
|
||||||
<img
|
<img
|
||||||
src={isExpanded ? "/images/Settings_Arrow_Down.png" : "/images/Settings_Arrow_Right.png"}
|
src={
|
||||||
|
isExpanded
|
||||||
|
? "/images/Settings_Arrow_Down.png"
|
||||||
|
: "/images/Settings_Arrow_Right.png"
|
||||||
|
}
|
||||||
className="w-3 h-3 object-contain opacity-80"
|
className="w-3 h-3 object-contain opacity-80"
|
||||||
style={{ imageRendering: "pixelated" }}
|
style={{ imageRendering: "pixelated" }}
|
||||||
/>
|
/>
|
||||||
@@ -130,7 +164,11 @@ export default function PckEditorView() {
|
|||||||
<div className="w-3" />
|
<div className="w-3" />
|
||||||
)}
|
)}
|
||||||
<img
|
<img
|
||||||
src={node.isFolder ? "/images/Folder_Icon.png" : "/images/tools/pck.png"}
|
src={
|
||||||
|
node.isFolder
|
||||||
|
? "/images/Folder_Icon.png"
|
||||||
|
: "/images/tools/pck.png"
|
||||||
|
}
|
||||||
className={`w-4 h-4 object-contain ${isSelected ? "" : "grayscale opacity-60"}`}
|
className={`w-4 h-4 object-contain ${isSelected ? "" : "grayscale opacity-60"}`}
|
||||||
style={{ imageRendering: "pixelated" }}
|
style={{ imageRendering: "pixelated" }}
|
||||||
/>
|
/>
|
||||||
@@ -179,7 +217,7 @@ export default function PckEditorView() {
|
|||||||
endianness: "little",
|
endianness: "little",
|
||||||
xmlSupport: false,
|
xmlSupport: false,
|
||||||
properties: ["ANIM", "BOX"],
|
properties: ["ANIM", "BOX"],
|
||||||
files: []
|
files: [],
|
||||||
};
|
};
|
||||||
setPck(newPck);
|
setPck(newPck);
|
||||||
setOpenedPath(null);
|
setOpenedPath(null);
|
||||||
@@ -188,7 +226,10 @@ export default function PckEditorView() {
|
|||||||
showNotification("New PCK Created");
|
showNotification("New PCK Created");
|
||||||
};
|
};
|
||||||
|
|
||||||
const showNotification = (message: string, type: "success" | "error" = "success") => {
|
const showNotification = (
|
||||||
|
message: string,
|
||||||
|
type: "success" | "error" = "success",
|
||||||
|
) => {
|
||||||
setNotification({ message, type });
|
setNotification({ message, type });
|
||||||
setTimeout(() => setNotification(null), 3000);
|
setTimeout(() => setNotification(null), 3000);
|
||||||
};
|
};
|
||||||
@@ -196,7 +237,11 @@ export default function PckEditorView() {
|
|||||||
const handleExportAsset = async (asset: PCKAsset) => {
|
const handleExportAsset = async (asset: PCKAsset) => {
|
||||||
try {
|
try {
|
||||||
const fileName = asset.path.split("/").pop() || "asset";
|
const fileName = asset.path.split("/").pop() || "asset";
|
||||||
const path = await TauriService.saveFileDialog("Export Asset", fileName, []);
|
const path = await TauriService.saveFileDialog(
|
||||||
|
"Export Asset",
|
||||||
|
fileName,
|
||||||
|
[],
|
||||||
|
);
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
playPressSound();
|
playPressSound();
|
||||||
await TauriService.writeBinaryFile(path, asset.data);
|
await TauriService.writeBinaryFile(path, asset.data);
|
||||||
@@ -209,8 +254,8 @@ export default function PckEditorView() {
|
|||||||
const handleDeleteAsset = (id: string) => {
|
const handleDeleteAsset = (id: string) => {
|
||||||
if (!pck) return;
|
if (!pck) return;
|
||||||
playBackSound();
|
playBackSound();
|
||||||
const newFiles = pck.files.filter(f => f.id !== id);
|
const newFiles = pck.files.filter((f) => f.id !== id);
|
||||||
const assetPath = pck.files.find(f => f.id === id)?.path;
|
const assetPath = pck.files.find((f) => f.id === id)?.path;
|
||||||
setPck({ ...pck, files: newFiles });
|
setPck({ ...pck, files: newFiles });
|
||||||
if (selectedAssetId === id) setSelectedAssetId(newFiles[0]?.id || null);
|
if (selectedAssetId === id) setSelectedAssetId(newFiles[0]?.id || null);
|
||||||
showNotification(`Deleted: ${assetPath?.split("/").pop()}`);
|
showNotification(`Deleted: ${assetPath?.split("/").pop()}`);
|
||||||
@@ -225,7 +270,9 @@ export default function PckEditorView() {
|
|||||||
const buffer = await file.arrayBuffer();
|
const buffer = await file.arrayBuffer();
|
||||||
const data = new Uint8Array(buffer);
|
const data = new Uint8Array(buffer);
|
||||||
|
|
||||||
const newFiles = pck.files.map(f => f.id === selectedAssetId ? { ...f, data, size: data.length } : f);
|
const newFiles = pck.files.map((f) =>
|
||||||
|
f.id === selectedAssetId ? { ...f, data, size: data.length } : f,
|
||||||
|
);
|
||||||
setPck({ ...pck, files: newFiles });
|
setPck({ ...pck, files: newFiles });
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
showNotification("Asset Replaced");
|
showNotification("Asset Replaced");
|
||||||
@@ -251,7 +298,7 @@ export default function PckEditorView() {
|
|||||||
type,
|
type,
|
||||||
size: file.data.length,
|
size: file.data.length,
|
||||||
data: file.data,
|
data: file.data,
|
||||||
properties: []
|
properties: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === PCKAssetType.SKIN || type === PCKAssetType.CAPE) {
|
if (type === PCKAssetType.SKIN || type === PCKAssetType.CAPE) {
|
||||||
@@ -266,7 +313,7 @@ export default function PckEditorView() {
|
|||||||
|
|
||||||
const handlePropertyEdit = (idx: number, newVal: string, isKey = false) => {
|
const handlePropertyEdit = (idx: number, newVal: string, isKey = false) => {
|
||||||
if (!pck || !selectedAssetId) return;
|
if (!pck || !selectedAssetId) return;
|
||||||
const newFiles = pck.files.map(f => {
|
const newFiles = pck.files.map((f) => {
|
||||||
if (f.id === selectedAssetId) {
|
if (f.id === selectedAssetId) {
|
||||||
const newProps = [...f.properties];
|
const newProps = [...f.properties];
|
||||||
if (isKey) {
|
if (isKey) {
|
||||||
@@ -287,11 +334,11 @@ export default function PckEditorView() {
|
|||||||
const handleAddProperty = () => {
|
const handleAddProperty = () => {
|
||||||
if (!pck || !selectedAssetId) return;
|
if (!pck || !selectedAssetId) return;
|
||||||
playPressSound();
|
playPressSound();
|
||||||
const newFiles = pck.files.map(f => {
|
const newFiles = pck.files.map((f) => {
|
||||||
if (f.id === selectedAssetId) {
|
if (f.id === selectedAssetId) {
|
||||||
return {
|
return {
|
||||||
...f,
|
...f,
|
||||||
properties: [...f.properties, { key: "NEW_PROPERTY", value: "0" }]
|
properties: [...f.properties, { key: "NEW_PROPERTY", value: "0" }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return f;
|
return f;
|
||||||
@@ -302,7 +349,7 @@ export default function PckEditorView() {
|
|||||||
const handleRemoveProperty = (idx: number) => {
|
const handleRemoveProperty = (idx: number) => {
|
||||||
if (!pck || !selectedAssetId) return;
|
if (!pck || !selectedAssetId) return;
|
||||||
playBackSound();
|
playBackSound();
|
||||||
const newFiles = pck.files.map(f => {
|
const newFiles = pck.files.map((f) => {
|
||||||
if (f.id === selectedAssetId) {
|
if (f.id === selectedAssetId) {
|
||||||
const newProps = [...f.properties];
|
const newProps = [...f.properties];
|
||||||
newProps.splice(idx, 1);
|
newProps.splice(idx, 1);
|
||||||
@@ -316,7 +363,7 @@ export default function PckEditorView() {
|
|||||||
const handleTypeChange = (newType: PCKAssetType) => {
|
const handleTypeChange = (newType: PCKAssetType) => {
|
||||||
if (!pck || !selectedAssetId) return;
|
if (!pck || !selectedAssetId) return;
|
||||||
playPressSound();
|
playPressSound();
|
||||||
const newFiles = pck.files.map(f => {
|
const newFiles = pck.files.map((f) => {
|
||||||
if (f.id === selectedAssetId) {
|
if (f.id === selectedAssetId) {
|
||||||
return { ...f, type: newType };
|
return { ...f, type: newType };
|
||||||
}
|
}
|
||||||
@@ -325,11 +372,11 @@ export default function PckEditorView() {
|
|||||||
setPck({ ...pck, files: newFiles });
|
setPck({ ...pck, files: newFiles });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoveAsset = (direction: 'up' | 'down') => {
|
const handleMoveAsset = (direction: "up" | "down") => {
|
||||||
if (!pck || !selectedAssetId) return;
|
if (!pck || !selectedAssetId) return;
|
||||||
const idx = pck.files.findIndex(f => f.id === selectedAssetId);
|
const idx = pck.files.findIndex((f) => f.id === selectedAssetId);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
const newIdx = direction === 'up' ? idx - 1 : idx + 1;
|
const newIdx = direction === "up" ? idx - 1 : idx + 1;
|
||||||
if (newIdx < 0 || newIdx >= pck.files.length) return;
|
if (newIdx < 0 || newIdx >= pck.files.length) return;
|
||||||
playPressSound();
|
playPressSound();
|
||||||
const newFiles = [...pck.files];
|
const newFiles = [...pck.files];
|
||||||
@@ -340,7 +387,7 @@ export default function PckEditorView() {
|
|||||||
const handleRenameAsset = (id: string, newPath: string) => {
|
const handleRenameAsset = (id: string, newPath: string) => {
|
||||||
if (!pck) return;
|
if (!pck) return;
|
||||||
playPressSound();
|
playPressSound();
|
||||||
const newFiles = pck.files.map(f => {
|
const newFiles = pck.files.map((f) => {
|
||||||
if (f.id === id) {
|
if (f.id === id) {
|
||||||
return { ...f, path: newPath };
|
return { ...f, path: newPath };
|
||||||
}
|
}
|
||||||
@@ -359,9 +406,11 @@ export default function PckEditorView() {
|
|||||||
showNotification("Exporting all assets...");
|
showNotification("Exporting all assets...");
|
||||||
for (const asset of pck.files) {
|
for (const asset of pck.files) {
|
||||||
const parts = asset.path.split("/");
|
const parts = asset.path.split("/");
|
||||||
let currentPath = baseFolder;
|
|
||||||
const fileName = parts.join("_");
|
const fileName = parts.join("_");
|
||||||
await TauriService.writeBinaryFile(`${baseFolder}/${fileName}`, asset.data);
|
await TauriService.writeBinaryFile(
|
||||||
|
`${baseFolder}/${fileName}`,
|
||||||
|
asset.data,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
showNotification("All Assets Exported");
|
showNotification("All Assets Exported");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -377,7 +426,11 @@ export default function PckEditorView() {
|
|||||||
try {
|
try {
|
||||||
let targetPath = openedPath;
|
let targetPath = openedPath;
|
||||||
if (!targetPath) {
|
if (!targetPath) {
|
||||||
targetPath = await TauriService.saveFileDialog("Save PCK", pck.files.length > 0 ? "new.pck" : "empty.pck", ["pck"]);
|
targetPath = await TauriService.saveFileDialog(
|
||||||
|
"Save PCK",
|
||||||
|
pck.files.length > 0 ? "new.pck" : "empty.pck",
|
||||||
|
["pck"],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetPath) {
|
if (targetPath) {
|
||||||
@@ -409,15 +462,24 @@ export default function PckEditorView() {
|
|||||||
|
|
||||||
const getTypeColor = (type: PCKAssetType) => {
|
const getTypeColor = (type: PCKAssetType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case PCKAssetType.SKIN: return "#FFFF55";
|
case PCKAssetType.SKIN:
|
||||||
case PCKAssetType.SKIN_DATA: return "#FFFF55";
|
return "#FFFF55";
|
||||||
case PCKAssetType.CAPE: return "#AA00AA";
|
case PCKAssetType.SKIN_DATA:
|
||||||
case PCKAssetType.TEXTURE: return "#55FFFF";
|
return "#FFFF55";
|
||||||
case PCKAssetType.AUDIO_DATA: return "#55FF55";
|
case PCKAssetType.CAPE:
|
||||||
case PCKAssetType.UI_DATA: return "#FFAA00";
|
return "#AA00AA";
|
||||||
case PCKAssetType.LOCALISATION: return "#FF55FF";
|
case PCKAssetType.TEXTURE:
|
||||||
case PCKAssetType.MODELS: return "#5555FF";
|
return "#55FFFF";
|
||||||
default: return "#AAAAAA";
|
case PCKAssetType.AUDIO_DATA:
|
||||||
|
return "#55FF55";
|
||||||
|
case PCKAssetType.UI_DATA:
|
||||||
|
return "#FFAA00";
|
||||||
|
case PCKAssetType.LOCALISATION:
|
||||||
|
return "#FF55FF";
|
||||||
|
case PCKAssetType.MODELS:
|
||||||
|
return "#5555FF";
|
||||||
|
default:
|
||||||
|
return "#AAAAAA";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -440,22 +502,31 @@ export default function PckEditorView() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleFileLoad}
|
onClick={handleFileLoad}
|
||||||
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Open PCK
|
Open PCK
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleNewPCK}
|
onClick={handleNewPCK}
|
||||||
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
New PCK
|
New PCK
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleExportAll}
|
onClick={handleExportAll}
|
||||||
disabled={!pck || pck.files.length === 0}
|
disabled={!pck || pck.files.length === 0}
|
||||||
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${(!pck || pck.files.length === 0) ? "opacity-50 grayscale" : ""}`}
|
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!pck || pck.files.length === 0 ? "opacity-50 grayscale" : ""}`}
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Export All
|
Export All
|
||||||
</button>
|
</button>
|
||||||
@@ -463,7 +534,10 @@ export default function PckEditorView() {
|
|||||||
onClick={handleSavePCK}
|
onClick={handleSavePCK}
|
||||||
disabled={!pck}
|
disabled={!pck}
|
||||||
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!pck ? "opacity-50 grayscale" : ""}`}
|
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!pck ? "opacity-50 grayscale" : ""}`}
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Save PCK
|
Save PCK
|
||||||
</button>
|
</button>
|
||||||
@@ -474,42 +548,86 @@ export default function PckEditorView() {
|
|||||||
<div className="w-full flex gap-4 px-8 mb-4">
|
<div className="w-full flex gap-4 px-8 mb-4">
|
||||||
<div className="flex gap-6 bg-black/40 border-2 border-[#373737] p-3 w-full">
|
<div className="flex gap-6 bg-black/40 border-2 border-[#373737] p-3 w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">Endianness:</span>
|
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">
|
||||||
|
Endianness:
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => { playPressSound(); setPck({ ...pck, endianness: pck.endianness === "little" ? "big" : "little" }); }}
|
onClick={() => {
|
||||||
|
playPressSound();
|
||||||
|
setPck({
|
||||||
|
...pck,
|
||||||
|
endianness: pck.endianness === "little" ? "big" : "little",
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="text-[#FFFF55] text-sm uppercase hover:underline"
|
className="text-[#FFFF55] text-sm uppercase hover:underline"
|
||||||
>
|
>
|
||||||
{pck.endianness}
|
{pck.endianness}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">XML Support:</span>
|
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">
|
||||||
|
XML Support:
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => { playPressSound(); setPck({ ...pck, xmlSupport: !pck.xmlSupport }); }}
|
onClick={() => {
|
||||||
|
playPressSound();
|
||||||
|
setPck({ ...pck, xmlSupport: !pck.xmlSupport });
|
||||||
|
}}
|
||||||
className={`${pck.xmlSupport ? "text-[#FFFF55]" : "text-white/20"} text-sm uppercase hover:underline`}
|
className={`${pck.xmlSupport ? "text-[#FFFF55]" : "text-white/20"} text-sm uppercase hover:underline`}
|
||||||
>
|
>
|
||||||
{pck.xmlSupport ? "Enabled" : "Disabled"}
|
{pck.xmlSupport ? "Enabled" : "Disabled"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 ml-auto">
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">Version:</span>
|
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">
|
||||||
|
Version:
|
||||||
|
</span>
|
||||||
<span className="text-white text-sm">{pck.version}</span>
|
<span className="text-white text-sm">{pck.version}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input type="file" ref={replaceInputRef} onChange={handleReplaceAsset} className="hidden" />
|
<input
|
||||||
<input type="file" ref={addAssetInputRef} onChange={handleAddAsset} className="hidden" />
|
type="file"
|
||||||
|
ref={replaceInputRef}
|
||||||
|
onChange={handleReplaceAsset}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={addAssetInputRef}
|
||||||
|
onChange={handleAddAsset}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
{!pck ? (
|
{!pck ? (
|
||||||
<div className="flex-1 w-full flex flex-col items-center justify-center p-12"
|
<div
|
||||||
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
|
className="flex-1 w-full flex flex-col items-center justify-center p-12"
|
||||||
<img src="/images/tools/pck.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} />
|
style={{
|
||||||
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open a PCK file to begin editing</h3>
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/tools/pck.png"
|
||||||
|
className="w-32 h-32 mb-8 opacity-20 grayscale"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
|
<h3 className="text-2xl text-white/40 mc-text-shadow italic">
|
||||||
|
Open a PCK file to begin editing
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 w-full flex gap-4 overflow-hidden">
|
<div className="flex-1 w-full flex gap-4 overflow-hidden">
|
||||||
<div className="w-2/3 flex flex-col p-4" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
|
<div
|
||||||
|
className="w-2/3 flex flex-col p-4"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="mb-4 flex gap-4">
|
<div className="mb-4 flex gap-4">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -521,7 +639,10 @@ export default function PckEditorView() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => addAssetInputRef.current?.click()}
|
onClick={() => addAssetInputRef.current?.click()}
|
||||||
className="px-4 py-2 text-white mc-text-shadow text-sm shrink-0"
|
className="px-4 py-2 text-white mc-text-shadow text-sm shrink-0"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Add Asset
|
Add Asset
|
||||||
</button>
|
</button>
|
||||||
@@ -530,11 +651,22 @@ export default function PckEditorView() {
|
|||||||
{renderTree(treeData)}
|
{renderTree(treeData)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/3 flex flex-col p-6 overflow-y-auto" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
|
<div
|
||||||
|
className="w-1/3 flex flex-col p-6 overflow-y-auto"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{!selectedAsset ? (
|
{!selectedAsset ? (
|
||||||
<div className="flex-1 flex flex-col items-center justify-center text-white/20 italic gap-4">
|
<div className="flex-1 flex flex-col items-center justify-center text-white/20 italic gap-4">
|
||||||
<img src="/images/tools/pck.png" className="w-16 h-16 opacity-10 grayscale" style={{ imageRendering: "pixelated" }} />
|
<img
|
||||||
|
src="/images/tools/pck.png"
|
||||||
|
className="w-16 h-16 opacity-10 grayscale"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
<span>Select an asset to view details</span>
|
<span>Select an asset to view details</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -554,8 +686,14 @@ export default function PckEditorView() {
|
|||||||
onClick={() => setIsChangingType(!isChangingType)}
|
onClick={() => setIsChangingType(!isChangingType)}
|
||||||
className="flex items-center gap-1.5 px-2 py-0.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-sm transition-all group/type"
|
className="flex items-center gap-1.5 px-2 py-0.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-sm transition-all group/type"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] uppercase tracking-widest mc-text-shadow font-bold" style={{ color: getTypeColor(selectedAsset.type) }}>
|
<span
|
||||||
{PCKAssetType[selectedAsset.type].replace(/_/g, " ")}
|
className="text-[10px] uppercase tracking-widest mc-text-shadow font-bold"
|
||||||
|
style={{ color: getTypeColor(selectedAsset.type) }}
|
||||||
|
>
|
||||||
|
{PCKAssetType[selectedAsset.type].replace(
|
||||||
|
/_/g,
|
||||||
|
" ",
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<img
|
<img
|
||||||
src="/images/Settings_Arrow_Down.png"
|
src="/images/Settings_Arrow_Down.png"
|
||||||
@@ -580,16 +718,21 @@ export default function PckEditorView() {
|
|||||||
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
||||||
className="absolute top-full left-0 mt-2 z-[130] p-2 min-w-[180px] grid grid-cols-1 gap-1 shadow-2xl"
|
className="absolute top-full left-0 mt-2 z-[130] p-2 min-w-[180px] grid grid-cols-1 gap-1 shadow-2xl"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: "url('/images/frame_background.png')",
|
backgroundImage:
|
||||||
|
"url('/images/frame_background.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
imageRendering: "pixelated"
|
imageRendering: "pixelated",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.keys(PCKAssetType)
|
{Object.keys(PCKAssetType)
|
||||||
.filter(k => isNaN(Number(k)))
|
.filter((k) => isNaN(Number(k)))
|
||||||
.map((typeName) => {
|
.map((typeName) => {
|
||||||
const typeVal = PCKAssetType[typeName as keyof typeof PCKAssetType];
|
const typeVal =
|
||||||
const isActive = selectedAsset.type === typeVal;
|
PCKAssetType[
|
||||||
|
typeName as keyof typeof PCKAssetType
|
||||||
|
];
|
||||||
|
const isActive =
|
||||||
|
selectedAsset.type === typeVal;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={typeName}
|
key={typeName}
|
||||||
@@ -597,13 +740,20 @@ export default function PckEditorView() {
|
|||||||
handleTypeChange(typeVal);
|
handleTypeChange(typeVal);
|
||||||
setIsChangingType(false);
|
setIsChangingType(false);
|
||||||
}}
|
}}
|
||||||
className={`w-full text-left px-3 py-2 text-[10px] uppercase tracking-widest transition-all border-l-2 ${isActive
|
className={`w-full text-left px-3 py-2 text-[10px] uppercase tracking-widest transition-all border-l-2 ${
|
||||||
? "bg-white/10 border-[#FFFF55] text-white"
|
isActive
|
||||||
: "border-transparent text-white/40 hover:text-white/80 hover:bg-white/5"
|
? "bg-white/10 border-[#FFFF55] text-white"
|
||||||
}`}
|
: "border-transparent text-white/40 hover:text-white/80 hover:bg-white/5"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: getTypeColor(typeVal) }} />
|
<div
|
||||||
|
className="w-1.5 h-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
getTypeColor(typeVal),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{typeName.replace(/_/g, " ")}
|
{typeName.replace(/_/g, " ")}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -616,24 +766,53 @@ export default function PckEditorView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 shrink-0">
|
<div className="flex gap-2 shrink-0">
|
||||||
<button onClick={() => handleMoveAsset('up')} className="hover:scale-110 active:scale-95 transition-transform">
|
<button
|
||||||
<img src="/images/Settings_Arrow_Up.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} />
|
onClick={() => handleMoveAsset("up")}
|
||||||
|
className="hover:scale-110 active:scale-95 transition-transform"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/Settings_Arrow_Up.png"
|
||||||
|
className="w-4 h-4 object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => handleMoveAsset('down')} className="hover:scale-110 active:scale-95 transition-transform">
|
<button
|
||||||
<img src="/images/Settings_Arrow_Down.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} />
|
onClick={() => handleMoveAsset("down")}
|
||||||
|
className="hover:scale-110 active:scale-95 transition-transform"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/Settings_Arrow_Down.png"
|
||||||
|
className="w-4 h-4 object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{assetPreviewUrl && (
|
{assetPreviewUrl && (
|
||||||
<div className="w-full h-[550px] bg-black/40 border-2 border-[#373737] mb-6 flex items-center justify-center overflow-hidden relative group">
|
<div className="w-full h-[550px] bg-black/40 border-2 border-[#373737] mb-6 flex items-center justify-center overflow-hidden relative group">
|
||||||
{(selectedAsset.type === PCKAssetType.SKIN || selectedAsset.type === PCKAssetType.CAPE || selectedAsset.type === PCKAssetType.SKIN_DATA) ? (
|
{selectedAsset.type === PCKAssetType.SKIN ||
|
||||||
<SkinPreview3D key={selectedAsset.id} asset={selectedAsset} previewUrl={assetPreviewUrl || undefined} className="w-full h-full" />
|
selectedAsset.type === PCKAssetType.CAPE ||
|
||||||
|
selectedAsset.type === PCKAssetType.SKIN_DATA ? (
|
||||||
|
<SkinPreview3D
|
||||||
|
key={selectedAsset.id}
|
||||||
|
asset={selectedAsset}
|
||||||
|
previewUrl={assetPreviewUrl || undefined}
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img src={assetPreviewUrl} className="max-w-full max-h-full object-contain" style={{ imageRendering: "pixelated" }} />
|
<img
|
||||||
|
src={assetPreviewUrl}
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/60 px-2 py-1 rounded text-[10px] text-white/60 pointer-events-none uppercase tracking-widest">
|
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/60 px-2 py-1 rounded text-[10px] text-white/60 pointer-events-none uppercase tracking-widest">
|
||||||
{selectedAsset.type === PCKAssetType.SKIN ? "3D Skin View" : selectedAsset.type === PCKAssetType.CAPE ? "3D Cape View" : "Texture Preview"}
|
{selectedAsset.type === PCKAssetType.SKIN
|
||||||
|
? "3D Skin View"
|
||||||
|
: selectedAsset.type === PCKAssetType.CAPE
|
||||||
|
? "3D Cape View"
|
||||||
|
: "Texture Preview"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -641,7 +820,9 @@ export default function PckEditorView() {
|
|||||||
<div className="space-y-6 flex-1">
|
<div className="space-y-6 flex-1">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-end mb-2 px-1">
|
<div className="flex justify-between items-end mb-2 px-1">
|
||||||
<div className="text-white/40 text-[10px] uppercase tracking-widest text-[#FFFF55]/60">Metadata Properties</div>
|
<div className="text-white/40 text-[10px] uppercase tracking-widest text-[#FFFF55]/60">
|
||||||
|
Metadata Properties
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddProperty}
|
onClick={handleAddProperty}
|
||||||
className="text-[#FFFF55] text-[10px] uppercase hover:underline"
|
className="text-[#FFFF55] text-[10px] uppercase hover:underline"
|
||||||
@@ -651,12 +832,17 @@ export default function PckEditorView() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{selectedAsset.properties.map((prop, idx) => (
|
{selectedAsset.properties.map((prop, idx) => (
|
||||||
<div key={idx} className="flex flex-col gap-1 group/prop">
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex flex-col gap-1 group/prop"
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center px-1">
|
<div className="flex justify-between items-center px-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={prop.key}
|
value={prop.key}
|
||||||
onChange={(e) => handlePropertyEdit(idx, e.target.value, true)}
|
onChange={(e) =>
|
||||||
|
handlePropertyEdit(idx, e.target.value, true)
|
||||||
|
}
|
||||||
className="bg-transparent text-white/40 text-[10px] outline-none hover:text-white/60 focus:text-[#FFFF55] w-2/3"
|
className="bg-transparent text-white/40 text-[10px] outline-none hover:text-white/60 focus:text-[#FFFF55] w-2/3"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -670,7 +856,9 @@ export default function PckEditorView() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={prop.value}
|
value={prop.value}
|
||||||
onChange={(e) => handlePropertyEdit(idx, e.target.value)}
|
onChange={(e) =>
|
||||||
|
handlePropertyEdit(idx, e.target.value)
|
||||||
|
}
|
||||||
className="w-full bg-black/40 p-2 text-white border border-[#373737] text-sm focus:border-[#FFFF55] outline-none transition-colors"
|
className="w-full bg-black/40 p-2 text-white border border-[#373737] text-sm focus:border-[#FFFF55] outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -694,7 +882,10 @@ export default function PckEditorView() {
|
|||||||
{ label: "04: No Body", flag: 0x2000 },
|
{ label: "04: No Body", flag: 0x2000 },
|
||||||
{ label: "04: No R. Leg", flag: 0x4000 },
|
{ label: "04: No R. Leg", flag: 0x4000 },
|
||||||
{ label: "04: No L. Leg", flag: 0x8000 },
|
{ label: "04: No L. Leg", flag: 0x8000 },
|
||||||
{ label: "05: No Head Overlay", flag: 0x10000 },
|
{
|
||||||
|
label: "05: No Head Overlay",
|
||||||
|
flag: 0x10000,
|
||||||
|
},
|
||||||
{ label: "05: Back Crouch", flag: 0x20000 },
|
{ label: "05: Back Crouch", flag: 0x20000 },
|
||||||
{ label: "05: Modern Skin", flag: 0x40000 },
|
{ label: "05: Modern Skin", flag: 0x40000 },
|
||||||
{ label: "05: Slim Skin", flag: 0x80000 },
|
{ label: "05: Slim Skin", flag: 0x80000 },
|
||||||
@@ -703,18 +894,40 @@ export default function PckEditorView() {
|
|||||||
{ label: "06: No L. Pant", flag: 0x400000 },
|
{ label: "06: No L. Pant", flag: 0x400000 },
|
||||||
{ label: "06: No R. Pant", flag: 0x800000 },
|
{ label: "06: No R. Pant", flag: 0x800000 },
|
||||||
{ label: "07: No Jacket", flag: 0x1000000 },
|
{ label: "07: No Jacket", flag: 0x1000000 },
|
||||||
{ label: "07: Rend Head Arm", flag: 0x2000000 },
|
{
|
||||||
{ label: "07: Rend R.Arm Arm", flag: 0x4000000 },
|
label: "07: Rend Head Arm",
|
||||||
{ label: "07: Rend L.Arm Arm", flag: 0x8000000 },
|
flag: 0x2000000,
|
||||||
{ label: "08: Rend Body Arm", flag: 0x10000000 },
|
},
|
||||||
{ label: "08: Rend R.Leg Arm", flag: 0x20000000 },
|
{
|
||||||
{ label: "08: Rend L.Leg Arm", flag: 0x40000000 },
|
label: "07: Rend R.Arm Arm",
|
||||||
|
flag: 0x4000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "07: Rend L.Arm Arm",
|
||||||
|
flag: 0x8000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "08: Rend Body Arm",
|
||||||
|
flag: 0x10000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "08: Rend R.Leg Arm",
|
||||||
|
flag: 0x20000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "08: Rend L.Leg Arm",
|
||||||
|
flag: 0x40000000,
|
||||||
|
},
|
||||||
{ label: "08: Dinnerbone", flag: 0x80000000 },
|
{ label: "08: Dinnerbone", flag: 0x80000000 },
|
||||||
].map((item) => {
|
].map((item) => {
|
||||||
const currentVal = parseInt(prop.value) || 0;
|
const currentVal = parseInt(prop.value) || 0;
|
||||||
const isChecked = (currentVal & item.flag) !== 0;
|
const isChecked =
|
||||||
|
(currentVal & item.flag) !== 0;
|
||||||
return (
|
return (
|
||||||
<label key={item.label} className="flex items-center gap-2 cursor-pointer group/flag">
|
<label
|
||||||
|
key={item.label}
|
||||||
|
className="flex items-center gap-2 cursor-pointer group/flag"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
@@ -722,12 +935,19 @@ export default function PckEditorView() {
|
|||||||
const newVal = e.target.checked
|
const newVal = e.target.checked
|
||||||
? currentVal | item.flag
|
? currentVal | item.flag
|
||||||
: currentVal & ~item.flag;
|
: currentVal & ~item.flag;
|
||||||
handlePropertyEdit(idx, `0x${newVal.toString(16).toUpperCase().padStart(8, "0")}`);
|
handlePropertyEdit(
|
||||||
|
idx,
|
||||||
|
`0x${newVal.toString(16).toUpperCase().padStart(8, "0")}`,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<div className={`w-3 h-3 border transition-colors ${isChecked ? "bg-[#FFFF55] border-[#FFFF55]" : "border-white/20 group-hover/flag:border-white/40"}`} />
|
<div
|
||||||
<span className={`text-[9px] uppercase tracking-tight ${isChecked ? "text-[#FFFF55]" : "text-white/40 group-hover/flag:text-white/60"}`}>
|
className={`w-3 h-3 border transition-colors ${isChecked ? "bg-[#FFFF55] border-[#FFFF55]" : "border-white/20 group-hover/flag:border-white/40"}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`text-[9px] uppercase tracking-tight ${isChecked ? "text-[#FFFF55]" : "text-white/40 group-hover/flag:text-white/60"}`}
|
||||||
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -738,7 +958,9 @@ export default function PckEditorView() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{selectedAsset.properties.length === 0 && (
|
{selectedAsset.properties.length === 0 && (
|
||||||
<div className="text-white/20 italic text-sm px-1 py-4 border-2 border-dashed border-[#373737] text-center">No metadata properties</div>
|
<div className="text-white/20 italic text-sm px-1 py-4 border-2 border-dashed border-[#373737] text-center">
|
||||||
|
No metadata properties
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -748,14 +970,22 @@ export default function PckEditorView() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleExportAsset(selectedAsset)}
|
onClick={() => handleExportAsset(selectedAsset)}
|
||||||
className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
|
className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => replaceInputRef.current?.click()}
|
onClick={() => replaceInputRef.current?.click()}
|
||||||
className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
|
className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Replace
|
Replace
|
||||||
</button>
|
</button>
|
||||||
@@ -763,14 +993,22 @@ export default function PckEditorView() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsRenamingAsset(selectedAsset.id)}
|
onClick={() => setIsRenamingAsset(selectedAsset.id)}
|
||||||
className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
|
className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Rename Asset (Path)
|
Rename Asset (Path)
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteAsset(selectedAsset.id)}
|
onClick={() => handleDeleteAsset(selectedAsset.id)}
|
||||||
className="w-full py-2 text-red-500/80 mc-text-shadow text-sm transition-all hover:text-red-500 hover:scale-[1.02]"
|
className="w-full py-2 text-red-500/80 mc-text-shadow text-sm transition-all hover:text-red-500 hover:scale-[1.02]"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Delete This Asset
|
Delete This Asset
|
||||||
</button>
|
</button>
|
||||||
@@ -808,7 +1046,7 @@ export default function PckEditorView() {
|
|||||||
style={{
|
style={{
|
||||||
backgroundImage: "url('/images/frame_background.png')",
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
imageRendering: "pixelated"
|
imageRendering: "pixelated",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-white text-lg mc-text-shadow font-bold tracking-widest uppercase">
|
<span className="text-white text-lg mc-text-shadow font-bold tracking-widest uppercase">
|
||||||
@@ -836,17 +1074,23 @@ export default function PckEditorView() {
|
|||||||
style={{
|
style={{
|
||||||
backgroundImage: "url('/images/frame_background.png')",
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
imageRendering: "pixelated"
|
imageRendering: "pixelated",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">Select Asset Type</h3>
|
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
|
||||||
|
Select Asset Type
|
||||||
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4 w-full">
|
<div className="grid grid-cols-2 gap-4 w-full">
|
||||||
{Object.keys(PCKAssetType)
|
{Object.keys(PCKAssetType)
|
||||||
.filter(k => isNaN(Number(k)))
|
.filter((k) => isNaN(Number(k)))
|
||||||
.map((typeName) => (
|
.map((typeName) => (
|
||||||
<button
|
<button
|
||||||
key={typeName}
|
key={typeName}
|
||||||
onClick={() => confirmAddAsset(PCKAssetType[typeName as keyof typeof PCKAssetType])}
|
onClick={() =>
|
||||||
|
confirmAddAsset(
|
||||||
|
PCKAssetType[typeName as keyof typeof PCKAssetType],
|
||||||
|
)
|
||||||
|
}
|
||||||
className="py-3 px-4 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55] border-2 border-transparent hover:border-[#FFFF55]/30 bg-black/40"
|
className="py-3 px-4 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55] border-2 border-transparent hover:border-[#FFFF55]/30 bg-black/40"
|
||||||
>
|
>
|
||||||
{typeName.replace(/_/g, " ")}
|
{typeName.replace(/_/g, " ")}
|
||||||
@@ -866,7 +1110,9 @@ export default function PckEditorView() {
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isRenamingAsset && (
|
{isRenamingAsset && (
|
||||||
<RenameAssetModal
|
<RenameAssetModal
|
||||||
initialPath={pck?.files.find(f => f.id === isRenamingAsset)?.path || ""}
|
initialPath={
|
||||||
|
pck?.files.find((f) => f.id === isRenamingAsset)?.path || ""
|
||||||
|
}
|
||||||
onClose={() => setIsRenamingAsset(null)}
|
onClose={() => setIsRenamingAsset(null)}
|
||||||
onConfirm={(newPath) => {
|
onConfirm={(newPath) => {
|
||||||
handleRenameAsset(isRenamingAsset, newPath);
|
handleRenameAsset(isRenamingAsset, newPath);
|
||||||
@@ -879,7 +1125,15 @@ export default function PckEditorView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RenameAssetModal({ initialPath, onClose, onConfirm }: { initialPath: string, onClose: () => void, onConfirm: (path: string) => void }) {
|
function RenameAssetModal({
|
||||||
|
initialPath,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
}: {
|
||||||
|
initialPath: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: (path: string) => void;
|
||||||
|
}) {
|
||||||
const [path, setPath] = useState(initialPath);
|
const [path, setPath] = useState(initialPath);
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4">
|
||||||
@@ -898,13 +1152,17 @@ function RenameAssetModal({ initialPath, onClose, onConfirm }: { initialPath: st
|
|||||||
style={{
|
style={{
|
||||||
backgroundImage: "url('/images/frame_background.png')",
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
imageRendering: "pixelated"
|
imageRendering: "pixelated",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">Rename Asset</h3>
|
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
|
||||||
|
Rename Asset
|
||||||
|
</h3>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">New Asset Path</label>
|
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
|
||||||
|
New Asset Path
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={path}
|
value={path}
|
||||||
@@ -923,7 +1181,10 @@ function RenameAssetModal({ initialPath, onClose, onConfirm }: { initialPath: st
|
|||||||
<button
|
<button
|
||||||
onClick={() => onConfirm(path)}
|
onClick={() => onConfirm(path)}
|
||||||
className="px-8 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
className="px-8 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
||||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
|
style={{
|
||||||
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Rename
|
Rename
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+416
-123
@@ -1,50 +1,157 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
|
||||||
const TRACKS = [
|
const TRACKS = [
|
||||||
"/music/Blind Spots.ogg",
|
"music/Blind Spots.ogg",
|
||||||
"/music/Key.ogg",
|
"music/Key.ogg",
|
||||||
"/music/Living Mice.ogg",
|
"music/Living Mice.ogg",
|
||||||
"/music/Oxygene.ogg",
|
"music/Oxygene.ogg",
|
||||||
"/music/Subwoofer Lullaby.ogg",
|
"music/Subwoofer Lullaby.ogg",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const resolveAudioUrl = (path: string) => {
|
||||||
|
const relativePath = path.startsWith("/") ? path.slice(1) : path;
|
||||||
|
return new URL(relativePath, window.location.origin).href;
|
||||||
|
};
|
||||||
|
|
||||||
const SPLASHES = [
|
const SPLASHES = [
|
||||||
"Legacy is back!", "Pixelated goodness!", "Console Edition vibe!", "100% Not Microsoft!",
|
"Legacy is back!",
|
||||||
"Symmetry is key!", "Does anyone even read these?", "Task failed successfully.",
|
"Pixelated goodness!",
|
||||||
"Hardware accelerated!", "It's a feature, not a bug.", "Look behind you.",
|
"Console Edition vibe!",
|
||||||
"Works on my machine.", "Now gluten-free!", "Mom, get the camera!", "Batteries not included.",
|
"100% Not Microsoft!",
|
||||||
"May contain nuts.", "Press Alt+F4 for diamonds!", "Downloading more RAM...",
|
"Symmetry is key!",
|
||||||
"Reinventing the wheel!", "The cake is a lie.", "Powered by copious amounts of coffee.",
|
"Does anyone even read these?",
|
||||||
"I'm running out of ideas.", "That's no moon...", "Now with 100% more nostalgia!",
|
"Task failed successfully.",
|
||||||
"Legacy is the new modern.", "No microtransactions!", "As seen on TV!", "Ironic, isn't it?",
|
"Hardware accelerated!",
|
||||||
"Creeper? Aww man.", "Technoblade never dies!", "is smartcmd dead ?", "NO BUILT IN MS AUTH !",
|
"It's a feature, not a bug.",
|
||||||
"Mr_Anilex wasn't here!", "Who's Jack ?", "This text is blue!", "Bonjour!", "Hola!",
|
"Look behind you.",
|
||||||
"Salam!", "Hi!", "Reverse engineering WiiU version", "Try Terraria", "Don't try Valorant",
|
"Works on my machine.",
|
||||||
"This could never be a sad place!", "Made without microslop", "Thank you C418", "Bread is pain",
|
"Now gluten-free!",
|
||||||
"From the star!", "Never gonna give you up!", "9+10=21", ".party() was successful", "Not Kogama",
|
"Mom, get the camera!",
|
||||||
"You can be proud of you!", "Let's drink Orange Joe", "Kirater is a great singer!",
|
"Batteries not included.",
|
||||||
"Mirkette My beloved", "Started in Bordeaux", "Oui Oui Baguette", "Milk In The Microwave",
|
"May contain nuts.",
|
||||||
"8-3: DISINTEGRATION LOOP", "Turn the light OFF", "Not written by Mr_Anilex",
|
"Press Alt+F4 for diamonds!",
|
||||||
"The One Who's Running the Show!", "Playing Forever", "The World looks cubic!", "huh?",
|
"Downloading more RAM...",
|
||||||
"Sybau", "Available on Toaster", "Try ArchLinux", "67% Accurate", "A molecule of meow",
|
"Reinventing the wheel!",
|
||||||
"http://localhost:3000", "uuhhhh...", "Oyasumi", "XDDCC", "I don't want to set the world on fire",
|
"The cake is a lie.",
|
||||||
"Directed by Michael Bay", "We see you, Opal!", "A Cool Cat in Town", "pikmin",
|
"Powered by copious amounts of coffee.",
|
||||||
"Not BrainRotted!", "Farting is Natural -Leon", "93/100 on metacritic", "Not (anymore) on Steam",
|
"I'm running out of ideas.",
|
||||||
"Sudo apt install EmeraldLauncher", "Sudo pacman -S EmeraldLauncher", "Kay-Chan my beloved! <3",
|
"That's no moon...",
|
||||||
"Peak!", "OpenSource!", "made by human with bone and flesh", "Made with hate against microslop",
|
"Now with 100% more nostalgia!",
|
||||||
"Steelorse :fire:", "It's Minecraft but i'm not sure", "Look at you!", "You're beautiful",
|
"Legacy is the new modern.",
|
||||||
"Mr_Anilex has a big ego", "Traduis-moi !", "May contains Mr_Anilex", "Neoapps didn't write this splash",
|
"No microtransactions!",
|
||||||
"Where's Kinger ?", "KayJann, Breakcore and code", "Hey Goku!", "Vegeta is a DZ mashallah",
|
"As seen on TV!",
|
||||||
"Bogos Binted? Vorp", "YOU SHALL NOT PASS !", "Bready, Steady, GO !", "Not-so-Empty-house",
|
"Ironic, isn't it?",
|
||||||
"We'll Meet Again", "idk", "wdym", "Not making sense", "Dw!", "i forgor", "Remember to be patient!",
|
"Creeper? Aww man.",
|
||||||
"NOW'S YOUR CHANCE TO BE A.", "BIG SHOT", "A burning memory", "FREE MONEY!",
|
"Technoblade never dies!",
|
||||||
|
"is smartcmd dead ?",
|
||||||
|
"NO BUILT IN MS AUTH !",
|
||||||
|
"Mr_Anilex wasn't here!",
|
||||||
|
"Who's Jack ?",
|
||||||
|
"This text is blue!",
|
||||||
|
"Bonjour!",
|
||||||
|
"Salam!",
|
||||||
|
"Reverse engineering Wii U version",
|
||||||
|
"Don't try Valorant!",
|
||||||
|
"This could never be a sad place!",
|
||||||
|
"Made without microslop",
|
||||||
|
"Thank you C418",
|
||||||
|
"Bread is pain",
|
||||||
|
"From the star!",
|
||||||
|
"Never gonna give you up!",
|
||||||
|
"9+10=21",
|
||||||
|
".party() was successful",
|
||||||
|
"Not Kogama",
|
||||||
|
"You can be proud of you!",
|
||||||
|
"Let's drink Orange Joe",
|
||||||
|
"Kirater is a great singer!",
|
||||||
|
"Mirkette My beloved",
|
||||||
|
"Started in Bordeaux",
|
||||||
|
"Oui Oui Baguette",
|
||||||
|
"Milk In The Microwave",
|
||||||
|
"8-3: DISINTEGRATION LOOP",
|
||||||
|
"Turn the light OFF",
|
||||||
|
"Not written by Mr_Anilex",
|
||||||
|
"The One Who's Running the Show!",
|
||||||
|
"Playing Forever",
|
||||||
|
"The World looks cubic!",
|
||||||
|
"huh?",
|
||||||
|
"Sybau",
|
||||||
|
"Available on Toaster",
|
||||||
|
"Try ArchLinux",
|
||||||
|
"69% Accurate",
|
||||||
|
"A molecule of meow",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"uuhhhh...",
|
||||||
|
"Oyasumi",
|
||||||
|
"I don't want to set the world on fire",
|
||||||
|
"Directed by Michael Bay",
|
||||||
|
"We see you, Opal!",
|
||||||
|
"A Cool Cat in Town",
|
||||||
|
"Not BrainRotted!",
|
||||||
|
"Farting is Natural -Leon",
|
||||||
|
"93/100 on metacritic",
|
||||||
|
"Not (anymore) on Steam",
|
||||||
|
"Sudo apt install EmeraldLauncher",
|
||||||
|
"Sudo pacman -S EmeraldLauncher",
|
||||||
|
"Kay-Chan my beloved! <3",
|
||||||
|
"Peak!",
|
||||||
|
"OpenSource!",
|
||||||
|
"made by human with bone and flesh",
|
||||||
|
"Made with hate against microslop",
|
||||||
|
"Steelorse :fire:",
|
||||||
|
"It's Minecraft but i'm not sure",
|
||||||
|
"Look at you!",
|
||||||
|
"You're beautiful",
|
||||||
|
"Mr_Anilex has a big ego",
|
||||||
|
"Traduis-moi !",
|
||||||
|
"May contains Mr_Anilex",
|
||||||
|
"Neoapps didn't write this splash",
|
||||||
|
"Where's Kinger ?",
|
||||||
|
"KayJann, Breakcore and code",
|
||||||
|
"Hey Goku!",
|
||||||
|
"Vegeta is a DZ mashallah",
|
||||||
|
"Bogos Binted? Vorp",
|
||||||
|
"YOU SHALL NOT PASS !",
|
||||||
|
"Bready, Steady, GO !",
|
||||||
|
"Not-so-Empty-house",
|
||||||
|
"We'll Meet Again",
|
||||||
|
"idk",
|
||||||
|
"wdym",
|
||||||
|
"Not making sense",
|
||||||
|
"Dw!",
|
||||||
|
"i forgor",
|
||||||
|
"Remember to be patient!",
|
||||||
|
"NOW'S YOUR CHANCE TO BE A.",
|
||||||
|
"BIG SHOT",
|
||||||
|
"A burning memory",
|
||||||
|
"FREE MONEY!",
|
||||||
"Can You Really Call This A Hotel. I didn't Reveive A Mint On My Pillow Or Anything",
|
"Can You Really Call This A Hotel. I didn't Reveive A Mint On My Pillow Or Anything",
|
||||||
"Try Indie Game", "SHARK WITH LEGS!", "it's a seal!", "Shrimp.", "Limited edition!",
|
"Try Indie Game",
|
||||||
"Fat free!", "GOTY!", "Water proof!", "LALALA-LAVA", "CHICHICHI-CHICKEN", "Tasty ah hell",
|
"SHARK WITH LEGS!",
|
||||||
"1% sugar!", "150% hyperbole!", "Hotter than the sun!", "Woo, reddit!",
|
"it's a seal!",
|
||||||
"J'ai fait une blague à un poisson il m'a dit que c'était trop marin !",
|
"Shrimp.",
|
||||||
"piebot was here!", "Legacy in an evolved manner.", "neoapps is cool!", "Also try neoLegacy!",
|
"Limited edition!",
|
||||||
"$20 is $20", "no RenderDragon!", "Iggy Jiggy", "Arch, btw!", "Bedrock bad!"
|
"Fat free!",
|
||||||
|
"GOTY!",
|
||||||
|
"Water proof!",
|
||||||
|
"LALALA-LAVA",
|
||||||
|
"CHICHICHI-CHICKEN",
|
||||||
|
"Tasty ah hell",
|
||||||
|
"1% sugar!",
|
||||||
|
"150% hyperbole!",
|
||||||
|
"Hotter than the sun!",
|
||||||
|
"Woo, reddit!",
|
||||||
|
"piebot was here!",
|
||||||
|
"Legacy in an evolved manner.",
|
||||||
|
"neoapps is cool!",
|
||||||
|
"Also try neoLegacy!",
|
||||||
|
"$20 is $20",
|
||||||
|
"no RenderDragon!",
|
||||||
|
"Iggy Jiggy",
|
||||||
|
"Arch, btw!",
|
||||||
|
"Bedrock bad!",
|
||||||
|
"Also try Terraria!",
|
||||||
|
"Also try LC Launcher!",
|
||||||
];
|
];
|
||||||
|
|
||||||
interface AudioControllerProps {
|
interface AudioControllerProps {
|
||||||
@@ -55,68 +162,215 @@ interface AudioControllerProps {
|
|||||||
isWindowVisible: boolean;
|
isWindowVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAudioController({ musicVol, sfxVol, showIntro, isGameRunning, isWindowVisible }: AudioControllerProps) {
|
export function useAudioController({
|
||||||
|
musicVol,
|
||||||
|
sfxVol,
|
||||||
|
showIntro,
|
||||||
|
isGameRunning,
|
||||||
|
isWindowVisible,
|
||||||
|
}: AudioControllerProps) {
|
||||||
const [currentTrack, setCurrentTrack] = useState(0);
|
const [currentTrack, setCurrentTrack] = useState(0);
|
||||||
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null);
|
|
||||||
const [splashIndex, setSplashIndex] = useState(-1);
|
const [splashIndex, setSplashIndex] = useState(-1);
|
||||||
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
|
const musicSourceRef = useRef<AudioBufferSourceNode | null>(null);
|
||||||
|
const musicGainRef = useRef<GainNode | null>(null);
|
||||||
|
const trackBuffersRef = useRef<Map<number, AudioBuffer>>(new Map());
|
||||||
|
const sfxBufferCacheRef = useRef<Map<string, AudioBuffer>>(new Map());
|
||||||
const musicPausedRef = useRef<{ at: number; track: number } | null>(null);
|
const musicPausedRef = useRef<{ at: number; track: number } | null>(null);
|
||||||
const fadeIntervalRef = useRef<any>(null);
|
const fadeIntervalRef = useRef<number | null>(null);
|
||||||
const playSfx = useCallback((file: string) => {
|
const targetVolumeRef = useRef(musicVol / 100);
|
||||||
const a = new Audio(`/sounds/${file}`);
|
const getAudioContext = useCallback(() => {
|
||||||
a.volume = sfxVol / 100;
|
if (!audioContextRef.current) {
|
||||||
a.play().catch(() => { });
|
audioContextRef.current = new AudioContext();
|
||||||
}, [sfxVol]);
|
}
|
||||||
|
if (audioContextRef.current.state === "suspended") {
|
||||||
|
audioContextRef.current.resume();
|
||||||
|
}
|
||||||
|
return audioContextRef.current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadAudioBuffer = useCallback(
|
||||||
|
async (url: string): Promise<AudioBuffer | undefined> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
return await ctx.decodeAudioData(arrayBuffer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load audio:", url, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getAudioContext],
|
||||||
|
);
|
||||||
|
|
||||||
|
const playSfx = useCallback(
|
||||||
|
async (file: string) => {
|
||||||
|
const url = resolveAudioUrl(`/sounds/${file}`);
|
||||||
|
let buffer = sfxBufferCacheRef.current.get(file);
|
||||||
|
if (!buffer) {
|
||||||
|
buffer = await loadAudioBuffer(url);
|
||||||
|
if (buffer) {
|
||||||
|
sfxBufferCacheRef.current.set(file, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer) {
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const source = ctx.createBufferSource();
|
||||||
|
const gainNode = ctx.createGain();
|
||||||
|
source.buffer = buffer;
|
||||||
|
gainNode.gain.value = sfxVol / 100;
|
||||||
|
source.connect(gainNode);
|
||||||
|
gainNode.connect(ctx.destination);
|
||||||
|
source.start();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[sfxVol, loadAudioBuffer, getAudioContext],
|
||||||
|
);
|
||||||
|
|
||||||
const playPressSound = useCallback(() => playSfx("press.wav"), [playSfx]);
|
const playPressSound = useCallback(() => playSfx("press.wav"), [playSfx]);
|
||||||
const playBackSound = useCallback(() => playSfx("back.ogg"), [playSfx]);
|
const playBackSound = useCallback(() => playSfx("back.ogg"), [playSfx]);
|
||||||
const playSplashSound = useCallback(() => playSfx("orb.ogg"), [playSfx]);
|
const playSplashSound = useCallback(() => playSfx("orb.ogg"), [playSfx]);
|
||||||
const fadeOut = useCallback((audio: HTMLAudioElement, duration: number = 500) => {
|
|
||||||
return new Promise<void>((resolve) => {
|
const stopMusic = useCallback(() => {
|
||||||
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
|
if (musicSourceRef.current) {
|
||||||
const initialVolume = audio.volume;
|
try {
|
||||||
const steps = 5;
|
musicSourceRef.current.stop();
|
||||||
const stepDuration = duration / steps;
|
} catch (e) {}
|
||||||
let currentStep = 0;
|
musicSourceRef.current.disconnect();
|
||||||
fadeIntervalRef.current = setInterval(() => {
|
musicSourceRef.current = null;
|
||||||
currentStep++;
|
}
|
||||||
const progress = currentStep / steps;
|
if (fadeIntervalRef.current) {
|
||||||
audio.volume = initialVolume * (1 - progress);
|
clearInterval(fadeIntervalRef.current);
|
||||||
if (currentStep >= steps) {
|
fadeIntervalRef.current = null;
|
||||||
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
|
}
|
||||||
fadeIntervalRef.current = null;
|
|
||||||
audio.pause();
|
|
||||||
audio.volume = initialVolume;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, stepDuration);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fadeIn = useCallback((audio: HTMLAudioElement, targetVolume: number, duration: number = 500) => {
|
const playMusicBuffer = useCallback(
|
||||||
return new Promise<void>((resolve) => {
|
(buffer: AudioBuffer, startTime: number = 0) => {
|
||||||
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
|
stopMusic();
|
||||||
audio.volume = 0;
|
const ctx = getAudioContext();
|
||||||
const playPromise = audio.play();
|
const source = ctx.createBufferSource();
|
||||||
if (playPromise !== undefined) {
|
const gainNode = ctx.createGain();
|
||||||
playPromise.catch(() => { });
|
|
||||||
}
|
source.buffer = buffer;
|
||||||
|
gainNode.gain.value = 0;
|
||||||
|
source.connect(gainNode);
|
||||||
|
gainNode.connect(ctx.destination);
|
||||||
|
|
||||||
|
const offset = startTime % buffer.duration;
|
||||||
|
source.start(0, offset);
|
||||||
|
|
||||||
|
musicSourceRef.current = source;
|
||||||
|
musicGainRef.current = gainNode;
|
||||||
|
|
||||||
|
const steps = 5;
|
||||||
|
const stepDuration = 100;
|
||||||
|
let currentStep = 0;
|
||||||
|
fadeIntervalRef.current = window.setInterval(() => {
|
||||||
|
currentStep++;
|
||||||
|
const progress = currentStep / steps;
|
||||||
|
if (musicGainRef.current) {
|
||||||
|
musicGainRef.current.gain.value = targetVolumeRef.current * progress;
|
||||||
|
}
|
||||||
|
if (currentStep >= steps) {
|
||||||
|
clearInterval(fadeIntervalRef.current || undefined);
|
||||||
|
fadeIntervalRef.current = null;
|
||||||
|
if (musicGainRef.current) {
|
||||||
|
musicGainRef.current.gain.value = targetVolumeRef.current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, stepDuration);
|
||||||
|
|
||||||
|
source.onended = () => {
|
||||||
|
if (musicSourceRef.current) {
|
||||||
|
setCurrentTrack((prev) => (prev + 1) % TRACKS.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[stopMusic, getAudioContext],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fadeOutMusic = useCallback(
|
||||||
|
(duration: number = 500): Promise<void> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
|
||||||
|
if (!musicGainRef.current) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = 5;
|
||||||
|
const stepDuration = duration / steps;
|
||||||
|
const initialVolume = musicGainRef.current.gain.value;
|
||||||
|
let currentStep = 0;
|
||||||
|
|
||||||
|
fadeIntervalRef.current = window.setInterval(() => {
|
||||||
|
currentStep++;
|
||||||
|
const progress = currentStep / steps;
|
||||||
|
if (musicGainRef.current) {
|
||||||
|
musicGainRef.current.gain.value = initialVolume * (1 - progress);
|
||||||
|
}
|
||||||
|
if (currentStep >= steps) {
|
||||||
|
clearInterval(fadeIntervalRef.current || undefined);
|
||||||
|
fadeIntervalRef.current = null;
|
||||||
|
stopMusic();
|
||||||
|
if (musicGainRef.current) {
|
||||||
|
musicGainRef.current.gain.value = initialVolume;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, stepDuration);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[stopMusic],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fadeInMusic = useCallback(
|
||||||
|
(buffer: AudioBuffer, targetVolume: number, duration: number = 500) => {
|
||||||
|
stopMusic();
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const source = ctx.createBufferSource();
|
||||||
|
const gainNode = ctx.createGain();
|
||||||
|
|
||||||
|
source.buffer = buffer;
|
||||||
|
gainNode.gain.value = 0;
|
||||||
|
source.connect(gainNode);
|
||||||
|
gainNode.connect(ctx.destination);
|
||||||
|
source.start();
|
||||||
|
|
||||||
|
musicSourceRef.current = source;
|
||||||
|
musicGainRef.current = gainNode;
|
||||||
|
targetVolumeRef.current = targetVolume;
|
||||||
|
|
||||||
const steps = 5;
|
const steps = 5;
|
||||||
const stepDuration = duration / steps;
|
const stepDuration = duration / steps;
|
||||||
let currentStep = 0;
|
let currentStep = 0;
|
||||||
fadeIntervalRef.current = setInterval(() => {
|
|
||||||
|
fadeIntervalRef.current = window.setInterval(() => {
|
||||||
currentStep++;
|
currentStep++;
|
||||||
const progress = currentStep / steps;
|
const progress = currentStep / steps;
|
||||||
audio.volume = targetVolume * progress;
|
if (musicGainRef.current) {
|
||||||
|
musicGainRef.current.gain.value = targetVolume * progress;
|
||||||
|
}
|
||||||
if (currentStep >= steps) {
|
if (currentStep >= steps) {
|
||||||
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
|
clearInterval(fadeIntervalRef.current || undefined);
|
||||||
fadeIntervalRef.current = null;
|
fadeIntervalRef.current = null;
|
||||||
audio.volume = targetVolume;
|
if (musicGainRef.current) {
|
||||||
resolve();
|
musicGainRef.current.gain.value = targetVolume;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, stepDuration);
|
}, stepDuration);
|
||||||
});
|
|
||||||
}, []);
|
source.onended = () => {
|
||||||
|
if (musicSourceRef.current) {
|
||||||
|
setCurrentTrack((prev) => (prev + 1) % TRACKS.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[stopMusic, getAudioContext],
|
||||||
|
);
|
||||||
|
|
||||||
const cycleSplash = useCallback(() => {
|
const cycleSplash = useCallback(() => {
|
||||||
playSplashSound();
|
playSplashSound();
|
||||||
@@ -129,66 +383,105 @@ export function useAudioController({ musicVol, sfxVol, showIntro, isGameRunning,
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showIntro) return;
|
if (showIntro) return;
|
||||||
if (audioElement) return;
|
|
||||||
const audio = new Audio(TRACKS[currentTrack]);
|
|
||||||
audio.volume = musicVol / 100;
|
|
||||||
const handleEnded = () => setCurrentTrack((prev) => (prev + 1) % TRACKS.length);
|
|
||||||
audio.addEventListener("ended", handleEnded);
|
|
||||||
const playPromise = audio.play();
|
|
||||||
if (playPromise !== undefined) {
|
|
||||||
playPromise.catch(() => {
|
|
||||||
const startMusic = () => {
|
|
||||||
audio.play().catch(() => { });
|
|
||||||
document.removeEventListener("click", startMusic);
|
|
||||||
document.removeEventListener("keydown", startMusic);
|
|
||||||
};
|
|
||||||
document.addEventListener("click", startMusic, { once: true });
|
|
||||||
document.addEventListener("keydown", startMusic, { once: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setAudioElement(audio);
|
const loadAndPlay = async () => {
|
||||||
return () => {
|
let buffer = trackBuffersRef.current.get(currentTrack);
|
||||||
audio.removeEventListener("ended", handleEnded);
|
if (!buffer) {
|
||||||
audio.pause();
|
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
|
||||||
|
if (buffer) {
|
||||||
|
trackBuffersRef.current.set(currentTrack, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buffer) {
|
||||||
|
playMusicBuffer(buffer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [showIntro, audioElement, currentTrack, musicVol]);
|
|
||||||
|
loadAndPlay();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopMusic();
|
||||||
|
};
|
||||||
|
}, [showIntro, currentTrack, loadAudioBuffer, playMusicBuffer, stopMusic]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!audioElement) return;
|
if (!audioContextRef.current || showIntro) return;
|
||||||
audioElement.src = TRACKS[currentTrack];
|
|
||||||
audioElement.play().catch(() => { });
|
const loadAndPlay = async () => {
|
||||||
}, [currentTrack]);
|
let buffer = trackBuffersRef.current.get(currentTrack);
|
||||||
|
if (!buffer) {
|
||||||
|
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
|
||||||
|
if (buffer) {
|
||||||
|
trackBuffersRef.current.set(currentTrack, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buffer) {
|
||||||
|
fadeInMusic(buffer, musicVol / 100, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadAndPlay();
|
||||||
|
}, [currentTrack, showIntro, musicVol, loadAudioBuffer, fadeInMusic]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!audioElement) return;
|
|
||||||
const shouldPause = isGameRunning || !isWindowVisible;
|
const shouldPause = isGameRunning || !isWindowVisible;
|
||||||
|
|
||||||
if (shouldPause) {
|
if (shouldPause) {
|
||||||
if (!audioElement.paused || fadeIntervalRef.current) {
|
if (musicSourceRef.current || fadeIntervalRef.current) {
|
||||||
if (!musicPausedRef.current) {
|
if (!musicPausedRef.current) {
|
||||||
musicPausedRef.current = {
|
const ctx = getAudioContext();
|
||||||
at: audioElement.currentTime,
|
if (musicGainRef.current) {
|
||||||
track: currentTrack,
|
musicPausedRef.current = {
|
||||||
};
|
at: ctx.currentTime,
|
||||||
|
track: currentTrack,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
musicPausedRef.current = { at: 0, track: currentTrack };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fadeOut(audioElement, 500);
|
fadeOutMusic(500);
|
||||||
}
|
}
|
||||||
} else if (musicPausedRef.current) {
|
} else if (musicPausedRef.current) {
|
||||||
const { at, track } = musicPausedRef.current;
|
const { track } = musicPausedRef.current;
|
||||||
musicPausedRef.current = null;
|
musicPausedRef.current = null;
|
||||||
|
targetVolumeRef.current = musicVol / 100;
|
||||||
|
|
||||||
|
const playWithPos = async () => {
|
||||||
|
let buffer = trackBuffersRef.current.get(currentTrack);
|
||||||
|
if (!buffer) {
|
||||||
|
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
|
||||||
|
if (buffer) {
|
||||||
|
trackBuffersRef.current.set(currentTrack, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buffer) {
|
||||||
|
fadeInMusic(buffer, musicVol / 100, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (track === currentTrack) {
|
if (track === currentTrack) {
|
||||||
audioElement.currentTime = at;
|
playWithPos();
|
||||||
|
} else {
|
||||||
|
setCurrentTrack(track);
|
||||||
}
|
}
|
||||||
fadeIn(audioElement, musicVol / 100, 500);
|
|
||||||
}
|
}
|
||||||
}, [isGameRunning, isWindowVisible, audioElement, currentTrack, musicVol, fadeOut, fadeIn]);
|
}, [
|
||||||
|
isGameRunning,
|
||||||
|
isWindowVisible,
|
||||||
|
currentTrack,
|
||||||
|
musicVol,
|
||||||
|
fadeOutMusic,
|
||||||
|
fadeInMusic,
|
||||||
|
loadAudioBuffer,
|
||||||
|
getAudioContext,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (audioElement) {
|
targetVolumeRef.current = musicVol / 100;
|
||||||
audioElement.volume = musicVol / 100;
|
if (musicGainRef.current && !fadeIntervalRef.current) {
|
||||||
|
musicGainRef.current.gain.value = musicVol / 100;
|
||||||
}
|
}
|
||||||
}, [musicVol, audioElement]);
|
}, [musicVol]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentTrack,
|
currentTrack,
|
||||||
|
|||||||
Reference in New Issue
Block a user