mirror of
https://github.com/gradenGnostic/LegacyLauncher.git
synced 2026-04-23 07:27:28 +00:00
v2.9.0
This commit is contained in:
parent
32b02f11f8
commit
68f77fc77a
160
index.html
160
index.html
|
|
@ -27,39 +27,56 @@
|
|||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-title">PATCH NOTES</div>
|
||||
<div class="sidebar collapsed" id="main-sidebar">
|
||||
<div class="sidebar-title" onclick="toggleSidebar()">
|
||||
<span id="sidebar-title-text">PATCH NOTES</span>
|
||||
<span id="sidebar-toggle-icon" title="Expand Patch Notes">▶</span>
|
||||
</div>
|
||||
<div id="updates-list">
|
||||
<div class="update-item">Loading updates...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="relative">
|
||||
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
|
||||
<div id="splash-text" class="splash-text">Splash!</div>
|
||||
</div>
|
||||
|
||||
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
|
||||
|
||||
<div class="version-row">
|
||||
<span class="version-label">Version:</span>
|
||||
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
|
||||
<span id="current-version-display">Loading...</span>
|
||||
<div class="select-arrow">▼</div>
|
||||
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
|
||||
<option>Loading...</option>
|
||||
</select>
|
||||
<div class="content-area" style="flex-direction: row; align-items: stretch; padding: 0;">
|
||||
|
||||
<!-- Left Side: Main Menu -->
|
||||
<div class="menu-column" style="flex: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; z-index: 5;">
|
||||
<div class="relative">
|
||||
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
|
||||
<div id="splash-text" class="splash-text">Splash!</div>
|
||||
</div>
|
||||
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
|
||||
<img src="restart.png" alt="Update">
|
||||
|
||||
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
|
||||
|
||||
<div class="version-row">
|
||||
<span class="version-label">Version:</span>
|
||||
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
|
||||
<span id="current-version-display">Loading...</span>
|
||||
<div class="select-arrow">▼</div>
|
||||
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
|
||||
<option>Loading...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
|
||||
<img src="restart.png" alt="Update">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 w-[500px] max-w-[90%]">
|
||||
<div class="btn-mc flex-grow nav-item" id="btn-instances" onclick="toggleInstances(true)" tabindex="0" style="font-size: 18px;">INSTANCES</div>
|
||||
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0" style="font-size: 18px;">PROFILE</div>
|
||||
<div class="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0" style="font-size: 18px;">SERVERS</div>
|
||||
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0" style="font-size: 18px;">OPTIONS</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 w-[500px] max-w-[90%]">
|
||||
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0">PROFILE</div>
|
||||
<div class="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0">SERVERS</div>
|
||||
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0">OPTIONS</div>
|
||||
|
||||
<!-- Right Side: Skin Viewer -->
|
||||
<div class="skin-column" style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; min-width: 300px; padding-right: 0px;">
|
||||
<div id="main-skin-viewer" style="width: 100%; height: 500px; cursor: grab; display: flex; align-items: center; justify-content: center;"></div>
|
||||
|
||||
<div class="btn-mc nav-item" onclick="openSkinManager()" style="width: 250px; height: 48px; font-size: 20px; margin-top: -20px; z-index: 10; margin-left: auto; margin-right: auto;" tabindex="0">
|
||||
CHANGE SKIN
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-container" id="progress-container">
|
||||
|
|
@ -103,6 +120,43 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="instances-modal">
|
||||
<div class="modal-box" style="max-width: 900px;">
|
||||
<div class="modal-title">GAME INSTANCES</div>
|
||||
|
||||
<div id="instances-list-container" class="w-full max-h-[400px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
|
||||
<!-- Instances will be listed here -->
|
||||
<div class="text-center text-gray-400 py-4">No instances found.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 w-full">
|
||||
<div class="btn-mc flex-grow nav-item" onclick="createNewInstance()" tabindex="0">ADD INSTANCE</div>
|
||||
<div class="btn-mc flex-grow nav-item" onclick="toggleInstances(false)" tabindex="0">DONE</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="add-instance-modal">
|
||||
<div class="modal-box" style="max-width: 600px;">
|
||||
<div class="modal-title" style="font-size: 32px;">NEW INSTANCE</div>
|
||||
|
||||
<div class="mc-input-group">
|
||||
<label class="mc-label">Instance Name:</label>
|
||||
<input type="text" id="new-instance-name" class="mc-input nav-item" placeholder="My Instance" tabindex="0">
|
||||
</div>
|
||||
|
||||
<div class="mc-input-group">
|
||||
<label class="mc-label">GitHub Repo (user/repo):</label>
|
||||
<input type="text" id="new-instance-repo" class="mc-input nav-item" placeholder="smartcmd/MinecraftConsoles" tabindex="0">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 w-full mt-4">
|
||||
<div class="btn-mc flex-grow nav-item" onclick="saveNewInstance()" tabindex="0">CREATE</div>
|
||||
<div class="btn-mc flex-grow nav-item" onclick="toggleAddInstance(false)" tabindex="0">CANCEL</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="options-modal">
|
||||
<div class="modal-box">
|
||||
<div class="modal-title">LAUNCHER OPTIONS</div>
|
||||
|
|
@ -168,7 +222,7 @@
|
|||
|
||||
<div class="mc-input-group">
|
||||
<label class="mc-label">In-Game Username:</label>
|
||||
<input type="text" id="username-input" class="mc-input nav-item" placeholder="Steve" tabindex="0">
|
||||
<input type="text" id="username-input" class="mc-input nav-item" placeholder="Steve" tabindex="0" maxlength="16">
|
||||
</div>
|
||||
|
||||
<div class="mc-input-group mt-4">
|
||||
|
|
@ -194,12 +248,66 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="skin-modal" style="display: none;">
|
||||
<div class="modal-box" style="max-width: 900px; background: var(--mc-gui-bg); border: 4px solid #000; padding: 30px; box-shadow: inset 4px 4px 0 #fff;">
|
||||
<div class="modal-title" style="margin-bottom: 25px;">SKIN UPLOADER</div>
|
||||
|
||||
<div id="drop-zone" class="nav-item" style="width: 100%; height: 120px; display: flex; align-items: center; justify-content: center; background: #000; border: 2px solid #555; cursor: pointer; margin-bottom: 20px;" tabindex="0">
|
||||
<input type="file" id="skin-input" accept=".png" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
||||
<div id="upload-prompt" class="text-center">
|
||||
<p class="text-xl text-white" style="text-shadow: 2px 2px 0 #000;">CLICK OR DRAG SKIN HERE</p>
|
||||
<p class="text-xs text-gray-400 mt-1 uppercase font-bold" style="letter-spacing: 2px;">64x32 or 64x64 PNG</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="preview-container" class="hidden flex flex-col items-center w-full">
|
||||
<div class="flex flex-row gap-6 items-stretch w-full justify-center mb-6" style="height: 400px;">
|
||||
<!-- 3D Preview Panel -->
|
||||
<div class="relative bg-black rounded-none border-2 border-[#555] flex flex-col items-center overflow-hidden w-1/2">
|
||||
<div id="skin-viewer-container" class="w-full h-full"></div>
|
||||
<div style="position: absolute; bottom: 10px; left: 10px; color: #55ff55; font-size: 14px; text-shadow: 1px 1px 0 #000; z-index: 5;">3D PREVIEW</div>
|
||||
</div>
|
||||
|
||||
<!-- info/2D Panel -->
|
||||
<div class="flex-1 w-1/2 flex flex-col gap-4">
|
||||
<div class="bg-black p-4 border-2 border-[#555] flex flex-col items-center flex-1 justify-center">
|
||||
<div class="relative" style="margin-bottom: 15px;">
|
||||
<canvas id="skin-canvas" width="64" height="32" class="pixelated" style="width: 256px; height: 128px; image-rendering: pixelated; border: 2px solid #555;"></canvas>
|
||||
</div>
|
||||
<div style="color: #aaa; font-size: 14px; text-align: center; text-transform: uppercase;">LEGACY OUTPUT (64x32)</div>
|
||||
<div id="status-message" style="margin-top: 10px; font-size: 18px;"></div>
|
||||
</div>
|
||||
|
||||
<div style="background: rgba(0,0,0,0.3); padding: 15px; border: 2px solid #555; color: #eee; font-size: 16px;">
|
||||
<div style="display: flex; justify-content: space-between; border-bottom: 2px solid #555; padding-bottom: 8px; margin-bottom: 8px;">
|
||||
<span>DETECTED:</span>
|
||||
<span id="format-label" style="color: #fff;">--</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>TARGET:</span>
|
||||
<span style="color: #888; font-size: 14px;">Common/res/mob/char.png</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="save-skin-btn" class="btn-mc w-full nav-item" tabindex="0">SAVE SKIN</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center w-full mt-6">
|
||||
<div id="btn-close-skin" class="btn-mc nav-item !mb-0" onclick="closeSkinManager()" tabindex="0">DONE</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast">NOTIFICATION</div>
|
||||
|
||||
<div id="music-toggle" class="music-btn nav-item" onclick="toggleMusic()" title="Toggle Music" tabindex="0">
|
||||
<span id="music-icon">♪</span>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="skin_manager.js"></script>
|
||||
<script src="renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "legacylauncher",
|
||||
"version": "2.0.2",
|
||||
"version": "2.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "legacylauncher",
|
||||
"version": "2.0.2",
|
||||
"version": "2.2.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"electron-store": "^6.0.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "legacylauncher",
|
||||
"version": "2.2.0",
|
||||
"version": "2.9.0",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
937
renderer.js
937
renderer.js
File diff suppressed because it is too large
Load diff
390
skin_manager.js
Normal file
390
skin_manager.js
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
// skin_manager.js
|
||||
// Handles skin uploading, conversion, 3D preview, and saving to char.png
|
||||
|
||||
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
|
||||
let isMainSkinDragging = false;
|
||||
|
||||
let skinScene, skinCamera, skinRenderer, skinPlayerGroup;
|
||||
let isSkinDragging = false;
|
||||
let previousSkinMousePosition = { x: 0, y: 0 };
|
||||
let processedSkinDataUrl = null;
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const skinInput = document.getElementById('skin-input');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const saveSkinBtn = document.getElementById('save-skin-btn');
|
||||
const closeSkinBtn = document.getElementById('btn-close-skin');
|
||||
|
||||
if (skinInput) skinInput.addEventListener('change', (e) => handleSkinFile(e.target.files[0]));
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('border-green-500');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('border-green-500'));
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('border-green-500');
|
||||
handleSkinFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if (saveSkinBtn) {
|
||||
saveSkinBtn.addEventListener('click', saveSkinToDisk);
|
||||
}
|
||||
|
||||
if (closeSkinBtn) {
|
||||
closeSkinBtn.addEventListener('click', closeSkinManager);
|
||||
}
|
||||
|
||||
// Initialize Main Menu Viewer
|
||||
initMainMenuSkinViewer();
|
||||
});
|
||||
|
||||
function initMainMenuSkinViewer() {
|
||||
const container = document.getElementById('main-skin-viewer');
|
||||
if (!container) return;
|
||||
|
||||
mainMenuScene = new THREE.Scene();
|
||||
mainMenuCamera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 0.1, 1000);
|
||||
mainMenuCamera.position.set(0, 5, 60);
|
||||
|
||||
mainMenuRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
mainMenuRenderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||
mainMenuRenderer.setPixelRatio(window.devicePixelRatio);
|
||||
mainMenuRenderer.outputEncoding = THREE.sRGBEncoding;
|
||||
container.appendChild(mainMenuRenderer.domElement);
|
||||
|
||||
mainMenuScene.add(new THREE.AmbientLight(0xffffff, 1.1));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
dl.position.set(10, 20, 15);
|
||||
mainMenuScene.add(dl);
|
||||
|
||||
mainMenuPlayerGroup = new THREE.Group();
|
||||
mainMenuScene.add(mainMenuPlayerGroup);
|
||||
|
||||
// Interaction for Main Menu Viewer
|
||||
let prevX = 0;
|
||||
container.addEventListener('mousedown', (e) => {
|
||||
isMainSkinDragging = true;
|
||||
prevX = e.clientX;
|
||||
});
|
||||
window.addEventListener('mouseup', () => isMainSkinDragging = false);
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (isMainSkinDragging && mainMenuPlayerGroup) {
|
||||
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
|
||||
prevX = e.clientX;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-rotate slowly
|
||||
function animateMain() {
|
||||
requestAnimationFrame(animateMain);
|
||||
if (!isMainSkinDragging && mainMenuPlayerGroup) mainMenuPlayerGroup.rotation.y += 0.005;
|
||||
if (mainMenuRenderer && mainMenuScene && mainMenuCamera) mainMenuRenderer.render(mainMenuScene, mainMenuCamera);
|
||||
}
|
||||
animateMain();
|
||||
|
||||
// Load current skin after short delay to ensure paths are ready
|
||||
setTimeout(loadMainMenuSkin, 500);
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
if (mainMenuCamera && mainMenuRenderer && container) {
|
||||
mainMenuCamera.aspect = container.offsetWidth / container.offsetHeight;
|
||||
mainMenuCamera.updateProjectionMatrix();
|
||||
mainMenuRenderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadMainMenuSkin() {
|
||||
try {
|
||||
const installDir = await window.getInstallDir();
|
||||
const skinPath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
|
||||
|
||||
if (fs.existsSync(skinPath)) {
|
||||
const skinData = fs.readFileSync(skinPath);
|
||||
const blob = new Blob([skinData]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const isLegacy = img.height === 32;
|
||||
updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup);
|
||||
};
|
||||
img.src = url;
|
||||
} else {
|
||||
console.log("No skin found at " + skinPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load main menu skin:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSkinModel(dataUrl, isLegacy, targetGroup) {
|
||||
if (!targetGroup) return;
|
||||
|
||||
new THREE.TextureLoader().load(dataUrl, (texture) => {
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
texture.encoding = THREE.sRGBEncoding;
|
||||
|
||||
// Clear existing children
|
||||
while(targetGroup.children.length > 0) targetGroup.remove(targetGroup.children[0]);
|
||||
|
||||
const createFaceMaterial = (tex, x, y, w, h) => {
|
||||
const texWidth = tex.image.width;
|
||||
const texHeight = tex.image.height;
|
||||
const matTex = tex.clone();
|
||||
matTex.magFilter = THREE.NearestFilter;
|
||||
matTex.minFilter = THREE.NearestFilter;
|
||||
matTex.repeat.set(w / texWidth, h / texHeight);
|
||||
matTex.offset.set(x / texWidth, 1 - (y + h) / texHeight);
|
||||
matTex.needsUpdate = true;
|
||||
return new THREE.MeshLambertMaterial({ map: matTex, transparent: true, alphaTest: 0.5, side: THREE.FrontSide });
|
||||
};
|
||||
|
||||
const createBodyPart = (w, h, d, tex, uv, offset = 0) => {
|
||||
const geometry = new THREE.BoxGeometry(w, h, d);
|
||||
const materials = [
|
||||
createFaceMaterial(tex, uv.left[0], uv.left[1], uv.left[2], uv.left[3]), // Left (Standard MC "Right")
|
||||
createFaceMaterial(tex, uv.right[0], uv.right[1], uv.right[2], uv.right[3]), // Right (Standard MC "Left")
|
||||
createFaceMaterial(tex, uv.top[0], uv.top[1], uv.top[2], uv.top[3]),
|
||||
createFaceMaterial(tex, uv.bottom[0], uv.bottom[1], uv.bottom[2], uv.bottom[3]),
|
||||
createFaceMaterial(tex, uv.front[0], uv.front[1], uv.front[2], uv.front[3]),
|
||||
createFaceMaterial(tex, uv.back[0], uv.back[1], uv.back[2], uv.back[3])
|
||||
];
|
||||
const mesh = new THREE.Mesh(geometry, materials);
|
||||
if (offset !== 0) mesh.scale.set(1 + offset, 1 + offset, 1 + offset);
|
||||
return mesh;
|
||||
};
|
||||
|
||||
const limbUv = (x, y) => ({
|
||||
top: [x+4, y, 4, 4], bottom: [x+8, y, 4, 4],
|
||||
right: [x, y+4, 4, 12], front: [x+4, y+4, 4, 12],
|
||||
left: [x+8, y+4, 4, 12], back: [x+12, y+4, 4, 12]
|
||||
});
|
||||
|
||||
// Head
|
||||
const headUvs = { 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 head = createBodyPart(8, 8, 8, texture, headUvs);
|
||||
head.position.y = 10;
|
||||
targetGroup.add(head);
|
||||
|
||||
// Hat
|
||||
const hatUvs = { 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 hat = createBodyPart(8, 8, 8, texture, hatUvs, 0.12);
|
||||
hat.position.y = 10;
|
||||
targetGroup.add(hat);
|
||||
|
||||
// Torso
|
||||
const torsoUvs = { 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] };
|
||||
targetGroup.add(createBodyPart(8, 12, 4, texture, torsoUvs));
|
||||
|
||||
// Jacket (non-legacy only)
|
||||
if (!isLegacy) {
|
||||
const jacketUvs = { 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] };
|
||||
targetGroup.add(createBodyPart(8, 12, 4, texture, jacketUvs, 0.05));
|
||||
}
|
||||
|
||||
// Limbs
|
||||
const limbs = [
|
||||
{ pos: [-6, 0, 0], uv: limbUv(40, 16), layerUv: limbUv(40, 32) },
|
||||
{ pos: [6, 0, 0], uv: isLegacy ? limbUv(40, 16) : limbUv(32, 48), layerUv: limbUv(48, 48) },
|
||||
{ pos: [-2, -12, 0], uv: limbUv(0, 16), layerUv: limbUv(0, 32) },
|
||||
{ pos: [2, -12, 0], uv: isLegacy ? limbUv(0, 16) : limbUv(16, 48), layerUv: limbUv(0, 48) }
|
||||
];
|
||||
|
||||
limbs.forEach(l => {
|
||||
const base = createBodyPart(4, 12, 4, texture, l.uv);
|
||||
base.position.set(...l.pos);
|
||||
targetGroup.add(base);
|
||||
if (!isLegacy) {
|
||||
const layer = createBodyPart(4, 12, 4, texture, l.layerUv, 0.05);
|
||||
layer.position.set(...l.pos);
|
||||
targetGroup.add(layer);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openSkinManager() {
|
||||
const modal = document.getElementById('skin-modal');
|
||||
modal.style.display = 'flex';
|
||||
modal.style.opacity = '1';
|
||||
|
||||
// Clear previous state in modal
|
||||
const previewContainer = document.getElementById('preview-container');
|
||||
if (previewContainer) previewContainer.classList.add('hidden');
|
||||
|
||||
const sysMsg = document.getElementById('sys-message');
|
||||
if (sysMsg) sysMsg.classList.add('hidden');
|
||||
|
||||
// Load current skin into preview (optional, maybe we only want to see uploaded ones)
|
||||
// loadCurrentSkinToPreview();
|
||||
}
|
||||
|
||||
function closeSkinManager() {
|
||||
const modal = document.getElementById('skin-modal');
|
||||
modal.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
|
||||
// Reset state
|
||||
const prompt = document.getElementById('upload-prompt');
|
||||
if (prompt) prompt.style.display = 'block';
|
||||
|
||||
const previewContainer = document.getElementById('preview-container');
|
||||
if (previewContainer) previewContainer.classList.add('hidden');
|
||||
|
||||
processedSkinDataUrl = null;
|
||||
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleSkinFile(file) {
|
||||
if (!file || !file.type.includes('png')) return window.showToast("Only PNG files are supported!");
|
||||
|
||||
const prompt = document.getElementById('upload-prompt');
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
processSkinImage(img, e.target.result);
|
||||
};
|
||||
img.onerror = () => {
|
||||
window.showToast("Failed to load image");
|
||||
if(prompt) prompt.style.display = 'block';
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function processSkinImage(img, srcUrl, isInitialLoad = false) {
|
||||
const canvas = document.getElementById('skin-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const formatLabel = document.getElementById('format-label');
|
||||
const statusMessage = document.getElementById('status-message');
|
||||
const previewContainer = document.getElementById('preview-container');
|
||||
const saveBtn = document.getElementById('save-skin-btn');
|
||||
const prompt = document.getElementById('upload-prompt');
|
||||
|
||||
if (img.width !== 64) {
|
||||
if(prompt) prompt.style.display = 'block';
|
||||
if(previewContainer) previewContainer.classList.add('hidden');
|
||||
return window.showToast("Invalid skin width. Must be 64px.");
|
||||
}
|
||||
|
||||
if(prompt) prompt.style.display = 'none';
|
||||
|
||||
const isLegacy = img.height === 32;
|
||||
|
||||
ctx.clearRect(0, 0, 64, 32);
|
||||
ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32);
|
||||
processedSkinDataUrl = canvas.toDataURL('image/png');
|
||||
|
||||
previewContainer.classList.remove('hidden');
|
||||
|
||||
if (isInitialLoad) {
|
||||
if(formatLabel) formatLabel.textContent = "Current Skin";
|
||||
if(statusMessage) statusMessage.innerHTML = "<span class='text-blue-400 font-black' style='color: #60a5fa;'>LOADED FROM DISK</span>";
|
||||
if(saveBtn) {
|
||||
saveBtn.textContent = "SAVED";
|
||||
saveBtn.classList.add('disabled');
|
||||
}
|
||||
} else {
|
||||
if(formatLabel) formatLabel.textContent = isLegacy ? "64x32 (Legacy)" : "64x64 (Modern)";
|
||||
if(statusMessage) statusMessage.innerHTML = isLegacy ? "<span class='text-green-400 font-black' style='color: #4ade80;'>LEGACY READY</span>" : "<span class='text-yellow-400 font-black' style='color: #facc15;'>CONVERTED TO 64x32</span>";
|
||||
if(saveBtn) {
|
||||
saveBtn.textContent = "SAVE SKIN";
|
||||
saveBtn.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
if (!skinScene) initPreviewEngine();
|
||||
updateSkinModel(srcUrl, isLegacy, skinPlayerGroup);
|
||||
}
|
||||
|
||||
function initPreviewEngine() {
|
||||
const container = document.getElementById('skin-viewer-container');
|
||||
if (!container) return;
|
||||
|
||||
skinScene = new THREE.Scene();
|
||||
skinCamera = new THREE.PerspectiveCamera(35, container.offsetWidth / container.offsetHeight, 0.1, 1000);
|
||||
skinCamera.position.set(0, 5, 70);
|
||||
|
||||
skinRenderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
|
||||
skinRenderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||
skinRenderer.setPixelRatio(window.devicePixelRatio);
|
||||
skinRenderer.outputEncoding = THREE.sRGBEncoding;
|
||||
container.appendChild(skinRenderer.domElement);
|
||||
|
||||
skinScene.add(new THREE.AmbientLight(0xffffff, 0.9));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.35);
|
||||
dl.position.set(10, 20, 15);
|
||||
skinScene.add(dl);
|
||||
|
||||
skinPlayerGroup = new THREE.Group();
|
||||
skinScene.add(skinPlayerGroup);
|
||||
|
||||
container.addEventListener('mousedown', () => isSkinDragging = true);
|
||||
window.addEventListener('mouseup', () => isSkinDragging = false);
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (isSkinDragging && skinPlayerGroup) {
|
||||
skinPlayerGroup.rotation.y += (e.movementX) * 0.01;
|
||||
}
|
||||
});
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
if (!isSkinDragging && skinPlayerGroup) skinPlayerGroup.rotation.y += 0.008;
|
||||
if (skinRenderer && skinScene && skinCamera) skinRenderer.render(skinScene, skinCamera);
|
||||
}
|
||||
animate();
|
||||
}
|
||||
|
||||
async function saveSkinToDisk() {
|
||||
if (!processedSkinDataUrl) return;
|
||||
|
||||
try {
|
||||
const installDir = await window.getInstallDir();
|
||||
const savePath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
|
||||
const dir = path.dirname(savePath);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const base64Data = processedSkinDataUrl.replace(/^data:image\/png;base64,/, "");
|
||||
fs.writeFileSync(savePath, base64Data, 'base64');
|
||||
|
||||
window.showToast("Skin Saved Successfully!");
|
||||
|
||||
const saveBtn = document.getElementById('save-skin-btn');
|
||||
if(saveBtn) {
|
||||
saveBtn.textContent = "SAVED!";
|
||||
saveBtn.classList.add('disabled');
|
||||
}
|
||||
|
||||
// Refresh main menu skin
|
||||
loadMainMenuSkin();
|
||||
|
||||
// Close modal after short delay?
|
||||
setTimeout(closeSkinManager, 1000);
|
||||
|
||||
} catch (e) {
|
||||
window.showToast("Error Saving Skin: " + e.message);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Global Export
|
||||
window.openSkinManager = openSkinManager;
|
||||
window.initMainMenuSkinViewer = initMainMenuSkinViewer;
|
||||
window.loadMainMenuSkin = loadMainMenuSkin;
|
||||
|
|
@ -476,3 +476,8 @@ JAKE: Pip pip!
|
|||
JAKE: By jove!
|
||||
JAKE: Egads!
|
||||
JAKE: Jolly good show
|
||||
Jade Harley!!!
|
||||
Jade Harley!!!
|
||||
Jade Harley!!!
|
||||
Jade Harley!!!
|
||||
Jade Harley!!!
|
||||
42
style.css
42
style.css
|
|
@ -112,6 +112,43 @@ body {
|
|||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), padding 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 80px;
|
||||
padding: 30px 10px;
|
||||
}
|
||||
|
||||
.sidebar.collapsed #updates-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-title {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
justify-content: center;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.sidebar.collapsed #sidebar-title-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed #sidebar-toggle-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
#sidebar-toggle-icon {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#sidebar-toggle-icon:hover {
|
||||
color: #55ff55;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
|
|
@ -119,9 +156,12 @@ body {
|
|||
color: #fff;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
margin-bottom: 25px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #555;
|
||||
padding-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.commit-author {
|
||||
|
|
|
|||
Loading…
Reference in a new issue