/* ============================================================ RAIN WALKER main.js Main application entry point. Responsibilities: - Three.js renderer - Scene - Camera - Lighting - Character loading - World creation - Rain - Character traversal - Camera composition - Loading screen - Main animation loop ============================================================ */ /* ============================================================ THREE.JS ============================================================ */ import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js"; /* ============================================================ PROJECT MODULES ============================================================ */ import { loadCharacter, updateCharacter, getCharacter, isCharacterReady } from "./character.js"; import { createWorld, updateWorld } from "./world.js"; import { createRain, updateRain } from "./rain.js"; /* ============================================================ DOM ============================================================ */ const game = document.getElementById("game"); const loadingScreen = document.getElementById("loading-screen"); const loadingText = document.getElementById("loading-text"); const screenOverlay = document.getElementById("screen-overlay"); /* ============================================================ CONFIGURATION ============================================================ */ const CONFIG = { /* Render below native resolution and enlarge with CSS. This creates the chunky/pixel presentation. */ pixelScale: 0.55, /* Character world movement. */ characterSpeed: 0.72, /* Character starts off-screen left and exits right. */ walkStart: -7.5, walkEnd: 7.5, /* Camera. */ cameraHeight: 3.0, cameraDistance: 10, /* Keep camera mostly stationary. This gives us the original concept: character crosses the actual screen. */ cameraFollowAmount: 0.06 }; /* ============================================================ SCENE ============================================================ */ const scene = new THREE.Scene(); scene.background = new THREE.Color( 0x000000 ); /* Heavy black fog makes the city disappear naturally into the background. */ scene.fog = new THREE.FogExp2( 0x000000, 0.055 ); /* ============================================================ CAMERA ============================================================ */ const camera = new THREE.PerspectiveCamera( 38, window.innerWidth / window.innerHeight, 0.1, 100 ); camera.position.set( 0, CONFIG.cameraHeight, CONFIG.cameraDistance ); camera.lookAt( 0, 1.65, 0 ); /* ============================================================ RENDERER ============================================================ */ const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance" }); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.82; /* ============================================================ SHADOWS ============================================================ */ renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap; /* ============================================================ PIXEL RESOLUTION ============================================================ */ function updateRendererSize() { const width = Math.max( 1, Math.floor( window.innerWidth * CONFIG.pixelScale ) ); const height = Math.max( 1, Math.floor( window.innerHeight * CONFIG.pixelScale ) ); renderer.setPixelRatio(1); renderer.setSize( width, height, false ); /* Stretch low-resolution WebGL canvas back to the full browser viewport. style.css uses image-rendering: pixelated. */ renderer.domElement.style.width = "100vw"; renderer.domElement.style.height = "100vh"; } updateRendererSize(); game.appendChild( renderer.domElement ); /* ============================================================ LIGHTING ============================================================ */ /* ------------------------------------------------------------ VERY DARK SKY / GROUND LIGHT ------------------------------------------------------------ */ const hemisphereLight = new THREE.HemisphereLight( 0x566675, 0x071008, 0.48 ); scene.add( hemisphereLight ); /* ------------------------------------------------------------ COLD NIGHT LIGHT ------------------------------------------------------------ */ const moonLight = new THREE.DirectionalLight( 0x9fb8cc, 1.45 ); moonLight.position.set( -5, 9, 7 ); moonLight.castShadow = true; moonLight.shadow.mapSize.set( 512, 512 ); moonLight.shadow.camera.left = -12; moonLight.shadow.camera.right = 12; moonLight.shadow.camera.top = 12; moonLight.shadow.camera.bottom = -4; moonLight.shadow.bias = -0.0005; scene.add( moonLight ); /* ------------------------------------------------------------ WARM CITY LIGHT ------------------------------------------------------------ */ /* This lets the character's green tunic, hair and leather briefly stand out from the blue-black environment. */ const cityLight = new THREE.PointLight( 0xd49a55, 2.7, 10, 2 ); cityLight.position.set( -2.5, 3.5, 3 ); scene.add( cityLight ); /* ------------------------------------------------------------ SECOND COLD STREET LIGHT ------------------------------------------------------------ */ const coldLight = new THREE.PointLight( 0x628aa2, 1.5, 9, 2 ); coldLight.position.set( 5, 4, 2 ); scene.add( coldLight ); /* ============================================================ WORLD ============================================================ */ createWorld( scene ); /* ============================================================ RAIN ============================================================ */ createRain( scene ); /* ============================================================ CHARACTER STATE ============================================================ */ let character = null; let characterLoaded = false; /* Separate locomotion clock. This lets us reset the character cleanly when it reaches the other side of the screen. */ let walkTime = 0; /* ============================================================ LOAD CHARACTER ============================================================ */ async function initializeCharacter() { try { setLoadingMessage( "LOADING WANDERER..." ); const result = await loadCharacter( scene ); character = result.character; characterLoaded = true; /* Start outside the left edge. */ character.position.x = CONFIG.walkStart; /* Keep character on the gameplay plane. */ character.position.z = 0; /* IMPORTANT: Depending on how your GLB was exported, its forward direction may need changing. Try: 0 Math.PI Math.PI / 2 -Math.PI / 2 We begin with a side-facing assumption. */ character.rotation.y = Math.PI / 2; setLoadingMessage( "THE RAIN BEGINS" ); /* Give textures one render frame to upload. */ requestAnimationFrame( finishLoading ); } catch (error) { console.error( "Character initialization failed:", error ); setLoadingMessage( "CHARACTER FAILED TO LOAD" ); /* World still runs even if model fails. */ setTimeout( finishLoading, 1200 ); } } /* ============================================================ LOADING SCREEN ============================================================ */ function setLoadingMessage( message ) { if (!loadingText) { return; } loadingText.textContent = message; } function finishLoading() { setTimeout( () => { if (loadingScreen) { loadingScreen.classList.add( "hidden" ); } if (screenOverlay) { screenOverlay.classList.add( "hidden" ); } }, 450 ); } /* ============================================================ CHARACTER MOVEMENT ============================================================ */ function updateCharacterMovement( delta ) { if ( !characterLoaded || !character ) { return; } /* Actual horizontal traversal. */ character.position.x += CONFIG.characterSpeed * delta; walkTime += delta; /* Character exits the right edge and reappears from the left. Reset is deliberately outside the visible frame. */ if ( character.position.x > CONFIG.walkEnd ) { character.position.x = CONFIG.walkStart; walkTime = 0; } } /* ============================================================ CAMERA ============================================================ */ function updateCamera( delta ) { /* The camera stays mostly stationary. A tiny amount of follow gives the shot some life without preventing the character from visibly crossing the screen. */ if ( characterLoaded && character ) { const desiredX = character.position.x * CONFIG.cameraFollowAmount; camera.position.x = THREE.MathUtils.lerp( camera.position.x, desiredX, 1 - Math.exp( -2 * delta ) ); } camera.lookAt( camera.position.x * 0.15, 1.65, 0 ); } /* ============================================================ LIGHT ANIMATION ============================================================ */ function updateLights( elapsedTime ) { /* Imperfect city electricity. Deliberately subtle. */ cityLight.intensity = 2.7 + Math.sin( elapsedTime * 2.15 ) * 0.12 + Math.sin( elapsedTime * 7.7 ) * 0.035; /* Tiny movement makes reflected illumination feel less completely static in the rain. */ cityLight.position.x = -2.5 + Math.sin( elapsedTime * 0.18 ) * 0.15; } /* ============================================================ RESIZE ============================================================ */ function resize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); updateRendererSize(); } window.addEventListener( "resize", resize ); /* ============================================================ VISIBILITY ============================================================ */ /* Prevent huge time jumps when returning to a background tab. */ document.addEventListener( "visibilitychange", () => { if ( document.visibilityState === "visible" ) { clock.getDelta(); } } ); /* ============================================================ CLOCK ============================================================ */ const clock = new THREE.Clock(); let elapsedTime = 0; /* ============================================================ UPDATE ============================================================ */ function update( delta ) { elapsedTime += delta; /* -------------------------------------------------------- WORLD -------------------------------------------------------- */ updateWorld( delta, elapsedTime ); /* -------------------------------------------------------- RAIN -------------------------------------------------------- */ updateRain( delta, elapsedTime ); /* -------------------------------------------------------- CHARACTER TRAVERSAL -------------------------------------------------------- */ updateCharacterMovement( delta ); /* -------------------------------------------------------- CHARACTER SKELETON character.js performs the mathematical walk if bones exist in the GLB. -------------------------------------------------------- */ updateCharacter( delta, walkTime ); /* -------------------------------------------------------- CAMERA -------------------------------------------------------- */ updateCamera( delta ); /* -------------------------------------------------------- LIGHTING -------------------------------------------------------- */ updateLights( elapsedTime ); } /* ============================================================ RENDER ============================================================ */ function render() { renderer.render( scene, camera ); } /* ============================================================ GAME LOOP ============================================================ */ function animate() { requestAnimationFrame( animate ); /* Clamp delta to prevent giant simulation jumps. */ const delta = Math.min( clock.getDelta(), 0.05 ); update( delta ); render(); } /* ============================================================ START ============================================================ */ async function start() { /* Begin rendering immediately. This means the city/rain can exist while the GLB loads. */ animate(); /* Load character asynchronously. */ await initializeCharacter(); } start(); /* ============================================================ DEVELOPMENT ACCESS Handy while we're tuning the model in DevTools. You can type: RainWalker.character into the console. ============================================================ */ window.RainWalker = { scene, camera, renderer, get character() { return getCharacter(); }, get characterReady() { return isCharacterReady(); } };