Improve summit terrain, mobile HUD and multiplayer night equipment
Add the Blender flashlight asset with a 600 lm beam and reduced bloom, shared-mode self badge, and compact phone controls. Relocate the summit marker, blend terrain seams, widen walking routes, and align local server configuration on port 2027.
This commit is contained in:
+48
-10
@@ -49,6 +49,9 @@ import { createRainGearState, type RainGearItem } from '../controls/rainGear.ts'
|
||||
import { faceTravel, updateThirdPersonCamera } from '../controls/thirdPersonCamera.ts';
|
||||
import { createRemotePlayers } from '../multiplayer/remotePlayers.ts';
|
||||
import type { PlayerState, PlayerUpdate } from '../multiplayer/protocol.ts';
|
||||
import { createSelfBadge } from '../ui/selfBadge.ts';
|
||||
import { createFlashlight } from './flashlight.ts';
|
||||
import { FLASHLIGHT } from '../constants/flashlight.ts';
|
||||
|
||||
export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<ReturnType<typeof loadTerrain>>, buildingData: BuildingAssets | null,
|
||||
onMove: (location: { latitude: number; longitude: number; elevation: number }) => void, coast?: LoadedCoast, structureAssets?: StructureAssets, reconstructSummit = false, summitGeometry?: BufferGeometry,
|
||||
@@ -66,6 +69,10 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
const horizon = createHorizon(camera.position, terrainData.metadata.observerSurfaceElevation);
|
||||
const terrain = createTerrain(terrainData, coast, reconstructSummit);
|
||||
const remotePlayers = sheep ? createRemotePlayers(sheep, scene, terrain.heightAt) : undefined;
|
||||
const selfBadge = sheep ? createSelfBadge(sheep.group, canvas) : undefined;
|
||||
const flashlight = sheep ? createFlashlight(sheep.group) : undefined;
|
||||
if (flashlight) scene.add(flashlight.light, flashlight.light.target);
|
||||
let flashlightOn = false;
|
||||
let networkBlocked = false;
|
||||
let sharedNow: (() => number) | null = null;
|
||||
let playerMoving = false;
|
||||
@@ -181,8 +188,17 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
const umbrellaButton = document.querySelector<HTMLButtonElement>('#gear-umbrella')!;
|
||||
const raincoatButton = document.querySelector<HTMLButtonElement>('#gear-raincoat')!;
|
||||
const hoodButton = document.querySelector<HTMLButtonElement>('#gear-hood')!;
|
||||
const flashlightButton = document.querySelector<HTMLButtonElement>('#gear-flashlight')!;
|
||||
document.getElementById('flashlight-lumens')!.textContent = `${FLASHLIGHT.lumens} lm`;
|
||||
flashlightButton.title = `손전등 · ${FLASHLIGHT.lumens} lm · 단축키 6`;
|
||||
const gearStatus = document.getElementById('gear-status')!;
|
||||
const refreshGear = () => {
|
||||
flashlightButton.disabled = !sheep || overview || networkBlocked;
|
||||
const dark = Boolean(solarState && solarState.altitude <= FLASHLIGHT.nightAltitude);
|
||||
flashlightButton.setAttribute('aria-disabled', String(flashlightButton.disabled || !dark));
|
||||
flashlightButton.dataset.unavailableReason = dark ? '' : '손전등은 해가 진 뒤 사용할 수 있습니다';
|
||||
flashlightButton.setAttribute('aria-pressed', String(flashlightOn));
|
||||
flashlightButton.setAttribute('aria-label', `손전등 ${flashlightOn ? '끄기' : '켜기'} · ${FLASHLIGHT.lumens} lm`);
|
||||
umbrellaButton.disabled = raincoatButton.disabled = hoodButton.disabled = !sheep || overview || networkBlocked;
|
||||
for (const button of [umbrellaButton, raincoatButton, hoodButton]) {
|
||||
const reason = !rainGear.available ? EQUIPMENT_NOTICE.weather
|
||||
@@ -207,12 +223,19 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
};
|
||||
const toggleGear = (item: RainGearItem) => {
|
||||
if (!sheep || overview || networkBlocked || !rainGear.toggle(item)) return;
|
||||
sheep.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood);
|
||||
sheep.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood, flashlightOn);
|
||||
refreshGear(); rendering.invalidate();
|
||||
};
|
||||
umbrellaButton.addEventListener('click', () => toggleGear('umbrella'), { signal: emoteEvents.signal });
|
||||
raincoatButton.addEventListener('click', () => toggleGear('raincoat'), { signal: emoteEvents.signal });
|
||||
hoodButton.addEventListener('click', () => toggleGear('hood'), { signal: emoteEvents.signal });
|
||||
const toggleFlashlight = () => {
|
||||
if (!sheep || overview || networkBlocked || !solarState || solarState.altitude > FLASHLIGHT.nightAltitude) return;
|
||||
flashlightOn = !flashlightOn;
|
||||
sheep.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood, flashlightOn);
|
||||
refreshGear(); rendering.invalidate();
|
||||
};
|
||||
flashlightButton.addEventListener('click', toggleFlashlight, { signal: emoteEvents.signal });
|
||||
const prayerAvailable = () => Boolean(solarState && canPray(solarState.date, solarState.sunrise));
|
||||
const refreshEmotes = () => {
|
||||
refreshGear();
|
||||
@@ -240,8 +263,9 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
waveButton.addEventListener('click', () => startEmote('wave'), { signal: emoteEvents.signal });
|
||||
prayButton.addEventListener('click', () => startEmote('pray'), { signal: emoteEvents.signal });
|
||||
window.addEventListener('keydown', event => {
|
||||
if (!controls.active() || event.repeat || !['Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5'].includes(event.code)) return;
|
||||
if (!controls.active() || event.repeat || !['Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6'].includes(event.code)) return;
|
||||
event.preventDefault();
|
||||
if (event.code === 'Digit6') { toggleFlashlight(); return; }
|
||||
if (['Digit3', 'Digit4', 'Digit5'].includes(event.code)) {
|
||||
toggleGear(event.code === 'Digit3' ? 'umbrella' : event.code === 'Digit4' ? 'raincoat' : 'hood'); return;
|
||||
}
|
||||
@@ -344,6 +368,7 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
updateThirdPersonCamera(followCamera, camera, (x, z) => harbour?.heightAt(x, z) ?? terrain.heightAt(x, z));
|
||||
}
|
||||
const activeCamera = overview ? overviewCamera : sheep ? followCamera : camera;
|
||||
flashlight?.update(flashlightOn && !overview);
|
||||
const precipitating = precipitation.update(delta, activeCamera.position, atmosphere.rain, atmosphere.snowfall,
|
||||
MathUtils.smoothstep(solarState?.altitude ?? -12, -8, 15), canvas.height);
|
||||
if(profile?.active()) rendering.invalidate();
|
||||
@@ -353,10 +378,21 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
compass.update(activeCamera);
|
||||
const minimapEnd = cpuStart ? performance.now() : 0;
|
||||
if (profile) renderer.info.reset();
|
||||
if (bloom) {
|
||||
const altitude = solarState?.altitude ?? 90;
|
||||
const torchActive = flashlightOn && !overview;
|
||||
bloom.enabled = altitude < BUILDINGS.lighting.fadeEndAltitude;
|
||||
bloom.strength = BUILDINGS.lighting.bloomStrength * (1 - MathUtils.smoothstep(altitude,
|
||||
BUILDINGS.lighting.fadeStartAltitude, BUILDINGS.lighting.fadeEndAltitude))
|
||||
* (torchActive ? FLASHLIGHT.bloomStrengthScale : 1);
|
||||
bloom.threshold = torchActive ? FLASHLIGHT.bloomThreshold : BUILDINGS.lighting.bloomThreshold;
|
||||
bloom.radius = torchActive ? FLASHLIGHT.bloomRadius : BUILDINGS.lighting.bloomRadius;
|
||||
}
|
||||
if (composer && !profile?.disablePost) {
|
||||
renderPass.camera = activeCamera;
|
||||
composer.render(delta);
|
||||
} else renderer.render(scene, activeCamera);
|
||||
selfBadge?.update(activeCamera, sharedNow !== null && !overview);
|
||||
if (cpuStart) profile?.end(now, { update: updateEnd - cpuStart, minimap: minimapEnd - updateEnd,
|
||||
submit: performance.now() - minimapEnd });
|
||||
if (profile?.observe(now, true)) resize();
|
||||
@@ -367,7 +403,7 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
const yaw = sheep?.group.rotation.y ?? camera.rotation.y;
|
||||
return { x: camera.position.x, z: camera.position.z, yaw: Math.atan2(Math.sin(yaw), Math.cos(yaw)),
|
||||
moving: playerMoving, emote: emotes.current,
|
||||
equipment: { umbrella: rainGear.umbrella, raincoat: rainGear.raincoat, hood: rainGear.hood } };
|
||||
equipment: { umbrella: rainGear.umbrella, raincoat: rainGear.raincoat, hood: rainGear.hood, flashlight: flashlightOn } };
|
||||
},
|
||||
setConnection(blocked: boolean, clock: (() => number) | null) {
|
||||
networkBlocked = blocked; sharedNow = clock;
|
||||
@@ -379,7 +415,8 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
camera.position.x = state.x; camera.position.z = state.z;
|
||||
if (sheep) sheep.group.rotation.y = state.yaw;
|
||||
rainGear.apply(state.equipment);
|
||||
sheep?.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood);
|
||||
flashlightOn = Boolean(state.equipment.flashlight && solarState && solarState.altitude <= FLASHLIGHT.nightAltitude);
|
||||
sheep?.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood, flashlightOn);
|
||||
placeObserver(); refreshEmotes(); rendering.invalidate();
|
||||
},
|
||||
synchronizePlayers(players: PlayerState[], selfId: string) {
|
||||
@@ -389,7 +426,7 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
setWeather(weather: WeatherSample | null) {
|
||||
weatherTarget = weatherAppearance(weather);
|
||||
rainGear.updateWeather(weatherTarget.rain, weatherTarget.snowfall);
|
||||
sheep?.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood);
|
||||
sheep?.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood, flashlightOn);
|
||||
refreshGear();
|
||||
// Do not blend an old precipitation type into the newly selected weather.
|
||||
if (weatherTarget.rain === 0) atmosphere.rain = 0;
|
||||
@@ -429,6 +466,10 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
},
|
||||
update(state: SunState) {
|
||||
solarState = state;
|
||||
if (flashlightOn && state.altitude > FLASHLIGHT.nightAltitude) {
|
||||
flashlightOn = false;
|
||||
sheep?.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood, false);
|
||||
}
|
||||
emotes.update(0, false, prayerAvailable());
|
||||
refreshEmotes();
|
||||
// LIVE calls this every second; never skip a new solar/time state.
|
||||
@@ -437,11 +478,6 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
applyAtmosphere();
|
||||
ocean?.update(direction, state.altitude);
|
||||
buildings?.update(state.altitude);
|
||||
if (bloom) {
|
||||
bloom.enabled = state.altitude < BUILDINGS.lighting.fadeEndAltitude;
|
||||
bloom.strength = BUILDINGS.lighting.bloomStrength * (1 - MathUtils.smoothstep(state.altitude,
|
||||
BUILDINGS.lighting.fadeStartAltitude, BUILDINGS.lighting.fadeEndAltitude));
|
||||
}
|
||||
horizon.update(state.altitude);
|
||||
fog.color.copy(nightFog).lerp(dawnFog, MathUtils.smoothstep(state.altitude, -8, 0));
|
||||
fog.color.lerp(dayFog, MathUtils.smoothstep(state.altitude, 0, 15));
|
||||
@@ -453,6 +489,8 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
renderer.setAnimationLoop(null);
|
||||
minimap.dispose();
|
||||
compass.dispose();
|
||||
selfBadge?.dispose();
|
||||
flashlight?.dispose();
|
||||
bloom?.dispose();
|
||||
outputPass?.dispose();
|
||||
renderPass.dispose();
|
||||
|
||||
Reference in New Issue
Block a user