Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,128 @@
transition: all .15s; color: var(--muted); }
.wep-type-btn:hover { border-color: var(--accent); color: var(--text); }
.wep-type-btn.active { border-color: var(--accent); background: rgba(110,231,183,.1); color: var(--accent); }

/* ── Forge tree styles ──────────────────────────────────────────────── */
.forge-node { display: flex; align-items: center; gap: 4px; padding: 2px 4px;
border-bottom: 1px solid rgba(42,49,80,.3); cursor: default; font-size: .65rem; }
.forge-node:hover { background: rgba(110,231,183,.05); }
.forge-node.forge-hidden { opacity: .4; }
.forge-toggle { cursor: pointer; font-size: .6rem; min-width: 12px; user-select: none; }
.forge-name { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; }
.forge-badge { font-size: .5rem; padding: 1px 4px; border-radius: 3px; color: #000; font-weight: 600; }
.forge-geo { font-size: .55rem; color: var(--muted); }
.forge-mat { font-size: .55rem; color: #8b5cf6; }
.forge-vis, .forge-focus { background: none; border: none; cursor: pointer; font-size: .6rem;
padding: 1px 3px; border-radius: 2px; margin-left: auto; }
.forge-vis:hover, .forge-focus:hover { background: rgba(110,231,183,.15); }

/* ── Mobile toggle button (hidden on desktop) ─────────────────────── */
.mobile-toggle { display: none; }

/* ── Responsive: tablet (≤ 900px) — drop right panel below ──────── */
@media (max-width: 900px) {
#app {
grid-template-columns: 220px 1fr;
grid-template-rows: 50px 1fr auto;
}
#rightPanel {
grid-column: 1 / -1;
grid-row: 3;
max-height: 40vh;
border-top: 2px solid var(--border);
}
#bottomPanel { grid-column: 2; }
}

/* ── Responsive: mobile (≤ 768px) — single column, panels toggle ── */
@media (max-width: 768px) {
body { overflow: auto; height: auto; }
#app {
display: flex;
flex-direction: column;
height: auto;
min-height: 100vh;
}
header {
flex-wrap: wrap;
padding: 8px 12px;
gap: 8px;
}
header h1 { font-size: .95rem; }
header .status { width: 100%; margin-left: 0; font-size: .7rem; }

/* Toggle button visible on mobile */
.mobile-toggle {
display: inline-flex;
align-items: center; gap: 4px;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 4px;
background: rgba(0,0,0,.3);
color: var(--accent);
cursor: pointer;
font-size: .8rem;
font-family: 'Jost', sans-serif;
}
.mobile-toggle:hover { border-color: var(--accent); }

/* Panels hidden by default on mobile, shown when toggled */
#leftPanel, #rightPanel {
display: none;
max-height: none;
border: none;
border-bottom: 1px solid var(--border);
}
#leftPanel.mobile-open, #rightPanel.mobile-open {
display: block;
}

/* Viewport fills available width, fixed aspect ratio */
#viewport {
width: 100%;
height: 0;
padding-bottom: 75%; /* 4:3 aspect */
position: relative;
}
#viewport canvas {
position: absolute;
inset: 0;
}

/* Bottom panel stacks vertically */
#bottomPanel {
position: static;
flex-direction: column;
max-height: none;
background: var(--card);
pointer-events: auto;
}
#bottomPanel > * {
max-height: none;
border-top: 1px solid var(--border);
}

