feat: initialize sunrise with 100-player shared rooms
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { BoxGeometry, BufferGeometry, Float32BufferAttribute, Group, MathUtils, Mesh, MeshStandardMaterial, Vector3 } from 'three';
|
||||
import type { Material } from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { STRUCTURES, STRUCTURE_ASSETS } from '../constants/structures.ts';
|
||||
import { SUMMIT_RELIEF } from '../constants/summitRelief.ts';
|
||||
import { SUMMIT_PLATEAU } from '../constants/summitPlateau.ts';
|
||||
import { summitPosition } from '../controls/summitCoordinates.ts';
|
||||
|
||||
export type StructureAssets = Map<string, Group>;
|
||||
type SupportAnchor = { position: [number, number, number]; postWidth: number };
|
||||
|
||||
export async function loadStructures(includeRelief = false): Promise<StructureAssets> {
|
||||
const loader = new GLTFLoader();
|
||||
const ids = [...STRUCTURES.towers.map(s => s.id), 'weather-radar', ...STRUCTURES.decks.map(s => s.id),
|
||||
...(includeRelief ? [SUMMIT_RELIEF.assetId, SUMMIT_PLATEAU.marker.assetId] : [])];
|
||||
return new Map(await Promise.all(ids.map(async id => {
|
||||
const model = await loader.loadAsync(STRUCTURE_ASSETS.baseUrl + '/' + id + '.glb');
|
||||
return [id, model.scene] as const;
|
||||
})));
|
||||
}
|
||||
|
||||
/** Local WGS84 tangent approximation, sufficient for the < 1 km structure area. +z south. */
|
||||
export function structurePosition(latitude: number, longitude: number, origin: { latitude: number; longitude: number }) {
|
||||
const { x, z } = summitPosition(latitude, longitude, origin);
|
||||
return new Vector3(x, 0, z);
|
||||
}
|
||||
|
||||
export function createStructures(origin: { latitude: number; longitude: number }, groundAt: (x: number, z: number) => number,
|
||||
assets: StructureAssets) {
|
||||
const group = new Group();
|
||||
group.name = 'Refined summit assets (estimated dimensions)';
|
||||
const geometries = new Set<BufferGeometry>();
|
||||
const materials = new Set<Material>();
|
||||
const wall = new MeshStandardMaterial({ color: STRUCTURE_ASSETS.foundationColor, roughness: 1 });
|
||||
const rail = new MeshStandardMaterial({ color: STRUCTURE_ASSETS.supportColor, roughness: .86 });
|
||||
materials.add(wall); materials.add(rail);
|
||||
function box(parent: Group, width: number, height: number, depth: number, x: number, y: number, z: number, material: Material) {
|
||||
const geometry = new BoxGeometry(width, height, depth);
|
||||
geometries.add(geometry);
|
||||
const result = new Mesh(geometry, material);
|
||||
result.position.set(x, y, z);
|
||||
parent.add(result);
|
||||
}
|
||||
function site(id: string, latitude: number, longitude: number) {
|
||||
const parent = new Group();
|
||||
parent.name = id;
|
||||
parent.position.copy(structurePosition(latitude, longitude, origin));
|
||||
parent.position.y = groundAt(parent.position.x, parent.position.z);
|
||||
const model = assets.get(id)!;
|
||||
model.traverse(object => {
|
||||
if (object instanceof Mesh) {
|
||||
geometries.add(object.geometry);
|
||||
(Array.isArray(object.material) ? object.material : [object.material]).forEach(m => materials.add(m));
|
||||
if (object.userData.materialBatch === 'groove') {
|
||||
const material = object.material as Material;
|
||||
material.polygonOffset = true;
|
||||
material.polygonOffsetFactor = -1;
|
||||
material.polygonOffsetUnits = -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
parent.add(model);
|
||||
group.add(parent);
|
||||
return parent;
|
||||
}
|
||||
// Solid foundations span the local terrain relief instead of floating on a single sample.
|
||||
function foundation(parent: Group, width: number, depth: number) {
|
||||
const samples = [-width / 2, width / 2].flatMap(x => [-depth / 2, depth / 2].map(z => groundAt(parent.position.x + x, parent.position.z + z)));
|
||||
const top = Math.max(parent.position.y, ...samples) + .2;
|
||||
const bottom = Math.min(parent.position.y, ...samples) - .4;
|
||||
parent.position.y = top;
|
||||
box(parent, width, top - bottom, depth, 0, -(top - bottom) / 2, 0, wall);
|
||||
}
|
||||
for (const spec of STRUCTURES.towers) {
|
||||
const tower = site(spec.id, spec.latitude, spec.longitude);
|
||||
tower.userData = { source: spec.source, dimensionsEstimated: true, positionEstimated: spec.estimatedPosition };
|
||||
foundation(tower, spec.width + 1, spec.width + 1);
|
||||
}
|
||||
const radarSpec = STRUCTURES.radar;
|
||||
const radar = site('weather-radar', radarSpec.latitude, radarSpec.longitude);
|
||||
radar.userData = { source: radarSpec.source, dimensionsEstimated: true, positionEstimated: true };
|
||||
foundation(radar, 8, 8);
|
||||
|
||||
for (const spec of STRUCTURES.decks) {
|
||||
const deck = site(spec.id, spec.latitude, spec.longitude);
|
||||
deck.rotation.y = MathUtils.degToRad(spec.heading);
|
||||
deck.userData = { source: spec.source, dimensionsEstimated: true, positionEstimated: true };
|
||||
const modelRoot = assets.get(spec.id)!.getObjectByName(spec.id)!;
|
||||
const anchors = JSON.parse(modelRoot.userData.terrainSupportAnchorsJson) as SupportAnchor[];
|
||||
const localGround = (x: number, z: number) => {
|
||||
const v = new Vector3(x, 0, z).applyAxisAngle(new Vector3(0, 1, 0), deck.rotation.y).add(deck.position);
|
||||
return groundAt(v.x, v.z);
|
||||
};
|
||||
const bottomZ = -spec.length / 2 - spec.stairLength;
|
||||
const samples = [-spec.width / 2, spec.width / 2].flatMap(x => [-spec.length / 2, 0, spec.length / 2].map(z => localGround(x, z)));
|
||||
samples.push(localGround(-spec.width / 2, bottomZ) + spec.stairDrop, localGround(spec.width / 2, bottomZ) + spec.stairDrop);
|
||||
// Only the walking surface needs clearance; buried post feet must not lift the entire deck.
|
||||
deck.position.y = Math.max(...samples) + STRUCTURES.deck.clearance;
|
||||
const supports = new Group();
|
||||
supports.name = 'terrain-supports';
|
||||
deck.add(supports);
|
||||
for (const { position: [x, y, z], postWidth } of anchors) {
|
||||
const bottom = localGround(x, z) - deck.position.y - .1;
|
||||
const top = y + .02;
|
||||
if (top > bottom) box(supports, postWidth, top - bottom, postWidth, x, (top + bottom) / 2, z, rail);
|
||||
}
|
||||
let wood: Material = wall;
|
||||
modelRoot.traverse(object => {
|
||||
if (object instanceof Mesh && object.userData.materialBatch === 'wood0') wood = object.material as Material;
|
||||
});
|
||||
function connectEnd(name: string, startZ: number, topY: number, direction: number) {
|
||||
const config = STRUCTURE_ASSETS.access;
|
||||
const access = new Group();
|
||||
access.name = name;
|
||||
deck.add(access);
|
||||
const ground = (x: number, z: number) => localGround(x, z) - deck.position.y;
|
||||
let count = 1;
|
||||
let landingY = topY;
|
||||
for (; count <= config.maxSteps; count++) {
|
||||
const z = startZ + direction * count * config.treadDepth;
|
||||
landingY = Math.max(ground(-spec.width / 2, z), ground(0, z), ground(spec.width / 2, z)) + .02;
|
||||
if (Math.abs(topY - landingY) <= count * config.maxRiser) break;
|
||||
}
|
||||
count = Math.min(count, config.maxSteps);
|
||||
const rise = (topY - landingY) / count;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const z = startZ + direction * (i + .5) * config.treadDepth;
|
||||
const y = topY - (i + 1) * rise;
|
||||
const bottom = Math.min(...[-spec.width / 2, spec.width / 2].flatMap(x =>
|
||||
[-.5, .5].map(dz => ground(x, z + dz * config.treadDepth)))) - config.groundOverlap;
|
||||
// Closed footing beneath each tread reaches the slope instead of ending in mid-air.
|
||||
box(access, spec.width, Math.max(.02, y - .04 - bottom), config.treadDepth,
|
||||
0, (y - .04 + bottom) / 2, z, wall);
|
||||
box(access, spec.width, .04, config.treadDepth + .002, 0, y - .02, z, wood);
|
||||
}
|
||||
const z0 = startZ + direction * count * config.treadDepth;
|
||||
const endZ = z0 + direction * config.landingLength;
|
||||
// The outer landing edge follows the cross-slope too, not just its highest corner.
|
||||
const vertices = [-spec.width / 2, spec.width / 2].flatMap(x =>
|
||||
[x, landingY, z0, x, ground(x, endZ) + .02, endZ]);
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3));
|
||||
geometry.setIndex(direction > 0 ? [0, 1, 2, 2, 1, 3] : [0, 2, 1, 2, 3, 1]);
|
||||
geometry.computeVertexNormals();
|
||||
geometries.add(geometry);
|
||||
access.add(new Mesh(geometry, wood));
|
||||
access.userData = { startZ, endZ, topY, stepCount: count };
|
||||
}
|
||||
if (STRUCTURE_ASSETS.access.deckIds.includes(spec.id)) {
|
||||
connectEnd('entry-access', spec.length / 2, 0, 1);
|
||||
connectEnd('exit-access', bottomZ, -spec.stairDrop, -1);
|
||||
}
|
||||
}
|
||||
return { group, dispose() { geometries.forEach(g => g.dispose()); materials.forEach(m => m.dispose()); } };
|
||||
}
|
||||
Reference in New Issue
Block a user