Compare commits
46 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42c1a9b0b | ||
|
|
cb4c62aff2 | ||
|
|
e733a250ec | ||
|
|
aa7c257553 | ||
|
|
3e094c6a1d | ||
|
|
90d40ba2af | ||
|
|
c514c981c7 | ||
|
|
4858f83ea1 | ||
|
|
124fb9dbcb | ||
|
|
8c36195731 | ||
|
|
1c1fbc3d46 | ||
|
|
1e27d63fae | ||
|
|
8e05e6b29d | ||
|
|
033315526c | ||
|
|
1377ece419 | ||
|
|
38092692a7 | ||
|
|
b54331475a | ||
|
|
6cda5ad9ac | ||
|
|
9c189b56df | ||
|
|
32b2909fee | ||
|
|
4c42fd0e55 | ||
|
|
3a104398f4 | ||
|
|
bb721c5ae0 | ||
|
|
08c332ecb7 | ||
|
|
0a06148338 | ||
|
|
a8a972a0f6 | ||
|
|
9d3e4fcc57 | ||
|
|
25116b6eba | ||
|
|
e44695860c | ||
|
|
6c59ad20d6 | ||
|
|
3355244dc4 | ||
|
|
e1a550674a | ||
|
|
12b8a503d9 | ||
|
|
3a5b37a8ca | ||
|
|
8f1aec38be | ||
|
|
dceef71f87 | ||
|
|
7b4942ee7e | ||
|
|
1937c941ea | ||
|
|
c50b36e50d | ||
|
|
0fa8df9a21 | ||
|
|
68f77fc77a | ||
|
|
32b02f11f8 | ||
|
|
46a484d5df | ||
|
|
69a0ebbfce | ||
|
|
c37a43449f | ||
|
|
c9ba08d5d2 |
31
.github/workflows/release.yml
vendored
|
|
@ -33,7 +33,6 @@ jobs:
|
||||||
name: linux-dist
|
name: linux-dist
|
||||||
path: dist/*.AppImage
|
path: dist/*.AppImage
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
name: Build Windows Installer
|
name: Build Windows Installer
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
@ -59,9 +58,8 @@ jobs:
|
||||||
dist/*.exe
|
dist/*.exe
|
||||||
dist/*.msi
|
dist/*.msi
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-mac:
|
build-mac:
|
||||||
name: Build macOS DMG
|
name: Build macOS DMG (Apple Silicon)
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -83,10 +81,32 @@ jobs:
|
||||||
name: mac-dist
|
name: mac-dist
|
||||||
path: dist/*.dmg
|
path: dist/*.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
build-mac-intel:
|
||||||
|
name: Build macOS DMG (Intel)
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: "npm"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Build macOS DMG (Intel)
|
||||||
|
run: npm run dist:mac -- --x64
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Upload macOS Intel artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mac-intel-dist
|
||||||
|
path: dist/*.dmg
|
||||||
|
if-no-files-found: error
|
||||||
release:
|
release:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
needs: [build-linux, build-windows, build-mac]
|
needs: [build-linux, build-windows, build-mac, build-mac-intel]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
|
|
@ -104,5 +124,6 @@ jobs:
|
||||||
artifacts/linux-dist/*
|
artifacts/linux-dist/*
|
||||||
artifacts/windows-dist/*
|
artifacts/windows-dist/*
|
||||||
artifacts/mac-dist/*
|
artifacts/mac-dist/*
|
||||||
|
artifacts/mac-intel-dist/*
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
20
README.md
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
A custom launcher for Minecraft Legacy Console Edition.
|
A custom launcher for Minecraft Legacy Console Edition.
|
||||||
|
|
||||||
<img width="1278" height="709" alt="image" src="https://github.com/user-attachments/assets/b28cf6e0-150a-4790-b30a-04548a123295" />
|
<img width="1277" height="717" alt="image" src="https://github.com/user-attachments/assets/eaa9bae6-3b3b-4e39-a3c1-156e34abf3cc" />
|
||||||
|
|
||||||
|
[](https://ko-fi.com/gradengnostic)
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
@ -76,6 +79,11 @@ The launcher supports several compatibility options for Linux:
|
||||||
- **extract-zip**: ZIP archive extraction
|
- **extract-zip**: ZIP archive extraction
|
||||||
- **Tailwind CSS**: UI styling (via CDN)
|
- **Tailwind CSS**: UI styling (via CDN)
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
- Controller button sprites: [greatdocbrown](https://greatdocbrown.itch.io/gamepad-ui)
|
||||||
|
- UI Sounds: Using the free version of [JDSherbert's Ultimate UI SFX Pack on itch.io](https://jdsherbert.itch.io/ultimate-ui-sfx-pack)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
The launcher is built with:
|
The launcher is built with:
|
||||||
|
|
@ -101,3 +109,13 @@ The launcher is built with:
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Feel free to submit issues and pull requests for improvements.</content>
|
Feel free to submit issues and pull requests for improvements.</content>
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
<a href="https://www.star-history.com/?repos=gradenGnostic%2FLegacyLauncher&type=date&legend=top-left">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=gradenGnostic/LegacyLauncher&type=date&theme=dark&legend=top-left" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=gradenGnostic/LegacyLauncher&type=date&legend=top-left" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=gradenGnostic/LegacyLauncher&type=date&legend=top-left" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
BIN
assets/Click_stereo.ogg.mp3
Normal file
BIN
assets/gdb-keyboard-2.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/gdb-switch-2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/gdb-xbox-2.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 987 KiB After Width: | Height: | Size: 987 KiB |
|
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
339
index.html
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LegacyLauncher</title>
|
<title>LegacyLauncher</title>
|
||||||
<link rel="icon" type="image/png" href="512x512.png">
|
<link rel="icon" type="image/png" href="assets/512x512.png">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -13,54 +13,92 @@
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<div class="title-bar-text">LegacyLauncher</div>
|
<div class="title-bar-text">LegacyLauncher</div>
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
<div class="win-btn" onclick="minimizeWindow()">−</div>
|
<div class="win-btn nav-item" onclick="minimizeWindow()" tabindex="0" title="Minimize">−</div>
|
||||||
<div class="win-btn" id="maximize-btn" onclick="toggleMaximize()">▢</div>
|
<div class="win-btn nav-item" id="maximize-btn" onclick="toggleMaximize()" tabindex="0" title="Maximize / Restore">▢</div>
|
||||||
<div class="win-btn close" onclick="closeWindow()">×</div>
|
<div class="win-btn close nav-item" onclick="closeWindow()" tabindex="0" title="Close">×</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loader">
|
<div id="loader">
|
||||||
<div class="loader-content">
|
<div class="loader-content">
|
||||||
<div class="loader-spinner"></div>
|
<div class="loader-spinner"></div>
|
||||||
<div id="loader-text">CONNECTING...</div>
|
<div id="loader-text"><span id="loader-text-label">CONNECTING</span><span class="loading-dots animate" id="loader-dots" aria-hidden="true"><span>.</span><span>.</span><span>.</span></span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="offline-indicator" style="display: none; position: fixed; top: 40px; left: 50%; transform: translateX(-50%); background: #c42b1c; color: #fff; padding: 5px 20px; border: 2px solid #000; z-index: 100; font-weight: bold; font-size: 14px; box-shadow: 0 4px 10px rgba(0,0,0,0.5);">
|
||||||
|
OFFLINE MODE — UPDATES DISABLED
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="sidebar">
|
<div class="sidebar collapsed" id="main-sidebar">
|
||||||
<div class="sidebar-title">PATCH NOTES</div>
|
<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 id="updates-list">
|
||||||
<div class="update-item">Loading updates...</div>
|
<div class="update-item">Loading updates...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-area">
|
<div class="content-area" style="flex-direction: row; align-items: stretch; padding: 0;">
|
||||||
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
|
|
||||||
|
|
||||||
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
|
<!-- Classic Mode: Logo + Splash (only visible in classic theme) -->
|
||||||
|
<div class="classic-logo-area" id="classic-logo-area">
|
||||||
<div class="version-row">
|
<div style="position: relative;">
|
||||||
<span class="version-label">Version:</span>
|
<img src="assets/minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo" style="margin-bottom: 0; margin-top: 0;">
|
||||||
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
|
<div id="classic-splash-text" class="splash-text">Splash!</div>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 w-[500px] max-w-[90%]">
|
<!-- Left Side: Main Menu -->
|
||||||
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0">PROFILE</div>
|
<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="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0">SERVERS</div>
|
<div class="relative">
|
||||||
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0">OPTIONS</div>
|
<img src="assets/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>
|
||||||
|
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
|
||||||
|
<img src="assets/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>
|
||||||
|
|
||||||
|
<!-- 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="skin-action-row" style="margin-top: -20px; z-index: 10; margin-left: auto; margin-right: auto;">
|
||||||
|
<div class="btn-mc nav-item skin-action-btn skin-action-btn-main" onclick="openSkinManager()" tabindex="0">
|
||||||
|
CHANGE SKIN
|
||||||
|
</div>
|
||||||
|
<div id="btn-skin-render-mode" class="btn-mc nav-item skin-action-btn skin-action-btn-mode" onclick="toggleMainSkinRenderMode()" tabindex="0">3D</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress-container" id="progress-container">
|
<div class="progress-container" id="progress-container">
|
||||||
<div class="progress-text" id="progress-text">Downloading...</div>
|
<div class="progress-text-wrap" id="progress-text-wrap">
|
||||||
|
<span class="loader-spinner progress-spinner" id="progress-spinner" aria-hidden="true"></span>
|
||||||
|
<div class="progress-text" id="progress-text"><span id="progress-text-label">Downloading</span><span class="loading-dots" id="progress-dots" aria-hidden="true"><span>.</span><span>.</span><span>.</span></span><span id="progress-text-suffix"></span></div>
|
||||||
|
</div>
|
||||||
<div class="progress-bar-bg">
|
<div class="progress-bar-bg">
|
||||||
<div class="progress-bar-fill" id="progress-bar-fill"></div>
|
<div class="progress-bar-fill" id="progress-bar-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,7 +107,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-overlay" id="servers-modal">
|
<div class="modal-overlay" id="servers-modal">
|
||||||
<div class="modal-box" style="width: 850px;">
|
<div class="modal-box">
|
||||||
<div class="modal-title">CUSTOM SERVERS</div>
|
<div class="modal-title">CUSTOM SERVERS</div>
|
||||||
|
|
||||||
<div id="servers-list-container" class="w-full max-h-[300px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
|
<div id="servers-list-container" class="w-full max-h-[300px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
|
||||||
|
|
@ -100,13 +138,84 @@
|
||||||
</div>
|
</div>
|
||||||
</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="snapshots-modal">
|
||||||
|
<div class="modal-box" style="max-width: 800px;">
|
||||||
|
<div class="modal-title">SNAPSHOTS / ROLLBACK</div>
|
||||||
|
<div id="snapshot-instance-name" style="color: #aaa; text-align: center; margin-top: -15px; margin-bottom: 20px; text-transform: uppercase;">Instance Name</div>
|
||||||
|
|
||||||
|
<div id="snapshots-list-container" class="w-full max-h-[350px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
|
||||||
|
<div class="text-center text-gray-400 py-4">No snapshots found.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4 w-full">
|
||||||
|
<div class="btn-mc flex-grow nav-item" onclick="createSnapshotManual()" tabindex="0">NEW SNAPSHOT</div>
|
||||||
|
<div class="btn-mc flex-grow nav-item" onclick="toggleSnapshots(false)" tabindex="0">DONE</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-overlay" id="options-modal">
|
<div class="modal-overlay" id="options-modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<div class="modal-title">LAUNCHER OPTIONS</div>
|
<div class="modal-title">LAUNCHER OPTIONS</div>
|
||||||
|
|
||||||
|
<div class="mc-input-group">
|
||||||
|
<label class="mc-label">Installation Directory:</label>
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<input type="text" id="install-path-input" class="mc-input nav-item !mb-0" placeholder="Default: Documents/LegacyClient" tabindex="0">
|
||||||
|
<div class="btn-mc btn-mini nav-item !w-[60px] !h-[48px]" onclick="browseInstallDir()" tabindex="0">...</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="btn-mc flex-grow !h-[40px] !text-lg !mb-0 nav-item" onclick="openGameDir()" tabindex="0">GAME FOLDER</div>
|
||||||
|
<div class="btn-mc flex-grow !h-[40px] !text-lg !mb-0 nav-item" onclick="openScreenshotsGallery()" tabindex="0">SCREENSHOTS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
<label class="mc-label">GitHub Repository Source:</label>
|
<label class="mc-label">GitHub Repository Source:</label>
|
||||||
<input type="text" id="repo-input" class="mc-input nav-item" placeholder="user/repo" tabindex="0">
|
<input type="text" id="repo-input" class="mc-input nav-item" placeholder="user/repo" tabindex="0">
|
||||||
|
<label class="mc-label" style="margin-top: 10px;">Repository Preset:</label>
|
||||||
|
<select id="repo-preset-select" class="mc-input nav-item" onchange="applyRepoPreset()" tabindex="0" style="height: 48px; margin-top: 8px;">
|
||||||
|
<option value="custom">Custom</option>
|
||||||
|
<option value="smartcmd/MinecraftConsoles">smartcmd (default)</option>
|
||||||
|
<option value="cath0degaytube/MinecraftConsoles">cath0degaytube (no watermark)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
|
|
@ -125,6 +234,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-input-group" id="custom-proton-group" style="display: none;">
|
||||||
|
<label class="mc-label">Custom Proton Path:</label>
|
||||||
|
<input type="text" id="custom-proton-path" class="mc-input nav-item" placeholder="/path/to/proton" tabindex="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-input-group">
|
||||||
|
<label class="mc-label">Launcher Music Volume:</label>
|
||||||
|
<div class="mc-slider-container">
|
||||||
|
<input type="range" id="volume-slider" min="0" max="1" step="0.01" value="1" class="mc-slider nav-item w-full" tabindex="0">
|
||||||
|
<div id="volume-percent" class="mc-slider-percent">100%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 w-full mt-2">
|
<div class="grid grid-cols-2 gap-4 w-full mt-2">
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
<label class="mc-label">Connect/Bind IP:</label>
|
<label class="mc-label">Connect/Bind IP:</label>
|
||||||
|
|
@ -136,6 +258,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-input-group">
|
||||||
|
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="fullscreen-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
|
||||||
|
Launch in Fullscreen (-fullscreen)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
|
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
|
||||||
<input type="checkbox" id="server-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
|
<input type="checkbox" id="server-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
|
||||||
|
|
@ -143,6 +272,39 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-input-group">
|
||||||
|
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="classic-theme-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
|
||||||
|
Use Classic Minecraft Launcher UI
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-input-group">
|
||||||
|
<label class="mc-label" style="display: flex; align-items: center; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="steamdeck-mode-checkbox" class="nav-item" style="width: 24px; height: 24px; margin-right: 12px; cursor: pointer;" tabindex="0">
|
||||||
|
Use Steam Deck Optimized UI Mode
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-input-group">
|
||||||
|
<label class="mc-label">Controller Confirm/Cancel Layout:</label>
|
||||||
|
<select id="controller-layout-select" class="mc-input nav-item" tabindex="0" style="height: 48px; margin-top: 8px;">
|
||||||
|
<option value="auto">Auto Detect (Recommended)</option>
|
||||||
|
<option value="xbox">Xbox Style (A = Confirm, B = Cancel)</option>
|
||||||
|
<option value="nintendo">Nintendo Style (B = Confirm, A = Cancel)</option>
|
||||||
|
</select>
|
||||||
|
<div id="controller-layout-presets" class="controller-layout-presets" role="radiogroup" aria-label="Controller layout presets">
|
||||||
|
<button type="button" class="controller-layout-preset nav-item" data-layout="xbox" tabindex="0" aria-pressed="false">
|
||||||
|
<span class="controller-layout-icon controller-layout-icon-xbox" aria-hidden="true"></span>
|
||||||
|
<span class="controller-layout-text">Xbox Style (A = Confirm, B = Cancel)</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="controller-layout-preset nav-item" data-layout="nintendo" tabindex="0" aria-pressed="false">
|
||||||
|
<span class="controller-layout-icon controller-layout-icon-switch" aria-hidden="true"></span>
|
||||||
|
<span class="controller-layout-text">Nintendo Style (B = Confirm, A = Cancel)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 w-full mt-4">
|
<div class="flex gap-4 w-full mt-4">
|
||||||
<div id="btn-options-done" class="btn-mc flex-grow nav-item" onclick="saveOptions()" tabindex="0">DONE</div>
|
<div id="btn-options-done" class="btn-mc flex-grow nav-item" onclick="saveOptions()" tabindex="0">DONE</div>
|
||||||
<div id="btn-options-cancel" class="btn-mc flex-grow nav-item" onclick="toggleOptions(false)" tabindex="0">CANCEL</div>
|
<div id="btn-options-cancel" class="btn-mc flex-grow nav-item" onclick="toggleOptions(false)" tabindex="0">CANCEL</div>
|
||||||
|
|
@ -150,13 +312,28 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="gallery-modal">
|
||||||
|
<div class="modal-box" style="max-width: 1000px; width: 85vw; height: 85vh;">
|
||||||
|
<div class="modal-title">SCREENSHOTS GALLERY</div>
|
||||||
|
|
||||||
|
<div id="gallery-container" class="w-full flex-grow overflow-y-auto mb-6 border-2 border-[#555] bg-black p-4 grid grid-cols-2 md:grid-cols-3 gap-4 auto-rows-max">
|
||||||
|
<!-- Screenshots will be dynamically loaded here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4 w-full">
|
||||||
|
<div class="btn-mc flex-grow nav-item" onclick="openScreenshotsDir()" tabindex="0">OPEN FOLDER</div>
|
||||||
|
<div class="btn-mc flex-grow nav-item" onclick="toggleGallery(false)" tabindex="0">DONE</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-overlay" id="profile-modal">
|
<div class="modal-overlay" id="profile-modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<div class="modal-title">PLAYER PROFILE</div>
|
<div class="modal-title">PLAYER PROFILE</div>
|
||||||
|
|
||||||
<div class="mc-input-group">
|
<div class="mc-input-group">
|
||||||
<label class="mc-label">In-Game Username:</label>
|
<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>
|
||||||
|
|
||||||
<div class="mc-input-group mt-4">
|
<div class="mc-input-group mt-4">
|
||||||
|
|
@ -174,10 +351,7 @@
|
||||||
<div class="modal-overlay" id="update-modal">
|
<div class="modal-overlay" id="update-modal">
|
||||||
<div class="modal-box relative">
|
<div class="modal-box relative">
|
||||||
<div class="modal-close-btn" id="btn-close-update">×</div>
|
<div class="modal-close-btn" id="btn-close-update">×</div>
|
||||||
<div class="modal-title">UPDATE AVAILABLE</div>
|
<div id="update-modal-text" class="modal-body-text" style="display: none; margin-bottom: 20px;"></div>
|
||||||
<div id="update-modal-text" class="modal-body-text">
|
|
||||||
A new version is available. Would you like to update now?
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-4 w-full">
|
<div class="flex gap-4 w-full">
|
||||||
<div class="btn-mc flex-grow nav-item" id="btn-confirm-update">UPDATE NOW</div>
|
<div class="btn-mc flex-grow nav-item" id="btn-confirm-update">UPDATE NOW</div>
|
||||||
<div class="btn-mc flex-grow nav-item" id="btn-skip-update">LATER</div>
|
<div class="btn-mc flex-grow nav-item" id="btn-skip-update">LATER</div>
|
||||||
|
|
@ -185,8 +359,107 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
|
<!-- Classic Launcher Bottom Bar (only shown when classic theme is active) -->
|
||||||
|
<div id="classic-bottom-bar" class="classic-bottom-bar">
|
||||||
|
<!-- Left: Profile Section -->
|
||||||
|
<div class="classic-profile-section">
|
||||||
|
<div class="classic-avatar" id="classic-avatar">S</div>
|
||||||
|
<div class="classic-profile-info">
|
||||||
|
<div class="classic-username-label">Logged in as</div>
|
||||||
|
<div class="classic-username-display" id="classic-username-display">Player</div>
|
||||||
|
</div>
|
||||||
|
<div class="classic-mini-btn" onclick="toggleProfile(true)" title="Edit Profile">Edit Profile</div>
|
||||||
|
<div class="classic-mini-btn" onclick="openSkinManager()" title="Change Skin">Skin</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center: Play Button -->
|
||||||
|
<div class="classic-center-section">
|
||||||
|
<div class="btn-mc btn-play classic-play-btn nav-item" id="classic-btn-play" onclick="launchGame()" tabindex="0">PLAY</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Version + Nav -->
|
||||||
|
<div class="classic-right-section">
|
||||||
|
<div class="classic-version-row">
|
||||||
|
<span class="version-label" style="font-size: 16px; white-space: nowrap;">Version:</span>
|
||||||
|
<div id="classic-version-select-box" class="version-select-box nav-item" tabindex="0" style="height: 40px; font-size: 16px; flex-grow: 1; padding-right: 51px;">
|
||||||
|
<span id="classic-version-display">Loading...</span>
|
||||||
|
<div class="select-arrow" style="width: 36px;">▼</div>
|
||||||
|
<select id="classic-version-select" class="hidden-select" onchange="syncVersionFromClassic()">
|
||||||
|
<option>Loading...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0" style="width: 40px !important; height: 40px; max-width: 40px !important;">
|
||||||
|
<img src="restart.png" alt="Update">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="classic-nav-links">
|
||||||
|
<span class="classic-link nav-item" onclick="toggleInstances(true)" tabindex="0">Instances</span>
|
||||||
|
<span class="classic-link nav-item" onclick="toggleServers(true)" tabindex="0">Servers</span>
|
||||||
|
<span class="classic-link nav-item" onclick="toggleOptions(true)" tabindex="0">Options</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toast">NOTIFICATION</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>
|
<script src="renderer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
130
main.js
|
|
@ -1,4 +1,4 @@
|
||||||
const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
const { app, BrowserWindow, shell, ipcMain, dialog, globalShortcut, desktopCapturer } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
@ -7,9 +7,11 @@ const extractZip = require('extract-zip');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
let mainWindow = null;
|
||||||
|
let isGameRunning = false;
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const win = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
minWidth: 1024,
|
minWidth: 1024,
|
||||||
|
|
@ -27,25 +29,131 @@ function createWindow() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
win.loadFile('index.html');
|
mainWindow.loadFile('index.html');
|
||||||
|
|
||||||
ipcMain.on('window-minimize', () => win.minimize());
|
ipcMain.on('window-minimize', () => mainWindow.minimize());
|
||||||
ipcMain.on('window-maximize', () => {
|
ipcMain.on('window-maximize', () => {
|
||||||
if (win.isMaximized()) {
|
if (mainWindow.isMaximized()) {
|
||||||
win.unmaximize();
|
mainWindow.unmaximize();
|
||||||
} else {
|
} else {
|
||||||
win.maximize();
|
mainWindow.maximize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ipcMain.on('window-close', () => win.close());
|
ipcMain.on('window-close', () => mainWindow.close());
|
||||||
|
ipcMain.on('window-fullscreen', () => {
|
||||||
|
mainWindow.setFullScreen(!mainWindow.isFullScreen());
|
||||||
|
});
|
||||||
|
ipcMain.on('window-set-fullscreen', (event, enabled) => {
|
||||||
|
mainWindow.setFullScreen(Boolean(enabled));
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('take-screenshot', async (event) => {
|
||||||
|
try {
|
||||||
|
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
|
||||||
|
if (!fs.existsSync(screenshotsDir)) {
|
||||||
|
fs.mkdirSync(screenshotsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const fileName = `screenshot-${timestamp}.png`;
|
||||||
|
const filePath = path.join(screenshotsDir, fileName);
|
||||||
|
|
||||||
|
if (isGameRunning) {
|
||||||
|
const sources = await desktopCapturer.getSources({
|
||||||
|
types: ['window', 'screen'],
|
||||||
|
thumbnailSize: { width: 3840, height: 2160 } // High res for screenshot
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find Minecraft window or fallback to primary screen
|
||||||
|
const source = sources.find(s => s.name.toLowerCase().includes('minecraft')) ||
|
||||||
|
sources.find(s => s.id.startsWith('screen'));
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
fs.writeFileSync(filePath, source.thumbnail.toPNG());
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to launcher capture if game isn't running or not found
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender) || mainWindow;
|
||||||
|
if (!win) throw new Error("Window not found");
|
||||||
|
const image = await win.capturePage();
|
||||||
|
fs.writeFileSync(filePath, image.toPNG());
|
||||||
|
return filePath;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Screenshot capture error:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('game-running-state', (event, running) => {
|
||||||
|
isGameRunning = running;
|
||||||
|
if (running) {
|
||||||
|
globalShortcut.register('F2', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('trigger-screenshot');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
globalShortcut.unregister('F2');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('list-screenshots', async () => {
|
||||||
|
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
|
||||||
|
if (!fs.existsSync(screenshotsDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const files = fs.readdirSync(screenshotsDir);
|
||||||
|
return files
|
||||||
|
.filter(f => f.toLowerCase().endsWith('.png'))
|
||||||
|
.sort((a, b) => {
|
||||||
|
try {
|
||||||
|
// Extract timestamp from 'screenshot-YYYY-MM-DDTHH-mm-ss-SSSZ.png'
|
||||||
|
const timeA = a.replace('screenshot-', '').replace('.png', '').replace(/-/g, ':');
|
||||||
|
const timeB = b.replace('screenshot-', '').replace('.png', '').replace(/-/g, ':');
|
||||||
|
return new Date(timeB) - new Date(timeA);
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(f => ({
|
||||||
|
name: f,
|
||||||
|
path: path.join(screenshotsDir, f),
|
||||||
|
url: `file://${path.join(screenshotsDir, f)}`
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('delete-screenshot', async (event, fileName) => {
|
||||||
|
const filePath = path.join(app.getPath('userData'), 'Screenshots', fileName);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('open-screenshots-dir', async () => {
|
||||||
|
const screenshotsDir = path.join(app.getPath('userData'), 'Screenshots');
|
||||||
|
if (!fs.existsSync(screenshotsDir)) {
|
||||||
|
fs.mkdirSync(screenshotsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
shell.openPath(screenshotsDir);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('store-get', (event, key) => store.get(key));
|
ipcMain.handle('store-get', (event, key) => store.get(key));
|
||||||
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
|
ipcMain.handle('store-set', (event, key, value) => store.set(key, value));
|
||||||
|
|
||||||
win.on('maximize', () => win.webContents.send('window-is-maximized', true));
|
ipcMain.handle('select-directory', async () => {
|
||||||
win.on('unmaximize', () => win.webContents.send('window-is-maximized', false));
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openDirectory', 'createDirectory']
|
||||||
|
});
|
||||||
|
return result.filePaths[0];
|
||||||
|
});
|
||||||
|
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.on('maximize', () => mainWindow.webContents.send('window-is-maximized', true));
|
||||||
|
mainWindow.on('unmaximize', () => mainWindow.webContents.send('window-is-maximized', false));
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
10
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "legacylauncher",
|
"name": "legacylauncher",
|
||||||
"version": "1.1.1",
|
"version": "3.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "legacylauncher",
|
"name": "legacylauncher",
|
||||||
"version": "1.1.1",
|
"version": "3.5.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-store": "^6.0.1",
|
"electron-store": "^6.0.1",
|
||||||
|
|
@ -4690,9 +4690,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.10",
|
"version": "7.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||||
"integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
|
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
16
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "legacylauncher",
|
"name": "legacylauncher",
|
||||||
"version": "2.0.2",
|
"version": "3.5.0",
|
||||||
"description": "",
|
"description": "A Minecraft: Legacy Console Edition launcher",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
|
@ -30,7 +30,17 @@
|
||||||
"flatpak"
|
"flatpak"
|
||||||
],
|
],
|
||||||
"category": "Game",
|
"category": "Game",
|
||||||
"icon": "512x512.png"
|
"icon": "512x512.png",
|
||||||
|
"desktop": {
|
||||||
|
"entry": {
|
||||||
|
"Name": "Minecraft LCE Launcher",
|
||||||
|
"GenericName": "Minecraft Launcher",
|
||||||
|
"Comment": "A Minecraft: Legacy Console Edition launcher",
|
||||||
|
"Categories": "Game;Emulation;",
|
||||||
|
"StartupNotify": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"executableName": "legacylauncher"
|
||||||
},
|
},
|
||||||
"mac": {
|
"mac": {
|
||||||
"target": [
|
"target": [
|
||||||
|
|
|
||||||
1978
renderer.js
469
skin_manager.js
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
// skin_manager.js
|
||||||
|
// Handles skin uploading, conversion, 3D preview, and saving to char.png
|
||||||
|
|
||||||
|
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
|
||||||
|
let isMainSkinDragging = false;
|
||||||
|
let mainMenuSkinRenderMode = '3d';
|
||||||
|
|
||||||
|
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('click', () => skinInput?.click());
|
||||||
|
dropZone.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
skinInput?.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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) => {
|
||||||
|
if (mainMenuSkinRenderMode !== '3d') return;
|
||||||
|
isMainSkinDragging = true;
|
||||||
|
prevX = e.clientX;
|
||||||
|
});
|
||||||
|
window.addEventListener('mouseup', () => isMainSkinDragging = false);
|
||||||
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
if (mainMenuSkinRenderMode === '3d' && isMainSkinDragging && mainMenuPlayerGroup) {
|
||||||
|
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
|
||||||
|
prevX = e.clientX;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-rotate slowly
|
||||||
|
function animateMain() {
|
||||||
|
requestAnimationFrame(animateMain);
|
||||||
|
if (mainMenuSkinRenderMode === '3d' && !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 {
|
||||||
|
// Ensure install dir is available (currentInstance might not be ready)
|
||||||
|
const installDir = await window.getInstallDir();
|
||||||
|
if (!installDir) return;
|
||||||
|
|
||||||
|
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, mainMenuSkinRenderMode);
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
} else {
|
||||||
|
console.log("No skin found at " + skinPath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not load main menu skin (startup race condition?):", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMainSkinRenderMode() {
|
||||||
|
mainMenuSkinRenderMode = mainMenuSkinRenderMode === '3d' ? '2d' : '3d';
|
||||||
|
const modeButton = document.getElementById('btn-skin-render-mode');
|
||||||
|
if (modeButton) modeButton.textContent = mainMenuSkinRenderMode.toUpperCase();
|
||||||
|
|
||||||
|
const container = document.getElementById('main-skin-viewer');
|
||||||
|
if (container) {
|
||||||
|
container.style.cursor = mainMenuSkinRenderMode === '3d' ? 'grab' : 'default';
|
||||||
|
}
|
||||||
|
isMainSkinDragging = false;
|
||||||
|
|
||||||
|
if (mainMenuSkinRenderMode === '2d' && mainMenuPlayerGroup) {
|
||||||
|
mainMenuPlayerGroup.rotation.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMainMenuSkin();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSkinModel(dataUrl, isLegacy, targetGroup, renderMode = '3d') {
|
||||||
|
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]
|
||||||
|
});
|
||||||
|
|
||||||
|
const create2DPart = (tex, uvFront, width, height, x, y, scale = 1) => {
|
||||||
|
const texWidth = tex.image.width;
|
||||||
|
const texHeight = tex.image.height;
|
||||||
|
const partTex = tex.clone();
|
||||||
|
partTex.magFilter = THREE.NearestFilter;
|
||||||
|
partTex.minFilter = THREE.NearestFilter;
|
||||||
|
partTex.repeat.set(uvFront[2] / texWidth, uvFront[3] / texHeight);
|
||||||
|
partTex.offset.set(uvFront[0] / texWidth, 1 - (uvFront[1] + uvFront[3]) / texHeight);
|
||||||
|
partTex.needsUpdate = true;
|
||||||
|
|
||||||
|
const geometry = new THREE.PlaneGeometry(width, height);
|
||||||
|
const material = new THREE.MeshBasicMaterial({ map: partTex, transparent: true, alphaTest: 0.5, side: THREE.DoubleSide });
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.position.set(x, y, 0);
|
||||||
|
mesh.scale.set(scale, scale, 1);
|
||||||
|
return mesh;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (renderMode === '2d') {
|
||||||
|
const frontParts = [
|
||||||
|
create2DPart(texture, [8, 8, 8, 8], 8, 8, 0, 10),
|
||||||
|
create2DPart(texture, [40, 8, 8, 8], 8, 8, 0, 10, 1.12),
|
||||||
|
create2DPart(texture, [20, 20, 8, 12], 8, 12, 0, 0),
|
||||||
|
create2DPart(texture, [44, 20, 4, 12], 4, 12, -6, 0),
|
||||||
|
create2DPart(texture, isLegacy ? [44, 20, 4, 12] : [36, 52, 4, 12], 4, 12, 6, 0),
|
||||||
|
create2DPart(texture, [4, 20, 4, 12], 4, 12, -2, -12),
|
||||||
|
create2DPart(texture, isLegacy ? [4, 20, 4, 12] : [20, 52, 4, 12], 4, 12, 2, -12)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isLegacy) {
|
||||||
|
frontParts.push(
|
||||||
|
create2DPart(texture, [20, 36, 8, 12], 8, 12, 0, 0, 1.05),
|
||||||
|
create2DPart(texture, [44, 36, 4, 12], 4, 12, -6, 0, 1.05),
|
||||||
|
create2DPart(texture, [52, 52, 4, 12], 4, 12, 6, 0, 1.05),
|
||||||
|
create2DPart(texture, [4, 36, 4, 12], 4, 12, -2, -12, 1.05),
|
||||||
|
create2DPart(texture, [4, 52, 4, 12], 4, 12, 2, -12, 1.05)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
frontParts.forEach((part) => targetGroup.add(part));
|
||||||
|
targetGroup.rotation.y = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
targetGroup.rotation.y = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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, '3d');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
window.toggleMainSkinRenderMode = toggleMainSkinRenderMode;
|
||||||
483
strings.txt
Normal file
|
|
@ -0,0 +1,483 @@
|
||||||
|
Happy birthday, ez!
|
||||||
|
Happy birthday, Notch!
|
||||||
|
Let me tell you about Homestuck!
|
||||||
|
Let me tell you about Homestuck!
|
||||||
|
Let me tell you about Homestuck!
|
||||||
|
Merry X-mas!
|
||||||
|
Happy New Year!
|
||||||
|
As seen on TV!
|
||||||
|
A young man stands in his bedroom!
|
||||||
|
A young man stands in his bedroom!
|
||||||
|
A young woman stands in her bedroom!
|
||||||
|
A young woman stands in her bedroom!
|
||||||
|
Awesome!
|
||||||
|
100% pure!
|
||||||
|
Better than Prey!
|
||||||
|
More polygons!
|
||||||
|
Get the cruxite dowel!
|
||||||
|
Get the cruxite dowel!
|
||||||
|
Get the cruxite dowel!
|
||||||
|
Flashing letters!
|
||||||
|
Made by Notch!
|
||||||
|
It's here!
|
||||||
|
Best in class!
|
||||||
|
It's finished!
|
||||||
|
Kind of dragon free!
|
||||||
|
Excitement!
|
||||||
|
Where doing it man, where making this hapen!
|
||||||
|
Where doing it man, where making this hapen!
|
||||||
|
One of a kind!
|
||||||
|
Heaps of hits on YouTube!
|
||||||
|
Indev!
|
||||||
|
Spiders everywhere!
|
||||||
|
Check it out!
|
||||||
|
Holy cow, man!
|
||||||
|
It's a game!
|
||||||
|
Made in Sweden!
|
||||||
|
Uses LWJGL!
|
||||||
|
Reticulating splines!
|
||||||
|
Minecraft!
|
||||||
|
Yaaay!
|
||||||
|
413!
|
||||||
|
413!
|
||||||
|
413!
|
||||||
|
413!
|
||||||
|
Singleplayer!
|
||||||
|
Keyboard compatible!
|
||||||
|
Undocumented!
|
||||||
|
Ingots!
|
||||||
|
Exploding creepers!
|
||||||
|
That's no moon!
|
||||||
|
l33t!
|
||||||
|
Create!
|
||||||
|
Survive!
|
||||||
|
Dungeon!
|
||||||
|
I WARNED YOU ABOUT STAIRS BRO!!!!
|
||||||
|
I WARNED YOU ABOUT STAIRS BRO!!!!
|
||||||
|
I WARNED YOU ABOUT STAIRS BRO!!!!
|
||||||
|
Exclusive!
|
||||||
|
The bee's knees!
|
||||||
|
Closed source!
|
||||||
|
Classy!
|
||||||
|
Wow!
|
||||||
|
Not on steam!
|
||||||
|
Oh man!
|
||||||
|
Awesome community!
|
||||||
|
Pixels!
|
||||||
|
Teetsuuuuoooo!
|
||||||
|
Kaaneeeedaaaa!
|
||||||
|
Now with difficulty!
|
||||||
|
Enhanced!
|
||||||
|
90% bug free!
|
||||||
|
Pretty!
|
||||||
|
12 herbs and spices!
|
||||||
|
Fat free!
|
||||||
|
Absolutely no memes!
|
||||||
|
Free dental!
|
||||||
|
Cloud computing!
|
||||||
|
Hard to label!
|
||||||
|
Technically good!
|
||||||
|
Bringing home the bacon!
|
||||||
|
Indie!
|
||||||
|
GOTY!
|
||||||
|
Ceci n'est pas une title screen!
|
||||||
|
Euclidian!
|
||||||
|
Now in 3D!
|
||||||
|
Inspirational!
|
||||||
|
Herregud!
|
||||||
|
Complex cellular automata!
|
||||||
|
Yes, sir!
|
||||||
|
Played by cowboys!
|
||||||
|
Thousands of colors!
|
||||||
|
Try it!
|
||||||
|
Age of Wonders is better!
|
||||||
|
Try the mushroom stew!
|
||||||
|
Sensational!
|
||||||
|
Hot tamale, hot hot tamale!
|
||||||
|
Play him off, keyboard cat!
|
||||||
|
Guaranteed!
|
||||||
|
Alchemize ALL the things!
|
||||||
|
Alchemize ALL the things!
|
||||||
|
Macroscopic!
|
||||||
|
Bring it on!
|
||||||
|
Random splash!
|
||||||
|
Call your mother!
|
||||||
|
Monster infighting!
|
||||||
|
Loved by millions!
|
||||||
|
Ultimate edition!
|
||||||
|
Freaky!
|
||||||
|
Water proof!
|
||||||
|
Uninflammable!
|
||||||
|
Whoa, dude!
|
||||||
|
Roxy is best hacker!
|
||||||
|
Roxy is best hacker!
|
||||||
|
Roxy is best hacker!
|
||||||
|
All inclusive!
|
||||||
|
Tell your friends!
|
||||||
|
NP is not in P!
|
||||||
|
Notch <3 ez!
|
||||||
|
Music by C418!
|
||||||
|
Livestreamed!
|
||||||
|
Haunted!
|
||||||
|
Polynomial!
|
||||||
|
Terrestrial!
|
||||||
|
All is full of love!
|
||||||
|
Full of stars!
|
||||||
|
Scientific!
|
||||||
|
Cooler than Spock!
|
||||||
|
Collaborate and listen!
|
||||||
|
Never dig down!
|
||||||
|
Take frequent breaks!
|
||||||
|
Not linear!
|
||||||
|
Han shot first!
|
||||||
|
Nice to meet you!
|
||||||
|
Buckets of lava!
|
||||||
|
Ride the pig!
|
||||||
|
Larger than Earth!
|
||||||
|
sqrt(-1) love you!
|
||||||
|
Phobos anomaly!
|
||||||
|
Punching wood!
|
||||||
|
Falling off cliffs!
|
||||||
|
0% sugar!
|
||||||
|
150% hyperbole!
|
||||||
|
Synecdoche!
|
||||||
|
Let's danec!
|
||||||
|
Reference implementation!
|
||||||
|
Lewd with two dudes with food!
|
||||||
|
Kiss the sky!
|
||||||
|
20 GOTO 10!
|
||||||
|
Verlet intregration!
|
||||||
|
Peter Griffin!
|
||||||
|
Do not distribute!
|
||||||
|
Cogito ergo sum!
|
||||||
|
4815162342 lines of code!
|
||||||
|
A skeleton popped out!
|
||||||
|
The Work of Notch!
|
||||||
|
The sum of its parts!
|
||||||
|
BTAF used to be good!
|
||||||
|
I miss ADOM!
|
||||||
|
umop-apisdn!
|
||||||
|
OICU812!
|
||||||
|
Bring me Ray Cokes!
|
||||||
|
Finger-licking!
|
||||||
|
Thematic!
|
||||||
|
Pneumatic!
|
||||||
|
Sublime!
|
||||||
|
Octagonal!
|
||||||
|
Une baguette!
|
||||||
|
Gargamel plays it!
|
||||||
|
Rita is the new top dog!
|
||||||
|
SWM forever!
|
||||||
|
Representing Edsbyn!
|
||||||
|
Matt Damon!
|
||||||
|
Supercalifragilisticexpialidocious!
|
||||||
|
Consummate V's!
|
||||||
|
Cow Tools!
|
||||||
|
Double buffered!
|
||||||
|
Fan fiction!
|
||||||
|
Flaxkikare!
|
||||||
|
Jason! Jason! Jason!
|
||||||
|
Hotter than the sun!
|
||||||
|
Internet enabled!
|
||||||
|
Autonomous!
|
||||||
|
Engage!
|
||||||
|
Fantasy!
|
||||||
|
DRR! DRR! DRR!
|
||||||
|
Kick it root down!
|
||||||
|
Regional resources!
|
||||||
|
Woo, facepunch!
|
||||||
|
Woo, somethingawful!
|
||||||
|
Woo, /v/!
|
||||||
|
Woo, tigsource!
|
||||||
|
Woo, minecraftforum!
|
||||||
|
Woo, worldofminecraft!
|
||||||
|
Woo, reddit!
|
||||||
|
Woo, 2pp!
|
||||||
|
Google anlyticsed!
|
||||||
|
Now supports åäö!
|
||||||
|
Give us Gordon!
|
||||||
|
Tip your waiter!
|
||||||
|
Very fun!
|
||||||
|
12345 is a bad password!
|
||||||
|
Vote for net neutrality!
|
||||||
|
Lives in a pineapple under the sea!
|
||||||
|
MAP11 has two names!
|
||||||
|
Omnipotent!
|
||||||
|
Gasp!
|
||||||
|
...!
|
||||||
|
Bees, bees, bees, bees!
|
||||||
|
Jag känner en bot!
|
||||||
|
This text is hard to read if you play the game at the default resolution, but at 1080p it's fine!
|
||||||
|
Haha, LOL!
|
||||||
|
Hampsterdance!
|
||||||
|
Switches and ores!
|
||||||
|
Menger sponge!
|
||||||
|
idspispopd!
|
||||||
|
Eple (original edit)!
|
||||||
|
So fresh, so clean!
|
||||||
|
Slow acting portals!
|
||||||
|
Try the Nether!
|
||||||
|
Don't look directly at the bugs!
|
||||||
|
Oh, ok, Pigmen!
|
||||||
|
Finally with ladders!
|
||||||
|
Scary!
|
||||||
|
Play Minecraft, Watch Topgear, Get Pig!
|
||||||
|
Twittered about!
|
||||||
|
Jump up, jump up, and get down!
|
||||||
|
Joel is neat!
|
||||||
|
A riddle, wrapped in a mystery!
|
||||||
|
Huge tracts of land!
|
||||||
|
Welcome to your Doom!
|
||||||
|
Stay a while, stay forever!
|
||||||
|
Stay a while and listen!
|
||||||
|
Treatment for your rash!
|
||||||
|
"Autological" is!
|
||||||
|
Information wants to be free!
|
||||||
|
"Almost never" is an interesting concept!
|
||||||
|
Lots of truthiness!
|
||||||
|
The creeper is a spy!
|
||||||
|
Turing complete!
|
||||||
|
It's groundbreaking!
|
||||||
|
Let our battle's begin!
|
||||||
|
The sky is the limit!
|
||||||
|
Jeb has amazing hair!
|
||||||
|
Casual gaming!
|
||||||
|
Undefeated!
|
||||||
|
Kinda like Lemmings!
|
||||||
|
Follow the train, CJ!
|
||||||
|
Leveraging synergy!
|
||||||
|
This message will never appear on the splash screen, isn't that weird?
|
||||||
|
DungeonQuest is unfair!
|
||||||
|
110813!
|
||||||
|
90210!
|
||||||
|
Check out the far lands!
|
||||||
|
Tyrion would love it!
|
||||||
|
Also try VVVVVV!
|
||||||
|
Also try Super Meat Boy!
|
||||||
|
Also try Terraria!
|
||||||
|
Also try Mount And Blade!
|
||||||
|
Also try Project Zomboid!
|
||||||
|
Also try World of Goo!
|
||||||
|
Also try Limbo!
|
||||||
|
Also try Pixeljunk Shooter!
|
||||||
|
Also try Braid!
|
||||||
|
Also try Sburb!
|
||||||
|
Also try Sburb!
|
||||||
|
Also try Sburb!
|
||||||
|
That's super!
|
||||||
|
Bread is pain!
|
||||||
|
Read more books!
|
||||||
|
Khaaaaaaaaan!
|
||||||
|
Less addictive than TV Tropes!
|
||||||
|
More addictive than lemonade!
|
||||||
|
Bigger than a bread box!
|
||||||
|
Millions of peaches!
|
||||||
|
Fnord!
|
||||||
|
This is my true form!
|
||||||
|
Totally forgot about Dre!
|
||||||
|
Don't bother with the clones!
|
||||||
|
Pumpkinhead!
|
||||||
|
Hobo humping slobo babe!
|
||||||
|
Made by Jeb!
|
||||||
|
Has an ending!
|
||||||
|
Finally complete!
|
||||||
|
Feature packed!
|
||||||
|
Boots with the fur!
|
||||||
|
Stop, hammertime!
|
||||||
|
Testificates!
|
||||||
|
Conventional!
|
||||||
|
Homeomorphic to a 3-sphere!
|
||||||
|
Doesn't avoid double negatives!
|
||||||
|
Place ALL the blocks!
|
||||||
|
Does barrel rolls!
|
||||||
|
Meeting expectations!
|
||||||
|
PC gaming since 1873!
|
||||||
|
Ghoughpteighbteau tchoghs!
|
||||||
|
Déjà vu!
|
||||||
|
IT KEEPS HAPPENING!
|
||||||
|
IT KEEPS HAPPENING!
|
||||||
|
IT KEEPS HAPPENING!
|
||||||
|
I TOLD YOU DOG!
|
||||||
|
I TOLD YOU DOG!
|
||||||
|
Déjà vu!
|
||||||
|
Got your nose!
|
||||||
|
Afraid of the big, black bat!
|
||||||
|
Doesn't use the U-word!
|
||||||
|
Child's play!
|
||||||
|
See you next Friday or so!
|
||||||
|
From the streets of Södermalm!
|
||||||
|
150 bpm for 400000 minutes!
|
||||||
|
Technologic!
|
||||||
|
Funk soul brother!
|
||||||
|
Pumpa kungen!
|
||||||
|
Fetchstuck compatible!
|
||||||
|
Sylladex full!
|
||||||
|
Sylladex full!
|
||||||
|
Honk. HONK.
|
||||||
|
Honk. HONK.
|
||||||
|
You can't fight the Homestuck!
|
||||||
|
You can't fight the Homestuck!
|
||||||
|
==> Enter.
|
||||||
|
==> Enter.
|
||||||
|
STRIFE!
|
||||||
|
STRIFE!
|
||||||
|
8^y
|
||||||
|
8^y
|
||||||
|
Sweet Bro and Hella Jeff!
|
||||||
|
What pumpkin?
|
||||||
|
What pumpkin?
|
||||||
|
ABSCOND!
|
||||||
|
ABSCOND!
|
||||||
|
Check your Strife Specibus!
|
||||||
|
Ascend to God Tier!
|
||||||
|
Ascend to God Tier!
|
||||||
|
Got tiger!
|
||||||
|
Got tiger!
|
||||||
|
Motherfucking miracles!
|
||||||
|
Welcome to Skaia!
|
||||||
|
Welcome to Skaia!
|
||||||
|
Prospit or Derse?
|
||||||
|
Upgrading the Alchemiter!
|
||||||
|
Build Grist!
|
||||||
|
Build Grist!
|
||||||
|
Now with 100% more Faygo!
|
||||||
|
Sopor slime pie!
|
||||||
|
Grimdark!
|
||||||
|
Trickster mode engaged!
|
||||||
|
How HIGH do you even have to BE just to DO something like that........
|
||||||
|
Mayor of Can Town!
|
||||||
|
Spades Slick did nothing wrong!
|
||||||
|
It's hard and nobody understands.
|
||||||
|
Shenanigans!
|
||||||
|
The Scratch is happening!
|
||||||
|
612!
|
||||||
|
111111!
|
||||||
|
Jegus!
|
||||||
|
All the luck!
|
||||||
|
All the luck!
|
||||||
|
Dunkass!
|
||||||
|
Captchalogue this!
|
||||||
|
Captchalogue this!
|
||||||
|
Echeladder rung attained!
|
||||||
|
Megalovania originally played here!
|
||||||
|
Megalovania originally played here!
|
||||||
|
Music by Toby Fox!
|
||||||
|
Don't ask about the buckets!
|
||||||
|
Don't ask about the buckets!
|
||||||
|
Candy corn horns!
|
||||||
|
Your name is JOHN EGBERT.
|
||||||
|
Let me guess, your zodiac sign is a troll?
|
||||||
|
Put the bunny back in the box!
|
||||||
|
Do the windy thing!
|
||||||
|
Do the windy thing!
|
||||||
|
Betty Crocker is evil!
|
||||||
|
Betty Crocker is evil!
|
||||||
|
Beware the Batterwitch!
|
||||||
|
He is already here.
|
||||||
|
He is already here.
|
||||||
|
Doc Scratch is watching.
|
||||||
|
Con Air is a masterpiece!
|
||||||
|
Karkalicious!
|
||||||
|
SGRUB!
|
||||||
|
Sburb Beta!
|
||||||
|
Sburb Beta!
|
||||||
|
Knight of Time!
|
||||||
|
Seer of Light!
|
||||||
|
Heir of Breath!
|
||||||
|
Witch of Space!
|
||||||
|
Flipping the frog switch!
|
||||||
|
Forge the Bilious Sliver!
|
||||||
|
Midnight Crew!
|
||||||
|
We have the cue ball!
|
||||||
|
th1s 1s r3d1culous.
|
||||||
|
wweh...
|
||||||
|
glub glub...
|
||||||
|
DAVE: this is stupid
|
||||||
|
WHAT THE FUCK IS A CREEPER.
|
||||||
|
I AM GOING TO THROW A TANTRUM.
|
||||||
|
H3H3H3! 1 C4N SM3LL TH3 D14MOND 0R3!
|
||||||
|
T4ST3S L1K3 CH3RRY!
|
||||||
|
uHH, pLEASE DONT MINE THAT,,, iTS MINE.
|
||||||
|
ii jju2t want two play a game.
|
||||||
|
two much iinformatiion.
|
||||||
|
:33 < ac goes on a purrfect mining trip!
|
||||||
|
:33 < furrealsies!
|
||||||
|
I Cannot Believe You Just Mined That Dirt.
|
||||||
|
Please Stop Being Ridiculous.
|
||||||
|
All 8 of my 8lue diamonds! ::::::::)
|
||||||
|
You're w8ing for something?
|
||||||
|
D --> That block is insufficiently STRONG
|
||||||
|
D --> e%cuse me
|
||||||
|
tHaT sHiT iS mOtHeRfUcKiNg MiRaCuLoUs.
|
||||||
|
mY mOtHeRfUcKiNg BrOtHeR.
|
||||||
|
wwhats the point of minin anywway.
|
||||||
|
38) This is so GLUBBING exciting!
|
||||||
|
the dead p0pulati0n is b0thering me.
|
||||||
|
0k. i am t0tally fine with this.
|
||||||
|
ROXY: wonk ;)
|
||||||
|
ROXY: obfuscation!!!
|
||||||
|
ROXY: i am the best hacker
|
||||||
|
ROXY: meow
|
||||||
|
ROXY: dirk u there
|
||||||
|
ROXY: i am going to save u
|
||||||
|
ROXY: wtf is this block
|
||||||
|
ROXY: where r the fucking cats
|
||||||
|
ROXY: im kinda drunk
|
||||||
|
ROXY: the magic is real
|
||||||
|
ROXY: sweet catch!
|
||||||
|
ROXY: frigglish!
|
||||||
|
ROXY: where are all the wizards
|
||||||
|
ROXY: im putting on my god tier pjs
|
||||||
|
ROXY: pumpkins r legally outlawed
|
||||||
|
ROXY: mutieeeee
|
||||||
|
ROXY: cant wake up
|
||||||
|
ROXY: sup
|
||||||
|
ROXY: wonk ;)
|
||||||
|
JOHN: wow, okay.
|
||||||
|
JOHN: wow, okay.
|
||||||
|
JOHN: i am a true prankster!
|
||||||
|
JOHN: what are you even talking about?
|
||||||
|
JOHN: let's go do some ghostbusting!
|
||||||
|
JOHN: i'm not a homosexual!
|
||||||
|
JOHN: i am a true prankster!
|
||||||
|
Prankster's Gambit!
|
||||||
|
Prankster's Gambit!
|
||||||
|
Ectobiology!
|
||||||
|
Ectobiology!
|
||||||
|
JADE: hooray!!!
|
||||||
|
JADE: <3 <3 <3
|
||||||
|
JADE: beep beep meow
|
||||||
|
JADE: im going to build a tall tower!
|
||||||
|
JADE: hooray!!!
|
||||||
|
JADE: <3 <3 <3
|
||||||
|
Becquerel is a good boy!
|
||||||
|
Becquerel is a good boy!
|
||||||
|
Good dog. Best friend.
|
||||||
|
Good dog. Best friend.
|
||||||
|
Squiddleknit!
|
||||||
|
Squiddles!
|
||||||
|
DAVE: i am not a hero
|
||||||
|
DAVE: ironically of course
|
||||||
|
DAVE: time shenanigans
|
||||||
|
DAVE: lets drop it like its hot
|
||||||
|
DAVE: apple juice
|
||||||
|
DAVE: sweet catch
|
||||||
|
ROSE: Let the record state that I am not a fan of this.
|
||||||
|
ROSE: I will tear this game apart.
|
||||||
|
ROSE: The plot thickens.
|
||||||
|
ROSE: We are playing a game.
|
||||||
|
Pony Pals!
|
||||||
|
DIRK: You can't escape the miles.
|
||||||
|
DIRK: Sup.
|
||||||
|
DIRK: im going to build a robot.
|
||||||
|
Auto-Responder!
|
||||||
|
JAKE: Tally ho!
|
||||||
|
JAKE: Pip pip!
|
||||||
|
JAKE: By jove!
|
||||||
|
JAKE: Egads!
|
||||||
|
JAKE: Jolly good show
|
||||||
|
Jade Harley!!!
|
||||||
|
Jade Harley!!!
|
||||||
|
Jade Harley!!!
|
||||||
|
Jade Harley!!!
|
||||||
|
Jade Harley!!!
|
||||||