├── .gitignore ├── shader_minifier.exe ├── externs.js ├── src ├── core │ ├── state.ts │ ├── state-machine.ts │ ├── controls.ts │ └── first-person-player.ts ├── vite-game.d.ts ├── engine │ ├── shaders │ │ ├── depth.fragment.glsl │ │ ├── skybox.vertex.glsl │ │ ├── depth.vertex.glsl │ │ ├── skybox.fragment.glsl │ │ ├── vertex.glsl │ │ ├── fragment.glsl │ │ └── shaders.ts │ ├── renderer │ │ ├── texture.ts │ │ ├── mesh.ts │ │ ├── material.ts │ │ ├── camera.ts │ │ ├── texture-loader.ts │ │ ├── scene.ts │ │ ├── lil-gl.ts │ │ ├── object-3d.ts │ │ └── renderer.ts │ ├── plane-geometry.ts │ ├── physics │ │ ├── face.ts │ │ ├── parse-faces.ts │ │ └── surface-collision.ts │ ├── skybox.ts │ ├── new-new-noise.ts │ ├── svg-maker │ │ ├── converters.ts │ │ └── base.ts │ ├── helpers.ts │ ├── audio │ │ └── audio-player.ts │ ├── enhanced-dom-point.ts │ └── moldable-cube-geometry.ts ├── game-states │ ├── game-states.ts │ ├── menu.state.ts │ └── game.state.ts ├── game-state-machine.ts ├── draw-helpers.ts ├── index.ts ├── sound-effects.ts ├── modeling │ ├── building-blocks.ts │ ├── lever-door.ts │ ├── items.ts │ └── castle.ts └── textures.ts ├── index.html ├── README.md ├── style.css ├── package.json ├── .eslintrc.js ├── find-best-roadroller.js ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /dist/ 3 | /node_modules/ -------------------------------------------------------------------------------- /shader_minifier.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roblouie/upyri/HEAD/shader_minifier.exe -------------------------------------------------------------------------------- /externs.js: -------------------------------------------------------------------------------- 1 | let localStorage; 2 | let c3d; 3 | let Start; 4 | let tmpl; 5 | let Fullscreen; 6 | -------------------------------------------------------------------------------- /src/core/state.ts: -------------------------------------------------------------------------------- 1 | export interface State { 2 | onUpdate: (timeElapsed: number) => void; 3 | onEnter?: Function; 4 | onLeave?: Function; 5 | } -------------------------------------------------------------------------------- /src/vite-game.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | declare module '*.png'; 3 | 4 | // HTML Element Ids 5 | declare const tmpl; 6 | declare const c3d; 7 | declare const Fullscreen; 8 | declare const Start; 9 | -------------------------------------------------------------------------------- /src/engine/shaders/depth.fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | //[ 4 | precision highp float; 5 | //] 6 | 7 | out float fragDepth; 8 | 9 | void main(){ 10 | fragDepth = gl_FragCoord.z; 11 | } 12 | -------------------------------------------------------------------------------- /src/game-states/game-states.ts: -------------------------------------------------------------------------------- 1 | import { State } from '@/engine/state-machine/state'; 2 | 3 | export const gameStates = { 4 | gameState: {} as State, 5 | menuState: {} as State, 6 | levelOverState: {} as State, 7 | }; 8 | -------------------------------------------------------------------------------- /src/engine/shaders/skybox.vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | layout(location = 0) in vec4 aCoords; 4 | out vec4 v_position; 5 | 6 | void main() { 7 | v_position = aCoords; 8 | gl_Position = aCoords; 9 | gl_Position.z = 1.0; 10 | } 11 | -------------------------------------------------------------------------------- /src/engine/shaders/depth.vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | //[ 4 | precision highp float; 5 | //] 6 | 7 | layout(location=0) in vec4 aPosition; 8 | 9 | uniform mat4 lightPovMvp; 10 | 11 | void main(){ 12 | gl_Position = lightPovMvp * aPosition; 13 | } 14 | -------------------------------------------------------------------------------- /src/game-state-machine.ts: -------------------------------------------------------------------------------- 1 | import { StateMachine } from './core/state-machine'; 2 | import { State } from './core/state'; 3 | 4 | export let gameStateMachine: StateMachine; 5 | 6 | export function createGameStateMachine(initialState: State, ...initialArguments: any[]) { 7 | gameStateMachine = new StateMachine(initialState, ...initialArguments); 8 | } 9 | -------------------------------------------------------------------------------- /src/engine/renderer/texture.ts: -------------------------------------------------------------------------------- 1 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point"; 2 | 3 | export class Texture { 4 | id: number; 5 | source: TexImageSource; 6 | textureRepeat = new EnhancedDOMPoint(1, 1); 7 | 8 | constructor(id: number, source: TexImageSource) { 9 | this.source = source; 10 | this.id = id; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | UPYRI 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/engine/shaders/skybox.fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | //[ 3 | precision highp float; 4 | //] 5 | 6 | uniform samplerCube u_skybox; 7 | uniform mat4 u_viewDirectionProjectionInverse; 8 | 9 | in vec4 v_position; 10 | 11 | out vec4 outColor; 12 | 13 | void main() { 14 | vec4 t = u_viewDirectionProjectionInverse * v_position; 15 | outColor = texture(u_skybox, t.xyz); 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UPYRI 2 | 3 | My entry for the 2023 JS13K competition. The game placed 5th overall. 4 | 5 | ## Trailer 6 | [![IMAGE ALT TEXT](http://img.youtube.com/vi/sATZin4rFwQ/0.jpg)](http://www.youtube.com/watch?v=sATZin4rFwQ "UPYRI") 7 | 8 | ## Making Of / Post Mortem 9 | https://roblouie.com/article/1154/the-making-of-upyri-js13k-2023-post-mortem/ 10 | 11 | ## Quick Start 12 | 13 | Install dependencies: `npm install` 14 | 15 | Run Server: `npm run serve` 16 | -------------------------------------------------------------------------------- /src/engine/renderer/mesh.ts: -------------------------------------------------------------------------------- 1 | import { Object3d } from './object-3d'; 2 | import { Material } from './material'; 3 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 4 | 5 | export class Mesh extends Object3d { 6 | geometry: MoldableCubeGeometry; 7 | material: Material; 8 | 9 | constructor(geometry: MoldableCubeGeometry, material: Material) { 10 | super(); 11 | this.geometry = geometry; 12 | this.material = material; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/engine/renderer/material.ts: -------------------------------------------------------------------------------- 1 | import { Texture } from '@/engine/renderer/texture'; 2 | 3 | export class Material { 4 | emissive = [0.0, 0.0, 0.0, 0.0]; 5 | texture?: Texture; 6 | isTransparent = false; 7 | 8 | constructor(props?: { texture?: Texture, emissive?: [number, number, number, number], isTransparent?: boolean }) { 9 | this.texture = props?.texture; 10 | this.emissive = props?.emissive ? props.emissive : this.emissive; 11 | this.isTransparent = props?.isTransparent ?? this.isTransparent; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | canvas, svg, #tmpl { 2 | width: min(100vw, calc(100vh * 16/9)); 3 | position: absolute; 4 | aspect-ratio: 16/9; 5 | } 6 | 7 | html, body { 8 | margin: 0; 9 | background-color: black; 10 | display: grid; 11 | place-items: center; 12 | height: 100%; 13 | } 14 | 15 | svg { 16 | font-family: sans-serif; 17 | font-size: 80px; 18 | font-weight: bold; 19 | } 20 | 21 | #Fullscreen,#Start { 22 | fill: #666; 23 | cursor: pointer; 24 | } 25 | 26 | #Fullscreen:hover,#Start:hover { 27 | fill: red; 28 | } 29 | -------------------------------------------------------------------------------- /src/engine/plane-geometry.ts: -------------------------------------------------------------------------------- 1 | import { MoldableCubeGeometry } from "@/engine/moldable-cube-geometry"; 2 | 3 | export class PlaneGeometry extends MoldableCubeGeometry { 4 | 5 | constructor(width_ = 1, depth = 1, subdivisionsWidth = 1, subdivisionsDepth = 1, heightmap?: number[]) { 6 | super(width_, 1, depth, subdivisionsWidth, 0, subdivisionsDepth, 1); 7 | 8 | if (heightmap) { 9 | this 10 | .modifyEachVertex((vertex, index) => { 11 | vertex.y = heightmap[index]; 12 | }) 13 | .computeNormals() 14 | .done_(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/engine/renderer/camera.ts: -------------------------------------------------------------------------------- 1 | import { Object3d } from './object-3d'; 2 | 3 | export class Camera extends Object3d { 4 | projection: DOMMatrix; 5 | 6 | constructor(fieldOfViewRadians: number, aspect: number, near: number, far: number) { 7 | super(); 8 | 9 | const f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewRadians); 10 | const rangeInv = 1.0 / (near - far); 11 | 12 | this.projection = new DOMMatrix([ 13 | f / aspect, 0, 0, 0, 14 | 0, f, 0, 0, 15 | 0, 0, (near + far) * rangeInv, -1, 16 | 0, 0, near * far * rangeInv * 2, 0 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/state-machine.ts: -------------------------------------------------------------------------------- 1 | import { State } from './state'; 2 | 3 | export class StateMachine { 4 | private currentState: State; 5 | 6 | constructor(initialState: State, ...enterArgs: any) { 7 | this.currentState = initialState; 8 | this.currentState.onEnter?.(...enterArgs); 9 | } 10 | 11 | async setState(newState: State, ...enterArgs: any) { 12 | await this.currentState.onLeave?.(); 13 | this.currentState = { onUpdate: () => {} }; 14 | await newState.onEnter?.(...enterArgs); 15 | this.currentState = newState; 16 | } 17 | 18 | getState() { 19 | return this.currentState; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/engine/physics/face.ts: -------------------------------------------------------------------------------- 1 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point"; 2 | import { calculateFaceNormal } from '@/engine/helpers'; 3 | 4 | export class Face { 5 | points: EnhancedDOMPoint[]; 6 | normal: EnhancedDOMPoint; 7 | upperY: number; 8 | lowerY: number; 9 | originOffset: number; 10 | 11 | constructor(points: EnhancedDOMPoint[], normal?: EnhancedDOMPoint) { 12 | this.points = points; 13 | this.normal = normal ?? calculateFaceNormal(points); 14 | this.originOffset = -this.normal.dot(points[0]); 15 | const ys = points.map(point => point.y); 16 | this.upperY = Math.max(...ys); 17 | this.lowerY = Math.min(...ys); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/engine/shaders/vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | layout(location = 0) in vec3 aCoords; 4 | layout(location = 1) in vec3 aNormal; 5 | layout(location = 2) in vec2 aTexCoord; 6 | layout(location = 3) in float aDepth; 7 | 8 | uniform mat4 modelviewProjection; 9 | uniform mat4 normalMatrix; 10 | uniform mat4 lightPovMvp; 11 | 12 | out vec2 vTexCoord; 13 | out float vDepth; 14 | out vec3 vNormal; 15 | out mat4 vNormalMatrix; 16 | out vec4 positionFromLightPov; 17 | 18 | void main() { 19 | vec4 coords = vec4(aCoords, 1.0); 20 | gl_Position = modelviewProjection * coords; 21 | 22 | vTexCoord = aTexCoord; 23 | vDepth = aDepth; 24 | vNormal = aNormal; 25 | vNormalMatrix = normalMatrix; 26 | positionFromLightPov = lightPovMvp * coords; 27 | } 28 | -------------------------------------------------------------------------------- /src/draw-helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LengthOrPercentage, 3 | rect, 4 | svg, 5 | SvgAttributes, 6 | SvgString, 7 | SvgTextAttributes, 8 | } from '@/engine/svg-maker/base'; 9 | import { drawBloodText } from '@/textures'; 10 | 11 | export function overlaySvg(additionalAttributes?: Partial, ...elements: string[]): SvgString { 12 | return svg({...additionalAttributes, viewBox: `0 0 1920 1080` }, ...elements); 13 | } 14 | 15 | export function drawFullScreenText(text: string, fontSize = 250) { 16 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 17 | rect({x: 0, y: 0, width_: '100%', height_: '100%' }), 18 | drawBloodText({ x: '50%', y: '52%', style: `font-size: ${fontSize}px; text-shadow: 1px 1px 20px` }, text, 40), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/engine/renderer/texture-loader.ts: -------------------------------------------------------------------------------- 1 | import { gl } from '@/engine/renderer/lil-gl'; 2 | import { Texture } from '@/engine/renderer/texture'; 3 | 4 | class TextureLoader { 5 | textures: Texture[] = []; 6 | 7 | load_(textureSource: TexImageSource): Texture { 8 | const texture = new Texture(this.textures.length, textureSource); 9 | this.textures.push(texture); 10 | return texture; 11 | } 12 | 13 | bindTextures() { 14 | gl.activeTexture(gl.TEXTURE0); 15 | gl.bindTexture(gl.TEXTURE_2D_ARRAY, gl.createTexture()); 16 | gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 8, gl.RGBA8, 512, 512, this.textures.length); 17 | 18 | this.textures.forEach((texture, index) => { 19 | gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, index, 512, 512, 1, gl.RGBA, gl.UNSIGNED_BYTE, texture.source); 20 | }); 21 | gl.generateMipmap(gl.TEXTURE_2D_ARRAY); 22 | } 23 | } 24 | 25 | export const textureLoader = new TextureLoader(); 26 | -------------------------------------------------------------------------------- /src/engine/skybox.ts: -------------------------------------------------------------------------------- 1 | import { AttributeLocation } from '@/engine/renderer/renderer'; 2 | import { gl } from '@/engine/renderer/lil-gl'; 3 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 4 | 5 | export class Skybox extends MoldableCubeGeometry { 6 | constructor(...textureSources: TexImageSource[]) { 7 | super(); 8 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); 9 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, gl.createTexture()); 10 | textureSources.forEach((tex, index) => { 11 | gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + index, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, tex); 12 | }); 13 | gl.generateMipmap(gl.TEXTURE_CUBE_MAP); 14 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 15 | this.setAttribute_(AttributeLocation.Positions, new Float32Array([ 16 | -1, -1, 17 | 1, -1, 18 | -1, 1, 19 | -1, 1, 20 | 1, -1, 21 | 1, 1, 22 | ]), 2); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/engine/new-new-noise.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ellipse, 3 | feTurbulence, 4 | filter, 5 | NoiseType, radialGradient, 6 | rect, 7 | svg, svgStop, 8 | } from '@/engine/svg-maker/base'; 9 | import { toHeightmap } from '@/engine/svg-maker/converters'; 10 | 11 | 12 | export async function newNoiseLandscape(size: number,seed_: number, baseFrequency: number, numOctaves_: number, type_: NoiseType, scale_: number) { 13 | const s = svg({ width_: 256, height_: 256 }, 14 | filter({ id_: 'n' }, 15 | feTurbulence({ seed_, baseFrequency, numOctaves_, type_ }), 16 | ), 17 | radialGradient({ id_: 'l' }, 18 | svgStop({ offset_: '10%', stopColor: '#0004' }), 19 | svgStop({ offset_: '22%', stopColor: '#0000' }), 20 | ), 21 | rect({ x: 0, y: 0, width_: '100%', height_: '100%', filter: 'n' }), 22 | ellipse({ cx: 128, cy: 128, fill: 'url(#l)', rx: 200, ry: 200 }), 23 | // 24 | ellipse({ cx: 128, cy: 128, fill: '#afafaf', rx: 26, ry: 26 }), 25 | 26 | // rect({ x: 109, y: 109, width_: 38, height_: 42, fill: '#afafaf' }), 27 | rect({ x: 125, y: 10, width_: 6, height_: 80, fill: '#afafaf' }) 28 | ); 29 | return toHeightmap(s, scale_); 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-template", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "devDependencies": { 6 | "@rollup/plugin-typescript": "^8.5.0", 7 | "@typescript-eslint/eslint-plugin": "^5.52.0", 8 | "@typescript-eslint/parser": "^5.52.0", 9 | "clean-css": "^5.3.1", 10 | "copy-webpack-plugin": "^9.0.1", 11 | "cross-env": "^7.0.3", 12 | "ect-bin": "1.4.1", 13 | "eslint": "^8.34.0", 14 | "eslint-plugin-unused-imports": "^2.0.0", 15 | "google-closure-compiler": "^20230206.0.0", 16 | "html-minifier": "^4.0.0", 17 | "html-webpack-plugin": "^5.3.2", 18 | "npm-run-all": "^4.1.5", 19 | "roadroller": "^2.1.0", 20 | "tmp": "^0.2.1", 21 | "ts-loader": "^9.2.5", 22 | "typescript": "^4.9.5", 23 | "vite": "^4.1.3", 24 | "webpack": "^5.50.0", 25 | "webpack-cli": "^4.8.0", 26 | "webpack-dev-server": "^4.0.0" 27 | }, 28 | "scripts": { 29 | "serve": "npm-run-all --parallel tsc vite", 30 | "build": "cross-env LEVEL_2_BUILD=true vite build", 31 | "find-best-roadroller": "node find-best-roadroller.js", 32 | "build-with-best-roadroller": "cross-env USE_RR_CONFIG=true vite build", 33 | "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore src", 34 | "vite": "vite serve", 35 | "tsc": "tsc --watch --noEmit" 36 | }, 37 | "author": "Rob Louie" 38 | } 39 | -------------------------------------------------------------------------------- /src/engine/renderer/scene.ts: -------------------------------------------------------------------------------- 1 | import { Object3d } from '@/engine/renderer/object-3d'; 2 | import { Skybox } from '@/engine/skybox'; 3 | import { Mesh } from '@/engine/renderer/mesh'; 4 | 5 | export class Scene extends Object3d { 6 | skybox?: Skybox; 7 | solidMeshes: Mesh[] = []; 8 | transparentMeshes: Mesh[] = []; 9 | 10 | add_(...object3ds: Object3d[]) { 11 | super.add_(...object3ds); 12 | const flatWithChildren = [...object3ds, ...object3ds.flatMap(object3d => object3d.allChildren())]; 13 | flatWithChildren.forEach(object3d => { 14 | // @ts-ignore 15 | if (object3d.geometry) { 16 | // @ts-ignore 17 | object3d.geometry.bindGeometry(); 18 | // @ts-ignore 19 | object3d.material.isTransparent ? this.transparentMeshes.push(object3d) : this.solidMeshes.push(object3d); 20 | } 21 | }) 22 | } 23 | 24 | remove_(object3d: Object3d) { 25 | super.remove_(object3d); 26 | [object3d, ...object3d.allChildren()] 27 | .forEach(obj => { 28 | // @ts-ignore 29 | if (obj.geometry) { 30 | // @ts-ignore 31 | if (obj.material.isTransparent) { 32 | this.transparentMeshes = this.transparentMeshes.filter(mesh => mesh !== obj); 33 | } else { 34 | this.solidMeshes = this.solidMeshes.filter(mesh => mesh !== obj); 35 | } 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createGameStateMachine, gameStateMachine } from './game-state-machine'; 2 | import { controls } from '@/core/controls'; 3 | import { initTextures } from '@/textures'; 4 | import { GameState } from '@/game-states/game.state'; 5 | import { gameStates } from '@/game-states/game-states'; 6 | import { MenuState } from '@/game-states/menu.state'; 7 | import { drawFullScreenText } from '@/draw-helpers'; 8 | import { castleContainer, createCastle } from '@/modeling/castle'; 9 | 10 | let previousTime = 0; 11 | const interval = 1000 / 60; 12 | 13 | (() => { 14 | drawFullScreenText('CLICK TO START', 200); 15 | document.onclick = async () => { 16 | drawFullScreenText('LOADING'); 17 | 18 | await initTextures(); 19 | castleContainer.value = createCastle().translate_(0, 21).done_(); 20 | 21 | gameStates.gameState = new GameState(); 22 | gameStates.menuState = new MenuState(); 23 | 24 | createGameStateMachine(gameStates.menuState); 25 | 26 | draw(0); 27 | 28 | document.onclick = null; 29 | }; 30 | 31 | function draw(currentTime: number) { 32 | const delta = currentTime - previousTime; 33 | 34 | if (delta >= interval) { 35 | previousTime = currentTime - (delta % interval); 36 | 37 | controls.queryController(); 38 | gameStateMachine.getState().onUpdate(delta); 39 | } 40 | requestAnimationFrame(draw); 41 | } 42 | })(); 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/engine/svg-maker/converters.ts: -------------------------------------------------------------------------------- 1 | import { SvgString } from '@/engine/svg-maker/base'; 2 | 3 | export function toImageDom(svgString: SvgString) { 4 | const image_ = document.createElement('img'); 5 | image_.src = `type:image/svg+xml,${btoa(svgString)}`; 6 | return image_; 7 | } 8 | 9 | export function toObjectUrl(svgString: SvgString) { 10 | return URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml' })); 11 | } 12 | 13 | export async function toImage(svgImageBuilder: SvgString): Promise { 14 | const image_ = new Image(); 15 | image_.src = toObjectUrl(svgImageBuilder); 16 | return new Promise(resolve => image_.addEventListener('load', () => resolve(image_))); 17 | } 18 | 19 | export async function toImageData(svgString: SvgString): Promise { 20 | const image_ = await toImage(svgString); 21 | const canvas = new OffscreenCanvas(image_.width, image_.height); 22 | const context = canvas.getContext('2d')!; 23 | // @ts-ignore 24 | context.drawImage(image_, 0, 0); 25 | // @ts-ignore 26 | return context.getImageData(0, 0, image_.width, image_.height); 27 | } 28 | 29 | export async function toHeightmap(svgString: SvgString, scale_: number): Promise { 30 | const imageData = await toImageData(svgString); 31 | return [...imageData.data] 32 | .filter((value, index) => !(index % 4)) 33 | .map(value => { 34 | return (value / 255 - 0.5) * scale_; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/engine/helpers.ts: -------------------------------------------------------------------------------- 1 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 2 | 3 | 4 | export function doTimes(times: number, callback: (index: number) => T): T[] { 5 | const result: T[] = []; 6 | for (let i = 0; i < times; i++) { 7 | result.push(callback(i)); 8 | } 9 | return result; 10 | } 11 | 12 | export function clamp(value: number, min: number, max: number): number { 13 | return Math.min(Math.max(value, min), max); 14 | } 15 | 16 | 17 | export function radsToDegrees(radians: number): number { 18 | return radians * (180 / Math.PI); 19 | } 20 | 21 | function unormalizedNormal(points: EnhancedDOMPoint[]): EnhancedDOMPoint { 22 | const u = points[2].clone_().subtract(points[1]); 23 | const v = points[0].clone_().subtract(points[1]); 24 | return new EnhancedDOMPoint().crossVectors(u, v); 25 | } 26 | 27 | export function calculateFaceNormal(points: EnhancedDOMPoint[]): EnhancedDOMPoint { 28 | return unormalizedNormal(points).normalize_(); 29 | } 30 | 31 | export function calculateVertexNormals(points: EnhancedDOMPoint[], indices: number[] | Uint16Array): EnhancedDOMPoint[] { 32 | const vertexNormals = points.map(_ => new EnhancedDOMPoint()); 33 | for (let i = 0; i < indices.length; i+= 3) { 34 | const faceNormal = unormalizedNormal([points[indices[i]], points[indices[i + 1]], points[indices[i + 2]]]); 35 | vertexNormals[indices[i]].add_(faceNormal); 36 | vertexNormals[indices[i + 1]].add_(faceNormal); 37 | vertexNormals[indices[i + 2]].add_(faceNormal); 38 | } 39 | 40 | return vertexNormals.map(vector => vector.normalize_()); 41 | } 42 | -------------------------------------------------------------------------------- /src/engine/shaders/fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | //[ 4 | precision highp float; 5 | //] 6 | in vec4 vColor; 7 | in vec2 vTexCoord; 8 | in float vDepth; 9 | in vec3 vNormal; 10 | in mat4 vNormalMatrix; 11 | in vec4 positionFromLightPov; 12 | 13 | uniform vec2 textureRepeat; 14 | uniform vec4 emissive; 15 | uniform mediump sampler2DArray uSampler; 16 | uniform mediump sampler2DShadow shadowMap; 17 | 18 | out vec4 outColor; 19 | 20 | vec3 light_direction = vec3(-1, 1.5, -1); 21 | 22 | float ambientLight = 0.2f; 23 | float maxLit = 0.6f; 24 | 25 | vec2 adjacentPixels[5] = vec2[]( 26 | vec2(0, 0), 27 | vec2(-1, 0), 28 | vec2(1, 0), 29 | vec2(0, 1), 30 | vec2(0, -1) 31 | ); 32 | 33 | float visibility = 1.0; 34 | float shadowSpread = 4200.0; 35 | 36 | void main() { 37 | for (int i = 0; i < 5; i++) { 38 | vec3 samplePosition = vec3(positionFromLightPov.xy + adjacentPixels[i]/shadowSpread, positionFromLightPov.z - 0.001); 39 | float hitByLight = texture(shadowMap, samplePosition); 40 | visibility *= max(hitByLight, 0.87); 41 | } 42 | 43 | vec3 correctedNormals = normalize(mat3(vNormalMatrix) * vNormal); 44 | vec3 normalizedLightPosition = normalize(light_direction); 45 | float litPercent = max(dot(normalizedLightPosition, correctedNormals) * visibility, ambientLight); 46 | 47 | 48 | vec3 litColor = length(emissive) > 0.0 ? emissive.rgb : (litPercent * vec3(1.0, 1.0, 1.0)); 49 | 50 | vec4 vColor = vec4(litColor.rgb, 1.0); 51 | 52 | if (vDepth < 0.0) { 53 | outColor = vColor; 54 | } else { 55 | outColor = texture(uSampler, vec3(vTexCoord * textureRepeat, vDepth)) * vColor; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/core/controls.ts: -------------------------------------------------------------------------------- 1 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 2 | 3 | class Controls { 4 | isConfirm? = false; 5 | inputDirection: EnhancedDOMPoint; 6 | private mouseMovement = new EnhancedDOMPoint(); 7 | private onMouseMoveCallback?: (mouseMovement: EnhancedDOMPoint) => void; 8 | 9 | keyMap: Map = new Map(); 10 | 11 | constructor() { 12 | document.addEventListener('keydown', event => this.toggleKey(event, true)); 13 | document.addEventListener('keyup', event => this.toggleKey(event, false)); 14 | document.addEventListener('mousedown', () => this.toggleKey({ code: 'KeyE' }, true)); 15 | document.addEventListener('mouseup', () => this.toggleKey({ code: 'KeyE' }, false)); 16 | 17 | document.addEventListener('mousemove', event => { 18 | this.mouseMovement.x = event.movementX; 19 | this.mouseMovement.y = event.movementY; 20 | this.onMouseMoveCallback?.(this.mouseMovement); 21 | }); 22 | this.inputDirection = new EnhancedDOMPoint(); 23 | } 24 | 25 | onMouseMove(callback: (mouseMovement: EnhancedDOMPoint) => void) { 26 | this.onMouseMoveCallback = callback; 27 | } 28 | 29 | queryController() { 30 | const leftVal = (this.keyMap.get('KeyA') || this.keyMap.get('ArrowLeft')) ? -1 : 0; 31 | const rightVal = (this.keyMap.get('KeyD') || this.keyMap.get('ArrowRight')) ? 1 : 0; 32 | const upVal = (this.keyMap.get('KeyW') || this.keyMap.get('ArrowUp')) ? -1 : 0; 33 | const downVal = (this.keyMap.get('KeyS') || this.keyMap.get('ArrowDown')) ? 1 : 0; 34 | this.inputDirection.x = (leftVal + rightVal); 35 | this.inputDirection.y = (upVal + downVal); 36 | this.isConfirm = this.keyMap.get('KeyE'); 37 | } 38 | 39 | private toggleKey(event: { code: string }, isPressed: boolean) { 40 | this.keyMap.set(event.code, isPressed); 41 | } 42 | } 43 | 44 | export const controls = new Controls(); 45 | -------------------------------------------------------------------------------- /src/engine/physics/parse-faces.ts: -------------------------------------------------------------------------------- 1 | import { Face } from './face'; 2 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point"; 3 | import { AttributeLocation } from '@/engine/renderer/renderer'; 4 | import { Mesh } from '@/engine/renderer/mesh'; 5 | 6 | function indexToFaceVertexPoint(index: number, positionData: Float32Array, matrix: DOMMatrix): EnhancedDOMPoint { 7 | return new EnhancedDOMPoint().set( 8 | matrix.transformPoint(new EnhancedDOMPoint(positionData[index], positionData[index + 1], positionData[index + 2])) 9 | ) 10 | } 11 | 12 | export function meshToFaces(meshes: Mesh[], transformMatrix?: DOMMatrix) { 13 | return meshes.flatMap(mesh => { 14 | const indices = mesh.geometry.getIndices(); 15 | 16 | const positions = mesh.geometry.getAttribute_(AttributeLocation.Positions); 17 | const triangles = []; 18 | for (let i = 0; i < indices.length; i += 3) { 19 | const firstIndex = indices[i] * 3; 20 | const secondIndex = indices[i + 1] * 3; 21 | const thirdIndex = indices[i + 2] * 3; 22 | 23 | const point0 = indexToFaceVertexPoint(firstIndex, positions.data, transformMatrix ?? mesh.worldMatrix); 24 | const point1 = indexToFaceVertexPoint(secondIndex, positions.data, transformMatrix ?? mesh.worldMatrix); 25 | const point2 = indexToFaceVertexPoint(thirdIndex, positions.data, transformMatrix ?? mesh.worldMatrix); 26 | 27 | triangles.push([ 28 | point0, 29 | point1, 30 | point2, 31 | ]); 32 | } 33 | 34 | return triangles.map(triangle => new Face(triangle)); 35 | }); 36 | } 37 | 38 | export function getGroupedFaces(faces: Face[]) { 39 | const result: {floorFaces: Face[], wallFaces: Face[]} = { floorFaces: [], wallFaces: [] }; 40 | faces.forEach(face => { 41 | if (face.normal.y > 0.2) { 42 | result.floorFaces.push(face); 43 | } else { 44 | result.wallFaces.push(face); 45 | } 46 | }); 47 | return result; 48 | } 49 | -------------------------------------------------------------------------------- /src/sound-effects.ts: -------------------------------------------------------------------------------- 1 | import { addGap, createAudioNode, createPannerNode, zzfxG } from '@/engine/audio/audio-player'; 2 | 3 | const pannerFunc = navigator.userAgent.includes('fox') ? createAudioNode : createPannerNode; 4 | 5 | export const outsideFootsteps = createAudioNode(addGap(zzfxG(...[.1,.65,986,.01,.03,.03,4,2.63,,,,,.25,6.8,,,.09,.14,.01]), 0.3)); 6 | export const indoorFootsteps = createAudioNode(zzfxG(...[.1,.65,1100,,.03,.42,4,2.63,,,,,,2.2,,,,.14,.01])); 7 | 8 | export const doorCreak = pannerFunc(zzfxG(...[.1,,349,.57,.51,1,2,,2.4,-2,,.09,,.4,,,.02,1.3,.72])); 9 | 10 | // export const draggingSound = createPannerNode(zzfxG(...[1.09,,152,.11,1,1,1,2.19,4,,-10,,.06,1.1,51,.2,.16,.86,.5,.31])); 11 | export const draggingSound2 = pannerFunc(zzfxG(...[0.05,0,50,.1,1.3,,4,9,,,-10,.01,,,,-0.2,,.87,1])); 12 | 13 | // export const draggingSound3 = createPannerNode(zzfxG(...[,.15,50,.34,1,1,,.11,-1.9,-0.9,20,3,.18,5,,,.31,1.01,1,.08])); 14 | 15 | export const draggingSound4 = pannerFunc(zzfxG(...[0.2,.15,50,.34,.91,.31,,.11,-1.9,-0.9,20,3,.01,7.2,,,.15,1.01,1])); 16 | 17 | export const ominousDiscovery1 = createAudioNode(zzfxG(...[,0,146.8324,.3,.3,.9,,.83,10.5,,40,.3,-0.01,,,.1,.2,,.3])); 18 | 19 | export const ominousDiscovery2 = createAudioNode(zzfxG(...[3,,87.30706,.08,.41,1.41,1,.83,10.5,,3,.5,-0.01,.1,,.1,.78,,.11,.14])); 20 | 21 | export const pickup1 = createAudioNode(zzfxG(...[1.09,,152,.01,.08,1,,2.19,,,,,.05,1.1,51,.2,.02,.86,.04])); 22 | 23 | export const scaryNote2 = (vol = 1) => createAudioNode(addGap(zzfxG(...[vol,0,50,.6,.1,1,,.11,-0.1,-0.1,,,.18,,,,.3,1.01,.5,.08]), .8)); 24 | 25 | export const upyriAttack = createAudioNode(zzfxG(...[3.2,0,276,,2,,,.71,7.5,,,,,.1,-430,.5,.19,.2,.2])); 26 | export const upyriAttack2 = createAudioNode(zzfxG(...[2.4,0,50,.01,.1,1,,.11,-0.1,-0.1,,,.18,,,,.31,.3,.5,.08])); 27 | 28 | export const upyriHit = createPannerNode(zzfxG(...[1.65,,57,.01,.09,.08,4,.11,,,,,.15,1.3,,.2,.08,.72,.04])); 29 | 30 | const music = (freqs: number[], durs: number[]) => createAudioNode(freqs.flatMap((freq, i) => zzfxG(...[,0,freq,.1,durs[i],.26,2,1.18,,,,,,.2,,,.11,.38,.03]))); 31 | 32 | export const makeSong = music([65.41 , 82.41, 77.78, 65.41 , 82.41, 77.78, 65.41 , 82.41, 77.78, 61.74, 65.41], [.25, .25, 1, .25, .25, 1, .25, .25, .25, .25, 2]) 33 | -------------------------------------------------------------------------------- /src/engine/audio/audio-player.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 3 | 4 | export const audioCtx = new AudioContext(); 5 | 6 | // zzfxV - global volume 7 | const zzfxV=.7; 8 | 9 | // zzfxR - global sample rate 10 | const zzfxR=44100 11 | 12 | // zzfxP() - the sound player -- returns a AudioBufferSourceNode 13 | export const zzfxP=(...t)=>{let e=audioCtx.createBufferSource(),f=audioCtx.createBuffer(t.length,t[0].length,zzfxR);t.map((d,i)=>f.getChannelData(i).set(d)),e.buffer=f;return e} 14 | 15 | // zzfxG() - the sound generator -- returns an array of sample data 16 | export const zzfxG = (p=1,k=.05,b=220,e=0,r=0,t=.1,q=0,D=1,u=0,y=0,v=0,z=0,l=0,E=0,A=0,F=0,c=0,w=1,m=0,B=0): number[] =>{let d=2*Math.PI,G=u*=500*d/zzfxR/zzfxR,C=b*=(1-k+2*k*Math.random(k=[]))*d/zzfxR,g=0,H=0,a=0,n=1,I=0,J=0,f=0,x,h;e=zzfxR*e+9;m*=zzfxR;r*=zzfxR;t*=zzfxR;c*=zzfxR;y*=500*d/zzfxR**3;A*=d/zzfxR;v*=d/zzfxR;z*=zzfxR;l=zzfxR*l|0;for(h=e+m+ r+t+c|0;aa?0:(az&&(b+=v,C+=v,n=0),!l||++I%l||(b=C,u=G,n=n||1);return k}; 17 | 18 | export function createPannerNode(buffer: number[]) { 19 | return (position_: EnhancedDOMPoint) => { 20 | const panner = new PannerNode(audioCtx, { 21 | distanceModel: 'linear', 22 | positionX: position_.x, 23 | positionY: position_.y, 24 | positionZ: position_.z, 25 | maxDistance: 60, 26 | rolloffFactor: 40, 27 | coneOuterGain: 0.4 28 | }); 29 | const node = zzfxP(buffer); 30 | node.connect(panner).connect(audioCtx.destination); 31 | return node; 32 | } 33 | } 34 | 35 | export function createAudioNode(buffer: number[]) { 36 | return () => { 37 | const node = zzfxP(buffer) 38 | node.connect(audioCtx.destination); 39 | return node; 40 | } 41 | } 42 | 43 | 44 | export function addGap(buffer: number[], seconds: number) { 45 | for (let i = 0; i < seconds * zzfxR; i++) { 46 | buffer.push(0); 47 | } 48 | return buffer; 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/engine/renderer/lil-gl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | depth_fragment_glsl, 3 | depth_vertex_glsl, 4 | fragment_glsl, shadowMap, skybox_fragment_glsl, skybox_vertex_glsl, uSampler, vertex_glsl 5 | } from '@/engine/shaders/shaders'; 6 | 7 | export class LilGl { 8 | gl: WebGL2RenderingContext; 9 | program: WebGLProgram; 10 | skyboxProgram: WebGLProgram; 11 | depthProgram: WebGLProgram; 12 | 13 | constructor() { 14 | // @ts-ignore 15 | this.gl = c3d.getContext('webgl2')!; 16 | const vertex = this.createShader(this.gl.VERTEX_SHADER, vertex_glsl); 17 | const fragment = this.createShader(this.gl.FRAGMENT_SHADER, fragment_glsl); 18 | this.program = this.createProgram(vertex, fragment); 19 | const skyboxVertex = this.createShader(this.gl.VERTEX_SHADER, skybox_vertex_glsl); 20 | const skyboxFragment = this.createShader(this.gl.FRAGMENT_SHADER, skybox_fragment_glsl); 21 | this.skyboxProgram = this.createProgram(skyboxVertex, skyboxFragment); 22 | const depthVertex = this.createShader(this.gl.VERTEX_SHADER, depth_vertex_glsl); 23 | const depthFragment = this.createShader(this.gl.FRAGMENT_SHADER, depth_fragment_glsl); 24 | this.depthProgram = this.createProgram(depthVertex, depthFragment); 25 | 26 | const shadowMapLocation = this.gl.getUniformLocation(this.program, shadowMap); 27 | const textureLocation = this.gl.getUniformLocation(this.program, uSampler); 28 | this.gl.useProgram(this.program); 29 | this.gl.uniform1i(textureLocation, 0); 30 | this.gl.uniform1i(shadowMapLocation, 1); 31 | 32 | } 33 | 34 | createShader(type: GLenum, source: string): WebGLShader { 35 | const shader = this.gl.createShader(type)!; 36 | this.gl.shaderSource(shader, source); 37 | this.gl.compileShader(shader); 38 | return shader; 39 | } 40 | 41 | createProgram(vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram { 42 | const program = this.gl.createProgram()!; 43 | this.gl.attachShader(program, vertexShader); 44 | this.gl.attachShader(program, fragmentShader); 45 | this.gl.linkProgram(program); 46 | 47 | if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { 48 | console.log(this.gl.getShaderInfoLog(vertexShader)); 49 | console.log(this.gl.getShaderInfoLog(fragmentShader)); 50 | } 51 | 52 | return program; 53 | } 54 | } 55 | 56 | export const lilgl = new LilGl(); 57 | export const gl = lilgl.gl; 58 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: "module" 10 | }, 11 | plugins: ['unused-imports', '@typescript-eslint'], 12 | rules: { 13 | 'no-console': 'off', 14 | 'no-debugger': 'off', 15 | 'prefer-destructuring': 'off', 16 | camelcase: 'off', 17 | 'no-use-before-define': ['error', { variables: true, functions: false, classes: true }], 18 | 'max-classes-per-file': ['error', 1], 19 | 'no-global-assign': ['error', { exceptions: ['Object'] }], 20 | 'no-unneeded-ternary': 'error', 21 | 'import/prefer-default-export': 'off', 22 | 'guard-for-in': 'error', 23 | 'arrow-parens': 'off', 24 | semi: 'warn', 25 | 'arrow-body-style': 'off', 26 | 'no-multiple-empty-lines': ['error', { max: 2, maxBOF: 1 }], 27 | 'lines-between-class-members': 'off', 28 | yoda: 'error', 29 | 'no-unused-vars': 'off', 30 | 'no-bitwise': 'off', 31 | 'no-plusplus': 'off', 32 | 'class-methods-use-this': 'warn', 33 | 'linebreak-style': 0, 34 | 'function-paren-newline': 'off', 35 | 'unused-imports/no-unused-imports': 'warn', 36 | 'id-denylist': [ 37 | 'warn', 'seed', 'direction', 'clone', 'normalize', 'setAttribute', 'done', 'all', 'translate', 'scale', 'rotate', 38 | 'position', 'rotation', 'children', 'parent', 'remove', 'setRotation', 'textureRepeat', 'load', 'image', 39 | 'width', 'height', 'offset', 'style', 'color', 'gradientTransform', 'operator', 'radius', 'result', 'stitchTiles', 40 | 'surfaceScale', 'baseFrequency', 'viewBox' 41 | ], 42 | 'unused-imports/no-unused-vars': [ 43 | 'warn', 44 | { 45 | vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_', 46 | }, 47 | ], 48 | '@typescript-eslint/no-explicit-any': 'off', 49 | '@typescript-eslint/explicit-module-boundary-types': 'off', 50 | 'no-shadow': 'off', 51 | '@typescript-eslint/no-shadow': ['error'], 52 | }, 53 | overrides: [ 54 | { 55 | files: [ 56 | '**/__tests__/*.{j,t}s?(x)', 57 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 58 | ], 59 | env: { 60 | jest: true, 61 | }, 62 | }, 63 | ], 64 | settings: { 65 | 'import/resolver': { 66 | typescript: {}, 67 | }, 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /find-best-roadroller.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('node:child_process'); 2 | const readline = require('readline'); 3 | const fs = require('fs'); 4 | 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout 8 | }); 9 | 10 | const cliToApiMaps = [ 11 | { cli: '-Zab', api: 'numAbbreviations', type: 'number' }, 12 | { cli: '-Zlr', api: 'recipLearningRate', type: 'number' }, 13 | { cli: '-Zmc', api: 'modelMaxCount', type: 'number' }, 14 | { cli: '-Zmd', api: 'modelRecipBaseCount', type: 'number' }, 15 | { cli: '-Zpr', api: 'precision', type: 'number' }, 16 | { cli: '-Zdy', api: 'dynamicModels', type: 'number' }, 17 | { cli: '-Zco', api: 'contextBits', type: 'number' }, 18 | { cli: '-S', api: 'sparseSelectors', type: 'array' } 19 | ] 20 | 21 | rl.question('How many seconds should RoadRoller spend looking for the best config? ', seconds => { 22 | console.log('Building...'); 23 | exec('vite build', () => { 24 | console.log(`Spending ${seconds} seconds searching for config...`); 25 | exec(`node node_modules/roadroller/cli.mjs ${__dirname}/dist/output.js -D -OO`, { timeout: seconds * 1000, killSignal: 'SIGINT', maxBuffer: 4069 * 1024 }, (error, stdout, stderr) => { 26 | const bestConfigJs = { allowFreeVars: true }; 27 | const bestConfigConsole = stderr.split('\n').reverse().find(line => line.includes('<-')); 28 | const itemCheckRemoved = bestConfigConsole.split(') ')[1]; 29 | const endSizeRemoved = itemCheckRemoved.split(': ')[0]; 30 | const configPieces = endSizeRemoved.split(' ').filter(param => !param.startsWith('-Sx')); 31 | configPieces.forEach(singleParam => { 32 | cliToApiMaps.forEach(mapper => { 33 | if (singleParam.startsWith(mapper.cli) && mapper.type !== 'unused') { 34 | bestConfigJs[mapper.api] = convertValue(mapper, singleParam); 35 | } 36 | }) 37 | }); 38 | fs.writeFileSync(`${__dirname}/roadroller-config.json`, JSON.stringify(bestConfigJs, null, 2)); 39 | console.log(`BEST CONFIG: ${bestConfigConsole}`); 40 | process.exit(0); 41 | }); 42 | }) 43 | }); 44 | 45 | function convertValue(mapper, cliSetting) { 46 | const stringValue = cliSetting.replace(mapper.cli, ''); 47 | if (mapper.type === 'number') { 48 | return parseInt(stringValue, 10); 49 | } else { 50 | return stringValue.split(',').map(value => parseInt(value, 10)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/engine/shaders/shaders.ts: -------------------------------------------------------------------------------- 1 | // Generated with Shader Minifier 1.3.4 (https://github.com/laurentlb/Shader_Minifier/) 2 | export const aCoords = 'E'; 3 | export const aDepth = 'L'; 4 | export const aNormal = 'B'; 5 | export const aPosition = 'm'; 6 | export const aTexCoord = 'K'; 7 | export const emissive = 't'; 8 | export const fragDepth = 'v'; 9 | export const lightPovMvp = 'f'; 10 | export const modelviewProjection = 'M'; 11 | export const normalMatrix = 'N'; 12 | export const outColor = 'g'; 13 | export const positionFromLightPov = 'u'; 14 | export const shadowMap = 'z'; 15 | export const textureRepeat = 'h'; 16 | export const uSampler = 's'; 17 | export const u_skybox = 'I'; 18 | export const u_viewDirectionProjectionInverse = 'H'; 19 | export const vColor = 'e'; 20 | export const vDepth = 'n'; 21 | export const vNormal = 'o'; 22 | export const vNormalMatrix = 'l'; 23 | export const vTexCoord = 'i'; 24 | export const v_position = 'G'; 25 | 26 | export const depth_fragment_glsl = `#version 300 es 27 | precision highp float; 28 | out float v;void main(){v=gl_FragCoord.z;}`; 29 | 30 | export const depth_vertex_glsl = `#version 300 es 31 | precision highp float; 32 | layout(location=0) in vec4 m;uniform mat4 f;void main(){gl_Position=f*m;}`; 33 | 34 | export const fragment_glsl = `#version 300 es 35 | precision highp float; 36 | in vec4 e;in vec2 i;in float n;in vec3 o;in mat4 l;in vec4 u;uniform vec2 h;uniform vec4 t;uniform mediump sampler2DArray s;uniform mediump sampler2DShadow z;out vec4 g;vec3 d=vec3(-1,1.5,-1);float A=.2f,C=.6f;vec2 D[5]=vec2[](vec2(0),vec2(-1,0),vec2(1,0),vec2(0,1),vec2(0,-1));float F=1.,J=4200.;void main(){for(int v=0;v<5;v++){vec3 m=vec3(u.xy+D[v]/J,u.z-.001);float f=texture(z,m);F*=max(f,.87);}vec3 v=normalize(mat3(l)*o),f=normalize(d);float m=max(dot(f,v)*F,A);vec3 e=length(t)>0.?t.xyz:m*vec3(1);vec4 C=vec4(e.xyz,1);g=n<0.?C:texture(s,vec3(i*h,n))*C;}`; 37 | 38 | export const skybox_fragment_glsl = `#version 300 es 39 | precision highp float; 40 | uniform samplerCube I;uniform mat4 H;in vec4 G;out vec4 g;void main(){vec4 v=H*G;g=texture(I,v.xyz);}`; 41 | 42 | export const skybox_vertex_glsl = `#version 300 es 43 | layout(location=0) in vec4 E;out vec4 G;void main(){G=E;gl_Position=E;gl_Position.z=1.;}`; 44 | 45 | export const vertex_glsl = `#version 300 es 46 | layout(location=0) in vec3 E;layout(location=1) in vec3 B;layout(location=2) in vec2 K;layout(location=3) in float L;uniform mat4 M,N,f;out vec2 i;out float n;out vec3 o;out mat4 l;out vec4 u;void main(){vec4 v=vec4(E,1);gl_Position=M*v;i=K;n=L;o=B;l=N;u=f*v;}`; 47 | 48 | -------------------------------------------------------------------------------- /src/engine/enhanced-dom-point.ts: -------------------------------------------------------------------------------- 1 | export interface VectorLike { 2 | x: number; 3 | y: number; 4 | z: number; 5 | w?: number; 6 | } 7 | 8 | export class EnhancedDOMPoint extends DOMPoint { 9 | add_(otherVector: VectorLike) { 10 | this.addVectors(this, otherVector); 11 | return this; 12 | } 13 | 14 | addVectors(v1: VectorLike, v2: VectorLike) { 15 | this.x = v1.x + v2.x; 16 | this.y = v1.y + v2.y; 17 | this.z = v1.z + v2.z; 18 | return this; 19 | } 20 | 21 | set(x?: number | VectorLike, y?: number, z?: number): EnhancedDOMPoint { 22 | if (x && typeof x === 'object') { 23 | y = x.y; 24 | z = x.z; 25 | x = x.x; 26 | } 27 | this.x = x ?? this.x; 28 | this.y = y ?? this.y; 29 | this.z = z ?? this.z; 30 | return this; 31 | } 32 | 33 | clone_() { 34 | return new EnhancedDOMPoint(this.x, this.y, this.z, this.w); 35 | } 36 | 37 | scale_(scaleBy: number) { 38 | this.x *= scaleBy; 39 | this.y *= scaleBy; 40 | this.z *= scaleBy; 41 | return this; 42 | } 43 | 44 | subtract(otherVector: VectorLike) { 45 | this.subtractVectors(this, otherVector); 46 | return this; 47 | } 48 | 49 | subtractVectors(v1: VectorLike, v2: VectorLike) { 50 | this.x = v1.x - v2.x; 51 | this.y = v1.y - v2.y; 52 | this.z = v1.z - v2.z; 53 | return this; 54 | } 55 | 56 | crossVectors(v1: EnhancedDOMPoint, v2: EnhancedDOMPoint) { 57 | const x = v1.y * v2.z - v1.z * v2.y; 58 | const y = v1.z * v2.x - v1.x * v2.z; 59 | const z = v1.x * v2.y - v1.y * v2.x; 60 | this.x = x 61 | this.y = y 62 | this.z = z 63 | return this; 64 | } 65 | 66 | dot(otherVector: VectorLike): number { 67 | return this.x * otherVector.x + this.y * otherVector.y + this.z * otherVector.z; 68 | } 69 | 70 | toArray() { 71 | return [this.x, this.y, this.z]; 72 | } 73 | 74 | get magnitude() { 75 | return Math.hypot(...this.toArray()); 76 | } 77 | 78 | normalize_() { 79 | const magnitude = this.magnitude; 80 | if (magnitude === 0) { 81 | return new EnhancedDOMPoint(); 82 | } 83 | this.x /= magnitude; 84 | this.y /= magnitude; 85 | this.z /= magnitude; 86 | return this; 87 | } 88 | 89 | moveTowards(otherVector: EnhancedDOMPoint, speed: number) { 90 | const distance = new EnhancedDOMPoint().subtractVectors(otherVector, this); 91 | 92 | if (distance.magnitude > 2) { 93 | const direction_ = distance.normalize_().scale_(speed); 94 | this.add_(direction_); 95 | } 96 | 97 | return this; 98 | } 99 | 100 | isEqualTo(otherVector: EnhancedDOMPoint): boolean { 101 | return this.x === otherVector.x && this.y === otherVector.y && this.z === otherVector.z; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/modeling/building-blocks.ts: -------------------------------------------------------------------------------- 1 | import { doTimes } from '@/engine/helpers'; 2 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 3 | 4 | export const mergeCubes = (cubes: MoldableCubeGeometry[]) => cubes.reduce((acc, curr) => acc.merge(curr)); 5 | 6 | export class SegmentedWall extends MoldableCubeGeometry { 7 | totalWidth = 0; 8 | constructor(segmentWidths: number[], segmentHeight: number, topSegments: number[], bottomSegments: number[], startingX = 0,startingY = 0, depth = 2, segmentTop = false, segmentBottom = false) { 9 | let runningSide = 0; 10 | let runningLeft = 0; 11 | 12 | super(segmentWidths[0], topSegments[0], depth, segmentTop ? 6 : 1, 1, 1, 6); 13 | this.translate_(0, segmentHeight - topSegments[0] / 2 + startingY).spreadTextureCoords(); 14 | 15 | topSegments.forEach((top, index) => { 16 | const currentWidth = segmentWidths.length === 1 ? segmentWidths[0] : segmentWidths[index]; 17 | if (index > 0 && top > 0) { 18 | this.merge(new MoldableCubeGeometry(currentWidth, top, depth,segmentTop ? 6 : 1,1,1,6).translate_(startingX + runningSide + (currentWidth / 2), segmentHeight - top / 2 + startingY).spreadTextureCoords()); 19 | } 20 | 21 | if (bottomSegments[index] > 0) { 22 | this.merge(new MoldableCubeGeometry(currentWidth, bottomSegments[index], depth, segmentBottom ? 6 : 1, 1, 1, 6).translate_(startingX + runningSide + (currentWidth / 2), bottomSegments[index] / 2 + startingY).spreadTextureCoords()); 23 | } 24 | runningSide+= (index === 0 ? currentWidth / 2 : currentWidth); 25 | runningLeft += currentWidth; 26 | }); 27 | 28 | this.totalWidth = runningLeft; 29 | this.all_().translate_((segmentWidths[0] - runningLeft) / 2, 0).computeNormals().done_(); 30 | } 31 | } 32 | 33 | export function createHallway(frontWall: SegmentedWall, backWall: MoldableCubeGeometry, spacing: number) { 34 | return frontWall.translate_(0, 0, spacing).merge(backWall.translate_(0, 0, -spacing)).done_(); 35 | } 36 | 37 | export function createBox(frontWall: SegmentedWall, backWall: SegmentedWall, leftWall: SegmentedWall, rightWall: SegmentedWall) { 38 | return createHallway(frontWall, backWall, (leftWall.totalWidth + 2) / 2) 39 | .merge(createHallway(leftWall, rightWall, (frontWall.totalWidth - 2) / 2).rotate_(0, Math.PI / 2)).computeNormals().done_(); 40 | } 41 | 42 | 43 | // TODO: Remove this if i stick with ramps only 44 | export function createStairs(stepCount: number, startingHeight = 0) { 45 | const stepHeight = 1; 46 | return mergeCubes(doTimes(stepCount, index => { 47 | const currentHeight = index * stepHeight + stepHeight + startingHeight; 48 | return new MoldableCubeGeometry(1, currentHeight, 3).translate_(index, currentHeight/2); 49 | })).done_(); 50 | } 51 | 52 | export function patternFill(pattern: number[], times: number) { 53 | return doTimes(times, (index) => pattern[index % pattern.length]); 54 | } 55 | -------------------------------------------------------------------------------- /src/engine/physics/surface-collision.ts: -------------------------------------------------------------------------------- 1 | import { Face } from './face'; 2 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point"; 3 | 4 | 5 | // TODO: simple optimization would be to sort floor faces first, as long as there are no moving floor pieces they 6 | // could be pre sorted 7 | export function findFloorHeightAtPosition(floorFaces: Face[], positionPoint: EnhancedDOMPoint) { 8 | let height: number; 9 | const collisions = []; 10 | 11 | for (const floor of floorFaces) { 12 | const { x: x1, z: z1 } = floor.points[0]; 13 | const { x: x2, z: z2 } = floor.points[1]; 14 | const { x: x3, z: z3 } = floor.points[2]; 15 | 16 | if ((z1 - positionPoint.z) * (x2 - x1) - (x1 - positionPoint.x) * (z2 - z1) < 0) { 17 | continue; 18 | } 19 | 20 | if ((z2 - positionPoint.z) * (x3 - x2) - (x2 - positionPoint.x) * (z3 - z2) < 0) { 21 | continue; 22 | } 23 | 24 | if ((z3 - positionPoint.z) * (x1 - x3) - (x3 - positionPoint.x) * (z1 - z3) < 0) { 25 | continue; 26 | } 27 | 28 | height = -(positionPoint.x * floor.normal.x + floor.normal.z * positionPoint.z + floor.originOffset) / floor.normal.y; 29 | 30 | const buffer = -3; // original mario 64 code uses a 78 unit buffer, but mario is 160 units tall compared to our presently much smaller sizes 31 | if (positionPoint.y - (height + buffer) < 0) { 32 | continue; 33 | } 34 | 35 | collisions.push({ height, floor }); 36 | } 37 | 38 | return collisions.sort((a, b) => b.height - a.height)[0]; 39 | } 40 | 41 | export function findWallCollisionsFromList(walls: Face[], position: EnhancedDOMPoint, offsetY: number, radius: number) { 42 | const collisionData = { 43 | xPush: 0, 44 | zPush: 0, 45 | walls: [] as Face[], 46 | numberOfWallsHit: 0, 47 | }; 48 | 49 | const { x, z } = position; 50 | const y = position.y + offsetY; 51 | 52 | for (const wall of walls) { 53 | if (y < wall.lowerY || y > wall.upperY) { 54 | continue; 55 | } 56 | 57 | const offset = wall.normal.dot(position) + wall.originOffset; 58 | if (offset < -radius || offset > radius) { 59 | continue; 60 | } 61 | 62 | const isXProjection = wall.normal.x < -0.707 || wall.normal.x > 0.707; 63 | const w = isXProjection ? -z : x; 64 | const wNormal = isXProjection ? wall.normal.x : wall.normal.z; 65 | 66 | let w1 = -wall.points[0].z; 67 | let w2 = -wall.points[1].z; 68 | let w3 = -wall.points[2].z; 69 | 70 | if (!isXProjection) { 71 | w1 = wall.points[0].x; 72 | w2 = wall.points[1].x; 73 | w3 = wall.points[2].x; 74 | } 75 | let y1 = wall.points[0].y; 76 | let y2 = wall.points[1].y; 77 | let y3 = wall.points[2].y; 78 | 79 | const invertSign = wNormal > 0 ? 1 : -1; 80 | 81 | if (((y1 - y) * (w2 - w1) - (w1 - w) * (y2 - y1)) * invertSign > 0) { 82 | continue; 83 | } 84 | if (((y2 - y) * (w3 - w2) - (w2 - w) * (y3 - y2)) * invertSign > 0) { 85 | continue; 86 | } 87 | if (((y3 - y) * (w1 - w3) - (w3 - w) * (y1 - y3)) * invertSign > 0) { 88 | continue; 89 | } 90 | 91 | collisionData.xPush += wall.normal.x * (radius - offset); 92 | collisionData.zPush += wall.normal.z * (radius - offset); 93 | collisionData.walls.push(wall); 94 | collisionData.numberOfWallsHit++; 95 | } 96 | return collisionData; 97 | } 98 | -------------------------------------------------------------------------------- /src/engine/renderer/object-3d.ts: -------------------------------------------------------------------------------- 1 | import { EnhancedDOMPoint } from "@/engine/enhanced-dom-point"; 2 | import { radsToDegrees } from '@/engine/helpers'; 3 | 4 | export class Object3d { 5 | position_: EnhancedDOMPoint; 6 | scale_: EnhancedDOMPoint; 7 | children_: Object3d[]; 8 | parent_?: Object3d; 9 | localMatrix: DOMMatrix; 10 | worldMatrix: DOMMatrix; 11 | up: EnhancedDOMPoint; 12 | rotationMatrix: DOMMatrix; 13 | 14 | constructor(...children_: Object3d[]) { 15 | this.position_ = new EnhancedDOMPoint(); 16 | this.scale_ = new EnhancedDOMPoint(1, 1, 1); 17 | this.children_ = []; 18 | this.localMatrix = new DOMMatrix(); 19 | this.worldMatrix = new DOMMatrix(); 20 | this.up = new EnhancedDOMPoint(0, 1, 0); 21 | this.rotationMatrix = new DOMMatrix(); 22 | if (children_) { 23 | this.add_(...children_); 24 | } 25 | } 26 | 27 | add_(...object3ds: Object3d[]) { 28 | object3ds.forEach(object3d => { 29 | if (object3d.parent_) { 30 | object3d.parent_.children_ = object3d.parent_.children_.filter(child => child !== this); 31 | } 32 | object3d.parent_ = this; 33 | this.children_.push(object3d); 34 | }) 35 | } 36 | 37 | remove_(object3d: Object3d) { 38 | this.children_ = this.children_.filter(child => child !== object3d); 39 | } 40 | 41 | rotation_ = new EnhancedDOMPoint(); 42 | rotate_(xRads: number, yRads: number, zRads: number) { 43 | this.rotation_.add_({x: radsToDegrees(xRads), y: radsToDegrees(yRads), z: radsToDegrees(zRads)}); 44 | this.rotationMatrix.rotateSelf(radsToDegrees(xRads), radsToDegrees(yRads), radsToDegrees(zRads)); 45 | } 46 | 47 | setRotation_(xRads: number, yRads: number, zRads: number) { 48 | this.rotationMatrix = new DOMMatrix(); 49 | this.rotation_.set(radsToDegrees(xRads), radsToDegrees(yRads), radsToDegrees(zRads)); 50 | this.rotationMatrix.rotateSelf(radsToDegrees(xRads), radsToDegrees(yRads), radsToDegrees(zRads)); 51 | } 52 | 53 | isUsingLookAt = false; 54 | getMatrix() { 55 | const matrix = new DOMMatrix(); 56 | matrix.translateSelf(this.position_.x, this.position_.y, this.position_.z); 57 | if (this.isUsingLookAt) { 58 | matrix.multiplySelf(this.rotationMatrix); 59 | } else { 60 | matrix.rotateSelf(this.rotation_.x, this.rotation_.y, this.rotation_.z); 61 | } 62 | matrix.scaleSelf(this.scale_.x, this.scale_.y, this.scale_.z); 63 | return matrix; 64 | } 65 | 66 | updateWorldMatrix() { 67 | this.localMatrix = this.getMatrix(); 68 | 69 | if (this.parent_) { 70 | this.worldMatrix = this.parent_.worldMatrix.multiply(this.localMatrix); 71 | } else { 72 | this.worldMatrix = DOMMatrix.fromMatrix(this.localMatrix); 73 | } 74 | 75 | this.children_.forEach(child => child.updateWorldMatrix()); 76 | } 77 | 78 | allChildren(): Object3d[] { 79 | function getChildren(object3d: Object3d, all_: Object3d[]) { 80 | object3d.children_.forEach(child => { 81 | all_.push(child); 82 | getChildren(child, all_); 83 | }); 84 | } 85 | 86 | const allChildren: Object3d[] = []; 87 | getChildren(this, allChildren); 88 | return allChildren; 89 | } 90 | 91 | private right = new EnhancedDOMPoint(); 92 | private lookatUp = new EnhancedDOMPoint(); 93 | forward = new EnhancedDOMPoint(); 94 | 95 | lookAt(target: EnhancedDOMPoint) { 96 | this.isUsingLookAt = true; 97 | this.forward.subtractVectors(this.position_, target).normalize_(); 98 | this.right.crossVectors(this.up, this.forward).normalize_(); 99 | this.lookatUp.crossVectors(this.forward, this.right).normalize_(); 100 | 101 | this.rotationMatrix = new DOMMatrix([ 102 | this.right.x, this.right.y, this.right.z, 0, 103 | this.lookatUp.x, this.lookatUp.y, this.lookatUp.z, 0, 104 | this.forward.x, this.forward.y, this.forward.z, 0, 105 | 0, 0, 0, 1, 106 | ]); 107 | } 108 | } 109 | 110 | 111 | export function createOrtho(bottom: number, top: number, left: number, right: number, near: number, far: number) { 112 | return new DOMMatrix([ 113 | 2 / (right - left), 0, 0, 0, 114 | 0, 2 / (top - bottom), 0, 0, 115 | 0, 0, -2 / (far - near), 0, 116 | -(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1, 117 | ]); 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/game-states/menu.state.ts: -------------------------------------------------------------------------------- 1 | import { State } from '@/core/state'; 2 | import { gameStateMachine } from '@/game-state-machine'; 3 | import { gameStates } from '@/game-states/game-states'; 4 | import { drawFullScreenText, overlaySvg } from '@/draw-helpers'; 5 | import { NoiseType, text } from '@/engine/svg-maker/base'; 6 | import { drawBloodText, materials, skyboxes, testHeightmap } from '@/textures'; 7 | import { newNoiseLandscape } from '@/engine/new-new-noise'; 8 | import { Mesh } from '@/engine/renderer/mesh'; 9 | import { PlaneGeometry } from '@/engine/plane-geometry'; 10 | import { castleContainer } from '@/modeling/castle'; 11 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 12 | import { Skybox } from '@/engine/skybox'; 13 | import { Scene } from '@/engine/renderer/scene'; 14 | import { Camera } from '@/engine/renderer/camera'; 15 | import { render } from '@/engine/renderer/renderer'; 16 | import { getLeverDoors, makeBanners } from '@/modeling/items'; 17 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 18 | import { makeSong, pickup1, scaryNote2 } from '@/sound-effects'; 19 | import { audioCtx } from '@/engine/audio/audio-player'; 20 | 21 | export class MenuState implements State { 22 | camera: Camera; 23 | 24 | scene: Scene; 25 | 26 | 27 | 28 | 29 | 30 | constructor() { 31 | this.camera = new Camera(Math.PI / 3, 16 / 9, 1, 600); 32 | this.camera.position_.y = 32; 33 | this.camera.position_.z = 120; 34 | this.scene = new Scene(); 35 | } 36 | 37 | song = makeSong(); 38 | drumHit = scaryNote2(0.6)(); 39 | isSongPlaying = false; 40 | 41 | async onEnter() { 42 | const heightmap = await newNoiseLandscape(256, 6, 0.05, 3, NoiseType.Fractal, 113); 43 | const floor = new Mesh(new PlaneGeometry(1024, 1024, 255, 255, heightmap).spreadTextureCoords(), materials.grass); 44 | const castle = new Mesh(castleContainer.value!, materials.brickWall); 45 | const bridge = new Mesh(new MoldableCubeGeometry(18, 1, 65).translate_(0, 20.5, -125).done_(), materials.planks); 46 | 47 | // Banners 48 | const bannerHeightmap = await testHeightmap(); 49 | 50 | this.scene.add_(floor, castle, bridge, ...getLeverDoors().flatMap(leverDoor => leverDoor.doorDatas), makeBanners(bannerHeightmap)); 51 | 52 | this.scene.skybox = new Skybox(...skyboxes.test); 53 | this.scene.updateWorldMatrix(); 54 | this.scene.skybox.bindGeometry(); 55 | 56 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 57 | drawBloodText({ x: '50%', y: 300 }, 'UPYRI'), 58 | text({ x: '50%', y: 900, id_: 'Start' }, 'Start'), 59 | text({ x: '50%', y: 1010, id_: 'Fullscreen' }, 'Fullscreen'), 60 | ); 61 | // TODO: Probably add this to the svg library, if I have enough space to keep it anyway 62 | tmpl.querySelectorAll('feTurbulence').forEach((el: HTMLElement) => { 63 | el.innerHTML = ``; 64 | }); 65 | 66 | Start.onclick = () => { 67 | this.drumHit.stop(); 68 | this.song.stop(); 69 | pickup1().start(); 70 | drawFullScreenText('LOADING'); 71 | setTimeout(() => gameStateMachine.setState(gameStates.gameState), 10); 72 | }; 73 | 74 | Fullscreen.onclick = () => { 75 | if (!document.fullscreenElement) { 76 | document.documentElement.requestFullscreen(); 77 | } else { 78 | document.exitFullscreen(); 79 | } 80 | }; 81 | 82 | if (!this.isSongPlaying) { 83 | this.song.loop = true; 84 | this.song.start(); 85 | 86 | this.drumHit.loop = true; 87 | this.drumHit.start(audioCtx.currentTime + 2); 88 | this.isSongPlaying = true; 89 | } 90 | } 91 | 92 | cameraRotationAngles = new EnhancedDOMPoint(); 93 | 94 | onUpdate(): void { 95 | render(this.camera, this.scene); 96 | 97 | this.cameraRotationAngles.x -= 0.0015; 98 | this.cameraRotationAngles.y -= 0.003; 99 | this.cameraRotationAngles.z -= 0.0015; 100 | 101 | this.camera.position_.x = (Math.cos(this.cameraRotationAngles.x) * 125); 102 | this.camera.position_.y = Math.cos(this.cameraRotationAngles.y) * 35 + 50; 103 | this.camera.position_.z = (Math.sin(this.cameraRotationAngles.z) * 125); 104 | 105 | this.camera.lookAt(new EnhancedDOMPoint(0.1, 31, 0.1)); 106 | this.camera.updateWorldMatrix(); 107 | } 108 | 109 | onLeave() { 110 | this.scene = undefined; 111 | this.camera = undefined; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/modeling/lever-door.ts: -------------------------------------------------------------------------------- 1 | import { Object3d } from '@/engine/renderer/object-3d'; 2 | import { Mesh } from '@/engine/renderer/mesh'; 3 | import { Material } from '@/engine/renderer/material'; 4 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 5 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 6 | import { Face } from '@/engine/physics/face'; 7 | import { getGroupedFaces, meshToFaces } from '@/engine/physics/parse-faces'; 8 | import { 9 | doorCreak, 10 | draggingSound2, 11 | draggingSound4, 12 | } from '@/sound-effects'; 13 | import { materials } from "@/textures"; 14 | 15 | export class DoorData extends Object3d { 16 | swapHingeSideX: -1 | 1; 17 | swapHingeSideZ: -1 | 1; 18 | closedDoorCollisionM: Mesh; 19 | openDoorCollisionM: Mesh; 20 | dragPlayer: AudioBufferSourceNode; 21 | creakPlayer: AudioBufferSourceNode; 22 | originalRot = 0; 23 | 24 | constructor(doorMesh: Mesh, position_: EnhancedDOMPoint, swapHingeSideX: 1 | -1 = 1, swapHingeSideZ: 1 | -1 = 1, swapOpenClosed?: boolean, isMainGate?: boolean) { 25 | super(doorMesh); 26 | this.swapHingeSideX = swapHingeSideX; 27 | this.swapHingeSideZ = swapHingeSideZ; 28 | 29 | this.position_.set(position_.x - 2 * swapHingeSideX, position_.y, position_.z); 30 | this.children_[0].position_.x = 2 * swapHingeSideX; 31 | 32 | this.closedDoorCollisionM = new Mesh( 33 | new MoldableCubeGeometry(isMainGate ? 6 : 4, 7, 1) 34 | .translate_(position_.x - (swapOpenClosed ? 4 : 0), position_.y, position_.z) 35 | .done_() 36 | , new Material()); 37 | 38 | this.openDoorCollisionM = new Mesh( 39 | new MoldableCubeGeometry(4, 7, 1) 40 | .rotate_(0, Math.PI / 2) 41 | .translate_(position_.x - 2 * swapHingeSideX, position_.y, position_.z - 2 * swapHingeSideZ) 42 | .done_() 43 | , new Material()); 44 | 45 | if (swapOpenClosed) { 46 | const temp = this.closedDoorCollisionM; 47 | this.closedDoorCollisionM = this.openDoorCollisionM; 48 | this.openDoorCollisionM = temp; 49 | this.rotation_.y = 90; 50 | this.originalRot = 90; 51 | } 52 | 53 | this.dragPlayer = draggingSound4(position_); 54 | this.creakPlayer = doorCreak(position_); 55 | } 56 | } 57 | 58 | export class LeverDoorObject3d extends Object3d { 59 | doorDatas: DoorData[] = []; 60 | isPulled = false; 61 | isFinished = false; 62 | switchPosition: EnhancedDOMPoint; 63 | closedDoorCollisionMs: Mesh[]; 64 | openDoorCollisionMs: Mesh[]; 65 | closedDoorCollision: Face[]; 66 | openDoorCollision: Face[]; 67 | audioPlayer: AudioBufferSourceNode; 68 | 69 | 70 | constructor(switchPosition: EnhancedDOMPoint, doorDatas: DoorData[], switchRotationDegrees = 0) { 71 | const base = new Mesh(new MoldableCubeGeometry(1, 2, 1).spreadTextureCoords(), materials.iron); 72 | const lever = new Mesh( 73 | new MoldableCubeGeometry(1, 1, 4, 3, 3) 74 | .cylindrify(0.25, 'z') 75 | .merge(new MoldableCubeGeometry(3, 1, 1, 1, 3, 3).cylindrify(0.25, 'x').translate_(0, 0, 2)) 76 | .computeNormals(true) 77 | .done_(), materials.wood); 78 | super(base, lever); 79 | 80 | this.doorDatas = doorDatas; 81 | this.switchPosition = switchPosition; 82 | this.position_.set(switchPosition); 83 | this.rotation_.y = switchRotationDegrees; 84 | 85 | lever.rotation_.x = -45; 86 | 87 | this.closedDoorCollisionMs = doorDatas.flatMap(door => door.closedDoorCollisionM); 88 | this.openDoorCollisionMs = doorDatas.flatMap(door => door.openDoorCollisionM); 89 | 90 | this.closedDoorCollision = getGroupedFaces(meshToFaces(this.closedDoorCollisionMs)).wallFaces; 91 | this.openDoorCollision = getGroupedFaces(meshToFaces(this.openDoorCollisionMs)).wallFaces; 92 | 93 | this.audioPlayer = draggingSound2(switchPosition); 94 | } 95 | 96 | pullLever() { 97 | this.isPulled = true; 98 | this.audioPlayer.start(); 99 | this.doorDatas.forEach(door => { 100 | door.dragPlayer.start(); 101 | door.creakPlayer.start(); 102 | }); 103 | } 104 | 105 | update(){ 106 | if (this.isPulled && !this.isFinished) { 107 | this.doorDatas.forEach(door => { 108 | door.rotation_.y += door.swapHingeSideZ * door.swapHingeSideX * 0.6; 109 | this.children_[1].rotation_.x += (1/this.doorDatas.length) * 0.6; 110 | if (Math.abs(door.rotation_.y) - door.originalRot >= 90) { 111 | this.isFinished = true; 112 | } 113 | }); 114 | } 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /src/core/first-person-player.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from '@/engine/renderer/camera'; 2 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 3 | import { Face } from '@/engine/physics/face'; 4 | import { controls } from '@/core/controls'; 5 | import { 6 | findFloorHeightAtPosition, 7 | findWallCollisionsFromList, 8 | } from '@/engine/physics/surface-collision'; 9 | import { audioCtx } from '@/engine/audio/audio-player'; 10 | import { clamp } from '@/engine/helpers'; 11 | import { indoorFootsteps, outsideFootsteps } from '@/sound-effects'; 12 | 13 | 14 | export class FirstPersonPlayer { 15 | isJumping = false; 16 | feetCenter = new EnhancedDOMPoint(0, 0, 0); 17 | velocity = new EnhancedDOMPoint(0, 0, 0); 18 | isFrozen = false; 19 | 20 | // mesh: Mesh; 21 | camera: Camera; 22 | cameraRotation = new EnhancedDOMPoint(0, 0, 0); 23 | listener: AudioListener; 24 | footstepsPlayer; 25 | isOnDirt = true; 26 | 27 | constructor(camera: Camera) { 28 | this.feetCenter.set(44, 21, -26); 29 | this.camera = camera; 30 | this.listener = audioCtx.listener; 31 | 32 | this.footstepsPlayer = outsideFootsteps(); 33 | this.footstepsPlayer.loop = true; 34 | this.footstepsPlayer.playbackRate.value = 0; 35 | this.footstepsPlayer.start(); 36 | 37 | const rotationSpeed = 0.002; 38 | controls.onMouseMove(mouseMovement => { 39 | this.cameraRotation.x += mouseMovement.y * -rotationSpeed; 40 | this.cameraRotation.y += mouseMovement.x * -rotationSpeed; 41 | this.cameraRotation.x = clamp(this.cameraRotation.x, -Math.PI / 2, Math.PI / 2); 42 | this.cameraRotation.y = this.cameraRotation.y % (Math.PI * 2); 43 | }); 44 | } 45 | 46 | private isFootstepsStopped = true; 47 | 48 | update(gridFaces: {floorFaces: Face[], wallFaces: Face[]}[]) { 49 | //debug.innerHTML = this.feetCenter.y; 50 | if (!this.isFrozen) { 51 | this.updateVelocityFromControls(); 52 | } 53 | 54 | if (!this.isJumping && this.velocity.magnitude > 0) { 55 | if (this.isFootstepsStopped) { 56 | this.footstepsPlayer.stop(); 57 | this.footstepsPlayer = this.determineFootstepPlayer(this.feetCenter.y); 58 | this.footstepsPlayer.loop = true; 59 | this.footstepsPlayer.start(); 60 | this.isFootstepsStopped = false; 61 | } 62 | } else { 63 | this.isFootstepsStopped = true; 64 | this.footstepsPlayer.loop = false; 65 | } 66 | 67 | this.velocity.y -= 0.003; // gravity 68 | this.feetCenter.add_(this.velocity); 69 | 70 | const playerGridPosition = this.feetCenter.x < 0 ? 0 : 1; 71 | 72 | 73 | // @ts-ignore 74 | this.collideWithLevel(gridFaces[playerGridPosition]); // do collision detection, if collision is found, feetCenter gets pushed out of the collision 75 | 76 | this.camera.position_.set(this.feetCenter); 77 | this.camera.position_.y += 3.5; 78 | 79 | 80 | // @ts-ignore 81 | this.camera.setRotation_(...this.cameraRotation.toArray()); 82 | 83 | this.camera.updateWorldMatrix(); 84 | 85 | this.updateAudio(); 86 | } 87 | 88 | wallCollision(wallFaces: Face[]) { 89 | const wallCollisions = findWallCollisionsFromList(wallFaces, this.feetCenter, 1.1, 1.5); 90 | this.feetCenter.x += wallCollisions.xPush; 91 | this.feetCenter.z += wallCollisions.zPush; 92 | if (wallCollisions.numberOfWallsHit > 0) { 93 | this.velocity.x = 0; 94 | this.velocity.z = 0; 95 | } 96 | } 97 | 98 | collideWithLevel(groupedFaces: {floorFaces: Face[], wallFaces: Face[]}) { 99 | this.wallCollision(groupedFaces.wallFaces); 100 | 101 | const floorData = findFloorHeightAtPosition(groupedFaces!.floorFaces, this.feetCenter); 102 | if (!floorData) { 103 | this.isJumping = true; 104 | return; 105 | } 106 | 107 | const collisionDepth = floorData.height - this.feetCenter.y; 108 | 109 | if (collisionDepth > 0) { 110 | this.feetCenter.y += collisionDepth; 111 | this.velocity.y = 0; 112 | 113 | if (this.isOnDirt && floorData.height > 21) { 114 | this.footstepsPlayer.stop(); 115 | this.footstepsPlayer = this.determineFootstepPlayer(floorData.height); 116 | this.footstepsPlayer.loop = true; 117 | this.footstepsPlayer.start(); 118 | this.isOnDirt = false; 119 | } 120 | 121 | if (!this.isOnDirt && floorData.height === 21) { 122 | this.footstepsPlayer.stop(); 123 | this.footstepsPlayer = this.determineFootstepPlayer(floorData.height); 124 | this.footstepsPlayer.loop = true; 125 | this.footstepsPlayer.start(); 126 | this.isOnDirt = true; 127 | } 128 | 129 | this.isJumping = false; 130 | } else { 131 | this.isJumping = true; 132 | } 133 | } 134 | 135 | determineFootstepPlayer(height_: number) { 136 | if (height_ === 21) { 137 | return outsideFootsteps(); 138 | } else { 139 | return indoorFootsteps(); 140 | } 141 | } 142 | 143 | protected updateVelocityFromControls() { 144 | const speed = 0.18; 145 | 146 | const depthMovementZ = Math.cos(this.cameraRotation.y) * controls.inputDirection.y; 147 | const depthMovementX = Math.sin(this.cameraRotation.y) * controls.inputDirection.y; 148 | 149 | const sidestepZ = Math.cos(this.cameraRotation.y + Math.PI / 2) * controls.inputDirection.x; 150 | const sidestepX = Math.sin(this.cameraRotation.y + Math.PI / 2) * controls.inputDirection.x; 151 | 152 | this.velocity.z = depthMovementZ + sidestepZ; 153 | this.velocity.x = depthMovementX + sidestepX; 154 | let oldY = this.velocity.y; 155 | this.velocity.normalize_().scale_(speed); 156 | this.velocity.y = oldY; 157 | } 158 | 159 | private updateAudio() { 160 | if (this.listener.positionX) { 161 | this.listener.positionX.value = this.camera.position_.x; 162 | this.listener.positionY.value = this.camera.position_.y; 163 | this.listener.positionZ.value = this.camera.position_.z; 164 | 165 | const lookingDirection = new EnhancedDOMPoint(0, 0, -1); 166 | const result_ = this.camera.rotationMatrix.transformPoint(lookingDirection); 167 | 168 | this.listener.forwardX.value = result_.x; 169 | this.listener.forwardY.value = result_.y; 170 | this.listener.forwardZ.value = result_.z; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["tools"], 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Basic Options */ 8 | // "incremental": true, /* Enable incremental compilation */ 9 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 10 | "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 11 | // "lib": [], /* Specify library files to be included in the compilation. */ 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 15 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "outDir": "./dist", /* Redirect output structure to the directory. */ 20 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 45 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 46 | 47 | /* Module Resolution Options */ 48 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 49 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 50 | "paths": { 51 | "@/*": ["src/*"] 52 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 53 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 54 | // "typeRoots": [], /* List of folders to include type definitions from. */ 55 | // "types": [], /* Type declaration files to be included in compilation. */ 56 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 57 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 59 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 60 | 61 | /* Source Map Options */ 62 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 65 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 66 | 67 | /* Experimental Options */ 68 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 69 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 70 | 71 | /* Advanced Options */ 72 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 73 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/engine/renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | import { gl, lilgl } from "@/engine/renderer/lil-gl"; 2 | import { Camera } from "@/engine/renderer/camera"; 3 | import { Skybox } from '@/engine/skybox'; 4 | 5 | import { Scene } from '@/engine/renderer/scene'; 6 | import { Mesh } from '@/engine/renderer/mesh'; 7 | import { 8 | emissive, lightPovMvp, 9 | modelviewProjection, 10 | normalMatrix, 11 | textureRepeat, u_skybox, u_viewDirectionProjectionInverse 12 | } from '@/engine/shaders/shaders'; 13 | import { createOrtho, Object3d } from '@/engine/renderer/object-3d'; 14 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 15 | 16 | // IMPORTANT! The index of a given buffer in the buffer array must match it's respective data location in the shader. 17 | // This allows us to use the index while looping through buffers to bind the attributes. So setting a buffer 18 | // happens by placing 19 | export const enum AttributeLocation { 20 | Positions, 21 | Normals, 22 | TextureCoords, 23 | TextureDepth, 24 | LocalMatrix, 25 | NormalMatrix = 8, 26 | } 27 | 28 | gl.enable(gl.CULL_FACE); 29 | gl.enable(gl.DEPTH_TEST); 30 | gl.enable(gl.BLEND); 31 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 32 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 33 | const modelviewProjectionLocation = gl.getUniformLocation(lilgl.program, modelviewProjection)!; 34 | const normalMatrixLocation = gl.getUniformLocation(lilgl.program, normalMatrix)!; 35 | const emissiveLocation = gl.getUniformLocation(lilgl.program, emissive)!; 36 | const textureRepeatLocation = gl.getUniformLocation(lilgl.program, textureRepeat)!; 37 | const skyboxLocation = gl.getUniformLocation(lilgl.skyboxProgram, u_skybox)!; 38 | const viewDirectionProjectionInverseLocation = gl.getUniformLocation(lilgl.skyboxProgram, u_viewDirectionProjectionInverse)!; 39 | 40 | 41 | const origin = new EnhancedDOMPoint(0, 0, 0); 42 | 43 | const lightPovProjection = createOrtho(-105,105,-105,105,-400,400); 44 | 45 | const inverseLightDirection = new EnhancedDOMPoint(-0.8, 1.5, -1).normalize_(); 46 | const lightPovView = new Object3d(); 47 | lightPovView.position_.set(inverseLightDirection); 48 | lightPovView.lookAt(origin); 49 | lightPovView.rotationMatrix.invertSelf(); 50 | 51 | const lightPovMvpMatrix = lightPovProjection.multiply(lightPovView.rotationMatrix); 52 | 53 | const lightPovMvpDepthLocation = gl.getUniformLocation(lilgl.depthProgram, lightPovMvp); 54 | gl.useProgram(lilgl.depthProgram); 55 | gl.uniformMatrix4fv(lightPovMvpDepthLocation, false, lightPovMvpMatrix.toFloat32Array()); 56 | 57 | const textureSpaceConversion = new DOMMatrix([ 58 | 0.5, 0.0, 0.0, 0.0, 59 | 0.0, 0.5, 0.0, 0.0, 60 | 0.0, 0.0, 0.5, 0.0, 61 | 0.5, 0.5, 0.5, 1.0 62 | ]); 63 | 64 | const textureSpaceMvp = textureSpaceConversion.multiplySelf(lightPovMvpMatrix); 65 | const lightPovMvpRenderLocation = gl.getUniformLocation(lilgl.program, lightPovMvp); 66 | gl.useProgram(lilgl.program); 67 | gl.uniformMatrix4fv(lightPovMvpRenderLocation, false, textureSpaceMvp.toFloat32Array()); 68 | 69 | const depthTextureSize = new DOMPoint(4096, 4096); 70 | const depthTexture = gl.createTexture(); 71 | gl.activeTexture(gl.TEXTURE1); 72 | gl.bindTexture(gl.TEXTURE_2D, depthTexture); 73 | gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT32F, depthTextureSize.x, depthTextureSize.y); 74 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE); 75 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 76 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 77 | 78 | const depthFramebuffer = gl.createFramebuffer(); 79 | gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer); 80 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0); 81 | 82 | 83 | export function render(camera: Camera, scene: Scene) { 84 | const viewMatrix = camera.worldMatrix.inverse(); 85 | const viewMatrixCopy = viewMatrix.scale(1, 1, 1); 86 | const viewProjectionMatrix = camera.projection.multiply(viewMatrix); 87 | 88 | const renderSkybox = (skybox: Skybox) => { 89 | gl.useProgram(lilgl.skyboxProgram); 90 | gl.uniform1i(skyboxLocation, 0); 91 | viewMatrixCopy.m41 = 0; 92 | viewMatrixCopy.m42 = 0; 93 | viewMatrixCopy.m43 = 0; 94 | const inverseViewProjection = camera.projection.multiply(viewMatrixCopy).inverse(); 95 | gl.uniformMatrix4fv(viewDirectionProjectionInverseLocation, false, inverseViewProjection.toFloat32Array()); 96 | gl.bindVertexArray(skybox.vao); 97 | gl.drawArrays(gl.TRIANGLES, 0, 6); 98 | }; 99 | 100 | const renderMesh = (mesh: Mesh, projection: DOMMatrix) => { 101 | // @ts-ignore 102 | gl.useProgram(lilgl.program); 103 | gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); 104 | const modelViewProjectionMatrix = projection.multiply(mesh.worldMatrix); 105 | 106 | gl.uniform4fv(emissiveLocation, mesh.material.emissive); 107 | gl.vertexAttrib1f(AttributeLocation.TextureDepth, mesh.material.texture?.id ?? -1.0); 108 | const textureRepeat = [mesh.material.texture?.textureRepeat.x ?? 1, mesh.material.texture?.textureRepeat.y ?? 1]; 109 | gl.uniform2fv(textureRepeatLocation, textureRepeat); 110 | 111 | gl.bindVertexArray(mesh.geometry.vao!); 112 | 113 | // @ts-ignore 114 | gl.uniformMatrix4fv(normalMatrixLocation, true, mesh.color ? mesh.cachedMatrixData : mesh.worldMatrix.inverse().toFloat32Array()); 115 | gl.uniformMatrix4fv(modelviewProjectionLocation, false, modelViewProjectionMatrix.toFloat32Array()); 116 | if (!mesh.material.isTransparent) { 117 | gl.uniformMatrix4fv(lightPovMvpRenderLocation, false, textureSpaceMvp.multiply(mesh.worldMatrix).toFloat32Array()); 118 | } 119 | gl.drawElements(gl.TRIANGLES, mesh.geometry.getIndices()!.length, gl.UNSIGNED_SHORT, 0); 120 | }; 121 | 122 | 123 | // Render shadow map to depth texture 124 | gl.useProgram(lilgl.depthProgram); 125 | // gl.cullFace(gl.FRONT); 126 | gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer); 127 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 128 | gl.viewport(0, 0, depthTextureSize.x, depthTextureSize.y); 129 | 130 | scene.solidMeshes.forEach((mesh, index) => { 131 | if (index > 0) { 132 | gl.bindVertexArray(mesh.geometry.vao!); 133 | gl.uniformMatrix4fv(lightPovMvpDepthLocation, false, lightPovMvpMatrix.multiply(mesh.worldMatrix).toFloat32Array()); 134 | gl.drawElements(gl.TRIANGLES, mesh.geometry.getIndices()!.length, gl.UNSIGNED_SHORT, 0); 135 | } 136 | }); 137 | 138 | // Render solid meshes first 139 | // gl.activeTexture(gl.TEXTURE0); 140 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 141 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 142 | gl.cullFace(gl.BACK); 143 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 144 | scene.solidMeshes.forEach(mesh => renderMesh(mesh, viewProjectionMatrix)); 145 | 146 | // Set the depthFunc to less than or equal so the skybox can be drawn at the absolute farthest depth. Without 147 | // this the skybox will be at the draw distance and so not drawn. After drawing set this back. 148 | if (scene.skybox) { 149 | gl.depthFunc(gl.LEQUAL); 150 | renderSkybox(scene.skybox!); 151 | gl.depthFunc(gl.LESS); 152 | } 153 | 154 | // Now render transparent items. For transparent items, stop writing to the depth mask. If we don't do this 155 | // the transparent portion of a transparent mesh will hide other transparent items. After rendering the 156 | // transparent items, set the depth mask back to writable. 157 | gl.depthMask(false); 158 | scene.transparentMeshes.forEach(mesh => renderMesh(mesh, viewProjectionMatrix)); 159 | gl.depthMask(true); 160 | 161 | // Unbinding the vertex array being used to make sure the last item drawn isn't still bound on the next draw call. 162 | // In theory this isn't necessary but avoids bugs. 163 | gl.bindVertexArray(null); 164 | } 165 | -------------------------------------------------------------------------------- /src/modeling/items.ts: -------------------------------------------------------------------------------- 1 | import { getTextureForSide, MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 2 | import { Mesh } from '@/engine/renderer/mesh'; 3 | import { Material } from '@/engine/renderer/material'; 4 | import { SegmentedWall } from '@/modeling/building-blocks'; 5 | import { materials } from "@/textures"; 6 | import { AttributeLocation } from "@/engine/renderer/renderer"; 7 | import { DoorData, LeverDoorObject3d } from '@/modeling/lever-door'; 8 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 9 | import { PlaneGeometry } from '@/engine/plane-geometry'; 10 | 11 | export function stake() { 12 | return new Mesh(new MoldableCubeGeometry(0.5, 0.5, 2, 2, 2) 13 | .selectBy(vert => vert.z > 0 && vert.x === 0) 14 | .translate_(0, 0, 1) 15 | .scale_(1, 0, 1) 16 | .all_() 17 | .spreadTextureCoords() 18 | .done_(), materials.wood); 19 | } 20 | 21 | export function key() { 22 | return new Mesh( 23 | new MoldableCubeGeometry(1, 1, 0.5, 1, 3) 24 | .cylindrify(1, 'z') 25 | .merge(new SegmentedWall([1, 0.5, 0.5, 0.5], 1, [0.25, 1, 0.25, 1], [0], 0, 0, 0.5).translate_(2, -0.75)) 26 | .scale_(0.5, 0.5, 0.5) 27 | .rotate_(0, Math.PI / 2) 28 | .translate_(-32,36,60) 29 | .done_(), 30 | materials.gold); 31 | } 32 | 33 | export function upyri() { 34 | const fang = () => new MoldableCubeGeometry(2, 0.5, 2, 3, 1, 3) 35 | .cylindrify(0.08) 36 | .selectBy(vert => vert.y > 0) 37 | .scale_(0, 1, 0) 38 | .all_() 39 | .translate_(0, 0.3, 0.53) 40 | .rotate_(0.2) 41 | .setAttribute_(AttributeLocation.TextureDepth, new Float32Array(getTextureForSide(9, 9, materials.silver.texture!)), 1); 42 | 43 | const obj = new Mesh(new MoldableCubeGeometry(1, 1, 1, 6, 6, 6) 44 | .spherify(0.8) 45 | .scale_(1, 1.3, 0.8) 46 | .selectBy(vert => vert.y > 0.7) 47 | .scale_(3, 1, 1) 48 | .selectBy(vert => vert.y > 0.8) 49 | .scale_(1.5, 8, 2) 50 | .setAttribute_(AttributeLocation.TextureDepth, new Float32Array(MoldableCubeGeometry.TexturePerSide(6, 6, 6, 51 | materials.iron.texture!, 52 | materials.iron.texture!, 53 | materials.iron.texture!, 54 | materials.iron.texture!, 55 | materials.face.texture!, 56 | materials.face.texture!, 57 | )), 1) 58 | .merge(fang().translate_(-0.2)) 59 | .merge(fang().translate_(0.2)) 60 | .computeNormals(true) 61 | .all_() 62 | .rotate_(Math.PI) 63 | .done_() 64 | , materials.face); 65 | 66 | obj.position_.set(0, 54, 2); 67 | 68 | return obj; 69 | } 70 | 71 | export function makeCoffinBottomTop() { 72 | return new MoldableCubeGeometry(3, 0.5, 7.5, 1, 1, 2) 73 | .selectBy(vertex => vertex.z === 0) 74 | .translate_(0, 0, -1) 75 | .scale_(1.5) 76 | .all_() 77 | .translate_(0, -0.2, 9) 78 | .spreadTextureCoords(); 79 | } 80 | 81 | export function makeCoffin() { 82 | function makeCoffinSide(swap = 1) { 83 | return new MoldableCubeGeometry(0.5, 2, 7.5, 1, 1, 2) 84 | .selectBy(vertex => vertex.z === 0) 85 | .translate_(swap, 0, -1) 86 | .all_() 87 | .translate_(1.5 * swap, 0.4, 9) 88 | .spreadTextureCoords(); 89 | } 90 | 91 | function makeCoffinFrontBack(isSwap = false) { 92 | return new MoldableCubeGeometry(3, 2, 0.5) 93 | .selectBy(vertex => (isSwap ? -vertex.z : vertex.z) > 0) 94 | .scale_(1.17) 95 | .all_() 96 | .translate_(0, 0.4, isSwap ? 13 : 5) 97 | .spreadTextureCoords(); 98 | } 99 | 100 | return makeCoffinSide(-1) 101 | .merge(makeCoffinSide()) 102 | .merge(makeCoffinFrontBack()) 103 | .merge(makeCoffinFrontBack(true)) 104 | .merge(makeCoffinBottomTop()) 105 | .computeNormals() 106 | .done_(); 107 | } 108 | 109 | export function fenceDoor() { 110 | return new Mesh( 111 | new SegmentedWall([0.25, 0.5, 0.1, 0.5, 0.1, 0.5, 0.1, 0.5, 0.1, 0.5, 0.1, 0.5, 0.25], 7, [7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7], [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], 0, 0, 0.15) 112 | .translate_(0, -3.5) 113 | .done_(), 114 | materials.silver); 115 | } 116 | 117 | export function woodenDoor(hasLock = false, width_ = 4, height_ = 7) { 118 | const doorTextures = MoldableCubeGeometry.TexturePerSide(1, 1, 1, 119 | materials.wood.texture!, 120 | materials.wood.texture!, 121 | materials.wood.texture!, 122 | materials.wood.texture!, 123 | materials.planks.texture!, 124 | materials.planks.texture!, 125 | ); 126 | const doorGeo = new MoldableCubeGeometry(width_, height_, 1); 127 | 128 | doorGeo.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(doorTextures), 1); 129 | 130 | const barGeo = new MoldableCubeGeometry(width_ + .05, 0.5, 1.2).translate_(0, width_ === 4 ? 2.5: 5).spreadTextureCoords(); 131 | const barGeo2 = new MoldableCubeGeometry(width_ + .05, 0.5, 1.2).translate_(0, width_ === 4 ? -2.5: -1).spreadTextureCoords(); 132 | 133 | const barTextures = MoldableCubeGeometry.TexturePerSide(1, 1, 1, 134 | materials.iron.texture!, 135 | materials.iron.texture!, 136 | materials.iron.texture!, 137 | materials.iron.texture!, 138 | materials.iron.texture!, 139 | materials.iron.texture!, 140 | ); 141 | 142 | barGeo.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(barTextures), 1); 143 | barGeo2.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(barTextures), 1); 144 | 145 | 146 | const lock = new MoldableCubeGeometry(1, 1, 1.2).translate_(-1.4).done_(); 147 | 148 | const lockTextures = MoldableCubeGeometry.TexturePerSide(1, 1, 1, 149 | materials.gold.texture!, 150 | materials.gold.texture!, 151 | materials.gold.texture!, 152 | materials.gold.texture!, 153 | materials.keyLock.texture!, 154 | materials.keyLock.texture!, 155 | ); 156 | 157 | lock.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(lockTextures), 1); 158 | 159 | doorGeo.merge(barGeo).merge(barGeo2); 160 | 161 | if (hasLock) { 162 | doorGeo.merge(lock); 163 | } 164 | 165 | doorGeo.done_(); 166 | 167 | return new Mesh(doorGeo, new Material()); 168 | } 169 | 170 | export function getLeverDoors() { 171 | return [ 172 | // Corner entrance 173 | new LeverDoorObject3d(new EnhancedDOMPoint(42, 36, -60), [ 174 | new DoorData(fenceDoor(), new EnhancedDOMPoint(53, 36.5, -49)) 175 | ], -90), 176 | 177 | 178 | // Keep entrance 179 | new LeverDoorObject3d(new EnhancedDOMPoint(57, 24, 42), [ 180 | new DoorData(woodenDoor(), new EnhancedDOMPoint(-2, 24.5, -15)), 181 | new DoorData(woodenDoor(), new EnhancedDOMPoint(2, 24.5, -15), -1, 1), 182 | new DoorData(woodenDoor(), new EnhancedDOMPoint(53, 24.5, 47), -1, -1) 183 | ], -90), 184 | 185 | // Locked door to upper keep 186 | new LeverDoorObject3d(new EnhancedDOMPoint(23, 0, 37.5), [ 187 | new DoorData(woodenDoor(true), new EnhancedDOMPoint(23, 24.5, 37.5), -1) 188 | ]), 189 | 190 | 191 | // Front gate 192 | new LeverDoorObject3d(new EnhancedDOMPoint(3, 58, -12), [ 193 | new DoorData(woodenDoor(false, 6, 15), new EnhancedDOMPoint(-3, 24, -60), 1, 1, false, true), 194 | new DoorData(woodenDoor(false, 6, 15), new EnhancedDOMPoint(3, 24, -60), -1, 1, false, true) 195 | ]), 196 | 197 | // Door to key 198 | new LeverDoorObject3d(new EnhancedDOMPoint(-11, 36, 50), [ 199 | new DoorData(fenceDoor(), new EnhancedDOMPoint(-25, 36, 61.5), 1, 1, true) 200 | ], -90) 201 | ]; 202 | } 203 | 204 | function bannerMaker(bannerHeightmap: number[]) { 205 | const banner = new PlaneGeometry(64, 64, 31, 31, bannerHeightmap) 206 | .selectBy(vert => vert.z <= 20) 207 | .modifyEachVertex(vert => vert.z -= Math.abs(vert.x) / 3) 208 | .all_() 209 | .spreadTextureCoords(60, 12, 0.5, 0.18) 210 | .scale_(0.05, 1, 0.2) 211 | .rotate_(-Math.PI / 2) 212 | .computeNormals(true); 213 | 214 | const textureDepths = banner.vertices.map(vert => { 215 | if (vert.y < -2.6 && vert.y > -5) { 216 | return materials.bannerIcon.texture!.id; 217 | } else { 218 | return materials.banner.texture!.id; 219 | } 220 | }); 221 | 222 | banner.setAttribute_(AttributeLocation.TextureDepth, new Float32Array(textureDepths), 1); 223 | 224 | return banner; 225 | } 226 | 227 | export function makeBanners(bannerHeightmap: number[]) { 228 | return new Mesh( 229 | bannerMaker(bannerHeightmap).translate_(18, 32, -16.6) 230 | .merge(bannerMaker(bannerHeightmap).translate_(-18, 32, -16.6)) 231 | .done_(), 232 | materials.banner); 233 | } 234 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, HmrContext, IndexHtmlTransformContext, Plugin } from 'vite'; 2 | import path from 'path'; 3 | import fs from 'fs/promises'; 4 | import typescriptPlugin from '@rollup/plugin-typescript'; 5 | import { OutputAsset, OutputChunk } from 'rollup'; 6 | import { Input, InputAction, InputType, Packer } from 'roadroller'; 7 | import CleanCSS from 'clean-css'; 8 | import { statSync } from 'fs'; 9 | const { execFileSync } = require('child_process'); 10 | import ect from 'ect-bin'; 11 | import { exec } from 'node:child_process'; 12 | 13 | const htmlMinify = require('html-minifier'); 14 | const tmp = require('tmp'); 15 | const ClosureCompiler = require('google-closure-compiler').compiler; 16 | 17 | const shaderMinifyConfig = { 18 | shouldMinify: true, 19 | shaderDirectory: './src/engine/shaders', 20 | output: './src/engine/shaders/shaders.ts', 21 | debounce: 2000, 22 | }; 23 | 24 | export default defineConfig(({ command, mode }) => { 25 | const config = { 26 | server: { 27 | port: 3001, 28 | }, 29 | resolve: { 30 | alias: { 31 | '@': path.resolve(__dirname, './src'), 32 | } 33 | }, 34 | plugins: [minifyShaders(shaderMinifyConfig)] 35 | }; 36 | 37 | if (command === 'build') { 38 | // @ts-ignore 39 | config.esbuild = false; 40 | // @ts-ignore 41 | config.base = ''; 42 | // @ts-ignore 43 | config.build = { 44 | minify: false, 45 | target: 'es2020', 46 | modulePreload: { polyfill: false }, 47 | assetsInlineLimit: 800, 48 | assetsDir: '', 49 | rollupOptions: { 50 | output: { 51 | inlineDynamicImports: true, 52 | manualChunks: undefined, 53 | assetFileNames: `[name].[ext]` 54 | }, 55 | } 56 | }; 57 | // @ts-ignore 58 | config.plugins.push(typescriptPlugin(), closurePlugin(), roadrollerPlugin(), ectPlugin()); 59 | } 60 | 61 | return config; 62 | }); 63 | 64 | function closurePlugin(): Plugin { 65 | return { 66 | name: 'closure-compiler', 67 | // @ts-ignore 68 | renderChunk: applyClosure, 69 | enforce: 'post', 70 | }; 71 | } 72 | 73 | async function applyClosure(js: string, chunk: any) { 74 | const tmpobj = tmp.fileSync(); 75 | // replace all consts with lets to save about 50-70 bytes 76 | // ts-ignore 77 | js = js.replaceAll('const ', 'let '); 78 | 79 | await fs.writeFile(tmpobj.name, js); 80 | const closureCompiler = new ClosureCompiler({ 81 | js: tmpobj.name, 82 | externs: 'externs.js', 83 | compilation_level: 'ADVANCED', 84 | language_in: 'ECMASCRIPT_2020', 85 | language_out: 'ECMASCRIPT_2020', 86 | }); 87 | return new Promise((resolve, reject) => { 88 | closureCompiler.run((_exitCode: string, stdOut: string, stdErr: string) => { 89 | if (stdOut !== '') { 90 | resolve({ code: stdOut }); 91 | } else if (stdErr !== '') { // only reject if stdout isn't generated 92 | reject(stdErr); 93 | return; 94 | } 95 | 96 | console.warn(stdErr); // If we make it here, there were warnings but no errors 97 | }); 98 | }); 99 | } 100 | 101 | 102 | function roadrollerPlugin(): Plugin { 103 | return { 104 | name: 'vite:roadroller', 105 | transformIndexHtml: { 106 | enforce: 'post', 107 | transform: async (html: string, ctx?: IndexHtmlTransformContext): Promise => { 108 | // Only use this plugin during build 109 | if (!ctx || !ctx.bundle) { 110 | return html; 111 | } 112 | 113 | const options = { 114 | includeAutoGeneratedTags: true, 115 | removeAttributeQuotes: true, 116 | removeComments: true, 117 | removeRedundantAttributes: true, 118 | removeScriptTypeAttributes: true, 119 | removeStyleLinkTypeAttributes: true, 120 | sortClassName: true, 121 | useShortDoctype: true, 122 | collapseWhitespace: true, 123 | collapseInlineTagWhitespace: true, 124 | removeEmptyAttributes: true, 125 | removeOptionalTags: true, 126 | sortAttributes: true, 127 | minifyCSS: true, 128 | }; 129 | 130 | const bundleOutputs = Object.values<(OutputAsset | OutputChunk)>(ctx.bundle); 131 | const javascript = bundleOutputs.find((output) => output.fileName.endsWith('.js')) as OutputChunk; 132 | const css = bundleOutputs.find((output) => output.fileName.endsWith('.css')) as OutputAsset; 133 | const otherBundleOutputs = bundleOutputs.filter((output) => output !== javascript); 134 | if (otherBundleOutputs.length > 0) { 135 | otherBundleOutputs.forEach((output) => console.warn(`WARN Asset not inlined: ${output.fileName}`)); 136 | } 137 | 138 | const cssInHtml = css ? embedCss(html, css) : html; 139 | const minifiedHtml = await htmlMinify.minify(cssInHtml, options); 140 | return embedJs(minifiedHtml, javascript); 141 | }, 142 | }, 143 | }; 144 | } 145 | 146 | /** 147 | * Transforms the given JavaScript code into a packed version. 148 | * @param html The original HTML. 149 | * @param chunk The JavaScript output chunk from Rollup/Vite. 150 | * @returns The transformed HTML with the JavaScript embedded. 151 | */ 152 | async function embedJs(html: string, chunk: OutputChunk): Promise { 153 | const scriptTagRemoved = html.replace(new RegExp(`]*?src=[\./]*${chunk.fileName}[^>]*?>`), ''); 154 | const htmlInJs = `document.write('${scriptTagRemoved}');` + chunk.code.trim(); 155 | 156 | const inputs: Input[] = [ 157 | { 158 | data: htmlInJs, 159 | type: 'js' as InputType, 160 | action: 'eval' as InputAction, 161 | }, 162 | ]; 163 | 164 | let options; 165 | if (process.env.USE_RR_CONFIG) { 166 | try { 167 | options = JSON.parse(await fs.readFile(`${__dirname}/roadroller-config.json`, 'utf-8')); 168 | } catch(error) { 169 | throw new Error('Roadroller config not found. Generate one or use the regular build option'); 170 | } 171 | } else { 172 | options = { allowFreeVars: true }; 173 | } 174 | 175 | const packer = new Packer(inputs, options); 176 | await Promise.all([ 177 | fs.writeFile(`${path.join(__dirname, 'dist')}/output.js`, htmlInJs), 178 | packer.optimize(process.env.LEVEL_2_BUILD ? 2 : 0) // Regular builds use level 2, but rr config builds use the supplied params 179 | ]); 180 | const { firstLine, secondLine } = packer.makeDecoder(); 181 | return ``; 182 | } 183 | 184 | /** 185 | * Embeds CSS into the HTML. 186 | * @param html The original HTML. 187 | * @param asset The CSS asset. 188 | * @returns The transformed HTML with the CSS embedded. 189 | */ 190 | function embedCss(html: string, asset: OutputAsset): string { 191 | const reCSS = new RegExp(`]*?href="[\./]*${asset.fileName}"[^>]*?>`); 192 | const code = ``; 193 | return html.replace(reCSS, code); 194 | } 195 | 196 | /** 197 | * Creates the ECT plugin that uses Efficient-Compression-Tool to build a zip file. 198 | * @returns The ECT plugin. 199 | */ 200 | function ectPlugin(): Plugin { 201 | return { 202 | name: 'vite:ect', 203 | writeBundle: async (): Promise => { 204 | try { 205 | const files = await fs.readdir('dist/'); 206 | const assetFiles = files.filter(file => { 207 | return !file.includes('.js') && !file.includes('.css') && !file.includes('.html') && !file.includes('.zip') && file !== 'assets'; 208 | }).map(file => 'dist/' + file); 209 | const args = ['-strip', '-zip', '-10009', 'dist/index.html', ...assetFiles]; 210 | const result = execFileSync(ect, args); 211 | console.log('ECT result', result.toString().trim()); 212 | const stats = statSync('dist/index.zip'); 213 | console.log('ZIP size', stats.size); 214 | } catch (err) { 215 | console.log('ECT error', err); 216 | } 217 | }, 218 | }; 219 | } 220 | 221 | function minifyShaders(config: { shouldMinify: boolean, shaderDirectory: string, output: string, debounce: number }): Plugin { 222 | function doMinification() { 223 | debounce(async () => { 224 | const filesInShaderDir = await fs.readdir(config.shaderDirectory); 225 | const shaderFiles = filesInShaderDir.filter(file => file.endsWith('glsl')); 226 | const fileArgs = shaderFiles.map(filename => { 227 | return config.shaderDirectory.endsWith('/') ? `${config.shaderDirectory}${filename}` : `${config.shaderDirectory}/${filename}`; 228 | }).join(' '); 229 | const monoRunner = process.platform === 'win32' ? '' : 'mono '; 230 | exec(`${monoRunner}shader_minifier.exe --format js ${fileArgs} -o ${config.output}`); 231 | }, config.debounce); 232 | } 233 | 234 | return { 235 | name: 'vite:shader-minify', 236 | handleHotUpdate(context: HmrContext) { 237 | if (!config.shouldMinify || !context.file.includes('glsl')) { 238 | return; 239 | } 240 | 241 | doMinification(); 242 | } 243 | }; 244 | } 245 | 246 | let debounceTimeout: ReturnType; 247 | 248 | export function debounce(callback: (...args: any[]) => any, wait: number) { 249 | clearTimeout(debounceTimeout); 250 | debounceTimeout = setTimeout(callback, wait); 251 | } 252 | -------------------------------------------------------------------------------- /src/engine/svg-maker/base.ts: -------------------------------------------------------------------------------- 1 | export type LengthOrPercentage = `${number}%` | `${number}` | number; 2 | export type SvgString = ``; 3 | type FeTurbulenceString = ``; 4 | type FeColorMatrixString = ``; 5 | type FeDisplacementMapString = ``; 6 | type FeFuncString = ``; 7 | type FeComponentTransferString = ``; 8 | type FeCompositeString = ``; 9 | type FeBlendString = ``; 10 | type FeDiffuseLightingString = ``; 11 | type FeDistanceLightString = `` 12 | type FeMorphologyString = ``; 13 | 14 | type FilterElements = FeTurbulenceString | FeColorMatrixString | FeFuncString | FeComponentTransferString | FeDisplacementMapString | FeCompositeString | FeBlendString | FeDiffuseLightingString | FeMorphologyString; 15 | type FilterString = ``; 16 | type RectString = ``; 17 | type EllipseString = ``; 18 | type TextString = ``; 19 | type LinearGradientString = ``; 20 | type RadialGradientString = ``; 21 | type SvgStopString = ``; 22 | type SvgMaskString = ``; 23 | 24 | interface HasId { 25 | id_?: string; 26 | } 27 | 28 | interface Maskable { 29 | mask?: string; 30 | } 31 | 32 | interface Placeable { 33 | x?: LengthOrPercentage; 34 | y?: LengthOrPercentage; 35 | } 36 | 37 | interface Sizeable { 38 | width_?: LengthOrPercentage; 39 | height_?: LengthOrPercentage; 40 | } 41 | 42 | interface Filterable { 43 | filter?: string; 44 | } 45 | 46 | interface Drawable { 47 | fill?: string; 48 | } 49 | 50 | interface Styleable { 51 | style?: string; 52 | } 53 | 54 | export const enum NoiseType { 55 | Turbulence = 'turbulence', 56 | Fractal = 'fractalNoise', 57 | } 58 | 59 | interface HasInputs { 60 | in?: string; 61 | in2?: string; 62 | } 63 | 64 | interface DoesColorTransformation { 65 | colorInterpolationFilters?: 'sRGB' | 'linearRGB'; 66 | } 67 | 68 | interface HasGradientTransform { 69 | gradientTransform?: string; 70 | } 71 | 72 | interface FeTurbulenceAttributes extends DoesColorTransformation, HasId { 73 | seed_?: number 74 | baseFrequency?: number | [number, number]; 75 | numOctaves_?: number; 76 | type_?: NoiseType; 77 | result?: string; 78 | stitchTiles_?: 'stitch' | 'noStitch' 79 | } 80 | 81 | interface SvgEllipseAttributes extends Filterable, Drawable, Maskable { 82 | cx: LengthOrPercentage, 83 | cy: LengthOrPercentage, 84 | rx: LengthOrPercentage, 85 | ry: LengthOrPercentage, 86 | } 87 | 88 | interface SvgFilterAttributes extends HasId, Placeable, Sizeable { 89 | primitiveUnits?: 'userSpaceOnUse' | 'objectBoundingBox'; 90 | } 91 | 92 | type SvgLinearGradientAttributes = HasId & HasGradientTransform; 93 | 94 | interface SvgRadialGradientAttributes extends HasId, HasGradientTransform { 95 | cx?: LengthOrPercentage, 96 | cy?: LengthOrPercentage, 97 | fr?: LengthOrPercentage, 98 | fx?: LengthOrPercentage, 99 | fy?: LengthOrPercentage, 100 | } 101 | 102 | interface SvgStopAttributes { 103 | offset_: LengthOrPercentage; 104 | stopColor: string; 105 | } 106 | 107 | interface FeColorMatrixAttributes extends DoesColorTransformation { 108 | in?: string; 109 | type_?: 'matrix' | 'saturate' | 'hueRotate' | 'luminanceToAlpha'; 110 | values?: number[] | string; 111 | } 112 | 113 | type SvgRectAttributes = HasId & Filterable & Placeable & Sizeable & Drawable & Maskable; 114 | 115 | export type SvgTextAttributes = HasId & Filterable & Placeable & Sizeable & Drawable & Styleable & Maskable; 116 | 117 | interface FeBlendAttributes extends HasInputs { 118 | mode: 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' 119 | | 'color-burn' | 'hard-light' | 'soft-light' | 'difference' | 'exclusion' | 'hue' | 'saturation' 120 | | 'color' | 'luminosity'; 121 | } 122 | 123 | interface FeDiffuseLightingAttributes extends HasInputs { 124 | lightingColor: string; 125 | surfaceScale: number; 126 | } 127 | 128 | interface HasOperator { 129 | operator: string; 130 | } 131 | 132 | interface FeCompositeAttributes extends HasInputs, HasOperator { 133 | operator: 'over' | 'in' | 'out' | 'atop' | 'xor' | 'lighter' | 'arithmetic'; 134 | k2?: number; 135 | k3?: number; 136 | } 137 | 138 | interface FeDisplacementMapAttributes extends HasInputs, DoesColorTransformation { 139 | scale_?: number; 140 | } 141 | 142 | interface FeMorphologyAttributes extends HasOperator { 143 | radius: LengthOrPercentage; 144 | operator: 'dilate' | 'erode'; 145 | } 146 | 147 | export interface SvgAttributes extends Sizeable, HasId, Styleable { 148 | viewBox?: string; 149 | } 150 | 151 | export type AllSvgAttributes = FeTurbulenceAttributes & SvgEllipseAttributes & HasId 152 | & FeColorMatrixAttributes & SvgRectAttributes & SvgTextAttributes 153 | & FeDisplacementMapAttributes & FeBlendAttributes & FeDiffuseLightingAttributes & SvgAttributes 154 | & SvgLinearGradientAttributes & SvgRadialGradientAttributes & SvgStopAttributes 155 | & HasOperator & Pick & Pick 156 | & Pick; 157 | 158 | 159 | export function svg(attributes: SvgAttributes, ...elements: string[]): SvgString { 160 | return `${elements.join('')}`; 161 | } 162 | 163 | export function group(attributes: Filterable, ...elements: string[]) { 164 | return `${elements.join('')}`; 165 | } 166 | 167 | export function filter(attributes: SvgFilterAttributes, ...filterElements: FilterElements[]): FilterString { 168 | return `${filterElements.join('')}`; 169 | } 170 | 171 | // Rectangle 172 | export function rect(attributes: SvgRectAttributes): RectString { 173 | return ``; 174 | } 175 | 176 | // Ellipse 177 | export function ellipse(attributes: SvgEllipseAttributes): EllipseString { 178 | return ``; 179 | } 180 | 181 | // Text 182 | export function text(attributes: SvgTextAttributes, textToDisplay?: any): TextString { 183 | return `${textToDisplay ?? ''}`; 184 | } 185 | 186 | // Gradients 187 | export function linearGradient(attributes: SvgLinearGradientAttributes, ...stops: SvgStopString[]): LinearGradientString { 188 | return `${stops.join('')}`; 189 | } 190 | 191 | export function radialGradient(attributes: SvgRadialGradientAttributes, ...stops: SvgStopString[]): RadialGradientString { 192 | return `${stops.join('')}`; 193 | } 194 | 195 | export function svgStop(attributes: SvgStopAttributes): SvgStopString { 196 | return ``; 197 | } 198 | 199 | // Mask 200 | export function mask(attributes: HasId, ...elements: string[]): SvgMaskString { 201 | return `${elements.join('')}`; 202 | } 203 | 204 | // Minify-safe attribute converter 205 | export function attributesToString(object: Partial) { 206 | const mapper = { 207 | 'baseFrequency': object.baseFrequency, 208 | 'color-interpolation-filters': object.colorInterpolationFilters, 209 | 'cx': object.cx, 210 | 'cy': object.cy, 211 | 'fill': object.fill, 212 | 'filter': object.filter ? `url(#${object.filter})` : object.filter, 213 | 'fr': object.fr, 214 | 'fx': object.fx, 215 | 'fy': object.fy, 216 | 'gradientTransform': object.gradientTransform, 217 | 'height': object.height_, 218 | 'id': object.id_, 219 | 'in': object.in, 220 | 'in2': object.in2, 221 | 'k2': object.k2, 222 | 'k3': object.k3, 223 | 'lighting-color': object.lightingColor, 224 | 'mask': object.mask, 225 | 'mode': object.mode, 226 | 'numOctaves': object.numOctaves_, 227 | 'offset': object.offset_, 228 | 'operator': object.operator, 229 | 'primitiveUnits': object.primitiveUnits, 230 | 'radius': object.radius, 231 | 'result': object.result, 232 | 'rx': object.rx, 233 | 'ry': object.ry, 234 | 'scale': object.scale_, 235 | 'seed': object.seed_, 236 | 'stitchTiles': object.stitchTiles_, 237 | 'stop-color': object.stopColor, 238 | 'style': object.style, 239 | 'surfaceScale': object.surfaceScale, 240 | 'type': object.type_, 241 | 'values': object.values, 242 | 'viewBox': object.viewBox, 243 | 'width': object.width_, 244 | 'x': object.x, 245 | 'y': object.y, 246 | }; 247 | 248 | return Object.entries(mapper).map(([key, value]: [string, any]) => value != null ? `${key}="${value}"` : '').join(' '); 249 | } 250 | 251 | // Turbulence 252 | export function feTurbulence(attributes: FeTurbulenceAttributes): FeTurbulenceString { 253 | // @ts-ignore 254 | return ``; 255 | } 256 | 257 | // Color Matrix 258 | export function feColorMatrix(attributes: FeColorMatrixAttributes): FeColorMatrixString { 259 | // @ts-ignore 260 | return ``; 261 | } 262 | 263 | // Component Transfer 264 | interface FeComponentTransferAttributes extends DoesColorTransformation { 265 | in?: string; 266 | } 267 | export function feComponentTransfer(attributes: FeComponentTransferAttributes, ...feFuncs: FeFuncString[]): FeComponentTransferString { 268 | return `${feFuncs.join('')}`; 269 | } 270 | 271 | export function feFunc(color: 'R' | 'G' | 'B' | 'A', type: 'linear' | 'discrete' | 'table' | 'gamma', values: number[]): FeFuncString { 272 | const fixFirefoxAttrs = type === 'gamma' ? 'amplitude="1" exponent="0.55"' : `tableValues="${values}"`; 273 | return ``; 274 | } 275 | 276 | // Displacement Map 277 | export function feDisplacementMap(attributes: FeDisplacementMapAttributes): FeDisplacementMapString { 278 | return ``; 279 | } 280 | 281 | // Morphology 282 | export function feMorphology(attributes: FeMorphologyAttributes): FeMorphologyString { 283 | return ``; 284 | } 285 | 286 | // Composite 287 | export function feComposite(attributes: FeCompositeAttributes): FeCompositeString { 288 | return ``; 289 | } 290 | 291 | // Blend 292 | export function feBlend(attributes: FeBlendAttributes): FeBlendString { 293 | return ``; 294 | } 295 | 296 | // Diffuse Lighting 297 | export function feDiffuseLighting(attributes: FeDiffuseLightingAttributes, ...lights: FeDistanceLightString[]): FeDiffuseLightingString { 298 | return `${lights.join('')}`; 299 | } 300 | 301 | export function feDistantLight(azimuth: number, elevation: number): FeDistanceLightString { 302 | return ``; 303 | } 304 | -------------------------------------------------------------------------------- /src/engine/moldable-cube-geometry.ts: -------------------------------------------------------------------------------- 1 | import { AttributeLocation } from '@/engine/renderer/renderer'; 2 | import { EnhancedDOMPoint, VectorLike } from '@/engine/enhanced-dom-point'; 3 | import { calculateVertexNormals, doTimes, radsToDegrees } from "@/engine/helpers"; 4 | import { Texture } from '@/engine/renderer/texture'; 5 | import { gl } from '@/engine/renderer/lil-gl'; 6 | import { randomNumber } from '@/engine/new-new-noise'; 7 | 8 | type BufferInfo = { data: Float32Array; size: number }; 9 | 10 | export function getTextureForSide(uDivisions: number, vDivisions: number, texture: Texture) { 11 | // @ts-ignore 12 | return new Array((uDivisions + 1) * (vDivisions + 1)).fill().map(_ => texture.id); 13 | } 14 | 15 | 16 | export class MoldableCubeGeometry { 17 | vertices: EnhancedDOMPoint[] = []; 18 | verticesToActOn: EnhancedDOMPoint[] = []; 19 | 20 | buffers: Map = new Map(); 21 | private indices: Uint16Array; 22 | vao: WebGLVertexArrayObject; 23 | widthSegments: number; 24 | heightSegments: number; 25 | depthSegments: number; 26 | 27 | static TexturePerSide(widthDivisions: number, heightDivisions: number, depthDivisions: number, 28 | left: Texture, right: Texture, top: Texture, bottom: Texture, back: Texture, front: Texture) { 29 | const leftTexture = getTextureForSide(depthDivisions, heightDivisions, left); 30 | const rightTexture = getTextureForSide(depthDivisions, heightDivisions, right); 31 | const topTexture = getTextureForSide(widthDivisions, depthDivisions, top); 32 | const bottomTexture = getTextureForSide(widthDivisions, depthDivisions, bottom); 33 | const backTexture = getTextureForSide(widthDivisions, heightDivisions, back); 34 | const frontTexture = getTextureForSide(widthDivisions, heightDivisions, front); 35 | return [...topTexture, ...bottomTexture, ...leftTexture, ...rightTexture, ...backTexture, ...frontTexture]; 36 | } 37 | 38 | constructor(width_ = 1, height_ = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1, sidesToDraw = 6) { 39 | this.widthSegments = widthSegments; 40 | this.depthSegments = depthSegments; 41 | this.heightSegments = heightSegments; 42 | 43 | this.vao = gl.createVertexArray()!; 44 | const indices: number[] = []; 45 | const uvs: number[] = []; 46 | 47 | let vertexCount = 0; 48 | 49 | const buildPlane = ( 50 | u: 'x' | 'y' | 'z', 51 | v: 'x' | 'y' | 'z', 52 | w: 'x' | 'y' | 'z', 53 | uDir: number, 54 | vDir: number, 55 | width_: number, 56 | height_: number, 57 | depth: number, 58 | gridX: number, 59 | gridY: number, 60 | ) => { 61 | const segmentWidth = width_ / gridX; 62 | const segmentHeight = height_ / gridY; 63 | 64 | const widthHalf = width_ / 2; 65 | const heightHalf = height_ / 2; 66 | const depthHalf = depth / 2; 67 | 68 | const gridX1 = gridX + 1; 69 | const gridY1 = gridY + 1; 70 | 71 | for (let iy = 0; iy < gridY1; iy++) { 72 | const y = iy * segmentHeight - heightHalf; 73 | 74 | for (let ix = 0; ix < gridX1; ix++) { 75 | const vector = new EnhancedDOMPoint(); 76 | 77 | const x = ix * segmentWidth - widthHalf; 78 | 79 | // set values to correct vector component 80 | vector[u] = x * uDir; 81 | vector[v] = y * vDir; 82 | vector[w] = depthHalf; 83 | 84 | // now apply vector to vertex buffer 85 | this.vertices.push(vector); 86 | 87 | uvs.push(ix / gridX); 88 | uvs.push(1 - (iy / gridY)); 89 | } 90 | } 91 | 92 | for (let iy = 0; iy < gridY; iy++) { 93 | for (let ix = 0; ix < gridX; ix++) { 94 | const a = vertexCount + ix + gridX1 * iy; 95 | const b = vertexCount + ix + gridX1 * (iy + 1); 96 | const c = vertexCount + (ix + 1) + gridX1 * (iy + 1); 97 | const d = vertexCount + (ix + 1) + gridX1 * iy; 98 | 99 | // Faces here, this could be updated to populate an array of faces rather than calculating them separately 100 | indices.push(a, b, d); 101 | indices.push(b, c, d); 102 | } 103 | } 104 | 105 | vertexCount += (gridX1 * gridY1); 106 | }; 107 | 108 | const sides = [ 109 | ['x', 'z', 'y', 1, 1, width_, depth, height_, widthSegments, depthSegments], // top 110 | ['x', 'z', 'y', 1, -1, width_, depth, -height_, widthSegments, depthSegments], // bottom 111 | ['z', 'y', 'x', -1, -1, depth, height_, width_, depthSegments, heightSegments], // left 112 | ['z', 'y', 'x', 1, -1, depth, height_, -width_, depthSegments, heightSegments], // right 113 | ['x', 'y', 'z', 1, -1, width_, height_, depth, widthSegments, heightSegments], // front 114 | ['x', 'y', 'z', -1, -1, width_, height_, -depth, widthSegments, heightSegments], // back 115 | ]; 116 | 117 | // @ts-ignore 118 | doTimes(sidesToDraw, index => buildPlane(...sides[index])); 119 | 120 | this.setAttribute_(AttributeLocation.TextureCoords, new Float32Array(uvs), 2); 121 | this.indices = new Uint16Array(indices); 122 | this 123 | .computeNormals() 124 | .done_() 125 | .all_(); 126 | } 127 | 128 | all_() { 129 | this.verticesToActOn = this.vertices; 130 | return this; 131 | } 132 | 133 | invertSelection() { 134 | this.verticesToActOn = this.vertices.filter(vertex => !this.verticesToActOn.includes(vertex)); 135 | return this; 136 | } 137 | 138 | selectBy(callback: (vertex: EnhancedDOMPoint, index: number, array: EnhancedDOMPoint[]) => boolean) { 139 | this.verticesToActOn = this.vertices.filter(callback); 140 | return this; 141 | } 142 | 143 | translate_(x = 0, y = 0, z = 0) { 144 | this.verticesToActOn.forEach(vertex => vertex.add_({x, y, z})); 145 | return this; 146 | } 147 | 148 | scale_(x = 1, y = 1, z = 1) { 149 | const scaleMatrix = new DOMMatrix().scaleSelf(x, y, z); 150 | this.verticesToActOn.forEach(vertex => vertex.set(scaleMatrix.transformPoint(vertex))); 151 | return this; 152 | } 153 | 154 | rotate_(x = 0, y = 0, z = 0) { 155 | const rotationMatrix = new DOMMatrix().rotateSelf(radsToDegrees(x), radsToDegrees(y), radsToDegrees(z)); 156 | this.verticesToActOn.forEach(vertex => vertex.set(rotationMatrix.transformPoint(vertex))); 157 | return this; 158 | } 159 | 160 | modifyEachVertex(callback: (vertex: EnhancedDOMPoint, index: number, array: EnhancedDOMPoint[]) => void) { 161 | this.verticesToActOn.forEach(callback); 162 | return this; 163 | } 164 | 165 | spherify(radius: number) { 166 | this.modifyEachVertex(vertex => { 167 | vertex.normalize_().scale_(radius); 168 | }); 169 | return this; 170 | } 171 | 172 | merge(otherMoldable: MoldableCubeGeometry) { 173 | const updatedOtherIndices = otherMoldable.getIndices()!.map(index => index + this.vertices.length); 174 | this.indices = new Uint16Array([...this.indices, ...updatedOtherIndices]); 175 | 176 | this.vertices.push(...otherMoldable.vertices); 177 | 178 | const thisTextureCoords = this.getAttribute_(AttributeLocation.TextureCoords).data; 179 | const otherTextureCoords = otherMoldable.getAttribute_(AttributeLocation.TextureCoords).data; 180 | const combinedCoords = new Float32Array([...thisTextureCoords, ...otherTextureCoords]); 181 | this.setAttribute_(AttributeLocation.TextureCoords, combinedCoords, 2); 182 | 183 | const thisNormals = this.getAttribute_(AttributeLocation.Normals).data; 184 | const otherNormals = otherMoldable.getAttribute_(AttributeLocation.Normals).data; 185 | const combinedNormals = new Float32Array([...thisNormals, ...otherNormals]); 186 | this.setAttribute_(AttributeLocation.Normals, combinedNormals, 3); 187 | 188 | if (this.getAttribute_(AttributeLocation.TextureDepth)) { 189 | const thisTextureDepth = this.getAttribute_(AttributeLocation.TextureDepth).data; 190 | const otherTextureDepth = otherMoldable.getAttribute_(AttributeLocation.TextureDepth).data; 191 | const combinedTextureDepth = new Float32Array([...thisTextureDepth, ...otherTextureDepth]); 192 | this.setAttribute_(AttributeLocation.TextureDepth, combinedTextureDepth, 1); 193 | } 194 | return this; 195 | } 196 | 197 | cylindrify(radius: number, aroundAxis: 'x' | 'y' | 'z' = 'y', circleCenter: VectorLike = {x: 0, y: 0, z: 0}) { 198 | this.modifyEachVertex(vertex => { 199 | const originalAxis = vertex[aroundAxis]; 200 | vertex[aroundAxis] = 0; 201 | vertex.subtract(circleCenter).normalize_().scale_(radius); 202 | vertex[aroundAxis] = originalAxis; 203 | }); 204 | return this; 205 | } 206 | 207 | spreadTextureCoords(scaleX = 12, scaleY = 12, shiftX = 0, shiftY = 0) { 208 | const texCoordSideCount = (u: number, v: number) => (2 + (u - 1)) * (2 + (v - 1)) * 2; 209 | const xzCount = texCoordSideCount(this.widthSegments, this.depthSegments); 210 | const zyCount = xzCount + texCoordSideCount(this.depthSegments, this.heightSegments); 211 | 212 | const textureCoords = this.getAttribute_(AttributeLocation.TextureCoords).data; 213 | let u,v; 214 | this.vertices.forEach((vert, index) => { 215 | if (index < xzCount) { 216 | u = vert.x; v = vert.z; 217 | } else if (index < zyCount) { 218 | u = vert.z; v = vert.y; 219 | } else { 220 | u = vert.x; v = vert.y; 221 | } 222 | const pointInTextureGrid = [u / scaleX + shiftX, v / scaleY + shiftY]; 223 | textureCoords.set(pointInTextureGrid, index * 2); 224 | }); 225 | this.setAttribute_(AttributeLocation.TextureCoords, textureCoords, 2); 226 | 227 | return this; 228 | } 229 | 230 | 231 | /** 232 | * Computes normals. By default it uses faces on a single plane. Use this on moldable planes or for moldable cube 233 | * shapes where each side should have it's own normals, like a cube, ramp, pyramid, etc. 234 | * 235 | * You can optionally pass the shouldCrossPlanes boolean to tell it to use faces from other sides of the cube to 236 | * compute the normals. Use this for shapes that should appear continuous, like spheres. 237 | */ 238 | computeNormals(shouldCrossPlanes = false) { 239 | const updatedNormals = calculateVertexNormals(this.vertices, shouldCrossPlanes ? this.getIndicesWithUniquePositions() : this.indices); 240 | this.setAttribute_(AttributeLocation.Normals, new Float32Array(updatedNormals.flatMap(point => point.toArray())), 3); 241 | return this; 242 | } 243 | 244 | getIndicesWithUniquePositions() { 245 | const checkedPositions: EnhancedDOMPoint[] = []; 246 | const indexCopy = this.indices.slice(); 247 | 248 | this.verticesToActOn.forEach(selectedVertex => { 249 | if (checkedPositions.find(compareVertex => selectedVertex.isEqualTo(compareVertex))) { 250 | return; 251 | } 252 | 253 | checkedPositions.push(selectedVertex); 254 | 255 | const originalIndex = this.vertices.findIndex(compareVertex => selectedVertex.isEqualTo(compareVertex)); 256 | 257 | this.vertices.forEach((compareVertex, vertexIndex) => { 258 | if (selectedVertex.isEqualTo(compareVertex)) { 259 | const indicesIndex = indexCopy.indexOf(vertexIndex); 260 | indexCopy[indicesIndex] = originalIndex; 261 | } 262 | }) 263 | }); 264 | 265 | return indexCopy; 266 | } 267 | 268 | done_() { 269 | this.setAttribute_(AttributeLocation.Positions, new Float32Array(this.vertices.flatMap(point => point.toArray())), 3); 270 | return this; 271 | } 272 | 273 | getAttribute_(attributeLocation: AttributeLocation) { 274 | return this.buffers.get(attributeLocation)!; 275 | } 276 | 277 | setAttribute_(attributeLocation: AttributeLocation, data: Float32Array, size: number) { 278 | this.buffers.set(attributeLocation, { data, size }); 279 | return this; 280 | } 281 | 282 | getIndices(): Uint16Array { 283 | return this.indices; 284 | } 285 | 286 | bindGeometry() { 287 | const fullSize = [...this.buffers.values()].reduce((total, current) => total += current.data.length , 0); 288 | const fullBuffer = new Float32Array(fullSize); 289 | 290 | gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()!); 291 | 292 | gl.bindVertexArray(this.vao); 293 | 294 | let byteOffset = 0; 295 | let lengthOffset = 0; 296 | this.buffers.forEach((buffer, position_) => { 297 | gl.vertexAttribPointer(position_, buffer.size, gl.FLOAT, false, 0, byteOffset); 298 | gl.enableVertexAttribArray(position_); 299 | fullBuffer.set(buffer.data, lengthOffset); 300 | 301 | byteOffset += buffer.data.length * buffer.data.BYTES_PER_ELEMENT; 302 | lengthOffset+= buffer.data.length; 303 | }); 304 | 305 | gl.bufferData(gl.ARRAY_BUFFER, fullBuffer, gl.STATIC_DRAW); 306 | 307 | 308 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()!); 309 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/textures.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AllSvgAttributes, 3 | ellipse, 4 | feColorMatrix, 5 | feComponentTransfer, 6 | feComposite, 7 | feDiffuseLighting, 8 | feDisplacementMap, 9 | feDistantLight, 10 | feFunc, 11 | feMorphology, 12 | feTurbulence, 13 | filter, 14 | group, 15 | linearGradient, 16 | mask, 17 | NoiseType, 18 | radialGradient, 19 | rect, 20 | svg, 21 | SvgAttributes, 22 | svgStop, 23 | SvgTextAttributes, 24 | text 25 | } from '@/engine/svg-maker/base'; 26 | import { toHeightmap, toImage } from '@/engine/svg-maker/converters'; 27 | import { doTimes } from '@/engine/helpers'; 28 | import { Material } from '@/engine/renderer/material'; 29 | import { textureLoader } from '@/engine/renderer/texture-loader'; 30 | 31 | const textureSize = 512; 32 | const skyboxSize = 2048; 33 | 34 | const fullSize = (otherProps: Partial): Partial => ({ x: 0, y: 0, width_: '100%', height_: '100%', ...otherProps }); 35 | 36 | export const materials: {[key: string]: Material} = {}; 37 | export const skyboxes: {[key: string]: TexImageSource[]} = {}; 38 | 39 | export async function initTextures() { 40 | materials.grass = new Material({texture: textureLoader.load_(await drawGrass())}); 41 | // materials.grass.texture!.textureRepeat.x = 160; 42 | // materials.grass.texture!.textureRepeat.y = 10; 43 | 44 | materials.brickWall = new Material({ texture: textureLoader.load_(await bricksRocksPlanksWood(true, true))}); 45 | materials.brickWall.texture?.textureRepeat.set(1.5, 1.5); 46 | materials.stone = new Material({texture: textureLoader.load_(await bricksRocksPlanksWood(true, false))}); 47 | materials.wood = new Material({ texture: textureLoader.load_(await bricksRocksPlanksWood(false, false))}); 48 | materials.planks = new Material({ texture: textureLoader.load_(await bricksRocksPlanksWood(false, true))}); 49 | materials.castleWriting = new Material({ texture: textureLoader.load_(await castleSign()), isTransparent: true, emissive: [0.5, 0.5, 0.5, 0.5] }); 50 | materials.handprint = new Material({ texture: textureLoader.load_(await handprint()), isTransparent: true, emissive: [0.5, 0.5, 0.5, 0.5] }); 51 | materials.face = new Material({ texture: textureLoader.load_(await face())}); 52 | materials.bloodCircle = new Material({ texture: textureLoader.load_(await drawBloodCircle()), isTransparent: true }); 53 | materials.gold = new Material({ texture: textureLoader.load_(await metals(0)), emissive: [0.7, 0.7, 0.7, 0.7] }); 54 | materials.silver = new Material({ texture: textureLoader.load_(await metals(1)) }); 55 | materials.iron = new Material({ texture: textureLoader.load_(await metals(2)) }); 56 | materials.keyLock = new Material({ texture: textureLoader.load_(await keyLock())}); 57 | materials.banner = new Material({ texture: textureLoader.load_(await banner()) }); 58 | materials.bannerIcon = new Material({ texture: textureLoader.load_(await bannerIcon() )}); 59 | 60 | const testSlicer = drawSkyboxHor(); 61 | const horSlices = [await testSlicer(), await testSlicer(), await testSlicer(), await testSlicer()]; 62 | skyboxes.test = [ 63 | horSlices[3], 64 | horSlices[1], 65 | await toImage(drawSkyboxTop()), 66 | horSlices[0], // Floor 67 | horSlices[2], 68 | horSlices[0], 69 | ]; 70 | 71 | textureLoader.bindTextures(); 72 | } 73 | 74 | function castleSign() { 75 | return toImage( 76 | svg({ width_: textureSize, height_: textureSize }, 77 | drawBloodText({ x: 10, y: '30%', style: 'font-size: 120px; transform: scaleY(1.5); font-family: sans-serif' }, 'KEEP', 30) 78 | ) 79 | ) 80 | } 81 | 82 | function handprint() { 83 | return toImage( 84 | svg({ width_: textureSize, height_: textureSize }, 85 | drawBloodText({ x: 10, y: '30%', style: 'font-size: 120px; transform: scaleY(1.5); font-family: sans-serif' }, '🖐️', 50) 86 | ) 87 | ) 88 | } 89 | 90 | function stars() { 91 | return filter(fullSize({id_: 's'}), 92 | feTurbulence({ baseFrequency: 0.2, stitchTiles_: 'stitch' }), 93 | feColorMatrix({ values: [ 94 | 0, 0, 0, 9, -5.5, 95 | 0, 0, 0, 9, -5.5, 96 | 0, 0, 0, 9, -5.5, 97 | 0, 0, 0, 0, 1 98 | ] 99 | }) 100 | ); 101 | } 102 | 103 | function drawClouds() { 104 | return stars() + filter(fullSize({ id_: 'f' }), 105 | feTurbulence({ seed_: 2, type_: NoiseType.Fractal, numOctaves_: 6, baseFrequency: 0.003, stitchTiles_: 'stitch' }), 106 | feComponentTransfer({}, 107 | feFunc('R', 'table', [0.8, 0.8]), 108 | feFunc('G', 'table', [0.8, 0.8]), 109 | feFunc('B', 'table', [1, 1]), 110 | feFunc('A', 'table', [0, 0, 1]) 111 | ) 112 | ) + 113 | mask({ id_: 'mask' }, 114 | radialGradient({ id_: 'g' }, 115 | svgStop({ offset_: '20%', stopColor: 'white' }), 116 | svgStop({ offset_: '30%', stopColor: '#666' }), 117 | svgStop({ offset_: '100%', stopColor: 'black' }) 118 | ), 119 | ellipse({ cx: 1000, cy: 1000, rx: '50%', ry: '50%', fill: 'url(#g)'}) 120 | ) 121 | + radialGradient({ id_: 'l' }, 122 | svgStop({ offset_: '10%', stopColor: '#fff' }), 123 | svgStop({ offset_: '30%', stopColor: '#0000' }) 124 | ) 125 | + rect(fullSize({ filter: 's' })) 126 | + ellipse({cx: 1000, cy: 1000, rx: 200, ry: 200, fill: 'url(#l)' }) 127 | + rect(fullSize({ filter: 'f', mask: 'url(#mask)' })); 128 | } 129 | 130 | function drawBetterClouds(width_: number) { 131 | const seeds = [2, 4]; 132 | const numOctaves = [6, 6]; 133 | const baseFrequencies = [0.005, 0.003]; 134 | const heights = [160, 820]; 135 | const yPositions = [800, 0]; 136 | const alphaTableValues = [ 137 | [0, 0, 0.6], 138 | [0, 0, 1.5] 139 | ]; 140 | 141 | const clouds = doTimes(2, index => { 142 | return filter(fullSize({ id_: `f${index}` }), 143 | feTurbulence({ seed_: seeds[index], type_: NoiseType.Fractal, numOctaves_: numOctaves[index], baseFrequency: baseFrequencies[index], stitchTiles_: 'stitch' }), 144 | feComponentTransfer({}, 145 | feFunc('R', 'table', [0.2, 0.2]), 146 | feFunc('G', 'table', [0.2, 0.2]), 147 | feFunc('B', 'table', [0.25, 0.25]), 148 | feFunc('A', 'table', alphaTableValues[index]) 149 | ) 150 | ) + 151 | linearGradient({ id_: `g${index}`, gradientTransform: 'rotate(90)'}, 152 | svgStop({ offset_: 0, stopColor: 'black'}), 153 | svgStop({ offset_: 0.3, stopColor: 'white'}), 154 | svgStop({ offset_: 0.7, stopColor: 'white'}), 155 | svgStop({ offset_: 1, stopColor: 'black'}), 156 | ) + 157 | mask({ id_: `m${index}`}, 158 | rect({ x: 0, y: yPositions[index], width_, height_: heights[index], fill: `url(#g${index})`}) 159 | ) + 160 | rect({ filter: `f${index}`, height_: heights[index], width_, x: 0, y: yPositions[index], mask: `url(#m${index})`}); 161 | }).join(''); 162 | 163 | return stars() + rect(fullSize({ filter: 's' })) + clouds; 164 | } 165 | 166 | 167 | function drawSkyboxHor() { 168 | return horizontalSkyboxSlice({ width_: skyboxSize * 4, height_: skyboxSize, style: `background: #000;` }, 169 | drawBetterClouds(skyboxSize * 4), 170 | //y: number, color: string, seed_: number, numOctaves: number 171 | filter({ id_: 'f', x: 0, width_: '100%', height_: '150%' }, 172 | feTurbulence({ type_: NoiseType.Fractal, baseFrequency: [0.008, 0], numOctaves_: 4, seed_: 15, stitchTiles_: 'stitch' }), 173 | feDisplacementMap({ in: 'SourceGraphic', scale_: 100 }), 174 | ) + 175 | filter({ id_: 'g', x: 0, width_: '100%' }, 176 | feTurbulence({ baseFrequency: [0.02, 0.01], numOctaves_: 4, type_: NoiseType.Fractal, result: `n`, seed_: 15, stitchTiles_: 'stitch' }), 177 | feDiffuseLighting({ in: 'n', lightingColor: '#1c1d2d', surfaceScale: 22 }, 178 | feDistantLight(45, 60) 179 | ), 180 | feComposite({ in2: 'SourceGraphic', operator: 'in' }), 181 | ) + 182 | group({ filter: 'g' }, 183 | rect({ x: 0, y: 1000, width_: skyboxSize * 4, height_: '50%', filter: 'f'}) 184 | ) 185 | ); 186 | } 187 | 188 | function drawSkyboxTop() { 189 | return svg({ width_: skyboxSize, height_: skyboxSize, style: `background: #000;` }, drawClouds()); 190 | } 191 | 192 | function horizontalSkyboxSlice(svgSetting: SvgAttributes, ...elements: string[]) { 193 | let xPos = 0; 194 | const context = new OffscreenCanvas(skyboxSize, skyboxSize).getContext('2d')!; 195 | 196 | return async (): Promise => { 197 | // @ts-ignore 198 | context.drawImage(await toImage(svg(svgSetting, ...elements)), xPos, 0); 199 | xPos -= skyboxSize; 200 | // @ts-ignore 201 | return context.getImageData(0, 0, skyboxSize, skyboxSize); 202 | }; 203 | } 204 | 205 | export function drawGrass() { 206 | return toImage(svg({ width_: textureSize, height_: textureSize }, 207 | filter(fullSize({ id_: 'n' }), 208 | feTurbulence({ seed_: 3, type_: NoiseType.Fractal, baseFrequency: 0.04, numOctaves_: 4, stitchTiles_: 'stitch' }), 209 | feMorphology({ operator: 'dilate', radius: 3 }), 210 | feComponentTransfer({}, 211 | feFunc('R', 'table', [0.2, 0.2]), 212 | feFunc('G', 'table', [0.2, 0.2]), 213 | feFunc('B', 'table', [0.25, 0.25]), 214 | // feFunc('A', 'table', [0.]) 215 | ) 216 | ), 217 | rect(fullSize({ fill: '#171717' })), 218 | rect(fullSize({ filter: 'n' })), 219 | )); 220 | } 221 | 222 | function getPattern(width_ = 160, height_ = 256) { 223 | return ``; 224 | } 225 | 226 | function rockWoodFilter(isRock = true) { 227 | return filter(fullSize({ id_: 'rw' }), 228 | `` + 229 | feTurbulence({ type_: NoiseType.Fractal, baseFrequency: isRock ? 0.007 : [0.1, 0.007], numOctaves_: isRock ? 9 : 6, stitchTiles_: 'stitch' }), 230 | feComposite({ in: 's', operator: 'arithmetic', k2: isRock ? 0.5 : 0.5, k3: 0.5 }), 231 | feComponentTransfer({}, feFunc('A', 'table', [0, .1, .2, .3, .4, .2, .4, .2, .4])), 232 | feDiffuseLighting({ surfaceScale: 2.5, lightingColor: isRock ? '#ffd' : '#6e5e42' }, 233 | feDistantLight(isRock ? 265 : 110, isRock ? 4 : 10), 234 | ), 235 | fffix(), 236 | ) 237 | } 238 | 239 | function bricksRocksPlanksWood(isRock = true, isPattern = true) { 240 | return toImage(svg({ width_: 512, height_: 512 }, 241 | (isPattern ? getPattern( isRock ? 160 : 75, isRock ? 256 : 1) : '') + 242 | rockWoodFilter(isRock), 243 | rect({ x: 0, y: 0, width_: '100%', height_: '100%', fill: isPattern ? 'url(#p)' : undefined, filter: 'rw' }) 244 | )); 245 | } 246 | 247 | export function drawBloodCircle() { 248 | return toImage( 249 | svg({ width_: textureSize, height_: textureSize }, 250 | bloodEffect(ellipse({ cx: 256, cy: 256, rx: 220, ry: 220, filter: 'd' }), 250, [0.03, 0.03]) 251 | ) 252 | ); 253 | } 254 | 255 | export function drawBloodText(attributes: SvgTextAttributes, textToDisplay: string, scale = 70) { 256 | return bloodEffect(text({ style: 'font-size: 360px; transform: scaleY(1.5);', ...attributes, filter: 'd' }, textToDisplay), scale) 257 | } 258 | 259 | export function bloodEffect(component: string, scale_ = 70, freq1: [number, number] = [0.13, 0.02], freq2 = 0.04) { 260 | return filter({ id_: 'd' }, 261 | feTurbulence({ baseFrequency: freq1, numOctaves_: 1, type_: NoiseType.Fractal, result: 'd' }), 262 | feDisplacementMap({ in: 'SourceGraphic', in2: 'd', scale_ }), 263 | ) + 264 | filter({ id_: 'b' }, 265 | feTurbulence({ baseFrequency: freq2, numOctaves_: 1, type_: NoiseType.Fractal }), 266 | feColorMatrix({ values: [ 267 | 0.4, 0.2, 0.2, 0, -0.1, 268 | 0, 2, 0, 0, -1.35, 269 | 0, 2, 0, 0, -1.35, 270 | 0, 0, 0, 0, 1, 271 | ] }), 272 | feComposite({ in2: 'SourceGraphic', operator: 'in' }), 273 | ) + 274 | group({ filter: 'b' }, component); 275 | } 276 | 277 | export function face() { 278 | return toImage(svg({ width_: 512, height_: 512, style: 'filter: invert()', viewBox: '0 0 512 512' }, 279 | filter({ id_: 'filter', x: '-0.01%', primitiveUnits: 'objectBoundingBox', width_: '100%', height_: '100%'}, 280 | feTurbulence({ seed_: 7, type_: NoiseType.Fractal, baseFrequency: 0.005, numOctaves_: 5, result: 'n'}), 281 | feComposite({ in: 'SourceAlpha', operator: 'in' }), 282 | feDisplacementMap({ in2: 'n', scale_: 0.9 }) 283 | ), 284 | rect(fullSize({ id_: 'l', filter: 'filter', y: -14 })), 285 | rect({ fill: '#fff', width_: '100%', height_: '100%' }), 286 | ` 287 | 288 | `, 289 | rect({ fill: '#777', x: 220, y: 230, width_: 50, height_: 50 }) 290 | )); 291 | } 292 | 293 | const matrices = [ 294 | [ 295 | 0.4, 0.5, 0.4, 0, 0.3, 296 | 0.2, 0.6, 0.2, 0, 0.3, 297 | 0, 0, 0.1, 0, 0, 298 | 1, 0, 0, 0, 1, 299 | ], 300 | 301 | [ 302 | 0.1, 0.1, 0.1, 0, -0.05, 303 | 0.1, 0.1, 0.1, 0, -0.05, 304 | 0.1, 0.1, 0.1, 0, -0.05, 305 | 0, 0, 0, 0, 1, 306 | ], 307 | 308 | [ 309 | 0.07, 0.05, 0.06, 0, -0.1, 310 | 0.07, 0.05, 0.06, 0, -0.1, 311 | 0.07, 0.05, 0.06, 0, -0.1, 312 | 0, 0, 0, 0, 1, 313 | ] 314 | ]; 315 | 316 | export function metals(goldSilverIron: number, isHeightmap = false) { 317 | const method = isHeightmap ? toHeightmap : toImage; 318 | return method(svg({ width_: isHeightmap ? 32 : 512, height_: isHeightmap ? 32 : 512 }, 319 | filter({ id_: 'b' }, 320 | feTurbulence({ baseFrequency: (goldSilverIron < 2 ? [0.1, 0.004] : 1.2), numOctaves_: (goldSilverIron < 2 ? 1 : 5), type_: NoiseType.Fractal }), 321 | feColorMatrix({ values: matrices[goldSilverIron] }) 322 | ), 323 | rect(fullSize({ filter: 'b' })), 324 | ), 1); 325 | } 326 | 327 | export function testHeightmap() { 328 | return metals(1, true); 329 | } 330 | 331 | function keyLock() { 332 | return toImage(svg({ width_: 512, height_: 512 }, 333 | filter({ id_: 'b' }, 334 | feTurbulence({ baseFrequency: [0.1, 0.004], numOctaves_: 1, type_: NoiseType.Fractal }), 335 | feColorMatrix({ values: matrices[0] }) 336 | ), 337 | rect(fullSize({ filter: 'b' })), 338 | ellipse({ cx: 256, cy: 170, rx: 100, ry: 100, fill: '#000'}), 339 | rect({ x: 216, y: 260, width_: 80, height_: 160 }) 340 | )); 341 | } 342 | 343 | 344 | function fffix() { 345 | if (navigator.userAgent.includes('fox')) { 346 | return feComponentTransfer({}, 347 | feFunc('R', 'gamma', []), 348 | feFunc('G', 'gamma', []), 349 | feFunc('B', 'gamma', []), 350 | feFunc('A', 'gamma', []) 351 | ); 352 | } else { 353 | return ''; 354 | } 355 | } 356 | 357 | const bannerColor = '#460c0c'; 358 | const symbolColor = '#ce9b3c'; 359 | 360 | function banner() { 361 | return toImage(svg({ width_: 512, height_: 512, }, 362 | rect(fullSize({ fill: bannerColor })), 363 | )); 364 | } 365 | 366 | function bannerIcon() { 367 | return toImage(svg({ width_: 512, height_: 512, }, 368 | rect(fullSize({ fill: bannerColor })), 369 | ellipse({ cx: 256, cy: 128, ry: 128, rx: 128, fill: symbolColor }), 370 | ellipse({ cx: 256, cy: 384, ry: 128, rx: 128, fill: symbolColor }), 371 | ellipse({ cx: 256, cy: 448, ry: 128, rx: 128, fill: bannerColor }) 372 | )); 373 | } 374 | 375 | 376 | -------------------------------------------------------------------------------- /src/modeling/castle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createBox, 3 | createHallway, 4 | createStairs, 5 | mergeCubes, 6 | patternFill, 7 | SegmentedWall 8 | } from '@/modeling/building-blocks'; 9 | import { doTimes } from '@/engine/helpers'; 10 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 11 | 12 | // TODO: Build castle at 0, translate whole thing up to 21 13 | const windowTopHeight = 6; 14 | const windowBottomHeight = 2; 15 | const doorTopHeight = 5; 16 | const windowWidth = 2; 17 | const doorWidth = 4; 18 | 19 | export const castleContainer: { value?: MoldableCubeGeometry } = { value: undefined }; 20 | 21 | export const frontLeftCornerRoom = [ 22 | // First Floor 23 | [ 24 | // Front Wall 25 | [ 26 | [9, 4, 5, 2, 2], [12, 5, 12, 6, 12], [0, 0, 0, 2, 0] 27 | ], 28 | // Back Wall 29 | [ 30 | [11, 11], [12, 12], [0,0] 31 | ], 32 | // Left Wall 33 | [ 34 | [4, 3, 13], [5, 12, 12], [0, 0, 0] 35 | ], 36 | // Right Wall 37 | [ 38 | [10, 10], [12, 12], [0, 0] 39 | ] 40 | ], 41 | // Second Floor 42 | [ 43 | // Front Wall 44 | [ 45 | [9, 4, 4, 2, 3], [12, 5, 12, 4, 12], [0, 0, 0, 3, 0] 46 | ], 47 | // Back Wall 48 | [ 49 | [10, 2, 10], [12, 3, 12], [0, 2, 0] 50 | ], 51 | // Left Wall 52 | [ 53 | [2, 2, 4, 4, 8], [12, 4, 12, 5, 12], [0, 2, 0, 0, 0] 54 | ], 55 | // Right Wall 56 | [ 57 | [9, 2, 9], [12, 3, 12], [0, 2, 0] 58 | ] 59 | ] 60 | ]; 61 | 62 | export const rearRightCornerRoom = structuredClone(frontLeftCornerRoom); 63 | rearRightCornerRoom[0][0] = [[9.25, doorWidth - 0.5, 6.25, 3], [12, 5, 12, 5], [0]]; 64 | rearRightCornerRoom[1][0] = [[9, 4, 4, 2, 3], [12, 5, 12, 4, 12], [0, 0, 0, 2, 0]]; 65 | 66 | export const otherCorners = [ 67 | // First Floor 68 | [ 69 | // Front Wall 70 | [ 71 | [2, 2, 5, 4, 5, 2, 2], [12, windowTopHeight, 12, doorTopHeight, 12, windowTopHeight, 12], [0, windowBottomHeight, 0, 0, 0, windowBottomHeight, 0] 72 | ], 73 | // Back Wall 74 | [ 75 | [3, 2, 12, 2, 3], [12, windowTopHeight, 12, 12, 12], [0, windowBottomHeight, 0, 0, 0] 76 | ], 77 | // Left Wall 78 | [ 79 | [1, 2, 14, 2, 1], [12, windowTopHeight, 12, 12, 12], [0, windowBottomHeight, 0, 0, 0] 80 | ], 81 | // Right Wall 82 | [ 83 | [1, 2, 14, 2, 1], [12, windowTopHeight, 12, windowTopHeight, 12], [0, windowBottomHeight, 0, windowBottomHeight, 0] 84 | ] 85 | ], 86 | // Second Floor 87 | [ 88 | // Front Wall 89 | [ 90 | [2, 2, 5, 4, 5, 2, 2], [12, windowTopHeight, 12, doorTopHeight, 12, windowTopHeight, 12], [0, windowBottomHeight, 0, 0, 0, windowBottomHeight, 0] 91 | ], 92 | // Back Wall 93 | [ 94 | [3, 2, 5, 2, 5, 2, 3], [12, windowTopHeight, 12, windowTopHeight, 12, windowTopHeight, 12], [0, windowBottomHeight, 0, windowBottomHeight, 0, windowBottomHeight, 0] 95 | ], 96 | // Left Wall 97 | [ 98 | [1, 2, 14, 2, 1], [12, windowTopHeight, 12, windowTopHeight, 12], [0, windowBottomHeight, 0, windowBottomHeight, 0] 99 | ], 100 | // Right Wall 101 | [ 102 | [3, 4, 6, 4, 3], [12, windowTopHeight, 12, windowTopHeight, 12], [0, windowBottomHeight, 0, windowBottomHeight, 0] 103 | ] 104 | ] 105 | ] 106 | 107 | export const getSize = (sizes: number[]) => sizes.reduce((acc, curr) => acc + curr); 108 | 109 | export function createCastle() { 110 | 111 | return solidCastleWall(-60, true) // front Wall 112 | 113 | // front-right Corner 114 | .merge(corner(otherCorners, true) 115 | .merge(cornerRamp()) 116 | .merge(castleTopper(5, 0, 0).rotate_(Math.PI / 2, -1, 0).translate_(-20, 1, 13)) 117 | .translate_(53, 0, -60) 118 | .computeNormals() 119 | ) 120 | 121 | // front-left Corner 122 | .merge( 123 | // walls 124 | corner(frontLeftCornerRoom, true) 125 | // floors 126 | .merge(createCastleFloors(getSize(frontLeftCornerRoom[0][2][0]), getSize(frontLeftCornerRoom[0][0][0]))) 127 | .translate_(-53, 0, -60) 128 | ) 129 | 130 | // rear-left Corner 131 | .merge( 132 | corner(otherCorners, true, true) 133 | .scale_(-1, 1, -1) 134 | .computeNormals(true) 135 | .merge( 136 | cornerRamp(true, true) 137 | .rotate_(0, -Math.PI / 2) 138 | ) 139 | // floors 140 | .merge( 141 | createCastleFloors(getSize(otherCorners[0][2][0]), getSize(otherCorners[0][0][0]), true, true) 142 | ) 143 | .translate_(-53, 0, 60) 144 | ) 145 | 146 | // rear-right corner 147 | .merge( 148 | corner(rearRightCornerRoom, true, true) 149 | .rotate_(0, Math.PI) 150 | .computeNormals(true) 151 | .merge( 152 | cornerRamp(true, true) 153 | .rotate_(0, -Math.PI / 4) 154 | ) 155 | // floors 156 | .merge( 157 | createCastleFloors(getSize(rearRightCornerRoom[0][2][0]), getSize(rearRightCornerRoom[0][0][0]), true, true) 158 | ) 159 | .translate_(53, 0, 60)) // rear-left Corner 160 | 161 | 162 | .merge(solidCastleWall(60)) // back Wall 163 | 164 | // Left Wall 165 | .merge(hollowCastleWall(-53)) 166 | .merge(hollowCastleWall(53)) 167 | .merge(castleKeep()) 168 | 169 | // Key pedestal 170 | .merge(new MoldableCubeGeometry(2, 3, 2, 2, 1, 2).cylindrify(1.2).translate_(-32,13.1,59.5).spreadTextureCoords()) 171 | 172 | // Key doorway 173 | .merge(new SegmentedWall([1.5, 4, 1.5], 8, [8, 1, 8], [0], 0, 0, 0.5).rotate_(0, Math.PI / 2).translate_(-27, 11.5, 59.5).computeNormals()) 174 | 175 | .done_(); 176 | } 177 | 178 | function castleKeep() { 179 | return corner([ 180 | [ 181 | [ 182 | [54], [12], [0], 183 | ], 184 | [ 185 | [19, 2, 2, 8, 2, 2, 19], [12, 2, 12, 5, 12, 2, 12], [0, 3, 0, 0, 0, 3, 0], 186 | ], 187 | [ 188 | [18, 2, 8, 2, 8, 2, 8, 2, 18], [12, 4, 12, 4, 12, 4, 12, 4, 12], [0, 3, 0, 3, 0, 3, 0, 3, 0] 189 | ], 190 | [ 191 | [18, 2, 8, 2, 8, 2, 8, 2, 18], [12, 4, 12, 4, 12, 4, 12, 4, 12], [0, 3, 0, 3, 0, 3, 0, 3, 0] 192 | ], 193 | ], 194 | [ 195 | [ 196 | [3, 12, 39], [12, 6, 12], [0, 2, 0], 197 | ], 198 | [ 199 | [15.5, 2, 5, 2, 5, 2, 5, 2, 15.5], [12, 3, 12, 3, 12, 3, 12, 3, 12], [0, 3, 0, 3, 0, 3, 0, 3, 0], 200 | ], 201 | [ 202 | [18, 2, 8, 2, 8, 2, 8, 2, 18], [12, 4, 12, 4, 12, 4, 12, 4, 12], [0, 3, 0, 3, 0, 3, 0, 3, 0] 203 | ], 204 | [ 205 | [18, 2, 8, 2, 8, 2, 8, 2, 18], [12, 4, 12, 4, 12, 4, 12, 4, 12], [0, 3, 0, 3, 0, 3, 0, 3, 0] 206 | ], 207 | ] 208 | ], true) 209 | // backdrop 210 | .merge(new MoldableCubeGeometry(22, 12, 24).translate_(0, 6, 22).spreadTextureCoords()) 211 | // ceiling 212 | .merge( 213 | new SegmentedWall([2.5, 9, 4, 19, 4, 9, 2.5], 69, [69, 5, 69, 7.5, 69, 50, 69], [0, 5, 0, 45, 0, 5, 0], 0, 0, 2) 214 | .rotate_(Math.PI / 2) 215 | .translate_(0, 22.5, -34.5) 216 | ) 217 | .merge(new MoldableCubeGeometry(9, 2, 38).translate_(-18, 22.5, 0).spreadTextureCoords().translate_(0, 0, 1.5)) 218 | 219 | 220 | // Ramp to second level 221 | .merge(cornerRamp(false, false, false).rotate_(0, -Math.PI / 2).translate_(16, 0.5, 25)) 222 | 223 | // Ramp to lever 224 | .merge( 225 | new MoldableCubeGeometry(4, 12, 20) 226 | .selectBy(vert => vert.y > 0 && vert.z < 0) 227 | .translate_(0, -12) 228 | .all_() 229 | .translate_(-18, 6, 16) 230 | .spreadTextureCoords() 231 | .merge(new MoldableCubeGeometry(14, 12, 8).translate_(-18, 6, 30).spreadTextureCoords()) 232 | ) 233 | 234 | // Transition to roof 235 | .merge( 236 | corner([ 237 | [ 238 | [ 239 | [2, 18, 2], [12, 1, 5], [0], 240 | ], 241 | [ 242 | [22], [12], [0], 243 | ], 244 | [ 245 | [4, 16], [5, 12], [0], 246 | ], 247 | [ 248 | [20], [12], [0], 249 | ], 250 | ], 251 | [ 252 | [ 253 | [22], [12], [0], 254 | ], 255 | [ 256 | [22], [12], [0], 257 | ], 258 | [ 259 | [1, 4, 15], [12, 5, 12], [0], 260 | ], 261 | [ 262 | [20], [12], [0], 263 | ], 264 | ] 265 | ], true) 266 | .merge(cornerRamp(false, false, false)) 267 | .translate_(0, 12, 22) 268 | ) 269 | 270 | // Final Tower 271 | .merge(corner([ 272 | [ 273 | [ 274 | [22], [12], [0], 275 | ], 276 | [ 277 | [10, 2, 10], [12, 12, 12], [0], 278 | ], 279 | [ 280 | [20], [12], [0], 281 | ], 282 | [ 283 | [20], [12], [0], 284 | ], 285 | ], 286 | [ 287 | [ 288 | [9, 4, 9], [12, 5, 12], [0], 289 | ], 290 | [ 291 | [10, 2, 10], [12, 7, 12], [0, 0.6, 0], 292 | ], 293 | [ 294 | [20], [12], [0], 295 | ], 296 | [ 297 | [20], [12], [0], 298 | ], 299 | ] 300 | ], true).selectBy(vert => vert.z < -10 && Math.abs(vert.x) <= 2 && vert.y > 11 && vert.y < 13) 301 | .translate_(0, -0.5, 0.5) 302 | .all_() 303 | .merge(createCastleFloors(21, 20)) 304 | .translate_(0, 22.5, -22)) 305 | 306 | // Ramp to final tower 307 | .merge(cornerRamp(false, true, false).translate_(-6.5, 23, -5)) 308 | 309 | // Corner separator for ramp 310 | .merge(new MoldableCubeGeometry(10, 24, 12).translate_(16, 10, 23).spreadTextureCoords()) 311 | 312 | 313 | // Floor 314 | .merge(new MoldableCubeGeometry(53, 0.5, 68).spreadTextureCoords()) 315 | 316 | // columns 317 | .merge(new MoldableCubeGeometry(2, 24, 2, 3, 1, 3).cylindrify(2).translate_(-10, 11).computeNormals(true).spreadTextureCoords()) 318 | .merge(new MoldableCubeGeometry(2, 24, 2, 3, 1, 3).cylindrify(2).translate_(-10, 11, -20).computeNormals(true).spreadTextureCoords()) 319 | .merge(new MoldableCubeGeometry(2, 24, 2, 3, 1, 3).cylindrify(2).translate_(10, 11).computeNormals(true).spreadTextureCoords()) 320 | .merge(new MoldableCubeGeometry(2, 24, 2, 3, 1, 3).cylindrify(2).translate_(10, 11, -20).computeNormals(true).spreadTextureCoords()) 321 | 322 | // banner holders 323 | .merge(new MoldableCubeGeometry(5, 0.8, 1.5).translate_(18, 18, -36).spreadTextureCoords()) 324 | .merge(new MoldableCubeGeometry(5, 0.8, 1.5).translate_(-18, 18, -36).spreadTextureCoords()) 325 | 326 | .translate_(0, 0, 20) 327 | .computeNormals(); 328 | } 329 | 330 | 331 | export function createCastleFloors(width_: number, depth: number, skipMiddle?: boolean, cylindrify?: boolean, skipTop?: boolean) { 332 | const cyli = (cube: MoldableCubeGeometry) => cylindrify ? cube.cylindrify(12) : cube; 333 | const floors = cyli(new MoldableCubeGeometry(width_, 1, depth, cylindrify ? 4 : 1, 1, cylindrify ? 4 : 1).translate_(0,cylindrify ? -0.3 : -0.4)).spreadTextureCoords(); 334 | 335 | if (!skipTop) { 336 | floors.merge(cyli(new MoldableCubeGeometry(width_, 1, depth, cylindrify ? 4 : 1, 1, cylindrify ? 4 : 1).translate_(0, 23)).spreadTextureCoords()); 337 | } 338 | 339 | if (!skipMiddle) { 340 | floors.merge(cyli(new MoldableCubeGeometry(width_, 1, depth, cylindrify ? 4 : 1, 1, cylindrify ? 4 : 1).translate_(0, 11)).spreadTextureCoords()); 341 | } 342 | 343 | return floors; 344 | } 345 | 346 | 347 | 348 | export function castleTopper(length: number, startingHeight: number, zPos: number, isRounded = false) { 349 | const segmentWidths = patternFill([1, 2], length * 2); 350 | return new SegmentedWall(segmentWidths, 3, patternFill([1.1, 3], segmentWidths.length / 3), [0, 0], 0, 0, 2, isRounded, isRounded) 351 | .rotate_(0, 0, Math.PI) 352 | .translate_(0, startingHeight + 3, zPos) 353 | .computeNormals(); 354 | } 355 | 356 | export function solidCastleWall(z: number, hasDoor?: boolean) { 357 | return new SegmentedWall([36, 12, 36], 11.5, [12, hasDoor ? 1 : 12, 12], [0, 0, 0], 0, 0, 8) 358 | .merge(castleTopper(hasDoor ? 76 : 82, 11.5, 4).translate_(hasDoor ? -4 : 0)) 359 | .merge(castleTopper(hasDoor ? 85 : 13, 11.5, -4).translate_(hasDoor ? 0 : 33.5)) 360 | .translate_(0,0, z) 361 | .done_(); 362 | } 363 | 364 | export function hollowCastleWall(x: number) { 365 | const walls = [ 366 | new SegmentedWall(patternFill([5, 2, 5], 24), 12, patternFill([12, 3, 12], 24), patternFill([0, 2, 0], 24), 0, 0), 367 | new MoldableCubeGeometry(96, 24, 2).spreadTextureCoords() 368 | ]; 369 | if (x > 0) { 370 | walls.reverse(); 371 | } 372 | // @ts-ignore 373 | return createHallway(...walls, 5) 374 | .merge(castleTopper(95, 12, 6)) 375 | .merge(castleTopper(95, 12, -6)) 376 | .merge(createCastleFloors(98, 9, false, false, true)) 377 | .rotate_(0, Math.PI / 2, 0) 378 | .translate_(x) 379 | .computeNormals(); 380 | } 381 | 382 | export function corner(floors: number[][][][], isTopped?: boolean, isRounded?: boolean) { 383 | const rooms = floors.map((walls, floorNumber) => { 384 | const segmentedWalls = walls.map(wall => { 385 | return new SegmentedWall(wall[0], 12, wall[1], wall[2], 0, floorNumber * 12, 2, isRounded, isRounded); 386 | }); 387 | 388 | // @ts-ignore 389 | return isRounded ? tubify(createBox(...segmentedWalls), 10, 11, 13.5) : createBox(...segmentedWalls); 390 | }); 391 | 392 | if (isTopped) { 393 | const top = createBox( 394 | castleTopper(getSize(floors[0][0][0]) + 2, 24, 0, isRounded), 395 | castleTopper(getSize(floors[0][0][0]) + 2, 24, 0, isRounded), 396 | castleTopper(getSize(floors[0][2][0]), 24, 0, isRounded), 397 | castleTopper(getSize(floors[0][2][0]), 24, 0, isRounded), 398 | ); 399 | rooms.push(isRounded ? tubify(top, 11, 12, 14) : top); 400 | } 401 | 402 | return mergeCubes(rooms); 403 | } 404 | 405 | function tubify(moldableCubeBox: MoldableCubeGeometry, selectSize: number, innerRadius: number, outerRadius: number) { 406 | return moldableCubeBox 407 | .selectBy(vertex => Math.abs(vertex.x) <= selectSize && Math.abs(vertex.z) <= selectSize) 408 | .cylindrify(innerRadius, 'y') 409 | .invertSelection() 410 | .cylindrify(outerRadius, 'y') 411 | .computeNormals(true) 412 | .all_() 413 | .done_(); 414 | } 415 | 416 | export function cornerRamp(isRounded?: boolean, isFlipped?: boolean, includeWalkway = true) { 417 | const rampWidth = 5; 418 | const flip = isFlipped ? -1 : 1; 419 | 420 | function makeRamp(length: number, baseHeight: number, endHeight: number, width: number, transformCallback: (cube: MoldableCubeGeometry) => MoldableCubeGeometry) { 421 | const subdivisions = 2; 422 | const stepCount = Math.floor(length / subdivisions); 423 | const stepWidth = length / stepCount; 424 | const heightDifference = endHeight - baseHeight; 425 | const stepHeight = heightDifference / stepCount; 426 | return mergeCubes(doTimes(stepCount, index => { 427 | const currentHeight = baseHeight + index * stepHeight + stepHeight; 428 | return transformCallback(new MoldableCubeGeometry(stepWidth, currentHeight, width) 429 | .selectBy(vert => vert.x < 0 && vert.y > 0) 430 | .translate_(0, -stepHeight) 431 | .all_() 432 | .translate_(index * stepWidth + stepWidth / 2, currentHeight / 2)).spreadTextureCoords() 433 | }) 434 | ).done_(); 435 | } 436 | 437 | 438 | // room are 22x20 439 | const ramp = makeRamp(11, 0, 5, rampWidth, cube => cube.translate_(-7, 0, -7.5 * flip)) 440 | .merge(new MoldableCubeGeometry(rampWidth, 5, rampWidth, 3).translate_(6.5, 2.5, -7.5 * flip).spreadTextureCoords()) 441 | .merge(makeRamp(10, 5, 11.5, rampWidth, cube => cube).rotate_(0, -Math.PI / 2 * flip).translate_(6.5, 0, -5 * flip)) 442 | 443 | if (includeWalkway) { 444 | ramp.merge(new MoldableCubeGeometry(18, 1, 5, 4).translate_(0, 11, 7.5 * flip).spreadTextureCoords()) 445 | .merge(new MoldableCubeGeometry(5, 1, 15, 1, 1, 6).translate_(-6.5, 11, -2.5 * flip).spreadTextureCoords()); 446 | } 447 | 448 | return isRounded ? ramp.selectBy(vert => Math.abs(vert.x) <= 8 && Math.abs(vert.z) <= 5).invertSelection().cylindrify(12).all_() : ramp; 449 | } 450 | -------------------------------------------------------------------------------- /src/game-states/game.state.ts: -------------------------------------------------------------------------------- 1 | import { State } from '@/core/state'; 2 | import { controls } from '@/core/controls'; 3 | import { FirstPersonPlayer } from '@/core/first-person-player'; 4 | import { Scene } from '@/engine/renderer/scene'; 5 | import { Camera } from '@/engine/renderer/camera'; 6 | import { Face } from '@/engine/physics/face'; 7 | import { render } from '@/engine/renderer/renderer'; 8 | import { Mesh } from '@/engine/renderer/mesh'; 9 | import { PlaneGeometry } from '@/engine/plane-geometry'; 10 | import { getGroupedFaces, meshToFaces } from '@/engine/physics/parse-faces'; 11 | import { Skybox } from '@/engine/skybox'; 12 | import { drawBloodText, materials, skyboxes, testHeightmap } from '@/textures'; 13 | import { newNoiseLandscape } from '@/engine/new-new-noise'; 14 | import { NoiseType } from '@/engine/svg-maker/base'; 15 | import { overlaySvg } from '@/draw-helpers'; 16 | import { MoldableCubeGeometry } from '@/engine/moldable-cube-geometry'; 17 | import { castleContainer } from '@/modeling/castle'; 18 | import { LeverDoorObject3d } from '@/modeling/lever-door'; 19 | import { EnhancedDOMPoint } from '@/engine/enhanced-dom-point'; 20 | import { 21 | draggingSound2, makeSong, 22 | ominousDiscovery1, 23 | ominousDiscovery2, 24 | pickup1, 25 | scaryNote2, 26 | upyriAttack, 27 | upyriAttack2, upyriHit 28 | } from '@/sound-effects'; 29 | import { 30 | key, 31 | makeCoffin, 32 | makeCoffinBottomTop, 33 | stake, 34 | upyri, 35 | getLeverDoors, makeBanners 36 | } from '@/modeling/items'; 37 | 38 | export class GameState implements State { 39 | player?: FirstPersonPlayer; 40 | scene: Scene; 41 | groupedFaces: {floorFaces: Face[], wallFaces: Face[] }; 42 | gridFaces: {floorFaces: Face[], wallFaces: Face[] }[] = []; 43 | 44 | leverDoors: LeverDoorObject3d[] =[]; 45 | 46 | stake = stake(); 47 | hasStake = false; 48 | 49 | key = key(); 50 | hasKey = false; 51 | 52 | upyri = upyri(); 53 | isUpyriKilled = false; 54 | isUpyriDying = false; 55 | isUpyriAttacking = false; 56 | upyriAttackingTimer = 0; 57 | coffinTop = new Mesh(makeCoffinBottomTop().translate_(0, 56.35, -9).done_(), materials.wood); 58 | coffinTopBloodstain = new Mesh(new MoldableCubeGeometry(3, 1, 3).translate_(0, 55.95, -0.5).done_(), materials.bloodCircle); 59 | 60 | constructor() { 61 | this.scene = new Scene(); 62 | this.groupedFaces = { floorFaces: [], wallFaces: [] }; 63 | 64 | this.leverDoors = getLeverDoors(); 65 | } 66 | 67 | async onEnter() { 68 | const camera = new Camera(Math.PI / 3, 16 / 9, 1, 500); 69 | this.player = new FirstPersonPlayer(camera); 70 | 71 | 72 | const heightmap = await newNoiseLandscape(256, 6, 0.05, 3, NoiseType.Fractal, 113); 73 | const floor = new Mesh(new PlaneGeometry(1024, 1024, 255, 255, heightmap).spreadTextureCoords(), materials.grass); 74 | const floorCollision = new Mesh( new PlaneGeometry(1024, 1024, 4, 4).translate_(0, 20.5).done_(), materials.grass); 75 | 76 | const castle = new Mesh(castleContainer.value!.done_(), materials.brickWall); 77 | 78 | const writing = new Mesh(new MoldableCubeGeometry(1, 6, 6).rotate_(0.2).translate_(57.4, 26, 43).done_(), materials.castleWriting) 79 | const handprint = new Mesh(new MoldableCubeGeometry(1, 6, 6).rotate_(0.2).translate_(47.4, 24, 42).done_(), materials.handprint) 80 | 81 | const coffin = new Mesh(makeCoffin().translate_(0, 55, -9).done_(), materials.wood); 82 | 83 | const bridge = new Mesh(new MoldableCubeGeometry(18, 1, 65).translate_(0, 20.5, -125).done_(), materials.planks); 84 | 85 | this.coffinTopBloodstain.scale_.set(0, 1, 0); 86 | 87 | // .rotate_(0, -1) 88 | // .translate_(-51, 21.5, -65) 89 | this.stake.position_.set(-51, 21.5, -65); 90 | this.stake.setRotation_(0, -1, 0); 91 | 92 | const doorsFromLeverDoors = this.leverDoors.flatMap(leverDoor => leverDoor.doorDatas); 93 | 94 | const groupedFaces = getGroupedFaces(meshToFaces([floorCollision, castle, coffin, this.coffinTop])); 95 | 96 | 97 | // Banners 98 | const bannerHeightmap = await testHeightmap(); 99 | 100 | function onlyUnique(value: any, index: number, array: any[]) { 101 | return array.indexOf(value) === index; 102 | } 103 | 104 | groupedFaces.floorFaces.forEach(face => { 105 | const gridPositions = face.points.map(point => point.x < 0 ? 0 : 1); 106 | 107 | gridPositions.filter(onlyUnique).forEach(position_ => { 108 | if (!this.gridFaces[position_]) { 109 | this.gridFaces[position_] = { floorFaces: [], wallFaces: [] }; 110 | } 111 | this.gridFaces[position_].floorFaces.push(face); 112 | }); 113 | }); 114 | 115 | groupedFaces.wallFaces.forEach(face => { 116 | const gridPositions = face.points.map(point => point.x < 0 ? 0 : 1); 117 | 118 | gridPositions.filter(onlyUnique).forEach(position_ => { 119 | if (!this.gridFaces[position_]) { 120 | this.gridFaces[position_] = { floorFaces: [], wallFaces: [] }; 121 | } 122 | this.gridFaces[position_].wallFaces.push(face); 123 | }); 124 | }); 125 | 126 | this.scene.add_(writing, handprint, floor, castle, ...this.leverDoors, ...doorsFromLeverDoors, this.stake, this.key, this.upyri, coffin, this.coffinTop, this.coffinTopBloodstain, bridge, makeBanners(bannerHeightmap)); 127 | 128 | this.scene.skybox = new Skybox(...skyboxes.test); 129 | this.scene.skybox.bindGeometry(); 130 | tmpl.innerHTML = ''; 131 | tmpl.addEventListener('click', () => { 132 | tmpl.requestPointerLock(); 133 | }); 134 | 135 | this.player.cameraRotation.set(0, 90, 0); 136 | } 137 | 138 | leverPlayerDistance = new EnhancedDOMPoint(); 139 | 140 | 141 | onUpdate(): void { 142 | this.player!.update(this.gridFaces); 143 | render(this.player!.camera, this.scene); 144 | 145 | 146 | this.handleEvents() 147 | 148 | this.leverDoors.forEach(leverDoor => { 149 | if (!leverDoor.isPulled) { 150 | const distance = this.leverPlayerDistance.subtractVectors(this.player.camera.position_, leverDoor.switchPosition).magnitude; 151 | if (distance < 7 && controls.isConfirm) { 152 | leverDoor.pullLever(); 153 | } 154 | this.player!.wallCollision(leverDoor.closedDoorCollision); 155 | } else { 156 | this.player!.wallCollision(leverDoor.openDoorCollision); 157 | } 158 | 159 | leverDoor.update(); 160 | }); 161 | 162 | this.upyri.lookAt(this.player!.camera.position_); 163 | this.scene.updateWorldMatrix(); 164 | 165 | if (this.winState) { 166 | this.winCounter++; 167 | if (this.winCounter > 1800) { 168 | tmpl.style.backgroundColor = `rgba(0, 0, 0, ${this.backgroundFade})`; 169 | this.backgroundFade += 0.004; 170 | } 171 | } 172 | 173 | if (this.isUpyriDying) { 174 | this.coffinTopBloodstain.scale_.x += 0.03; 175 | this.coffinTopBloodstain.scale_.z += 0.03; 176 | 177 | if (this.coffinTopBloodstain.scale_.x >= 1) { 178 | this.isUpyriDying = false; 179 | } 180 | } 181 | 182 | if (this.isUpyriAttacking) { 183 | c3d.style.filter = 'blur(9px) brightness(0.8)'; 184 | this.upyri.position_.moveTowards(this.player!.camera.position_, 0.6); 185 | this.upyriAttackingTimer++; 186 | if (this.upyriAttackingTimer > 30) { 187 | tmpl.style.backgroundColor = `rgb(0, 0, 0)`; 188 | this.isUpyriAttacking = false; 189 | 190 | // Reset 191 | setTimeout(() => { 192 | c3d.style.filter = ''; 193 | tmpl.style.backgroundColor = ''; 194 | this.upyri.position_.set(0, 54, 2); 195 | this.isUpyriAttacking = false; 196 | this.upyriTriggerCounter = 0; 197 | this.upyriAttackingTimer = 0; 198 | this.coffinTop.position_.set(0, 0, 0); 199 | this.coffinTop.rotation_.y = 0; 200 | this.isCoffinTopPlayed = false; 201 | this.player!.feetCenter.set(0, 49, 22); 202 | this.gameEvents[5].isFired = false; 203 | this.leverDoors[3].isPulled = false; 204 | this.leverDoors[3].isFinished = false; 205 | this.leverDoors[3].children_[1].rotation_.x = -45; 206 | this.leverDoors[3].audioPlayer = draggingSound2(this.leverDoors[3].switchPosition); 207 | this.leverDoors[3].doorDatas[0].rotation_.y = 0; 208 | this.leverDoors[3].doorDatas[1].rotation_.y = 0; 209 | // @ts-ignore 210 | this.leverDoors[3].doorDatas[0].dragPlayer = { start: () => {} }; 211 | // @ts-ignore 212 | this.leverDoors[3].doorDatas[0].creakPlayer = { start: () => {} }; 213 | // @ts-ignore 214 | this.leverDoors[3].doorDatas[1].dragPlayer = { start: () => {} }; 215 | // @ts-ignore 216 | this.leverDoors[3].doorDatas[1].creakPlayer = { start: () => {} }; 217 | 218 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 219 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 150px; text-shadow: 1px 1px 20px' }, 'YOU WOKE UPYRI', 40), 220 | ); 221 | 222 | setTimeout(() => { 223 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 224 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 150px; text-shadow: 1px 1px 20px' }, 'KILL HIM IN HIS COFFIN', 40), 225 | ); 226 | 227 | setTimeout(() => tmpl.innerHTML = '', 3000); 228 | 229 | }, 3000); 230 | 231 | }, 4000); 232 | } 233 | } 234 | 235 | // debug.innerHTML = `${this.player.camera.position_.x}, ${this.player.camera.position_.y} ${this.player.camera.position_.z} // ${this.player.camera.rotation_.x} ${this.player.camera.rotation_.y} ${this.player.camera.rotation_.z}`; 236 | } 237 | 238 | private upyriTriggerCounter = 0; 239 | 240 | private backgroundFade = 0; 241 | private winState = false; 242 | private winCounter = 0; 243 | 244 | gameEvents = [ 245 | // see blood stain on wall 246 | new GameEvent(new EnhancedDOMPoint(41, 21, 42), () => { ominousDiscovery2().start(); return true }, new EnhancedDOMPoint(11, -90)), 247 | 248 | 249 | // Enter coffin room 250 | new GameEvent(new EnhancedDOMPoint(0, 58, 8), () => { 251 | scaryNote2()().start(); 252 | return true; 253 | }, undefined), 254 | 255 | // Got stake 256 | new GameEvent(new EnhancedDOMPoint(-51, 24, -65),() => { 257 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 258 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 250px; text-shadow: 1px 1px 20px' }, 'GOT STAKE', 40), 259 | ); 260 | pickup1().start(); 261 | this.hasStake = true; 262 | this.stake.position_.y = -50; 263 | setTimeout(() => tmpl.innerHTML = '', 3000); 264 | return true; 265 | },undefined, 3), 266 | 267 | // Got Key 268 | new GameEvent(new EnhancedDOMPoint(-32,36,60.5),() => { 269 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 270 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 250px; text-shadow: 1px 1px 20px' }, 'GOT KEY', 40), 271 | ); 272 | pickup1().start(); 273 | this.hasKey = true; 274 | this.key.position_.y = -50; 275 | this.leverDoors[2].switchPosition.y = 24; 276 | setTimeout(() => tmpl.innerHTML = '', 3000); 277 | return true; 278 | },undefined, 4), 279 | 280 | // Kill Upyri 281 | new GameEvent(new EnhancedDOMPoint(0, 58.5, -1), () => { 282 | const point = new DOMPoint(0, 0, -1); 283 | const cameraRot = new EnhancedDOMPoint().set(this.player.camera.rotationMatrix.transformPoint(point)); 284 | const upyriRot = new EnhancedDOMPoint().set(this.upyri.rotationMatrix.transformPoint(point)); 285 | 286 | if (cameraRot.dot(upyriRot) < -0.70) { 287 | if (controls.isConfirm) { 288 | if (this.hasStake) { 289 | upyriHit(this.upyri.position_).start(); 290 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 291 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 250px; text-shadow: 1px 1px 20px' }, 'UPYRI KILLED', 40), 292 | ); 293 | this.stake.position_.set(0, 57, -0.5); 294 | this.stake.setRotation_(Math.PI / 2 , 0, 0); 295 | this.isUpyriKilled = true; 296 | this.isUpyriDying = true; 297 | setTimeout(() => tmpl.innerHTML = '', 3000); 298 | return true; 299 | } else { 300 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 301 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 250px; text-shadow: 1px 1px 20px' }, 'NEED STAKE', 40), 302 | ); 303 | setTimeout(() => tmpl.innerHTML = '', 3000); 304 | } 305 | } 306 | } 307 | }, undefined, 7), 308 | 309 | // Die From Upyri 310 | new GameEvent(new EnhancedDOMPoint(0, 58.5, 0), () => { 311 | if (!this.isUpyriKilled && this.leverDoors[3].isPulled) { 312 | this.upyriTriggerCounter++; 313 | this.upyri.position_.y = 61; 314 | this.coffinTop.position_.y = -1; 315 | this.coffinTop.position_.x = -5.5; 316 | this.coffinTop.rotation_.y = 25; 317 | 318 | if (!this.isCoffinTopPlayed) { 319 | upyriHit(new EnhancedDOMPoint(-7, 58, -3)).start(); 320 | this.isCoffinTopPlayed = true; 321 | } 322 | 323 | const point = new DOMPoint(0, 0, -1); 324 | const cameraRot = new EnhancedDOMPoint().set(this.player!.camera.rotationMatrix.transformPoint(point)); 325 | const upyriRot = new EnhancedDOMPoint().set(this.upyri.rotationMatrix.transformPoint(point)); 326 | 327 | if (cameraRot.dot(upyriRot) < -0.70 || this.upyriTriggerCounter > 240 || this.player!.camera.position_.z > 10) { 328 | setTimeout(() => { 329 | upyriAttack().start(); 330 | upyriAttack2().start(); 331 | this.isUpyriAttacking = true; 332 | }, 500); 333 | return true; 334 | } 335 | } 336 | }, undefined, 12), 337 | 338 | 339 | // Escape 340 | new GameEvent(new EnhancedDOMPoint(0, 24.5, -72), () => { 341 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 342 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 250px; text-shadow: 1px 1px 20px' }, 'ESCAPED', 40), 343 | ); 344 | this.winState = true; 345 | this.player!.isFrozen = true; 346 | this.player!.velocity.set(0, 0, -0.1); 347 | setTimeout(() => { 348 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 349 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 160px; text-shadow: 1px 1px 20px' }, 'THANKS FOR PLAYING', 40), 350 | ); 351 | }, 3000); 352 | return true; 353 | }, undefined, 6), 354 | 355 | // Look at broken wall piece 356 | new GameEvent(new EnhancedDOMPoint(32, 24, -40), () => { ominousDiscovery1().start(); return true }, new EnhancedDOMPoint(28, -15), 8), 357 | 358 | // Initial cue to give player instruction that their goal is to escape the castle 359 | new GameEvent(new EnhancedDOMPoint(44, 21, -26), () => { 360 | setTimeout(() => { 361 | tmpl.innerHTML = overlaySvg({ style: 'text-anchor: middle' }, 362 | drawBloodText({ x: '50%', y: '90%', style: 'font-size: 160px; text-shadow: 1px 1px 20px' }, 'ESCAPE THE CASTLE', 40), 363 | ); 364 | setTimeout(() => tmpl.innerHTML = '', 5000); 365 | }, 2000); 366 | return true; 367 | }, undefined), 368 | 369 | // Rooftop music cue 370 | new GameEvent(new EnhancedDOMPoint(15, 48, 48), () => { makeSong().start(); return true}), 371 | 372 | ]; 373 | 374 | isCoffinTopPlayed = false; 375 | 376 | 377 | 378 | handleEvents() { 379 | this.gameEvents.forEach(gameEvent => gameEvent.check(this.player!.camera.position_, this.player!.camera.rotation_)); 380 | } 381 | } 382 | 383 | class GameEvent { 384 | isFired = false; 385 | constructor(private targetPos: EnhancedDOMPoint, private actionCallback: () => boolean, private targetRot?: EnhancedDOMPoint, private posMargin = 6) {} 386 | 387 | check(currentPosition: EnhancedDOMPoint, currentRotation: EnhancedDOMPoint) { 388 | // debug.innerHTML = new EnhancedDOMPoint().subtractVectors(currentPosition, this.targetPos).magnitude + ' // ' + new EnhancedDOMPoint().subtractVectors(currentRotation, this.targetRot).magnitude % 360; 389 | if (!this.isFired && new EnhancedDOMPoint().subtractVectors(currentPosition, this.targetPos).magnitude < this.posMargin) { 390 | const lookMag = this.targetRot ? new EnhancedDOMPoint().subtractVectors(currentRotation, this.targetRot).magnitude % 360 : 0; 391 | if (lookMag < 20 || lookMag > 340) { 392 | this.isFired = this.actionCallback(); 393 | } 394 | } 395 | } 396 | } 397 | --------------------------------------------------------------------------------