/* Stat grid single column on very narrow screens */
.stat-grid { grid-template-columns: 1fr; }
.wep-type-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ── Responsive: small phone (≤ 480px) ──────────────────────────── */
@media (max-width: 480px) {
header h1 { font-size: .8rem; }
.panel h3 { font-size: .8rem; }
.tab-btn { font-size: .6rem; padding: 4px 6px; }
.wep-type-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div id="app">
<header>
<h1>GRUDGE CHARACTER CREATOR</h1>
<span style="color:var(--muted);font-size:.75rem;">v2.0</span>
<button class="mobile-toggle" id="toggleLeftPanel">☰ Chars</button>
<button class="mobile-toggle" id="toggleRightPanel">📊 Stats</button>
<button class="action-btn" id="saveBuildBtn" style="margin-left:12px;">💾 Save Build</button>
<button class="action-btn" id="updateBuildBtn" style="display:none;">🔄 Update Build</button>
<span class="status" id="statusBar">Select a race to begin</span>
Expand Down Expand Up @@ -285,6 +400,13 @@ <h3>Animations</h3>
</label>
</div>
</div>
<div id="forgePanel" style="padding:8px;">
<h3>⚒️ Forge</h3>
<p style="font-size:.65rem;color:var(--muted);margin-bottom:4px;">Drop any model onto the 3D viewport, or use the button:</p>
<input type="file" id="forgeFileInput" accept=".fbx,.glb,.gltf,.obj,.dae,.stl,.usdz,.usdc" style="font-size:.7rem;color:var(--muted);margin-bottom:6px;width:100%;">
<div id="forgeTree" style="max-height:140px;overflow-y:auto;font-size:.65rem;font-family:monospace;"></div>
<button class="action-btn" id="forgeExport" style="width:100%;margin-top:4px;">📋 Export Hierarchy JSON</button>
</div>
<div id="adminPanel">
<h3>Admin Panel</h3>
<button class="action-btn" id="spawnDummy" style="width:100%;margin-bottom:4px;">Spawn Test Dummy</button>
Expand All @@ -303,6 +425,17 @@ <h3>Admin Panel</h3>
</div>
</div>

<script>
// Mobile panel toggles — only functional at ≤768px (buttons hidden on desktop via CSS)
document.getElementById('toggleLeftPanel').addEventListener('click', () => {
document.getElementById('leftPanel').classList.toggle('mobile-open');
document.getElementById('rightPanel').classList.remove('mobile-open');
});
document.getElementById('toggleRightPanel').addEventListener('click', () => {
document.getElementById('rightPanel').classList.toggle('mobile-open');
document.getElementById('leftPanel').classList.remove('mobile-open');
});
</script>
<script type="module" src="./src/main.js"></script>
</body>
</html>
33 changes: 30 additions & 3 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { SkeletonHelper } from 'three';
import { loadModel, loadAnimationClips, prepareModel, fbxLoader } from './modules/SmartLoader.js';
import { loadModel, loadModelFromFile, loadAnimationClips, prepareModel, fbxLoader, initKTX2, SUPPORTED_EXTENSIONS } from './modules/SmartLoader.js';

import { EquipmentManager } from './modules/EquipmentManager.js';
import { getAllRaces, WEAPON_ANIMATION_PACKS, loadManifest, getAnimationPacks } from './modules/FactionRegistry.js';
Expand All @@ -24,6 +24,8 @@ import { PostFX } from './modules/PostFX.js';
import { BoneAttachment } from './modules/BoneAttachment.js';
import { BossFight } from './modules/BossFight.js';
import { VFXManager } from './modules/VFXManager.js';
import { ForgePanel } from './modules/ForgePanel.js';
import { resolveTextures, classifyStyle } from './modules/TextureResolver.js';

// ════════════════════════════════════════════════════════════
// State
Expand Down Expand Up @@ -51,6 +53,7 @@ let vfxMgr = null;
let postfx = null;
let boneAttach = new BoneAttachment();
let bossFight = null;
let forgePanel = null;

// ════════════════════════════════════════════════════════════
// Scene Setup
Expand All @@ -67,16 +70,20 @@ function initScene() {
camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 200);
camera.position.set(0, 2.5, 5);

// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
// Renderer — best-in-class settings
renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);

// Initialize Draco-compressed GLTF + KTX2 GPU texture support
initKTX2(renderer);

// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0);
Expand Down Expand Up @@ -135,6 +142,19 @@ function initScene() {

// Init boss fight controller
bossFight = new BossFight(scene, camera, { postfx, updateStatus });

// Init Forge panel (drag-drop model inspector)
forgePanel = new ForgePanel(scene, camera, controls, updateStatus);
forgePanel.initDropZone(container);

// Forge file input + export button
document.getElementById('forgeFileInput')?.addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (file && forgePanel) await forgePanel.loadDroppedFile(file);
});
document.getElementById('forgeExport')?.addEventListener('click', () => {
forgePanel?.exportJSON();
});
}

// ════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -164,6 +184,13 @@ async function loadCharacterModel(raceConfig) {
// Scale, center, shadows via SmartLoader helper
prepareModel(model);

// Auto-resolve missing textures (toon for low-poly, PBR for high-poly)
const texResult = await resolveTextures(model, raceConfig.model);
if (texResult.discovered || texResult.generated) {
const style = classifyStyle(model);
console.log(`[TextureResolver] ${texResult.discovered} discovered, ${texResult.generated} generated (${style} style)`);
}

scene.add(model);
currentModel = model;

Expand Down
Loading