mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 00:07:09 +00:00
feat(PCK): feature parity with Quiver
This commit is contained in:
parent
24a4599ecf
commit
f046e06e12
308
src/components/common/SkinPreview3D.tsx
Normal file
308
src/components/common/SkinPreview3D.tsx
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
import { useEffect, useRef, memo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { PCKAsset, PCKAssetType } from '../../types/pck';
|
||||
|
||||
interface SkinPreview3DProps {
|
||||
asset: PCKAsset;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
enum SKIN_ANIM {
|
||||
STATIONARY_ARMS = 1 << 0,
|
||||
ZOMBIE_ARMS = 1 << 1,
|
||||
STATIONARY_LEGS = 1 << 2,
|
||||
BAD_SANTA_IDLE = 1 << 3,
|
||||
UNKNOWN_EFFECT = 1 << 4,
|
||||
SYNCHRONIZED_LEGS = 1 << 5,
|
||||
SYNCHRONIZED_ARMS = 1 << 6,
|
||||
STATUE_OF_LIBERTY = 1 << 7,
|
||||
HIDE_ARMOR = 1 << 8,
|
||||
FIRST_PERSON_BOBBING_DISABLED = 1 << 9,
|
||||
HIDE_HEAD = 1 << 10,
|
||||
HIDE_RIGHT_ARM = 1 << 11,
|
||||
HIDE_LEFT_ARM = 1 << 12,
|
||||
HIDE_BODY = 1 << 13,
|
||||
HIDE_RIGHT_LEG = 1 << 14,
|
||||
HIDE_LEFT_LEG = 1 << 15,
|
||||
HIDE_HAT = 1 << 16,
|
||||
BACKWARDS_CROUCH = 1 << 17,
|
||||
MODERN_WIDE_FORMAT = 1 << 18,
|
||||
SLIM_FORMAT = 1 << 19,
|
||||
HIDE_LEFT_SLEEVE = 1 << 20,
|
||||
HIDE_RIGHT_SLEEVE = 1 << 21,
|
||||
HIDE_LEFT_PANT = 1 << 22,
|
||||
HIDE_RIGHT_PANT = 1 << 23,
|
||||
HIDE_JACKET = 1 << 24,
|
||||
ALLOW_HEAD_ARMOR = 1 << 25,
|
||||
ALLOW_RIGHT_ARM_ARMOR = 1 << 26,
|
||||
ALLOW_LEFT_ARM_ARMOR = 1 << 27,
|
||||
ALLOW_CHESTPLATE = 1 << 28,
|
||||
ALLOW_RIGHT_LEGGING = 1 << 29,
|
||||
ALLOW_LEFT_LEGGING = 1 << 30,
|
||||
DINNER_BONE_RENDERING = 1 << 31
|
||||
}
|
||||
|
||||
const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPreview3DProps) {
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
const playerGroupRef = useRef<THREE.Group | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mountRef.current) return;
|
||||
|
||||
const width = mountRef.current.clientWidth;
|
||||
const height = mountRef.current.clientHeight;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 0, 70);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
mountRef.current.innerHTML = "";
|
||||
mountRef.current.appendChild(renderer.domElement);
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
dl.position.set(10, 20, 10);
|
||||
scene.add(dl);
|
||||
|
||||
const playerGroup = new THREE.Group();
|
||||
playerGroup.position.y = -2;
|
||||
scene.add(playerGroup);
|
||||
playerGroupRef.current = playerGroup;
|
||||
|
||||
const render = () => {
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
const blob = new Blob([asset.data as any], { type: 'image/png' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
|
||||
textureLoader.load(url, (texture) => {
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
const img = texture.image;
|
||||
const isLegacy = img.height === 32;
|
||||
|
||||
const animProp = asset.properties.find(p => p.key === "ANIM");
|
||||
const animValue = animProp ? parseInt(animProp.value) || 0 : 0;
|
||||
const slimFormat = !!(animValue & SKIN_ANIM.SLIM_FORMAT);
|
||||
|
||||
const createFaceMaterial = (x: number, y: number, w: number, h: number, flipX = false, flipY = false) => {
|
||||
const matTex = texture.clone();
|
||||
matTex.repeat.set((flipX ? -w : w) / 64, (flipY ? -h : h) / img.height);
|
||||
matTex.offset.set((flipX ? (x + w) : x) / 64, 1 - (flipY ? y : (y + h)) / img.height);
|
||||
matTex.needsUpdate = true;
|
||||
return new THREE.MeshLambertMaterial({ map: matTex, transparent: true, alphaTest: 0.5, side: THREE.FrontSide });
|
||||
};
|
||||
|
||||
const createPart = (w: number, h: number, d: number, uv: any, overlayUv?: any, isMirror = false) => {
|
||||
const group = new THREE.Group();
|
||||
const geo = new THREE.BoxGeometry(w, h, d);
|
||||
|
||||
const getMats = (uvSet: any) => {
|
||||
return [
|
||||
createFaceMaterial(uvSet.right[0], uvSet.right[1], uvSet.right[2], uvSet.right[3], isMirror), // +x
|
||||
createFaceMaterial(uvSet.left[0], uvSet.left[1], uvSet.left[2], uvSet.left[3], isMirror), // -x
|
||||
createFaceMaterial(uvSet.top[0], uvSet.top[1], uvSet.top[2], uvSet.top[3], isMirror, true), // +y
|
||||
createFaceMaterial(uvSet.bottom[0], uvSet.bottom[1], uvSet.bottom[2], uvSet.bottom[3], isMirror, true), // -y
|
||||
createFaceMaterial(uvSet.front[0], uvSet.front[1], uvSet.front[2], uvSet.front[3], isMirror), // +z
|
||||
createFaceMaterial(uvSet.back[0], uvSet.back[1], uvSet.back[2], uvSet.back[3], !isMirror) // -z
|
||||
];
|
||||
};
|
||||
|
||||
const mesh = new THREE.Mesh(geo, getMats(uv));
|
||||
group.add(mesh);
|
||||
|
||||
if (overlayUv) {
|
||||
const oGeo = new THREE.BoxGeometry(w + 0.5, h + 0.5, d + 0.5);
|
||||
const oMesh = new THREE.Mesh(oGeo, getMats(overlayUv));
|
||||
group.add(oMesh);
|
||||
}
|
||||
return group;
|
||||
};
|
||||
|
||||
const limbUv = (x: number, y: number, w = 4) => ({
|
||||
top: [x + 4, y, w, 4], bottom: [x + 4 + w, y, w, 4],
|
||||
right: [x, y + 4, 4, 12], front: [x + 4, y + 4, w, 12],
|
||||
left: [x + 4 + w, y + 4, 4, 12], back: [x + 8 + w, y + 4, w, 12]
|
||||
});
|
||||
|
||||
if (asset.type === (PCKAssetType.CAPE as any)) {
|
||||
const capeUv = {
|
||||
top: [1, 0, 10, 1], bottom: [11, 0, 10, 1],
|
||||
right: [0, 1, 1, 16], front: [1, 1, 10, 16],
|
||||
left: [11, 1, 1, 16], back: [12, 1, 10, 16]
|
||||
};
|
||||
const cape = createPart(10, 16, 1, capeUv);
|
||||
cape.position.y = 2;
|
||||
playerGroup.add(cape);
|
||||
} else {
|
||||
const armW = slimFormat ? 3 : 4;
|
||||
|
||||
if (!(animValue & SKIN_ANIM.HIDE_HEAD)) {
|
||||
const headUv = { top: [8, 0, 8, 8], bottom: [16, 0, 8, 8], right: [0, 8, 8, 8], left: [16, 8, 8, 8], front: [8, 8, 8, 8], back: [24, 8, 8, 8] };
|
||||
const hatUv = (animValue & SKIN_ANIM.HIDE_HAT) ? undefined : { top: [40, 0, 8, 8], bottom: [48, 0, 8, 8], right: [32, 8, 8, 8], left: [48, 8, 8, 8], front: [40, 8, 8, 8], back: [56, 8, 8, 8] };
|
||||
const head = createPart(8, 8, 8, headUv, hatUv);
|
||||
head.position.y = 10;
|
||||
playerGroup.add(head);
|
||||
}
|
||||
|
||||
if (!(animValue & SKIN_ANIM.HIDE_BODY) && asset.type !== PCKAssetType.CAPE) {
|
||||
const bodyUv = { top: [20, 16, 8, 4], bottom: [28, 16, 8, 4], right: [16, 20, 4, 12], left: [28, 20, 4, 12], front: [20, 20, 8, 12], back: [32, 20, 8, 12] };
|
||||
const jacketUv = (isLegacy || (animValue & SKIN_ANIM.HIDE_JACKET)) ? undefined : { top: [20, 32, 8, 4], bottom: [28, 32, 8, 4], right: [16, 36, 4, 12], left: [28, 36, 4, 12], front: [20, 36, 8, 12], back: [32, 36, 8, 12] };
|
||||
playerGroup.add(createPart(8, 12, 4, bodyUv, jacketUv));
|
||||
}
|
||||
|
||||
if (!(animValue & SKIN_ANIM.HIDE_RIGHT_ARM)) {
|
||||
const rArmUv = limbUv(40, 16, armW);
|
||||
const rSleeveUv = (isLegacy || (animValue & SKIN_ANIM.HIDE_RIGHT_SLEEVE)) ? undefined : limbUv(40, 32, armW);
|
||||
const rightArm = createPart(armW, 12, 4, rArmUv, rSleeveUv);
|
||||
rightArm.position.set(slimFormat ? -5.5 : -6, 0, 0);
|
||||
playerGroup.add(rightArm);
|
||||
}
|
||||
|
||||
if (!(animValue & SKIN_ANIM.HIDE_LEFT_ARM)) {
|
||||
const lArmUv = isLegacy ? limbUv(40, 16, armW) : limbUv(32, 48, armW);
|
||||
const lSleeveUv = (isLegacy || (animValue & SKIN_ANIM.HIDE_LEFT_SLEEVE)) ? undefined : limbUv(48, 48, armW);
|
||||
const leftArm = createPart(armW, 12, 4, lArmUv, lSleeveUv, isLegacy);
|
||||
leftArm.position.set(slimFormat ? 5.5 : 6, 0, 0);
|
||||
playerGroup.add(leftArm);
|
||||
}
|
||||
|
||||
if (!(animValue & SKIN_ANIM.HIDE_RIGHT_LEG)) {
|
||||
const rLegUv = limbUv(0, 16);
|
||||
const rPantUv = (isLegacy || (animValue & SKIN_ANIM.HIDE_RIGHT_PANT)) ? undefined : limbUv(0, 32);
|
||||
const rightLeg = createPart(4, 12, 4, rLegUv, rPantUv);
|
||||
rightLeg.position.set(-2, -12, 0);
|
||||
playerGroup.add(rightLeg);
|
||||
}
|
||||
|
||||
if (!(animValue & SKIN_ANIM.HIDE_LEFT_LEG)) {
|
||||
const lLegUv = isLegacy ? limbUv(0, 16) : limbUv(16, 48);
|
||||
const lPantUv = (isLegacy || (animValue & SKIN_ANIM.HIDE_LEFT_PANT)) ? undefined : limbUv(0, 48);
|
||||
const leftLeg = createPart(4, 12, 4, lLegUv, lPantUv, isLegacy);
|
||||
leftLeg.position.set(2, -12, 0);
|
||||
playerGroup.add(leftLeg);
|
||||
}
|
||||
}
|
||||
|
||||
const boxProps = asset.properties.filter(p => p.key === "BOX");
|
||||
boxProps.forEach(prop => {
|
||||
const parts = prop.value.split(/\s+/);
|
||||
if (parts.length >= 11) {
|
||||
const type = parts[0];
|
||||
const bx = parseFloat(parts[1]);
|
||||
const by = parseFloat(parts[2]);
|
||||
const bz = parseFloat(parts[3]);
|
||||
const bw = parseFloat(parts[4]);
|
||||
const bh = parseFloat(parts[5]);
|
||||
const bd = parseFloat(parts[6]);
|
||||
const bu = parseFloat(parts[7]);
|
||||
const bv = parseFloat(parts[8]);
|
||||
const mir = parseInt(parts[10]) === 1;
|
||||
const scale = parseFloat(parts[11] || "0");
|
||||
|
||||
const uv = {
|
||||
top: [bu + bd, bv, bw, bd],
|
||||
bottom: [bu + bd + bw, bv, bw, bd],
|
||||
right: [bu, bv + bd, bd, bh],
|
||||
front: [bu + bd, bv + bd, bw, bh],
|
||||
left: [bu + bd + bw, bv + bd, bd, bh],
|
||||
back: [bu + 2 * bd + bw, bv + bd, bw, bh]
|
||||
};
|
||||
|
||||
const boxPart = createPart(bw, bh, bd, uv, undefined, mir);
|
||||
|
||||
let ox = 0, oy = 0, oz = 0;
|
||||
if (type === "HEAD_DEFAULT") oy = -4;
|
||||
else if (type === "HEAD") oy = -8;
|
||||
else if (type === "BODY") oy = 2;
|
||||
else if (type === "ARM0") { ox = -5; oy = 2; }
|
||||
else if (type === "ARM1") { ox = 5; oy = 2; }
|
||||
else if (type === "LEG0") { ox = -1.9; oy = 12; }
|
||||
else if (type === "LEG1") { ox = 1.9; oy = 12; }
|
||||
|
||||
boxPart.position.set(ox + bx + bw / 2, -(oy + by) - bh / 2, oz + bz + bd / 2);
|
||||
if (scale !== 0) boxPart.scale.set(1 + scale / bw, 1 + scale / bh, 1 + scale / bd);
|
||||
playerGroup.add(boxPart);
|
||||
}
|
||||
});
|
||||
|
||||
playerGroup.rotation.y = -0.3;
|
||||
render();
|
||||
});
|
||||
|
||||
let isDragging = false;
|
||||
let previousMousePosition = { x: 0, y: 0 };
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
isDragging = true;
|
||||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
||||
};
|
||||
const onMouseUp = () => { isDragging = false; };
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (isDragging && playerGroupRef.current) {
|
||||
playerGroupRef.current.rotation.y += (e.clientX - previousMousePosition.x) * 0.01;
|
||||
playerGroupRef.current.rotation.x += (e.clientY - previousMousePosition.y) * 0.01;
|
||||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
||||
render();
|
||||
}
|
||||
};
|
||||
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
camera.position.z += e.deltaY * 0.1;
|
||||
camera.position.z = Math.max(20, Math.min(camera.position.z, 200));
|
||||
render();
|
||||
};
|
||||
|
||||
renderer.domElement.addEventListener("mousedown", onMouseDown);
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
renderer.domElement.addEventListener("wheel", onWheel);
|
||||
|
||||
const handleResize = () => {
|
||||
if (!mountRef.current || !renderer) return;
|
||||
const w = mountRef.current.clientWidth;
|
||||
const h = mountRef.current.clientHeight;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
render();
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
if (object.geometry) object.geometry.dispose();
|
||||
if (object.material) {
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach((mat) => {
|
||||
if (mat.map) mat.map.dispose();
|
||||
mat.dispose();
|
||||
});
|
||||
} else {
|
||||
if (object.material.map) object.material.map.dispose();
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
renderer.dispose();
|
||||
};
|
||||
}, [asset]);
|
||||
|
||||
return <div ref={mountRef} className={`w-full h-full cursor-move ${className}`} />;
|
||||
});
|
||||
|
||||
export default SkinPreview3D;
|
||||
|
|
@ -3,6 +3,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||
import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
|
||||
import { PckService } from "../../services/PckService";
|
||||
import { PCKFile, PCKAsset, PCKAssetType } from "../../types/pck";
|
||||
import SkinPreview3D from "../common/SkinPreview3D";
|
||||
export default function PckEditorView() {
|
||||
const { setActiveView } = useUI();
|
||||
const { playPressSound, playBackSound } = useAudio();
|
||||
|
|
@ -13,6 +14,8 @@ export default function PckEditorView() {
|
|||
const [isEditingProperty, setIsEditingProperty] = useState<{ idx: number, key: string, 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: File, data: Uint8Array } | null>(null);
|
||||
const [isChangingType, setIsChangingType] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const replaceInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -148,9 +151,25 @@ export default function PckEditorView() {
|
|||
setExpandedFolders(new Set());
|
||||
} catch (err) {
|
||||
console.error("Failed to parse PCK", err);
|
||||
showNotification("Failed to parse PCK", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewPCK = () => {
|
||||
playPressSound();
|
||||
const newPck: PCKFile = {
|
||||
version: 2,
|
||||
endianness: "little",
|
||||
xmlSupport: false,
|
||||
properties: ["ANIM", "BOX"],
|
||||
files: []
|
||||
};
|
||||
setPck(newPck);
|
||||
setSelectedAssetId(null);
|
||||
setExpandedFolders(new Set());
|
||||
showNotification("New PCK Created");
|
||||
};
|
||||
|
||||
|
||||
const showNotification = (message: string, type: "success" | "error" = "success") => {
|
||||
setNotification({ message, type });
|
||||
|
|
@ -201,27 +220,45 @@ export default function PckEditorView() {
|
|||
playPressSound();
|
||||
const buffer = await file.arrayBuffer();
|
||||
const data = new Uint8Array(buffer);
|
||||
setShowTypeModal({ file, data });
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const confirmAddAsset = (type: PCKAssetType) => {
|
||||
if (!pck || !showTypeModal) return;
|
||||
const { file, data } = showTypeModal;
|
||||
const newAsset: PCKAsset = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
path: file.name,
|
||||
type: PCKAssetType.TEXTURE, //neo: this is the default btw
|
||||
type,
|
||||
size: data.length,
|
||||
data,
|
||||
properties: []
|
||||
};
|
||||
|
||||
if (type === PCKAssetType.SKIN || type === PCKAssetType.CAPE) {
|
||||
newAsset.properties.push({ key: "ANIM", value: "0" });
|
||||
}
|
||||
|
||||
setPck({ ...pck, files: [...pck.files, newAsset] });
|
||||
setSelectedAssetId(newAsset.id);
|
||||
e.target.value = "";
|
||||
setShowTypeModal(null);
|
||||
showNotification("Asset Added");
|
||||
};
|
||||
|
||||
const handlePropertyEdit = (idx: number, newVal: string) => {
|
||||
const handlePropertyEdit = (idx: number, newVal: string, isKey = false) => {
|
||||
if (!pck || !selectedAssetId) return;
|
||||
const newFiles = pck.files.map(f => {
|
||||
if (f.id === selectedAssetId) {
|
||||
const newProps = [...f.properties];
|
||||
newProps[idx] = { ...newProps[idx], value: newVal };
|
||||
if (isKey) {
|
||||
newProps[idx] = { ...newProps[idx], key: newVal };
|
||||
if (!pck.properties.includes(newVal)) {
|
||||
pck.properties.push(newVal);
|
||||
}
|
||||
} else {
|
||||
newProps[idx] = { ...newProps[idx], value: newVal };
|
||||
}
|
||||
return { ...f, properties: newProps };
|
||||
}
|
||||
return f;
|
||||
|
|
@ -229,6 +266,47 @@ export default function PckEditorView() {
|
|||
setPck({ ...pck, files: newFiles });
|
||||
};
|
||||
|
||||
const handleAddProperty = () => {
|
||||
if (!pck || !selectedAssetId) return;
|
||||
playPressSound();
|
||||
const newFiles = pck.files.map(f => {
|
||||
if (f.id === selectedAssetId) {
|
||||
return {
|
||||
...f,
|
||||
properties: [...f.properties, { key: "NEW_PROPERTY", value: "0" }]
|
||||
};
|
||||
}
|
||||
return f;
|
||||
});
|
||||
setPck({ ...pck, files: newFiles });
|
||||
};
|
||||
|
||||
const handleRemoveProperty = (idx: number) => {
|
||||
if (!pck || !selectedAssetId) return;
|
||||
playBackSound();
|
||||
const newFiles = pck.files.map(f => {
|
||||
if (f.id === selectedAssetId) {
|
||||
const newProps = [...f.properties];
|
||||
newProps.splice(idx, 1);
|
||||
return { ...f, properties: newProps };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
setPck({ ...pck, files: newFiles });
|
||||
};
|
||||
|
||||
const handleTypeChange = (newType: PCKAssetType) => {
|
||||
if (!pck || !selectedAssetId) return;
|
||||
playPressSound();
|
||||
const newFiles = pck.files.map(f => {
|
||||
if (f.id === selectedAssetId) {
|
||||
return { ...f, type: newType };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
setPck({ ...pck, files: newFiles });
|
||||
};
|
||||
|
||||
const handleMoveAsset = (direction: 'up' | 'down') => {
|
||||
if (!pck || !selectedAssetId) return;
|
||||
const idx = pck.files.findIndex(f => f.id === selectedAssetId);
|
||||
|
|
@ -272,6 +350,19 @@ export default function PckEditorView() {
|
|||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [playBackSound, setActiveView, isEditingProperty]);
|
||||
|
||||
const getTypeColor = (type: PCKAssetType) => {
|
||||
switch (type) {
|
||||
case PCKAssetType.SKIN: return "#FFFF55";
|
||||
case PCKAssetType.CAPE: return "#AA00AA";
|
||||
case PCKAssetType.TEXTURE: return "#55FFFF";
|
||||
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";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
|
|
@ -293,6 +384,13 @@ export default function PckEditorView() {
|
|||
>
|
||||
Open PCK
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNewPCK}
|
||||
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%" }}
|
||||
>
|
||||
New PCK
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSavePCK}
|
||||
disabled={!pck}
|
||||
|
|
@ -339,7 +437,10 @@ export default function PckEditorView() {
|
|||
<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">
|
||||
{!selectedAsset ? (
|
||||
<div className="flex-1 flex items-center justify-center text-white/20 italic">Select an asset</div>
|
||||
<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" }} />
|
||||
<span>Select an asset to view details</span>
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={selectedAsset.id}
|
||||
|
|
@ -348,9 +449,76 @@ export default function PckEditorView() {
|
|||
className="flex flex-col h-full"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4 border-b border-[#373737] pb-2">
|
||||
<h3 className="text-[#FFFF55] text-xl mc-text-shadow truncate pr-4">
|
||||
{selectedAsset.path.split("/").pop()}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-1 min-w-0 flex-1 pr-4">
|
||||
<h3 className="text-[#FFFF55] text-xl mc-text-shadow truncate">
|
||||
{selectedAsset.path.split("/").pop()}
|
||||
</h3>
|
||||
<div className="relative">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span className="text-[10px] uppercase tracking-widest mc-text-shadow font-bold" style={{ color: getTypeColor(selectedAsset.type) }}>
|
||||
{PCKAssetType[selectedAsset.type].replace(/_/g, " ")}
|
||||
</span>
|
||||
<img
|
||||
src="/images/Settings_Arrow_Down.png"
|
||||
className={`w-2 h-2 object-contain opacity-40 group-hover/type:opacity-60 transition-transform ${isChangingType ? "rotate-180" : ""}`}
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isChangingType && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[120]"
|
||||
onClick={() => setIsChangingType(false)}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: -10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
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"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated"
|
||||
}}
|
||||
>
|
||||
{Object.keys(PCKAssetType)
|
||||
.filter(k => isNaN(Number(k)))
|
||||
.map((typeName) => {
|
||||
const typeVal = PCKAssetType[typeName as keyof typeof PCKAssetType];
|
||||
const isActive = selectedAsset.type === typeVal;
|
||||
return (
|
||||
<button
|
||||
key={typeName}
|
||||
onClick={() => {
|
||||
handleTypeChange(typeVal);
|
||||
setIsChangingType(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-[10px] uppercase tracking-widest transition-all border-l-2 ${isActive
|
||||
? "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="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: getTypeColor(typeVal) }} />
|
||||
{typeName.replace(/_/g, " ")}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<button 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" }} />
|
||||
|
|
@ -362,19 +530,47 @@ export default function PckEditorView() {
|
|||
</div>
|
||||
|
||||
{assetPreviewUrl && (
|
||||
<div className="w-full h-40 bg-black/40 border-2 border-[#373737] mb-6 flex items-center justify-center overflow-hidden">
|
||||
<img src={assetPreviewUrl} className="max-w-full max-h-full object-contain" style={{ imageRendering: "pixelated" }} />
|
||||
<div className="w-full h-64 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) ? (
|
||||
<SkinPreview3D asset={selectedAsset} className="w-full h-full" />
|
||||
) : (
|
||||
<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">
|
||||
{selectedAsset.type === PCKAssetType.SKIN ? "3D Skin View" : selectedAsset.type === PCKAssetType.CAPE ? "3D Cape View" : "Texture Preview"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6 flex-1">
|
||||
<div>
|
||||
<div className="text-white/40 text-[10px] uppercase tracking-widest mb-2 px-1">Metadata Properties</div>
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
<button
|
||||
onClick={handleAddProperty}
|
||||
className="text-[#FFFF55] text-[10px] uppercase hover:underline"
|
||||
>
|
||||
+ Add Property
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{selectedAsset.properties.map((prop, idx) => (
|
||||
<div key={idx} className="flex flex-col gap-1">
|
||||
<span className="text-white/40 text-[10px] px-1">{prop.key}</span>
|
||||
<div className="relative group">
|
||||
<div key={idx} className="flex flex-col gap-1 group/prop">
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<input
|
||||
type="text"
|
||||
value={prop.key}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveProperty(idx)}
|
||||
className="text-red-500/0 group-hover/prop:text-red-500/40 hover:text-red-500 transition-colors text-[10px] uppercase"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={prop.value}
|
||||
|
|
@ -382,10 +578,55 @@ export default function PckEditorView() {
|
|||
className="w-full bg-black/40 p-2 text-white border border-[#373737] text-sm focus:border-[#FFFF55] outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{prop.key === "ANIM" && (
|
||||
<div className="mt-2 p-3 bg-black/30 border border-[#373737]/50 grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
{[
|
||||
{ label: "Slim Format", flag: 1 << 19 },
|
||||
{ label: "Modern Wide", flag: 1 << 18 },
|
||||
{ label: "Hide Hat", flag: 1 << 16 },
|
||||
{ label: "Hide Head", flag: 1 << 10 },
|
||||
{ label: "Hide Body", flag: 1 << 13 },
|
||||
{ label: "Hide Right Arm", flag: 1 << 11 },
|
||||
{ label: "Hide Left Arm", flag: 1 << 12 },
|
||||
{ label: "Hide Right Leg", flag: 1 << 14 },
|
||||
{ label: "Hide Left Leg", flag: 1 << 15 },
|
||||
{ label: "Hide Jacket", flag: 1 << 24 },
|
||||
{ label: "Hide Right Sleeve", flag: 1 << 21 },
|
||||
{ label: "Hide Left Sleeve", flag: 1 << 20 },
|
||||
{ label: "Hide Right Pant", flag: 1 << 23 },
|
||||
{ label: "Hide Left Pant", flag: 1 << 22 },
|
||||
{ label: "Zombie Arms", flag: 1 << 1 },
|
||||
{ label: "Upside Down", flag: 1 << 31 },
|
||||
].map((item) => {
|
||||
const currentVal = parseInt(prop.value) || 0;
|
||||
const isChecked = (currentVal & item.flag) !== 0;
|
||||
return (
|
||||
<label key={item.label} className="flex items-center gap-2 cursor-pointer group/flag">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
const newVal = e.target.checked
|
||||
? currentVal | item.flag
|
||||
: currentVal & ~item.flag;
|
||||
handlePropertyEdit(idx, newVal.toString());
|
||||
}}
|
||||
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"}`} />
|
||||
<span className={`text-[9px] uppercase tracking-tight ${isChecked ? "text-[#FFFF55]" : "text-white/40 group-hover/flag:text-white/60"}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{selectedAsset.properties.length === 0 && (
|
||||
<div className="text-white/20 italic text-sm px-1">No metadata properties available</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>
|
||||
|
|
@ -444,7 +685,7 @@ export default function PckEditorView() {
|
|||
initial={{ opacity: 0, y: 50, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 50, scale: 0.9 }}
|
||||
className="fixed top-12 right-12 z-50 p-6 flex flex-col items-center justify-center min-w-[240px]"
|
||||
className="fixed top-12 right-12 z-[110] p-6 flex flex-col items-center justify-center min-w-[240px]"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -457,6 +698,52 @@ export default function PckEditorView() {
|
|||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{showTypeModal && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={() => setShowTypeModal(null)}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
className="relative w-full max-w-md p-8 flex flex-col items-center"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated"
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
{Object.keys(PCKAssetType)
|
||||
.filter(k => isNaN(Number(k)))
|
||||
.map((typeName) => (
|
||||
<button
|
||||
key={typeName}
|
||||
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"
|
||||
>
|
||||
{typeName.replace(/_/g, " ")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowTypeModal(null)}
|
||||
className="mt-8 px-8 py-2 text-white/60 mc-text-shadow text-sm hover:text-white transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue