├── .gitignore ├── LICENSE ├── app ├── Experience │ ├── Camera.js │ ├── Experience.js │ ├── LocalStorage.js │ ├── Renderer.js │ ├── Utils │ │ ├── Loaders.js │ │ ├── Resources.js │ │ ├── Sizes.js │ │ ├── Time.js │ │ └── assets.js │ └── World │ │ ├── Interior │ │ ├── Components │ │ │ ├── Castle.js │ │ │ └── Interactions.js │ │ └── Interior.js │ │ ├── Player │ │ └── Player.js │ │ ├── Whiterun │ │ ├── Components │ │ │ ├── Buildings.js │ │ │ ├── Environment.js │ │ │ ├── Interactions.js │ │ │ ├── Items.js │ │ │ ├── Landscape.js │ │ │ └── Walls.js │ │ └── Whiterun.js │ │ └── World.js ├── Interface │ ├── Components │ │ └── Message.js │ ├── Interface.js │ └── Utils │ │ ├── Elements.js │ │ └── key.js ├── index.html ├── main.css ├── main.js └── styles │ ├── experience.css │ └── interface.css ├── package-lock.json ├── package.json ├── public ├── draco │ ├── README.md │ ├── draco_decoder.js │ ├── draco_decoder.wasm │ ├── draco_encoder.js │ ├── draco_wasm_wrapper.js │ └── gltf │ │ ├── draco_decoder.js │ │ ├── draco_decoder.wasm │ │ ├── draco_encoder.js │ │ └── draco_wasm_wrapper.js ├── models │ ├── Interior_w_collider.glb │ ├── buildings.glb │ ├── interior_interactions.glb │ ├── land_items.glb │ ├── land_w_collider.glb │ ├── outside_interactions.glb │ └── walls.glb ├── textures │ ├── buildings.webp │ ├── interior_baked.webp │ ├── items.webp │ ├── land.webp │ ├── skybox │ │ ├── nx.webp │ │ ├── ny.webp │ │ ├── nz.webp │ │ ├── px.webp │ │ ├── py.webp │ │ └── pz.webp │ └── walls_baked.webp └── vite.svg └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Andrew Woan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/Experience/Camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "./Experience.js"; 3 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 4 | 5 | export default class Camera { 6 | constructor() { 7 | this.experience = new Experience(); 8 | this.sizes = this.experience.sizes; 9 | this.scene = this.experience.scene; 10 | this.canvas = this.experience.canvas; 11 | this.params = { 12 | fov: 75, 13 | aspect: this.sizes.aspect, 14 | near: 0.01, 15 | far: 1000, 16 | }; 17 | 18 | this.setPerspectiveCamera(); 19 | // this.setOrbitControls(); 20 | } 21 | 22 | setPerspectiveCamera() { 23 | this.perspectiveCamera = new THREE.PerspectiveCamera( 24 | this.params.fov, 25 | this.params.aspect, 26 | this.params.near, 27 | this.params.far 28 | ); 29 | 30 | this.perspectiveCamera.position.set(12.64, 1.7, 64.0198); 31 | 32 | this.scene.add(this.perspectiveCamera); 33 | } 34 | 35 | setOrbitControls() { 36 | this.controls = new OrbitControls(this.perspectiveCamera, this.canvas); 37 | this.controls.enableDamping = true; 38 | } 39 | 40 | onResize() { 41 | this.perspectiveCamera.aspect = this.sizes.aspect; 42 | this.perspectiveCamera.updateProjectionMatrix(); 43 | } 44 | 45 | update() { 46 | // this.controls.update(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Experience/Experience.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | import Sizes from "./Utils/Sizes.js"; 4 | import Time from "./Utils/Time.js"; 5 | import Resources from "./Utils/Resources.js"; 6 | import assets from "./Utils/assets.js"; 7 | 8 | import Camera from "./Camera.js"; 9 | import Renderer from "./Renderer.js"; 10 | import LocalStorage from "./LocalStorage.js"; 11 | 12 | import World from "./World/World.js"; 13 | 14 | export default class Experience { 15 | static instance; 16 | 17 | constructor(canvas) { 18 | if (Experience.instance) { 19 | return Experience.instance; 20 | } 21 | 22 | Experience.instance = this; 23 | 24 | this.canvas = canvas; 25 | this.sizes = new Sizes(); 26 | this.time = new Time(); 27 | 28 | this.setScene(); 29 | this.setCamera(); 30 | this.setRenderer(); 31 | this.setLocalStorage(); 32 | this.setResources(); 33 | this.setWorld(); 34 | 35 | this.sizes.on("resize", () => { 36 | this.onResize(); 37 | }); 38 | 39 | this.update(); 40 | } 41 | 42 | setScene() { 43 | this.scene = new THREE.Scene(); 44 | } 45 | 46 | setCamera() { 47 | this.camera = new Camera(); 48 | } 49 | 50 | setRenderer() { 51 | this.renderer = new Renderer(); 52 | } 53 | 54 | setLocalStorage() { 55 | this.localStorage = new LocalStorage(); 56 | } 57 | 58 | setResources() { 59 | this.resources = new Resources(assets); 60 | } 61 | 62 | setWorld() { 63 | this.world = new World(); 64 | } 65 | 66 | onResize() { 67 | this.camera.onResize(); 68 | this.renderer.onResize(); 69 | } 70 | 71 | update() { 72 | if (this.camera) this.camera.update(); 73 | if (this.renderer) this.renderer.update(); 74 | if (this.world) this.world.update(); 75 | if (this.time) this.time.update(); 76 | 77 | window.requestAnimationFrame(() => { 78 | this.update(); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/Experience/LocalStorage.js: -------------------------------------------------------------------------------- 1 | import Experience from "./Experience.js"; 2 | 3 | export default class LocalStorage { 4 | constructor() { 5 | this.experience = new Experience(); 6 | this.camera = this.experience.camera; 7 | 8 | this.initPlayerState(); 9 | this.setStateObject(); 10 | } 11 | 12 | initPlayerState() { 13 | this.stringState = { 14 | playerPosition: "whiterun|0|0|0", 15 | playerRotation: "0|0|0", 16 | }; 17 | 18 | localStorage.clear(); 19 | 20 | if ( 21 | localStorage.getItem("playerPosition") && 22 | localStorage.getItem("playerRotation") 23 | ) { 24 | this.stringState.playerPosition = 25 | localStorage.getItem("playerPosition"); 26 | this.stringState.playerRotation = 27 | localStorage.getItem("playerRotation"); 28 | } else { 29 | localStorage.setItem( 30 | "playerPosition", 31 | this.stringState.playerPosition 32 | ); 33 | localStorage.setItem( 34 | "playerRotation", 35 | this.stringState.playerRotation 36 | ); 37 | } 38 | } 39 | 40 | updateLocalStorage() { 41 | localStorage.setItem( 42 | "playerPosition", 43 | `${this.state.location}|${this.camera.perspectiveCamera.position.x}|${this.camera.perspectiveCamera.position.y}|${this.camera.perspectiveCamera.position.z}` 44 | ); 45 | localStorage.setItem( 46 | "playerRotation", 47 | `${this.camera.perspectiveCamera.rotation._x}|${this.camera.perspectiveCamera.rotation._y}|${this.camera.perspectiveCamera.rotation._z}` 48 | ); 49 | 50 | this.stringState.playerPosition = 51 | localStorage.getItem("playerPosition"); 52 | this.stringState.playerRotation = 53 | localStorage.getItem("playerRotation"); 54 | } 55 | 56 | setLocation(location) { 57 | this.state.location = location; 58 | } 59 | 60 | setStateObject() { 61 | this.state = { 62 | location: this.stringState.playerPosition.split("|")[0], 63 | 64 | posX: Number(this.stringState.playerPosition.split("|")[1]), 65 | posY: Number(this.stringState.playerPosition.split("|")[2]), 66 | posZ: Number(this.stringState.playerPosition.split("|")[3]), 67 | 68 | rotX: Number(this.stringState.playerRotation.split("|")[1]), 69 | rotY: Number(this.stringState.playerRotation.split("|")[2]), 70 | rotZ: Number(this.stringState.playerRotation.split("|")[3]), 71 | }; 72 | } 73 | 74 | update() { 75 | this.updateLocalStorage(); 76 | this.setStateObject(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Experience/Renderer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "./Experience.js"; 3 | 4 | export default class Renderer { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.sizes = this.experience.sizes; 8 | this.scene = this.experience.scene; 9 | this.canvas = this.experience.canvas; 10 | this.camera = this.experience.camera; 11 | 12 | this.setRenderer(); 13 | } 14 | 15 | setRenderer() { 16 | this.renderer = new THREE.WebGLRenderer({ 17 | canvas: this.canvas, 18 | antialias: true, 19 | }); 20 | this.renderer.outputEncoding = THREE.sRGBEncoding; 21 | this.renderer.toneMapping = THREE.CineonToneMapping; 22 | this.renderer.toneMappingExposure = 1.75; 23 | this.renderer.setSize(this.sizes.width, this.sizes.height); 24 | this.renderer.setPixelRatio(this.sizes.pixelRatio); 25 | } 26 | 27 | onResize() { 28 | this.renderer.setSize(this.sizes.width, this.sizes.height); 29 | this.renderer.setPixelRatio(this.sizes.pixelRatio); 30 | } 31 | 32 | update() { 33 | this.renderer.render(this.scene, this.camera.perspectiveCamera); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Experience/Utils/Loaders.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; 4 | import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; 5 | 6 | export default class Loaders { 7 | constructor() { 8 | this.loaders = {}; 9 | 10 | this.setLoaders(); 11 | } 12 | 13 | setLoaders() { 14 | this.loaders.cubeTextureLoader = new THREE.CubeTextureLoader(); 15 | 16 | this.loaders.gltfLoader = new GLTFLoader(); 17 | this.loaders.dracoLoader = new DRACOLoader(); 18 | this.loaders.dracoLoader.setDecoderPath("/draco/"); 19 | this.loaders.gltfLoader.setDRACOLoader(this.loaders.dracoLoader); 20 | 21 | this.loaders.textureLoader = new THREE.TextureLoader(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Experience/Utils/Resources.js: -------------------------------------------------------------------------------- 1 | import Loaders from "./Loaders.js"; 2 | import { EventEmitter } from "events"; 3 | 4 | export default class Resources extends EventEmitter { 5 | constructor(assets) { 6 | super(); 7 | 8 | this.items = {}; 9 | this.assets = assets; 10 | this.location = null; 11 | 12 | this.loaders = new Loaders().loaders; 13 | } 14 | 15 | determineLoad(location) { 16 | this.location = location; 17 | 18 | if (!this.items.hasOwnProperty(this.location)) { 19 | this.items[this.location] = {}; 20 | this.startLoading(); 21 | } else { 22 | this.emitReady(); 23 | } 24 | } 25 | 26 | emitReady() { 27 | this.emit("ready"); 28 | } 29 | 30 | startLoading() { 31 | this.loaded = 0; 32 | this.queue = this.assets[0][this.location].assets.length; 33 | 34 | for (const asset of this.assets[0][this.location].assets) { 35 | if (asset.type === "glbModel") { 36 | this.loaders.gltfLoader.load(asset.path, (file) => { 37 | this.singleAssetLoaded(asset, file); 38 | }); 39 | } else if (asset.type === "imageTexture") { 40 | this.loaders.textureLoader.load(asset.path, (file) => { 41 | this.singleAssetLoaded(asset, file); 42 | }); 43 | } else if (asset.type === "cubeTexture") { 44 | this.loaders.cubeTextureLoader.load(asset.path, (file) => { 45 | this.singleAssetLoaded(asset, file); 46 | }); 47 | } 48 | } 49 | } 50 | 51 | singleAssetLoaded(asset, file) { 52 | this.items[this.location][asset.name] = file; 53 | this.loaded++; 54 | 55 | if (this.loaded === this.queue) { 56 | this.emitReady(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Experience/Utils/Sizes.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | export default class Sizes extends EventEmitter { 4 | constructor() { 5 | super(); 6 | this.handleSizes(); 7 | window.addEventListener("resize", () => { 8 | this.handleSizes(); 9 | this.emit("resize"); 10 | }); 11 | } 12 | 13 | handleSizes() { 14 | this.width = window.innerWidth; 15 | this.height = window.innerHeight; 16 | this.aspect = this.width / this.height; 17 | this.pixelRatio = Math.min(window.devicePixelRatio, 2); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Experience/Utils/Time.js: -------------------------------------------------------------------------------- 1 | export default class Time { 2 | constructor() { 3 | this.start = Date.now(); 4 | this.current = this.start; 5 | this.elapsed = 0; 6 | this.delta = 16; 7 | } 8 | 9 | update() { 10 | const currentTime = Date.now(); 11 | this.delta = (currentTime - this.current) / 1000; 12 | this.current = currentTime; 13 | this.elapsed = this.current - this.start; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Experience/Utils/assets.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | whiterun: { 4 | assets: [ 5 | { 6 | name: "land", 7 | type: "glbModel", 8 | path: "/models/land_w_collider.glb", 9 | }, 10 | { 11 | name: "items", 12 | type: "glbModel", 13 | path: "/models/land_items.glb", 14 | }, 15 | { 16 | name: "buildings", 17 | type: "glbModel", 18 | path: "/models/buildings.glb", 19 | }, 20 | { 21 | name: "interactions", 22 | type: "glbModel", 23 | path: "/models/outside_interactions.glb", 24 | }, 25 | { 26 | name: "walls", 27 | type: "glbModel", 28 | path: "/models/walls.glb", 29 | }, 30 | { 31 | name: "buildings_texture", 32 | type: "imageTexture", 33 | path: "/textures/buildings.webp", 34 | }, 35 | { 36 | name: "items_texture", 37 | type: "imageTexture", 38 | path: "/textures/items.webp", 39 | }, 40 | { 41 | name: "land_texture", 42 | type: "imageTexture", 43 | path: "/textures/land.webp", 44 | }, 45 | { 46 | name: "walls_texture", 47 | type: "imageTexture", 48 | path: "/textures/walls_baked.webp", 49 | }, 50 | { 51 | name: "skyBoxTexture", 52 | type: "cubeTexture", 53 | path: [ 54 | "/textures/skybox/px.webp", 55 | "/textures/skybox/nx.webp", 56 | "/textures/skybox/py.webp", 57 | "/textures/skybox/ny.webp", 58 | "/textures/skybox/pz.webp", 59 | "/textures/skybox/nz.webp", 60 | ], 61 | }, 62 | ], 63 | }, 64 | castleInterior: { 65 | assets: [ 66 | { 67 | name: "castle", 68 | type: "glbModel", 69 | path: "/models/interior_w_collider.glb", 70 | }, 71 | { 72 | name: "interactions", 73 | type: "glbModel", 74 | path: "/models/interior_interactions.glb", 75 | }, 76 | { 77 | name: "castle_texture", 78 | type: "imageTexture", 79 | path: "/textures/interior_baked.webp", 80 | }, 81 | ], 82 | }, 83 | }, 84 | ]; 85 | -------------------------------------------------------------------------------- /app/Experience/World/Interior/Components/Castle.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | export default class Castle { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.init(); 11 | this.setMaterials(); 12 | } 13 | 14 | init() { 15 | this.castle = this.resources.items.castleInterior.castle.scene; 16 | this.castle_texture = 17 | this.resources.items.castleInterior.castle_texture; 18 | } 19 | 20 | setMaterials() { 21 | this.castle_texture.flipY = false; 22 | this.castle_texture.encoding = THREE.sRGBEncoding; 23 | 24 | this.castle.children.find((child) => { 25 | child.material = new THREE.MeshBasicMaterial({ 26 | map: this.castle_texture, 27 | }); 28 | }); 29 | 30 | this.scene.add(this.castle); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Experience/World/Interior/Components/Interactions.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | export default class Interactions { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.init(); 11 | this.setMaterials(); 12 | } 13 | 14 | init() { 15 | this.interactions = 16 | this.resources.items.castleInterior.interactions.scene; 17 | // this.interactions_texture = 18 | // this.resources.items.whiterun.interactions_texture; 19 | } 20 | 21 | setMaterials() { 22 | // this.interactions_texture.flipY = false; 23 | // this.interactions_texture.encoding = THREE.sRGBEncoding; 24 | 25 | this.interactions.children.find((child) => { 26 | child.material = new THREE.MeshBasicMaterial({ 27 | // map: this.interactions_texture, 28 | color: 0xff0000, 29 | }); 30 | }); 31 | 32 | this.scene.add(this.interactions); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Experience/World/Interior/Interior.js: -------------------------------------------------------------------------------- 1 | import Castle from "./Components/Castle.js"; 2 | import Interactions from "./Components/Interactions.js"; 3 | 4 | export default class WhiteRun { 5 | constructor() { 6 | this.castle = new Castle(); 7 | this.interactions = new Interactions(); 8 | } 9 | 10 | resize() {} 11 | 12 | update() {} 13 | } 14 | -------------------------------------------------------------------------------- /app/Experience/World/Player/Player.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../Experience.js"; 3 | 4 | import { EventEmitter } from "events"; 5 | 6 | import { Capsule } from "three/examples/jsm/math/Capsule"; 7 | 8 | export default class Player extends EventEmitter { 9 | constructor() { 10 | super(); 11 | this.experience = new Experience(); 12 | this.time = this.experience.time; 13 | this.camera = this.experience.camera; 14 | this.octree = this.experience.world.octree; 15 | 16 | this.initPlayer(); 17 | this.initControls(); 18 | 19 | this.addEventListeners(); 20 | } 21 | 22 | initPlayer() { 23 | this.player = {}; 24 | this.player.body = this.camera.perspectiveCamera; 25 | 26 | this.player.onFloor = false; 27 | this.player.gravity = 60; 28 | 29 | this.player.spawn = { 30 | position: new THREE.Vector3(), 31 | rotation: new THREE.Euler(), 32 | velocity: new THREE.Vector3(), 33 | }; 34 | 35 | this.player.raycaster = new THREE.Raycaster(); 36 | this.player.raycaster.far = 5; 37 | 38 | this.player.height = 1.7; 39 | this.player.position = new THREE.Vector3(); 40 | this.player.rotation = new THREE.Euler(); 41 | this.player.rotation.order = "YXZ"; 42 | 43 | this.player.velocity = new THREE.Vector3(); 44 | this.player.direction = new THREE.Vector3(); 45 | 46 | this.player.speedMultiplier = 1.5; 47 | 48 | this.player.collider = new Capsule( 49 | new THREE.Vector3(), 50 | new THREE.Vector3(), 51 | 0.35 52 | ); 53 | } 54 | 55 | initControls() { 56 | this.actions = {}; 57 | } 58 | 59 | onDesktopPointerMove = (e) => { 60 | if (document.pointerLockElement !== document.body) return; 61 | this.player.body.rotation.order = this.player.rotation.order; 62 | 63 | this.player.body.rotation.x -= e.movementY / 500; 64 | this.player.body.rotation.y -= e.movementX / 500; 65 | 66 | this.player.body.rotation.x = THREE.MathUtils.clamp( 67 | this.player.body.rotation.x, 68 | -Math.PI / 2, 69 | Math.PI / 2 70 | ); 71 | }; 72 | 73 | onKeyDown = (e) => { 74 | if (document.pointerLockElement !== document.body) return; 75 | 76 | if (e.code === "KeyW") { 77 | this.actions.forward = true; 78 | } 79 | if (e.code === "KeyS") { 80 | this.actions.backward = true; 81 | } 82 | if (e.code === "KeyA") { 83 | this.actions.left = true; 84 | } 85 | if (e.code === "KeyD") { 86 | this.actions.right = true; 87 | } 88 | 89 | if (e.code === "ShiftLeft") { 90 | this.actions.run = true; 91 | } 92 | 93 | if (e.code === "Space") { 94 | this.actions.jump = true; 95 | } 96 | }; 97 | 98 | onKeyUp = (e) => { 99 | if (document.pointerLockElement !== document.body) return; 100 | 101 | if (e.code === "KeyW") { 102 | this.actions.forward = false; 103 | } 104 | if (e.code === "KeyS") { 105 | this.actions.backward = false; 106 | } 107 | if (e.code === "KeyA") { 108 | this.actions.left = false; 109 | } 110 | if (e.code === "KeyD") { 111 | this.actions.right = false; 112 | } 113 | 114 | if (e.code === "ShiftLeft") { 115 | this.actions.run = false; 116 | } 117 | 118 | if (e.code === "Space") { 119 | this.actions.jump = false; 120 | } 121 | }; 122 | 123 | onPointerDown = (e) => { 124 | if (e.pointerType === "mouse") { 125 | document.body.requestPointerLock(); 126 | return; 127 | } 128 | }; 129 | 130 | playerCollisions() { 131 | const result = this.octree.capsuleIntersect(this.player.collider); 132 | this.player.onFloor = false; 133 | 134 | if (result) { 135 | this.player.onFloor = result.normal.y > 0; 136 | 137 | this.player.collider.translate( 138 | result.normal.multiplyScalar(result.depth) 139 | ); 140 | } 141 | } 142 | 143 | getForwardVector() { 144 | this.camera.perspectiveCamera.getWorldDirection(this.player.direction); 145 | this.player.direction.y = 0; 146 | this.player.direction.normalize(); 147 | 148 | return this.player.direction; 149 | } 150 | 151 | getSideVector() { 152 | this.camera.perspectiveCamera.getWorldDirection(this.player.direction); 153 | this.player.direction.y = 0; 154 | this.player.direction.normalize(); 155 | this.player.direction.cross(this.camera.perspectiveCamera.up); 156 | 157 | return this.player.direction; 158 | } 159 | 160 | addEventListeners() { 161 | document.addEventListener("keydown", this.onKeyDown); 162 | document.addEventListener("keyup", this.onKeyUp); 163 | document.addEventListener("pointermove", this.onDesktopPointerMove); 164 | document.addEventListener("pointerdown", this.onPointerDown); 165 | } 166 | 167 | resize() {} 168 | 169 | spawnPlayerOutOfBounds() { 170 | const spawnPos = new THREE.Vector3(12.64, 1.7 + 10, 64.0198); 171 | this.player.velocity = this.player.spawn.velocity; 172 | this.player.body.position.copy(spawnPos); 173 | 174 | this.player.collider.start.copy(spawnPos); 175 | this.player.collider.end.copy(spawnPos); 176 | 177 | this.player.collider.end.y += this.player.height; 178 | } 179 | 180 | updateMovement() { 181 | const speed = 182 | (this.player.onFloor ? 1.75 : 0.2) * 183 | this.player.gravity * 184 | this.player.speedMultiplier; 185 | 186 | //The amount of distance we travel between each frame 187 | let speedDelta = this.time.delta * speed; 188 | 189 | if (this.actions.run) { 190 | speedDelta *= 1.6; 191 | } 192 | if (this.actions.forward) { 193 | this.player.velocity.add( 194 | this.getForwardVector().multiplyScalar(speedDelta) 195 | ); 196 | } 197 | if (this.actions.backward) { 198 | this.player.velocity.add( 199 | this.getForwardVector().multiplyScalar(-speedDelta * 0.5) 200 | ); 201 | } 202 | if (this.actions.left) { 203 | this.player.velocity.add( 204 | this.getSideVector().multiplyScalar(-speedDelta * 0.75) 205 | ); 206 | } 207 | if (this.actions.right) { 208 | this.player.velocity.add( 209 | this.getSideVector().multiplyScalar(speedDelta * 0.75) 210 | ); 211 | } 212 | 213 | if (this.player.onFloor) { 214 | if (this.actions.jump) { 215 | this.player.velocity.y = 30; 216 | } 217 | } 218 | 219 | let damping = Math.exp(-15 * this.time.delta) - 1; 220 | 221 | if (!this.player.onFloor) { 222 | this.player.velocity.y -= this.player.gravity * this.time.delta; 223 | damping *= 0.1; 224 | } 225 | 226 | this.player.velocity.addScaledVector(this.player.velocity, damping); 227 | 228 | const deltaPosition = this.player.velocity 229 | .clone() 230 | .multiplyScalar(this.time.delta); 231 | 232 | this.player.collider.translate(deltaPosition); 233 | this.playerCollisions(); 234 | 235 | this.player.body.position.copy(this.player.collider.end); 236 | this.player.body.updateMatrixWorld(); 237 | 238 | if (this.player.body.position.y < -20) { 239 | this.spawnPlayerOutOfBounds(); 240 | } 241 | } 242 | 243 | setInteractionObjects(interactionObjects) { 244 | this.player.interactionObjects = interactionObjects; 245 | } 246 | 247 | getgetCameraLookAtDirectionalVector() { 248 | const direction = new THREE.Vector3(0, 0, -1); 249 | return direction.applyQuaternion( 250 | this.camera.perspectiveCamera.quaternion 251 | ); 252 | } 253 | 254 | updateRaycaster() { 255 | this.player.raycaster.ray.origin.copy( 256 | this.camera.perspectiveCamera.position 257 | ); 258 | 259 | this.player.raycaster.ray.direction.copy( 260 | this.getgetCameraLookAtDirectionalVector() 261 | ); 262 | 263 | const intersects = this.player.raycaster.intersectObjects( 264 | this.player.interactionObjects.children 265 | ); 266 | 267 | if (intersects.length === 0) { 268 | this.currentIntersectObject = ""; 269 | } else { 270 | this.currentIntersectObject = intersects[0].object.name; 271 | } 272 | 273 | if (this.currentIntersectObject !== this.previousIntersectObject) { 274 | this.previousIntersectObject = this.currentIntersectObject; 275 | this.emit("updateMessage", this.previousIntersectObject); 276 | } 277 | } 278 | 279 | update() { 280 | this.updateMovement(); 281 | this.updateRaycaster(); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /app/Experience/World/Whiterun/Components/Buildings.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | export default class Buildings { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.init(); 11 | this.setMaterials(); 12 | } 13 | 14 | init() { 15 | this.buildings = this.resources.items.whiterun.buildings.scene; 16 | this.buildings_texture = 17 | this.resources.items.whiterun.buildings_texture; 18 | } 19 | 20 | setMaterials() { 21 | this.buildings_texture.flipY = false; 22 | this.buildings_texture.encoding = THREE.sRGBEncoding; 23 | 24 | this.buildings.children.find((child) => { 25 | child.material = new THREE.MeshBasicMaterial({ 26 | map: this.buildings_texture, 27 | }); 28 | }); 29 | 30 | this.scene.add(this.buildings); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Experience/World/Whiterun/Components/Environment.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | export default class Environment { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.init(); 11 | } 12 | 13 | init() { 14 | this.skyboxTexture = this.resources.items.whiterun.skyBoxTexture; 15 | this.skyboxTexture.encoding = THREE.sRGBEncoding; 16 | this.scene.background = this.skyboxTexture; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Experience/World/Whiterun/Components/Interactions.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | export default class Interactions { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.init(); 11 | this.setMaterials(); 12 | } 13 | 14 | init() { 15 | this.interactions = this.resources.items.whiterun.interactions.scene; 16 | // this.interactions_texture = 17 | // this.resources.items.whiterun.interactions_texture; 18 | } 19 | 20 | setMaterials() { 21 | // this.interactions_texture.flipY = false; 22 | // this.interactions_texture.encoding = THREE.sRGBEncoding; 23 | 24 | this.interactions.children.find((child) => { 25 | child.material = new THREE.MeshBasicMaterial({ 26 | // map: this.interactions_texture, 27 | color: 0xff0000, 28 | }); 29 | }); 30 | 31 | this.scene.add(this.interactions); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Experience/World/Whiterun/Components/Items.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | export default class Items { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.init(); 11 | this.setMaterials(); 12 | } 13 | 14 | init() { 15 | this.items = this.resources.items.whiterun.items.scene; 16 | this.items_texture = this.resources.items.whiterun.items_texture; 17 | } 18 | 19 | setMaterials() { 20 | this.items_texture.flipY = false; 21 | this.items_texture.encoding = THREE.sRGBEncoding; 22 | 23 | this.items.children.find((child) => { 24 | child.material = new THREE.MeshBasicMaterial({ 25 | map: this.items_texture, 26 | }); 27 | }); 28 | 29 | this.scene.add(this.items); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Experience/World/Whiterun/Components/Landscape.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | import { OctreeHelper } from "three/examples/jsm/helpers/OctreeHelper.js"; 5 | 6 | export default class Landscape { 7 | constructor() { 8 | this.experience = new Experience(); 9 | this.scene = this.experience.scene; 10 | this.resources = this.experience.resources; 11 | this.octree = this.experience.world.octree; 12 | 13 | this.init(); 14 | this.setMaterials(); 15 | this.setLandscapeCollider(); 16 | } 17 | 18 | init() { 19 | this.landscape = this.resources.items.whiterun.land.scene; 20 | this.land_texture = this.resources.items.whiterun.land_texture; 21 | } 22 | 23 | setMaterials() { 24 | this.land_texture.flipY = false; 25 | this.land_texture.encoding = THREE.sRGBEncoding; 26 | 27 | this.landscape.children.find((child) => { 28 | child.material = new THREE.MeshBasicMaterial({ 29 | map: this.land_texture, 30 | }); 31 | }); 32 | 33 | this.scene.add(this.landscape); 34 | } 35 | 36 | setLandscapeCollider() { 37 | const collider = this.landscape.getObjectByName("collider"); 38 | this.octree.fromGraphNode(collider); 39 | collider.removeFromParent(); 40 | collider.geometry.dispose(); 41 | collider.material.dispose(); 42 | 43 | // const helper = new OctreeHelper(this.octree); 44 | // helper.visible = true; 45 | // this.scene.add(helper); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Experience/World/Whiterun/Components/Walls.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../../Experience.js"; 3 | 4 | export default class Walls { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.init(); 11 | this.setMaterials(); 12 | } 13 | 14 | init() { 15 | this.walls = this.resources.items.whiterun.walls.scene; 16 | this.walls_texture = this.resources.items.whiterun.walls_texture; 17 | } 18 | 19 | setMaterials() { 20 | this.walls_texture.flipY = false; 21 | this.walls_texture.encoding = THREE.sRGBEncoding; 22 | 23 | this.walls.children.find((child) => { 24 | child.material = new THREE.MeshBasicMaterial({ 25 | map: this.walls_texture, 26 | }); 27 | }); 28 | 29 | this.scene.add(this.walls); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Experience/World/Whiterun/Whiterun.js: -------------------------------------------------------------------------------- 1 | import Landscape from "./Components/Landscape.js"; 2 | import Items from "./Components/Items.js"; 3 | import Interactions from "./Components/Interactions.js"; 4 | import Buildings from "./Components/Buildings.js"; 5 | import Walls from "./Components/Walls.js"; 6 | import Environment from "./Components/Environment.js"; 7 | 8 | export default class WhiteRun { 9 | constructor() { 10 | this.items = new Items(); 11 | this.landscape = new Landscape(); 12 | this.interactions = new Interactions(); 13 | this.buildings = new Buildings(); 14 | this.walls = new Walls(); 15 | this.environment = new Environment(); 16 | } 17 | 18 | resize() {} 19 | 20 | update() {} 21 | } 22 | -------------------------------------------------------------------------------- /app/Experience/World/World.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { EventEmitter } from "events"; 3 | import Experience from "../Experience.js"; 4 | 5 | import { Octree } from "three/examples/jsm/math/Octree"; 6 | 7 | import Player from "./Player/Player.js"; 8 | 9 | import Whiterun from "./Whiterun/Whiterun.js"; 10 | import Interior from "./Interior/Interior.js"; 11 | 12 | export default class World extends EventEmitter { 13 | constructor() { 14 | super(); 15 | this.experience = new Experience(); 16 | this.resources = this.experience.resources; 17 | 18 | this.octree = new Octree(); 19 | 20 | this.localStorage = this.experience.localStorage; 21 | this.state = this.localStorage.state; 22 | 23 | this.resources.determineLoad(this.state.location); 24 | 25 | this.player = null; 26 | 27 | this.resources.on("ready", () => { 28 | if (this.player === null) { 29 | this.player = new Player(); 30 | this.setPlayerEvents(); 31 | } 32 | this.setWorld(); 33 | }); 34 | } 35 | 36 | setWorld() { 37 | this.whiterun = new Whiterun(); 38 | this.player.setInteractionObjects( 39 | this.whiterun.interactions.interactions 40 | ); 41 | 42 | // this.interior = new Interior(); 43 | } 44 | 45 | setPlayerEvents() { 46 | // HERE------------------------------ 47 | this.player.on("updateMessage", (objectName) => { 48 | this.emit("updateMessage", objectName); 49 | }); 50 | 51 | this.player.on("interact", (objectName) => { 52 | this.emit("interact", objectName); 53 | }); 54 | } 55 | 56 | update() { 57 | if (this.player) this.player.update(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Interface/Components/Message.js: -------------------------------------------------------------------------------- 1 | export default class Message { 2 | constructor(elements, key) { 3 | this.elements = elements; 4 | this.key = key; 5 | console.log(this.elements); 6 | } 7 | 8 | setCrosshair(message) { 9 | if (message === "" || message === undefined || message === null) { 10 | this.elements.crosshair.classList.add("hidden"); 11 | } else { 12 | this.elements.crosshair.classList.remove("hidden"); 13 | } 14 | } 15 | 16 | setMessage(message) { 17 | const newMsg = message.toLowerCase(); 18 | 19 | if (newMsg.includes("door")) { 20 | this.elements.message.innerHTML = `Go through door`; 21 | } else if (newMsg.includes("sign")) { 22 | this.elements.message.innerHTML = `Read`; 23 | } else if (newMsg.includes("teleporter")) { 24 | this.elements.message.innerHTML = `Travel`; 25 | } else { 26 | this.elements.message.innerHTML = ""; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Interface/Interface.js: -------------------------------------------------------------------------------- 1 | import Message from "./Components/Message.js"; 2 | import Elements from "./Utils/Elements.js"; 3 | import key from "./Utils/key.js"; 4 | 5 | export default class Interface { 6 | constructor() { 7 | this.key = key; 8 | this.elements = new Elements().elements; 9 | this.message = new Message(this.elements, key); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/Interface/Utils/Elements.js: -------------------------------------------------------------------------------- 1 | export default class Elements { 2 | constructor() { 3 | this.domSelectors = { 4 | crosshair: ".crosshair", 5 | message: ".message", 6 | }; 7 | 8 | this.init(); 9 | } 10 | 11 | init() { 12 | this.elements = {}; 13 | 14 | Object.entries(this.domSelectors).forEach((selector) => { 15 | const key = selector[0]; 16 | const data = selector[1]; 17 | this.elements[key] = document.querySelector(data); 18 | }); 19 | } 20 | 21 | getElements() { 22 | return this.elements; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Interface/Utils/key.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: { 3 | teleporter: "teleporter", 4 | sign: "sign", 5 | }, 6 | 7 | locations: { 8 | whiterun: "whiterun", 9 | inside: "inside", 10 | }, 11 | 12 | messageContent: { 13 | door: "What's up doctor", 14 | sign: "read me bro", 15 | whiterunSign: ` 16 |