Compare commits
1 Commits
mimo/code/
...
fix/1542-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72958a5147 |
291
avatar-customization.js
Normal file
291
avatar-customization.js
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Avatar Customization Module (#1542)
|
||||
*
|
||||
* Provides avatar color, shape, and name tag for the Nexus 3D world.
|
||||
* Persists settings in localStorage.
|
||||
*
|
||||
* Usage:
|
||||
* window.AvatarCustomization.init(scene, camera) — called from init()
|
||||
* window.AvatarCustomization.update(playerPos) — called each frame
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ═══ Configuration ═══════════════════════════════════════════
|
||||
const STORAGE_KEY = 'nexus_avatar_settings';
|
||||
const COLOR_PRESETS = [
|
||||
{ name: 'Cyan', hex: '#4af0c0' },
|
||||
{ name: 'Gold', hex: '#ffd700' },
|
||||
{ name: 'Violet', hex: '#7b5cff' },
|
||||
{ name: 'Rose', hex: '#ff6b9d' },
|
||||
{ name: 'Emerald', hex: '#50c878' },
|
||||
{ name: 'Orange', hex: '#ff8c42' },
|
||||
{ name: 'Ice', hex: '#88ccff' },
|
||||
{ name: 'Silver', hex: '#c0c0c0' },
|
||||
];
|
||||
|
||||
const SHAPE_OPTIONS = [
|
||||
{ name: 'Sphere', geometry: 'sphere' },
|
||||
{ name: 'Capsule', geometry: 'capsule' },
|
||||
{ name: 'Diamond', geometry: 'diamond' },
|
||||
];
|
||||
|
||||
// ═══ State ═══════════════════════════════════════════════════
|
||||
let avatarMesh = null;
|
||||
let nameTag = null;
|
||||
let scene = null;
|
||||
let settings = loadSettings();
|
||||
|
||||
// ═══ Persistence ═════════════════════════════════════════════
|
||||
function loadSettings() {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) return JSON.parse(saved);
|
||||
} catch (_) { /* ignore */ }
|
||||
return {
|
||||
color: COLOR_PRESETS[0].hex,
|
||||
shape: 'sphere',
|
||||
name: 'Visitor',
|
||||
};
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
// ═══ Mesh Creation ══════════════════════════════════════════
|
||||
function createGeometry(shape) {
|
||||
switch (shape) {
|
||||
case 'capsule':
|
||||
return new THREE.CapsuleGeometry(0.3, 0.6, 8, 16);
|
||||
case 'diamond':
|
||||
return new THREE.OctahedronGeometry(0.4, 0);
|
||||
case 'sphere':
|
||||
default:
|
||||
return new THREE.SphereGeometry(0.35, 32, 32);
|
||||
}
|
||||
}
|
||||
|
||||
function createAvatar() {
|
||||
if (avatarMesh) {
|
||||
scene.remove(avatarMesh);
|
||||
avatarMesh.geometry.dispose();
|
||||
avatarMesh.material.dispose();
|
||||
}
|
||||
|
||||
const geometry = createGeometry(settings.shape);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(settings.color),
|
||||
emissive: new THREE.Color(settings.color).multiplyScalar(0.3),
|
||||
metalness: 0.6,
|
||||
roughness: 0.3,
|
||||
});
|
||||
|
||||
avatarMesh = new THREE.Mesh(geometry, material);
|
||||
avatarMesh.castShadow = true;
|
||||
scene.add(avatarMesh);
|
||||
|
||||
// Update name tag
|
||||
updateNameTag();
|
||||
}
|
||||
|
||||
// ═══ Name Tag ═══════════════════════════════════════════════
|
||||
function createNameTagSprite() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.roundRect(0, 0, 256, 64, 8);
|
||||
ctx.fill();
|
||||
|
||||
// Text
|
||||
ctx.fillStyle = settings.color;
|
||||
ctx.font = 'bold 28px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(settings.name, 128, 32);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(1.5, 0.4, 1);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
function updateNameTag() {
|
||||
if (nameTag) {
|
||||
avatarMesh.remove(nameTag);
|
||||
nameTag.material.map.dispose();
|
||||
nameTag.material.dispose();
|
||||
}
|
||||
nameTag = createNameTagSprite();
|
||||
nameTag.position.y = 0.8;
|
||||
avatarMesh.add(nameTag);
|
||||
}
|
||||
|
||||
// ═══ Settings UI ════════════════════════════════════════════
|
||||
function createSettingsPanel() {
|
||||
const existing = document.getElementById('avatar-settings-panel');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'avatar-settings-panel';
|
||||
panel.style.cssText = `
|
||||
position: fixed; bottom: 80px; right: 20px; z-index: 1000;
|
||||
background: rgba(10, 15, 40, 0.95); border: 1px solid #4af0c0;
|
||||
border-radius: 8px; padding: 16px; min-width: 220px;
|
||||
font-family: monospace; color: #aaa; display: none;
|
||||
`;
|
||||
|
||||
// Title
|
||||
panel.innerHTML = `
|
||||
<div style="color: #4af0c0; font-weight: bold; margin-bottom: 12px; font-size: 14px;">
|
||||
✦ Avatar Settings
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display: block; margin-bottom: 4px; font-size: 11px; color: #888;">Name</label>
|
||||
<input id="avatar-name-input" type="text" value="${settings.name}"
|
||||
style="width: 100%; background: #0a0f28; border: 1px solid #1a2a4a; color: #ddd;
|
||||
padding: 4px 8px; border-radius: 4px; font-family: monospace; box-sizing: border-box;">
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display: block; margin-bottom: 4px; font-size: 11px; color: #888;">Color</label>
|
||||
<div id="avatar-color-presets" style="display: flex; gap: 6px; flex-wrap: wrap;">
|
||||
${COLOR_PRESETS.map(c => `
|
||||
<div class="avatar-color-swatch" data-color="${c.hex}" title="${c.name}"
|
||||
style="width: 24px; height: 24px; border-radius: 4px; background: ${c.hex};
|
||||
cursor: pointer; border: 2px solid ${c.hex === settings.color ? '#fff' : 'transparent'};
|
||||
transition: border-color 0.15s;">
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shape -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display: block; margin-bottom: 4px; font-size: 11px; color: #888;">Shape</label>
|
||||
<div style="display: flex; gap: 6px;">
|
||||
${SHAPE_OPTIONS.map(s => `
|
||||
<button class="avatar-shape-btn" data-shape="${s.geometry}"
|
||||
style="flex: 1; padding: 4px 8px; background: ${s.geometry === settings.shape ? '#1a2a4a' : 'transparent'};
|
||||
border: 1px solid #1a2a4a; color: #ddd; border-radius: 4px; cursor: pointer;
|
||||
font-family: monospace; font-size: 11px; transition: background 0.15s;">
|
||||
${s.name}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 10px; color: #555; text-align: center;">
|
||||
Saved to localStorage
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(panel);
|
||||
|
||||
// Wire up events
|
||||
document.getElementById('avatar-name-input').addEventListener('input', (e) => {
|
||||
settings.name = e.target.value || 'Visitor';
|
||||
saveSettings();
|
||||
updateNameTag();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.avatar-color-swatch').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
settings.color = el.dataset.color;
|
||||
saveSettings();
|
||||
createAvatar();
|
||||
createSettingsPanel(); // Re-render to update selection
|
||||
toggleSettingsPanel(true);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.avatar-shape-btn').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
settings.shape = el.dataset.shape;
|
||||
saveSettings();
|
||||
createAvatar();
|
||||
createSettingsPanel();
|
||||
toggleSettingsPanel(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let panelVisible = false;
|
||||
|
||||
function toggleSettingsPanel(forceState) {
|
||||
const panel = document.getElementById('avatar-settings-panel');
|
||||
if (!panel) return;
|
||||
panelVisible = forceState !== undefined ? forceState : !panelVisible;
|
||||
panel.style.display = panelVisible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function createToggleButton() {
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'avatar-settings-btn';
|
||||
btn.textContent = '✦';
|
||||
btn.title = 'Avatar Settings';
|
||||
btn.style.cssText = `
|
||||
position: fixed; bottom: 20px; right: 20px; z-index: 1001;
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
background: rgba(10, 15, 40, 0.9); border: 1px solid #4af0c0;
|
||||
color: #4af0c0; font-size: 18px; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: background 0.15s;
|
||||
`;
|
||||
btn.addEventListener('mouseenter', () => btn.style.background = 'rgba(74, 240, 192, 0.2)');
|
||||
btn.addEventListener('mouseleave', () => btn.style.background = 'rgba(10, 15, 40, 0.9)');
|
||||
btn.addEventListener('click', () => toggleSettingsPanel());
|
||||
document.body.appendChild(btn);
|
||||
}
|
||||
|
||||
// ═══ Public API ═════════════════════════════════════════════
|
||||
window.AvatarCustomization = {
|
||||
init(sceneRef, cameraRef) {
|
||||
scene = sceneRef;
|
||||
createAvatar();
|
||||
createSettingsPanel();
|
||||
createToggleButton();
|
||||
console.log(`[Avatar] Initialized — ${settings.name}, ${settings.color}, ${settings.shape}`);
|
||||
},
|
||||
|
||||
update(playerPos) {
|
||||
if (!avatarMesh) return;
|
||||
avatarMesh.position.copy(playerPos);
|
||||
avatarMesh.position.y -= 1.5; // Below camera
|
||||
avatarMesh.rotation.y += 0.005; // Gentle spin
|
||||
},
|
||||
|
||||
getSettings() {
|
||||
return { ...settings };
|
||||
},
|
||||
|
||||
setColor(hex) {
|
||||
settings.color = hex;
|
||||
saveSettings();
|
||||
createAvatar();
|
||||
},
|
||||
|
||||
setName(name) {
|
||||
settings.name = name || 'Visitor';
|
||||
saveSettings();
|
||||
updateNameTag();
|
||||
},
|
||||
|
||||
setShape(shape) {
|
||||
settings.shape = shape;
|
||||
saveSettings();
|
||||
createAvatar();
|
||||
},
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user