├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── __tests__ ├── core │ └── handleEvent.test.ts └── utils │ ├── boot.test.ts │ ├── node.test.ts │ ├── save.test.ts │ └── site.test.ts ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── public └── models │ ├── cut_cube.glb │ ├── cyan_crystal.glb │ ├── gate_mirror.glb │ └── gold_node.glb ├── screenshots ├── 1.png ├── 2.jpg ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png └── 8.png ├── scripts ├── extract │ ├── Lapk.js │ ├── convert_sfx.mjs │ ├── extract.mjs │ ├── extract_lapks.mjs │ ├── extract_media.mjs │ ├── extract_site_images.mjs │ ├── extract_voice.mjs │ ├── jpsxdec │ │ ├── doc │ │ │ ├── CDDL-GPL-2-CP.txt │ │ │ ├── CHANGES.txt │ │ │ ├── CREDITS.txt │ │ │ ├── LICENSE.txt │ │ │ ├── New-BSD.txt │ │ │ ├── apache-2.0.txt │ │ │ ├── jPSXdec-manual.pdf │ │ │ └── lgpl-2.1.txt │ │ ├── jpsxdec-lib.jar │ │ └── jpsxdec.jar │ ├── lain_compress.js │ ├── lapk.ksy │ ├── lapks.json │ ├── site_A_images.json │ ├── site_B_images.json │ └── voice.json └── postinstall.js ├── src ├── additional.d.ts ├── components │ ├── canvas │ │ ├── objects │ │ │ ├── BootScene │ │ │ │ ├── BootAccela.tsx │ │ │ │ ├── BootAnimation.tsx │ │ │ │ ├── BootAuthorizeUser.tsx │ │ │ │ ├── BootLoadData.tsx │ │ │ │ └── BootMainMenuComponents.tsx │ │ │ ├── EndScene │ │ │ │ ├── EndCylinder.tsx │ │ │ │ ├── EndSelectionScreen.tsx │ │ │ │ └── EndSphere.tsx │ │ │ ├── GateScene │ │ │ │ ├── BlueDigit.tsx │ │ │ │ ├── GateHUD.tsx │ │ │ │ ├── GateMiddleObject.tsx │ │ │ │ ├── GateSide.tsx │ │ │ │ └── Mirror.tsx │ │ │ ├── IdleManager.tsx │ │ │ ├── Images.tsx │ │ │ ├── InputHandler.tsx │ │ │ ├── LainSpeak.tsx │ │ │ ├── Loading.tsx │ │ │ ├── MainScene │ │ │ │ ├── About.tsx │ │ │ │ ├── CyanCrystal.tsx │ │ │ │ ├── GrayPlane.tsx │ │ │ │ ├── GrayRing.tsx │ │ │ │ ├── HUD.tsx │ │ │ │ ├── Lain.tsx │ │ │ │ ├── LevelNodes.tsx │ │ │ │ ├── LevelSelection.tsx │ │ │ │ ├── MiddleRing.tsx │ │ │ │ ├── Node.tsx │ │ │ │ ├── NodeExplosion.tsx │ │ │ │ ├── NodeRip.tsx │ │ │ │ ├── NotFound.tsx │ │ │ │ ├── Pause.tsx │ │ │ │ ├── PauseLetter.tsx │ │ │ │ ├── PauseSquare.tsx │ │ │ │ ├── PermissionDenied.tsx │ │ │ │ ├── PurpleRing.tsx │ │ │ │ ├── Rings.tsx │ │ │ │ ├── Site.tsx │ │ │ │ ├── Starfield.tsx │ │ │ │ ├── StaticLevelNodes.tsx │ │ │ │ ├── StaticNode.tsx │ │ │ │ └── YellowOrb.tsx │ │ │ ├── MediaPlayer.tsx │ │ │ ├── MediaScene │ │ │ │ ├── AudioVisualizer.tsx │ │ │ │ ├── AudioVisualizerColumn.tsx │ │ │ │ ├── Cube.tsx │ │ │ │ ├── LeftSide.tsx │ │ │ │ ├── Lof.tsx │ │ │ │ ├── NodeNameContainer.tsx │ │ │ │ ├── RightSide.tsx │ │ │ │ ├── TriangularPrism.tsx │ │ │ │ └── Word.tsx │ │ │ ├── PolytanScene │ │ │ │ ├── PolytanBackground.tsx │ │ │ │ └── PolytanBear.tsx │ │ │ ├── Preloader.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── Prompt.tsx │ │ │ ├── SaveStatusDisplay.tsx │ │ │ ├── SsknScene │ │ │ │ ├── SsknBackground.tsx │ │ │ │ ├── SsknHUD.tsx │ │ │ │ └── SsknIcon.tsx │ │ │ └── TextRenderer │ │ │ │ ├── AnimatedBigTextRenderer.tsx │ │ │ │ ├── Letter.tsx │ │ │ │ └── TextRenderer.tsx │ │ └── scenes │ │ │ ├── BootScene.tsx │ │ │ ├── ChangeDiscScene.tsx │ │ │ ├── EndScene.tsx │ │ │ ├── GateScene.tsx │ │ │ ├── IdleMediaScene.tsx │ │ │ ├── MainScene.tsx │ │ │ ├── MediaScene.tsx │ │ │ ├── PolytanScene.tsx │ │ │ ├── SsknScene.tsx │ │ │ ├── TaKScene.tsx │ │ │ └── index.ts │ └── dom │ │ ├── Credit.tsx │ │ ├── Header.tsx │ │ ├── Keybinding.tsx │ │ ├── Language.tsx │ │ ├── QA.tsx │ │ └── Savefile.tsx ├── core │ ├── events.ts │ ├── handleEvent.ts │ ├── handleInput.ts │ └── index.ts ├── hooks │ ├── useCappedFrame.tsx │ ├── useLetterGeometry.tsx │ ├── useNodeTexture.tsx │ ├── usePrevious.tsx │ └── useText.tsx ├── json │ ├── font │ │ ├── big_font.json │ │ ├── jp_font.json │ │ └── medium_font.json │ ├── gate │ │ └── blue_digit_positions.json │ ├── initial_progress.json │ ├── legacy │ │ ├── initial_progress.json │ │ └── save.json │ ├── node_explosion_lines.json │ ├── node_huds.json │ ├── node_positions.json │ ├── nodes.json │ ├── site_a_layout.json │ ├── site_b_layout.json │ └── voice.json ├── pages │ ├── _app.tsx │ ├── game.tsx │ ├── guide.tsx │ ├── index.tsx │ ├── notes.tsx │ └── options.tsx ├── shaders │ ├── blue_digit.frag │ ├── blue_digit.vert │ ├── explosion_line.frag │ ├── explosion_line.vert │ ├── gate_side.vert │ ├── gate_side_left.frag │ ├── gate_side_right.frag │ ├── gray_ring.frag │ ├── gray_ring.vert │ ├── intro_star.frag │ ├── intro_star.vert │ ├── middle_ring.frag │ ├── middle_ring.vert │ ├── node.frag │ ├── node.vert │ ├── purple_ring.frag │ ├── purple_ring.vert │ ├── star.frag │ └── star.vert ├── store.ts ├── styles │ └── globals.css ├── types │ └── index.ts └── utils │ ├── audio.ts │ ├── boot.ts │ ├── end.ts │ ├── idle.ts │ ├── lain.ts │ ├── lcg.ts │ ├── log.ts │ ├── media.ts │ ├── node.ts │ ├── random.ts │ ├── range.ts │ ├── save.ts │ ├── site.ts │ └── sleep.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | public/* 38 | !public/models/ 39 | !public/media/ 40 | public/media/* 41 | !public/media/webvtt 42 | 43 | scripts/extract/discs 44 | scripts/extract/*.log 45 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "public/media/webvtt"] 2 | path = public/media/webvtt 3 | url = git@github.com:laingame-net/lainvtt.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## lainTSX 2 | 3 | A browser-based implementation of the Serial Experiments Lain PSX game using `react-three-fiber` with the aim to provide multi-language support and make it more accessible. 4 | 5 | ## History 6 | 7 | The original PSX game was released in Japan, back in 1998. The game never got a proper english adaptation, which resulted in all non-Japanese speaking players either having to play through the game while simultaneously reading through the translation, or simply not playing the game at all and only reading it. 8 | 9 | The goal of this project is to provide a better experience for those willing to play the game, and the way to do so is by implementing a subtitle system, which has the capability to support multiple languages. 10 | 11 | ## How do I contribute to the translations? 12 | Go to https://crowdin.com/project/lain-psx 13 | 14 | ## Building locally 15 | 16 | Building locally is currently not possible. This is because the repository lacks static assets ripped from the game due to it being copyrighted content. The plan is to write an extraction script (currently located inside `scripts/extract`), where the user who owns the game provides the disc binaries themselves, and the script automates the static file extraction. This script is still WIP. 17 | 18 | ## Code strutcure 19 | 20 | - **\_\_tests\_\_/** - Jest tests. 21 | 22 | - **src/** 23 | - **components/canvas/** - TSX components used in the actual 3D environment (sprites, meshes, etc.). 24 | - **components/dom/** - TSX components used for the website pages. 25 | - **core/** - State management. Contextual (scene/state influenced) processors, mutation handler, event templates. 26 | - **hooks/** - Custom hooks for React. 27 | - **json/** - Reverse-engineered JSON data the game uses for a variety of tasks (node positions, font texture atlas definitions, etc.). 28 | - **pages/** - Pages for the website (index, notes, guide, options, etc.). 29 | - **shaders/** - Fragment/Vertex shaders. 30 | - **styles/** - CSS. 31 | - **types/** - Type definitions. 32 | - **utils/** - Utilities/helper functions used to boostrap functionalities (node finding algorithm, name selection handlers, etc.). 33 | - **store.ts** - Zustand store used for managing global state. 34 | 35 | - **scripts/** 36 | 37 | - **extract/** - WIP extraction script to automate the local building process of the game. 38 | 39 | ## TODO 40 | 41 | - **Finish writing the extraction script** 42 | - **Improve/complete the translation** 43 | 44 | ## Screenshots 45 | 46 |

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |

56 | 57 | ## Reporting bugs and contributing 58 | 59 | If you have any ideas/suggestions/found an issue or want to help us with the translation or anything else, please [make an issue](https://github.com/ad044/lainTSX/issues). 60 | 61 | ## Tools used during development 62 | 63 | - [`jPSXdec`](https://github.com/m35/jpsxdec) - PlayStation 1 audio/video converter. 64 | - [`three.js`](https://github.com/mrdoob/three.js/) - JavaScript 3D renderer. 65 | - [`react-three-fiber`](https://github.com/pmndrs/react-three-fiber) - three.js wrapper for React. 66 | - [`zustand`](https://github.com/pmndrs/zustand) - State management library for React. 67 | - [`react-spring`](https://github.com/pmndrs/react-spring) - Animation library. 68 | - [`drei`](https://github.com/pmndrs/drei) - Utilities for react-three-fiber. 69 | - [`three-plain-animator`](https://github.com/MaciejWWojcik/three-plain-animator) - Sprite animation library. 70 | -------------------------------------------------------------------------------- /__tests__/core/handleEvent.test.ts: -------------------------------------------------------------------------------- 1 | import { enterSsknScene } from "@/core/events"; 2 | import handleEvent from "@/core/handleEvent"; 3 | import { useStore } from "@/store"; 4 | import { GameScene } from "@/types"; 5 | 6 | it("Checks whether handleEvent applies mutations correctly", () => { 7 | const initialState = useStore.getState(); 8 | handleEvent(enterSsknScene); 9 | const newState = useStore.getState(); 10 | expect(newState.scene).toEqual(GameScene.Sskn); 11 | expect(newState.prev.scene).toEqual(initialState.scene); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/utils/boot.test.ts: -------------------------------------------------------------------------------- 1 | import { addCharacter } from "@/utils/boot"; 2 | 3 | it("Handles the logic for Japanese characters", () => { 4 | // cant be first character check 5 | expect(addCharacter("", "ン")).toEqual(""); 6 | // if its not first, then fine 7 | expect(addCharacter("キ", "ン")).toEqual("キン"); 8 | //「ー」 cannot be added to 「ッ」 and 「ン」or itself 9 | expect(addCharacter("キッ", "ー")).toEqual("キッ"); 10 | expect(addCharacter("キン", "ー")).toEqual("キン"); 11 | expect(addCharacter("キー", "ー")).toEqual("キー"); 12 | // characters that can be followed by the lowercase characters 13 | expect(addCharacter("キ", "ャ")).toEqual("キャ"); 14 | // cant be followed by lowercase character 15 | expect(addCharacter("ー", "ャ")).toEqual("ー"); 16 | // for 「ッ」, it can added to any character except itself 17 | expect(addCharacter("ャ", "ッ")).toEqual("ャッ"); 18 | // cant be added 19 | expect(addCharacter("ッ", "ッ")).toEqual("ッ"); 20 | // dakuten 21 | expect(addCharacter("カ", "゛")).toEqual("ガ"); 22 | // cant be appended 23 | expect(addCharacter("ガ", "゛")).toEqual("ガ"); 24 | // handakuten 25 | expect(addCharacter("ハ", "゜")).toEqual("パ"); 26 | // cant be appended 27 | expect(addCharacter("キ", "゜")).toEqual("キ"); 28 | expect(addCharacter("パ", "゜")).toEqual("パ"); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/utils/site.test.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from "@/types"; 2 | import { getRotationForSegment } from "@/utils/site"; 3 | 4 | const doCircles = ( 5 | segment: number, 6 | count: number, 7 | direction: Direction.Left | Direction.Right, 8 | startRotation?: number 9 | ): [number, number] => { 10 | let currentCount = 0; 11 | let rotation = startRotation ?? getRotationForSegment(segment); 12 | let prevRotation = rotation; 13 | while (currentCount < count) { 14 | if (direction === Direction.Left) { 15 | segment -= 1; 16 | } 17 | 18 | if (direction === Direction.Right) { 19 | segment += 1; 20 | } 21 | 22 | if (segment > 7) { 23 | currentCount += 1; 24 | segment = 0; 25 | } 26 | 27 | if (segment < 0) { 28 | currentCount += 1; 29 | segment = 7; 30 | } 31 | 32 | prevRotation = rotation; 33 | rotation = getRotationForSegment(segment, prevRotation); 34 | if (direction === Direction.Left) { 35 | expect(rotation).toBeCloseTo(prevRotation - Math.PI / 4, 6); 36 | } 37 | 38 | if (direction === Direction.Right) { 39 | expect(rotation).toBeCloseTo(prevRotation + Math.PI / 4, 6); 40 | } 41 | } 42 | 43 | return [segment, rotation]; 44 | }; 45 | 46 | it("Checks if rotation calculator works", () => { 47 | // site is initially positioned like this 48 | doCircles(6, 3, Direction.Left); 49 | doCircles(6, 3, Direction.Right); 50 | doCircles(4, 6, Direction.Left); 51 | doCircles(0, 6, Direction.Left); 52 | // move to right then move to left based on last coordinates 53 | let [segment, rotation] = doCircles(7, 6, Direction.Right); 54 | doCircles(segment, 6, Direction.Left, rotation); 55 | }); 56 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require("next/jest"); 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: "./", 6 | }); 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | // Add more setup options before each test is run 11 | // setupFilesAfterEnv: ['/jest.setup.js'], 12 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 13 | moduleDirectories: ["node_modules", "/"], 14 | moduleNameMapper: { "@/(.*)": "/src/$1" }, 15 | testEnvironment: "jest-environment-jsdom", 16 | }; 17 | 18 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 19 | module.exports = createJestConfig(customJestConfig); 20 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack(config, { isServer }) { 4 | // Allow importing of shader files (e.g. `.glsl` -- filenames below) 5 | // @see: https://github.com/glslify/glslify-loader 6 | config.module.rules.push( 7 | { 8 | test: /\.(glsl|vs|fs|vert|frag|ps)$/, 9 | exclude: /node_modules/, 10 | use: ["raw-loader"], 11 | }, 12 | { 13 | test: /\.(mp4|vtt)$/, 14 | use: "file-loader", 15 | } 16 | ); 17 | 18 | return config; 19 | }, 20 | 21 | reactStrictMode: true, 22 | }; 23 | 24 | module.exports = nextConfig; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laintsx", 3 | "version": "0.4.4", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest --watch" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "^9.13.2", 14 | "@react-three/fiber": "^8.0.26", 15 | "next": "12.1.6", 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "rxjs": "^7.5.5", 19 | "three": "^0.141.0", 20 | "three-plain-animator": "^1.1.2", 21 | "zustand": "^4.0.0-rc.1" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^28.1.4", 25 | "@types/node": "18.0.0", 26 | "@types/react": "18.0.14", 27 | "@types/react-dom": "18.0.5", 28 | "@types/three": "^0.141.0", 29 | "eslint": "8.18.0", 30 | "eslint-config-next": "12.1.6", 31 | "file-loader": "^6.2.0", 32 | "jest": "^28.1.2", 33 | "jest-environment-jsdom": "^28.1.2", 34 | "raw-loader": "^4.0.2", 35 | "typescript": "4.7.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/models/cut_cube.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/public/models/cut_cube.glb -------------------------------------------------------------------------------- /public/models/cyan_crystal.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/public/models/cyan_crystal.glb -------------------------------------------------------------------------------- /public/models/gate_mirror.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/public/models/gate_mirror.glb -------------------------------------------------------------------------------- /public/models/gold_node.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/public/models/gold_node.glb -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/2.jpg -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/screenshots/8.png -------------------------------------------------------------------------------- /scripts/extract/Lapk.js: -------------------------------------------------------------------------------- 1 | // This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild 2 | 3 | (function (root, factory) { 4 | if (typeof define === 'function' && define.amd) { 5 | define(['kaitai-struct/KaitaiStream', 'LainCompress'], factory); 6 | } else if (typeof module === 'object' && module.exports) { 7 | module.exports = factory(require('kaitai-struct/KaitaiStream'), require('./lain_compress.js')); 8 | } else { 9 | root.Lapk = factory(root.KaitaiStream, root.LainCompress); 10 | } 11 | }(this, function (KaitaiStream, LainCompress) { 12 | var Lapk = (function() { 13 | function Lapk(_io, _parent, _root) { 14 | this._io = _io; 15 | this._parent = _parent; 16 | this._root = _root || this; 17 | 18 | this._read(); 19 | } 20 | Lapk.prototype._read = function() { 21 | this.magic = this._io.readBytes(4); 22 | if (!((KaitaiStream.byteArrayCompare(this.magic, [108, 97, 112, 107]) == 0))) { 23 | throw new KaitaiStream.ValidationNotEqualError([108, 97, 112, 107], this.magic, this._io, "/seq/0"); 24 | } 25 | this.lapkSize = this._io.readU4le(); 26 | this._raw_data = this._io.readBytes(this.lapkSize); 27 | var _io__raw_data = new KaitaiStream(this._raw_data); 28 | this.data = new LapkData(_io__raw_data, this, this._root); 29 | } 30 | 31 | var CellHeader = Lapk.CellHeader = (function() { 32 | function CellHeader(_io, _parent, _root) { 33 | this._io = _io; 34 | this._parent = _parent; 35 | this._root = _root || this; 36 | 37 | this._read(); 38 | } 39 | CellHeader.prototype._read = function() { 40 | this.cellOffset = this._io.readU4le(); 41 | this.negativeXPosition = this._io.readU2le(); 42 | this.negativeYPosition = this._io.readU2le(); 43 | this.unknown = this._io.readU4le(); 44 | } 45 | 46 | return CellHeader; 47 | })(); 48 | 49 | var CellData = Lapk.CellData = (function() { 50 | function CellData(_io, _parent, _root) { 51 | this._io = _io; 52 | this._parent = _parent; 53 | this._root = _root || this; 54 | 55 | this._read(); 56 | } 57 | CellData.prototype._read = function() { 58 | this.width = this._io.readU2le(); 59 | this.height = this._io.readU2le(); 60 | this.chrominanceQuantisationScale = this._io.readU2le(); 61 | this.luminanceQuantisationScale = this._io.readU2le(); 62 | this.imageDataSize = this._io.readU4le(); 63 | this.runLengthCodeCount = this._io.readU4le(); 64 | this.imageData = this._io.readBytes((this.imageDataSize - 4)); 65 | this._raw_bitMask = this._io.readBytesFull(); 66 | var _process = new LainCompress(); 67 | this.bitMask = _process.decode(this._raw_bitMask); 68 | } 69 | 70 | return CellData; 71 | })(); 72 | 73 | var LapkData = Lapk.LapkData = (function() { 74 | function LapkData(_io, _parent, _root) { 75 | this._io = _io; 76 | this._parent = _parent; 77 | this._root = _root || this; 78 | 79 | this._read(); 80 | } 81 | LapkData.prototype._read = function() { 82 | this.cellCount = this._io.readU4le(); 83 | this.cellHeaders = new Array(this.cellCount); 84 | for (var i = 0; i < this.cellCount; i++) { 85 | this.cellHeaders[i] = new CellHeader(this._io, this, this._root); 86 | } 87 | this._raw_cellData = new Array(this.cellCount); 88 | this.cellData = new Array(this.cellCount); 89 | for (var i = 0; i < this.cellCount; i++) { 90 | this._raw_cellData[i] = this._io.readBytes((i == (this.cellCount - 1) ? (((this._parent.lapkSize - 4) - (this.cellCount * 12)) - this.cellHeaders[i].cellOffset) : (this.cellHeaders[(i + 1)].cellOffset - this.cellHeaders[i].cellOffset))); 91 | var _io__raw_cellData = new KaitaiStream(this._raw_cellData[i]); 92 | this.cellData[i] = new CellData(_io__raw_cellData, this, this._root); 93 | } 94 | } 95 | 96 | return LapkData; 97 | })(); 98 | 99 | return Lapk; 100 | })(); 101 | return Lapk; 102 | })); 103 | -------------------------------------------------------------------------------- /scripts/extract/convert_sfx.mjs: -------------------------------------------------------------------------------- 1 | import { readdirSync } from "fs"; 2 | import { spawnSync } from "child_process"; 3 | import { join } from "path"; 4 | import * as mm from "music-metadata"; 5 | 6 | // stub implementation of upping the pitch of the sfx, still wip 7 | export async function convert_sfx() { 8 | let i = 0; 9 | const dir = join("..", "..", "src", "static", "sfx"); 10 | for (let file of readdirSync(dir)) { 11 | if (file.includes("snd")) { 12 | console.log(file); 13 | const metaData = await mm.parseFile(`${join(dir, file)}`); 14 | 15 | const sampleRate = metaData.format.sampleRate; 16 | 17 | spawnSync( 18 | "ffmpeg", 19 | [ 20 | "-i", 21 | join(dir, file), 22 | "-filter_complex", 23 | `asetrate=${sampleRate}*2^(6/12),atempo=1/2^(6/12)`, 24 | "-n", 25 | join(dir, "..", "t", file), 26 | ], 27 | { stdio: "inherit" } 28 | ); 29 | } 30 | } 31 | } 32 | 33 | convert_sfx(); 34 | -------------------------------------------------------------------------------- /scripts/extract/extract.mjs: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import { tmpdir } from "os"; 3 | import { mkdtempSync, rmSync, mkdirSync } from "fs"; 4 | import { join, resolve } from "path"; 5 | import { extract_video, extract_audio } from "./extract_media.mjs"; 6 | import { extract_voice } from "./extract_voice.mjs"; 7 | import { extract_lapks } from "./extract_lapks.mjs"; 8 | import yargs from "yargs"; 9 | import { hideBin } from "yargs/helpers"; 10 | import {extract_site_images} from "./extract_site_images.mjs"; 11 | 12 | const argv = yargs(hideBin(process.argv)) 13 | .option("tempdir", { 14 | type: "string", 15 | description: "Path to a temporary directory", 16 | default: mkdtempSync(join(tmpdir(), "extractor-")), 17 | }) 18 | .option("no_index", { 19 | type: "boolean", 20 | description: 21 | "Don't generate an index file for each disc, the index files MUST exist already", 22 | }) 23 | .option("no_video", { 24 | type: "boolean", 25 | description: "Don't extract video", 26 | }) 27 | .option("no_audio", { 28 | type: "boolean", 29 | description: "Don't extract audio", 30 | }) 31 | .option("no_voice", { 32 | type: "boolean", 33 | description: "Don't extract voice.bin", 34 | }) 35 | .option("no_delete", { 36 | type: "boolean", 37 | description: 38 | "Don't delete any temporary files or directories, useful when using --tempdir (WARNING: uses 6+ GB of space)", 39 | }) 40 | .option("no_lapks", { 41 | type: "boolean", 42 | description: "Don't extract lapks.bin", 43 | }) 44 | .option("no_site_images", { 45 | type: "boolean", 46 | description: "Don't extract sitea.bin or siteb.bin", 47 | }).argv; 48 | 49 | mkdirSync(argv.tempdir, { recursive: true }); 50 | 51 | const jpsxdec_jar = resolve(join("jpsxdec", "jpsxdec.jar")); 52 | 53 | // generate disc indexes 54 | if (!argv.no_index) { 55 | for (const disc of ["disc1", "disc2"]) { 56 | spawnSync( 57 | "java", 58 | [ 59 | "-jar", 60 | jpsxdec_jar, 61 | "-f", 62 | join("discs", disc + ".bin"), 63 | "-x", 64 | join(argv.tempdir, disc + ".idx"), 65 | ], 66 | { stdio: "inherit" } 67 | ); 68 | } 69 | } 70 | 71 | if (!argv.no_video) { 72 | extract_video(argv.tempdir, jpsxdec_jar, argv.no_delete); 73 | } 74 | 75 | if (!argv.no_audio) { 76 | extract_audio(argv.tempdir, jpsxdec_jar, argv.no_delete); 77 | } 78 | 79 | if (!argv.no_voice) { 80 | extract_voice(argv.tempdir, jpsxdec_jar); 81 | } 82 | 83 | if (!argv.no_lapks) { 84 | extract_lapks(argv.tempdir, jpsxdec_jar); 85 | } 86 | 87 | if (!argv.no_site_images) { 88 | extract_site_images(argv.tempdir, jpsxdec_jar); 89 | } 90 | 91 | if (!argv.no_delete) { 92 | rmSync(argv.tempdir, { recursive: true }); 93 | } 94 | -------------------------------------------------------------------------------- /scripts/extract/extract_media.mjs: -------------------------------------------------------------------------------- 1 | import { spawnSync, spawn } from "child_process"; 2 | import { mkdirSync, readdirSync, rmSync } from "fs"; 3 | import { join } from "path"; 4 | 5 | export function extract_video(tempdir, jpsxdec_jar, no_delete) { 6 | // extract all video 7 | for (const disc_index of ["disc1.idx", "disc2.idx"]) { 8 | spawnSync( 9 | "java", 10 | [ 11 | "-jar", 12 | jpsxdec_jar, 13 | "-x", 14 | join(tempdir, disc_index), 15 | "-a", 16 | "video", 17 | "-dir", 18 | tempdir, 19 | "-quality", 20 | "high", 21 | "-vf", 22 | "avi:rgb", 23 | "-up", 24 | "Lanczos3", 25 | ], 26 | { stdio: "inherit" } 27 | ); 28 | } 29 | 30 | const output_movie_folder = join( 31 | "..", 32 | "..", 33 | "src", 34 | "static", 35 | "media", 36 | "movie" 37 | ); 38 | 39 | // create destination folder 40 | mkdirSync(output_movie_folder, { recursive: true }); 41 | 42 | // convert all movies to mp4 43 | for (const movieDir of ["MOVIE", "MOVIE2"]) { 44 | for (let file of readdirSync(`${join(tempdir, movieDir)}`)) { 45 | if (file.endsWith(".wav")) continue; 46 | spawnSync( 47 | "ffmpeg", 48 | [ 49 | "-i", 50 | join(tempdir, movieDir, file), 51 | "-pix_fmt", 52 | "yuv420p", 53 | "-n", 54 | join(output_movie_folder, file.replace("avi", "mp4")), 55 | ], 56 | { stdio: "inherit" } 57 | ); 58 | } 59 | } 60 | 61 | if (!no_delete) { 62 | // cleanup source folders 63 | rmSync(join(tempdir, "MOVIE"), { recursive: true }); 64 | rmSync(join(tempdir, "MOVIE2"), { recursive: true }); 65 | } 66 | } 67 | 68 | export function extract_audio(tempdir, jpsxdec_jar, no_delete) { 69 | // extract all audio 70 | for (const disc_index of ["disc1.idx", "disc2.idx"]) { 71 | spawnSync( 72 | "java", 73 | [ 74 | "-jar", 75 | jpsxdec_jar, 76 | "-x", 77 | join(tempdir, disc_index), 78 | "-a", 79 | "audio", 80 | "-dir", 81 | tempdir, 82 | ], 83 | { stdio: "inherit" } 84 | ); 85 | } 86 | 87 | const output_audio_folder = join( 88 | "..", 89 | "..", 90 | "src", 91 | "static", 92 | "media", 93 | "audio" 94 | ); 95 | 96 | // create destination folder 97 | mkdirSync(output_audio_folder, { recursive: true }); 98 | 99 | // convert all audio to mp4 100 | for (let file of readdirSync(`${join(tempdir, "XA")}`)) { 101 | spawnSync("ffmpeg", [ 102 | "-i", 103 | join(tempdir, "XA", file), 104 | "-n", 105 | join(output_audio_folder, file.replace("wav", "mp4")), 106 | ], {stdio:'inherit'}); 107 | } 108 | 109 | if (!no_delete) { 110 | // cleanup source folder 111 | rmSync(join(tempdir, "XA"), { recursive: true }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /scripts/extract/extract_site_images.mjs: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import { readFileSync, mkdirSync, writeFileSync, renameSync } from "fs"; 3 | import { join, resolve } from "path"; 4 | import LainCompress from "./lain_compress.js"; 5 | 6 | export function extract_site_images(tempdir, jpsxdec_jar) { 7 | for (const [disc, site] of ["A", "B"].entries()) { 8 | spawnSync( 9 | "java", 10 | [ 11 | "-jar", 12 | jpsxdec_jar, 13 | "-x", 14 | join(tempdir, `disc${disc + 1}.idx`), 15 | "-i", 16 | `SITE${site}.BIN`, 17 | "-dir", 18 | tempdir, 19 | ], 20 | { stdio: "inherit" } 21 | ); 22 | const site_images = JSON.parse(readFileSync(`site_${site}_images.json`)); 23 | 24 | let image_data = readFileSync(join(tempdir, `SITE${site}.BIN`)); 25 | let output_folder = join("..", "..", "src", "static", "images", site.toLowerCase()); 26 | 27 | mkdirSync(output_folder, { recursive: true }); 28 | 29 | for (let [index, image] of site_images.entries()) { 30 | if (image.skip) continue; 31 | let compressed_data = image_data.slice( 32 | image.offset + 4, 33 | image.offset + image.size 34 | ); 35 | let tim_file = resolve(join(tempdir, `${index}.tim`)); 36 | let decompressed_data = new LainCompress().decode(compressed_data); 37 | writeFileSync(tim_file, decompressed_data); 38 | spawnSync( 39 | "java", 40 | ["-jar", jpsxdec_jar, "-f", tim_file, "-static", "tim"], 41 | { stdio: "inherit", cwd: tempdir } 42 | ); 43 | renameSync( 44 | join(tempdir, `${index}_p0.png`), 45 | join(output_folder, `${index}.png`) 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scripts/extract/extract_voice.mjs: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import { readFileSync, mkdirSync, writeFileSync } from "fs"; 3 | import { join } from "path"; 4 | 5 | export function extract_voice(tempdir, jpsxdec_jar) { 6 | spawnSync( 7 | "java", 8 | [ 9 | "-jar", 10 | jpsxdec_jar, 11 | "-x", 12 | join(tempdir, "disc1.idx"), 13 | "-i", 14 | "VOICE.BIN", 15 | "-dir", 16 | tempdir, 17 | ], 18 | { stdio: "inherit" } 19 | ); 20 | const voice_files = JSON.parse(readFileSync("voice.json")); 21 | 22 | let voice_data = readFileSync(join(tempdir, "VOICE.BIN")); 23 | let output_folder = join("..", "..", "src", "static", "voice"); 24 | 25 | mkdirSync(output_folder, { recursive: true }); 26 | 27 | for (let voice_file of voice_files) { 28 | let tempfile = join(tempdir, voice_file.translated_name); 29 | let outfile = join( 30 | output_folder, 31 | voice_file.translated_name.replace("WAV", "mp4") 32 | ); 33 | let data = voice_data.slice( 34 | voice_file.offset, 35 | voice_file.offset + voice_file.size 36 | ); 37 | writeFileSync(tempfile, data); 38 | if (["BYA.WAV", "BYO.WAV", "BYU.WAV"].includes(voice_file.original_name)) { 39 | spawnSync("ffmpeg", ["-i", tempfile, outfile]); 40 | } else { 41 | spawnSync("ffmpeg", [ 42 | "-sample_rate", 43 | 22050, 44 | "-f", 45 | "s16le", 46 | "-i", 47 | tempfile, 48 | outfile, 49 | ], {stdio:'inherit'}); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/extract/jpsxdec/doc/CHANGES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/scripts/extract/jpsxdec/doc/CHANGES.txt -------------------------------------------------------------------------------- /scripts/extract/jpsxdec/doc/CREDITS.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/scripts/extract/jpsxdec/doc/CREDITS.txt -------------------------------------------------------------------------------- /scripts/extract/jpsxdec/doc/LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/scripts/extract/jpsxdec/doc/LICENSE.txt -------------------------------------------------------------------------------- /scripts/extract/jpsxdec/doc/New-BSD.txt: -------------------------------------------------------------------------------- 1 | http://www.opensource.org/licenses/bsd-license.php 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | * Neither the name of the JSR305 expert group nor the names of its 12 | contributors may be used to endorse or promote products derived from 13 | this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 17 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /scripts/extract/jpsxdec/doc/jPSXdec-manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/scripts/extract/jpsxdec/doc/jPSXdec-manual.pdf -------------------------------------------------------------------------------- /scripts/extract/jpsxdec/jpsxdec-lib.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/scripts/extract/jpsxdec/jpsxdec-lib.jar -------------------------------------------------------------------------------- /scripts/extract/jpsxdec/jpsxdec.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ad044/LainTSX/b43999cf00d471c223c191c0df285b30d08fd5d5/scripts/extract/jpsxdec/jpsxdec.jar -------------------------------------------------------------------------------- /scripts/extract/lain_compress.js: -------------------------------------------------------------------------------- 1 | const KaitaiStream = require('kaitai-struct/KaitaiStream'); 2 | 3 | // based on https://github.com/magical/nlzss/blob/master/lzss3.py and https://github.com/m35/jpsxdec/blob/readme/laintools/src/laintools/Lain_Pk.java 4 | class LainCompress { 5 | decode (io) { 6 | const bits = (byte) => { return [(byte >> 7) & 1, (byte >> 6) & 1,(byte >> 5) & 1,(byte >> 4) & 1,(byte >> 3) & 1,(byte >> 2) & 1, (byte >> 1) & 1, (byte) & 1] }; 7 | 8 | let compressed = new KaitaiStream(io); 9 | let decompressed_size = compressed.readU4le(); 10 | let decompressed = Buffer.alloc(decompressed_size); 11 | let decompressed_pos = 0; 12 | 13 | while (decompressed_pos < decompressed_size) { 14 | let flags = bits(compressed.readU1()); 15 | 16 | for (let flag of flags) { 17 | if (flag === 0) { 18 | decompressed[decompressed_pos] = compressed.readBytes(1); 19 | decompressed_pos++; 20 | } 21 | 22 | else if (flag === 1) { 23 | let offset = compressed.readU1() + 1; 24 | let size = compressed.readU1() + 3; 25 | for (let i = 0; i < size; i++) { 26 | decompressed[decompressed_pos] = decompressed[decompressed_pos - offset]; 27 | decompressed_pos++; 28 | } 29 | } 30 | 31 | if (decompressed_size <= decompressed_pos) break; 32 | } 33 | } 34 | 35 | if (decompressed.length !== decompressed_size) 36 | throw new Error("Decompressed size does not match the expected size!"); 37 | return decompressed; 38 | } 39 | } 40 | module.exports = LainCompress; 41 | -------------------------------------------------------------------------------- /scripts/extract/lapk.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: lapk 3 | file-extension: bin 4 | endian: le 5 | ks-opaque-types: true 6 | seq: 7 | - id: magic 8 | contents: 'lapk' 9 | - id: lapk_size 10 | type: u4 11 | - id: data 12 | type: lapk_data 13 | size: lapk_size 14 | types: 15 | cell_header: 16 | seq: 17 | - id: cell_offset 18 | type: u4 19 | - id: negative_x_position 20 | type: u2 21 | - id: negative_y_position 22 | type: u2 23 | - id: unknown 24 | type: u4 25 | cell_data: 26 | seq: 27 | - id: width 28 | type: u2 29 | - id: height 30 | type: u2 31 | - id: chrominance_quantisation_scale 32 | type: u2 33 | - id: luminance_quantisation_scale 34 | type: u2 35 | - id: image_data_size 36 | type: u4 37 | - id: run_length_code_count 38 | type: u4 39 | - id: image_data 40 | size: image_data_size - 4 41 | - id: bit_mask 42 | size-eos: true 43 | process: lain_compress 44 | 45 | lapk_data: 46 | seq: 47 | - id: cell_count 48 | type: u4 49 | - id: cell_headers 50 | type: cell_header 51 | repeat: expr 52 | repeat-expr: cell_count 53 | - id: cell_data 54 | type: cell_data 55 | size: "_index == cell_count - 1 ? _parent.lapk_size - 4 - cell_count * 12 - cell_headers[_index].cell_offset : cell_headers[_index + 1].cell_offset - cell_headers[_index].cell_offset" 56 | repeat: expr 57 | repeat-expr: cell_count 58 | -------------------------------------------------------------------------------- /scripts/extract/lapks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "output_name": "", "offset": 0, "size": 88748 }, 3 | { "output_name": "", "offset": 90112, "size": 79188 }, 4 | { "output_name": "", "offset": 169984, "size": 77196 }, 5 | { "output_name": "", "offset": 247808, "size": 64020 }, 6 | { "output_name": "", "offset": 313344, "size": 278740 }, 7 | { "output_name": "", "offset": 593920, "size": 464668 }, 8 | { "output_name": "", "offset": 1058816, "size": 305600 }, 9 | { "output_name": "", "offset": 1366016, "size": 12304 }, 10 | { "output_name": "", "offset": 1380352, "size": 29096 }, 11 | { "output_name": "", "offset": 1411072, "size": 29196 }, 12 | { "output_name": "", "offset": 1441792, "size": 144216 }, 13 | { "output_name": "", "offset": 1587200, "size": 144384 }, 14 | { "output_name": "", "offset": 1732608, "size": 144148 }, 15 | { "output_name": "", "offset": 1878016, "size": 143984 }, 16 | { "output_name": "", "offset": 2023424, "size": 450176 }, 17 | { "output_name": "", "offset": 2473984, "size": 485652 }, 18 | { "output_name": "", "offset": 2961408, "size": 175544 }, 19 | { "output_name": "", "offset": 3137536, "size": 176852 }, 20 | { "output_name": "", "offset": 3315712, "size": 277336 }, 21 | { "output_name": "", "offset": 3594240, "size": 149004 }, 22 | { "output_name": "", "offset": 3743744, "size": 256896 }, 23 | { "output_name": "", "offset": 4001792, "size": 66872 }, 24 | { "output_name": "", "offset": 4069376, "size": 51500 }, 25 | { "output_name": "", "offset": 4122624, "size": 128272 }, 26 | { "output_name": "", "offset": 4251648, "size": 45952 }, 27 | { "output_name": "", "offset": 4298752, "size": 52884 }, 28 | { "output_name": "", "offset": 4352000, "size": 282900 }, 29 | { "output_name": "", "offset": 4636672, "size": 154348 }, 30 | { "output_name": "", "offset": 4792320, "size": 116584 }, 31 | { "output_name": "", "offset": 4909056, "size": 234832 }, 32 | { "output_name": "", "offset": 5144576, "size": 405952 }, 33 | { "output_name": "", "offset": 5552128, "size": 194376 }, 34 | { "output_name": "", "offset": 5746688, "size": 80684 }, 35 | { "output_name": "", "offset": 5828608, "size": 219864 }, 36 | { "output_name": "", "offset": 6049792, "size": 285248 }, 37 | { "output_name": "", "offset": 6336512, "size": 257924 }, 38 | { "output_name": "", "offset": 6594560, "size": 354416 }, 39 | { "output_name": "spin.png", "offset": 6950912, "size": 325140, "tile":"6x5" }, 40 | { "output_name": "", "offset": 7276544, "size": 355752 }, 41 | { "output_name": "", "offset": 7632896, "size": 371184 }, 42 | { "output_name": "", "offset": 8005632, "size": 391168 }, 43 | { "output_name": "", "offset": 8396800, "size": 314484 }, 44 | { "output_name": "", "offset": 8712192, "size": 238792 }, 45 | { "output_name": "", "offset": 8951808, "size": 475660 }, 46 | { "output_name": "", "offset": 9428992, "size": 281608 }, 47 | { "output_name": "", "offset": 9711616, "size": 311472 }, 48 | { "output_name": "", "offset": 10024960, "size": 233456 }, 49 | { "output_name": "", "offset": 10258432, "size": 339824 }, 50 | { "output_name": "", "offset": 10598400, "size": 276968 }, 51 | { "output_name": "", "offset": 10876928, "size": 488988 }, 52 | { "output_name": "", "offset": 11366400, "size": 395204 }, 53 | { "output_name": "", "offset": 11761664, "size": 432604 }, 54 | { "output_name": "", "offset": 12195840, "size": 624836 }, 55 | { "output_name": "", "offset": 12822528, "size": 315756 }, 56 | { "output_name": "", "offset": 13139968, "size": 391348 }, 57 | { "output_name": "", "offset": 13533184, "size": 12216 }, 58 | { "output_name": "", "offset": 13545472, "size": 12236 }, 59 | { "output_name": "", "offset": 13557760, "size": 365476 }, 60 | { "output_name": "", "offset": 13924352, "size": 12200 } 61 | ] 62 | 63 | -------------------------------------------------------------------------------- /scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | // https://github.com/pmndrs/react-spring/issues/1078 2 | // thanks to https://github.com/pmndrs/react-spring/issues/1078#issuecomment-677528907 3 | 4 | const replace = require("replace-in-file"); 5 | 6 | const removeAllSideEffectsFalseFromReactSpringPackages = async () => { 7 | try { 8 | const results = await replace({ 9 | files: "node_modules/@react-spring/*/package.json", 10 | from: `"sideEffects": false`, 11 | to: `"sideEffects": true`, 12 | }); 13 | 14 | // console.log(results); // uncomment to log changed files 15 | } catch (e) { 16 | console.log( 17 | 'error while trying to remove string "sideEffects:false" from react-spring packages', 18 | e 19 | ); 20 | } 21 | }; 22 | 23 | removeAllSideEffectsFalseFromReactSpringPackages(); 24 | -------------------------------------------------------------------------------- /src/additional.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mp4" { 2 | const src: string; 3 | export default src; 4 | } 5 | 6 | declare module "*.vert" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.frag" { 12 | const content: string; 13 | export default content; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/components/canvas/objects/BootScene/BootAccela.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { PlainAnimator } from "three-plain-animator/lib/plain-animator"; 4 | import { useTexture } from "@react-three/drei"; 5 | 6 | type BootAccelaProps = { 7 | visible: boolean; 8 | }; 9 | 10 | const BootAccela = (props: BootAccelaProps) => { 11 | const accelaBoot = useTexture( 12 | "/sprites/boot/login_intro_accela_spritesheet.png" 13 | ); 14 | const makeMeSad = useTexture("/sprites/boot/make_me_sad.png"); 15 | 16 | const [animator] = useState(() => { 17 | const anim = new PlainAnimator(accelaBoot, 10, 3, 29, 60); 18 | anim.init(0); 19 | return anim; 20 | }); 21 | 22 | useFrame(() => { 23 | animator.animate(); 24 | }); 25 | 26 | return ( 27 | <> 28 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default BootAccela; 47 | -------------------------------------------------------------------------------- /src/components/canvas/objects/BootScene/BootLoadData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Prompt from "../Prompt"; 3 | import { useStore } from "@/store"; 4 | import Status from "../SaveStatusDisplay"; 5 | import { useTexture } from "@react-three/drei"; 6 | 7 | type BootLoadDataProps = { 8 | visible: boolean; 9 | }; 10 | 11 | const BootLoadData = (props: BootLoadDataProps) => { 12 | const promptVisible = useStore((state) => state.promptVisible); 13 | 14 | const loadDataUnderline = useTexture( 15 | "/sprites/boot/load_data_header_underline.png" 16 | ); 17 | 18 | return ( 19 | <> 20 | {props.visible && ( 21 | <> 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | )} 37 | 38 | ); 39 | }; 40 | 41 | export default BootLoadData; 42 | -------------------------------------------------------------------------------- /src/components/canvas/objects/BootScene/BootMainMenuComponents.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { a, useSpring } from "@react-spring/three"; 3 | import { useStore } from "@/store"; 4 | import { BootSubscene, MainMenuComponent, Position } from "@/types"; 5 | import { useTexture } from "@react-three/drei"; 6 | import { sRGBEncoding } from "three"; 7 | 8 | type BootMainMenuProps = { 9 | visible: boolean; 10 | }; 11 | 12 | type MainMenuComponentSpring = { 13 | opacity: number; 14 | position: Position; 15 | }; 16 | 17 | const BootMainMenuComponents = (props: BootMainMenuProps) => { 18 | const bootSubscene = useStore((state) => state.bootSubscene); 19 | 20 | const authorizeUserActive = useTexture( 21 | "/sprites/boot/authorize_user_active.png" 22 | ); 23 | const authorizeUserInactive = useTexture( 24 | "/sprites/boot/authorize_user_inactive.png" 25 | ); 26 | const authorizeUserHeader = useTexture( 27 | "/sprites/boot/authorize_user_scene_header.png" 28 | ); 29 | 30 | const loadDataActive = useTexture("/sprites/boot/load_data_active.png"); 31 | const loadDataInactive = useTexture("/sprites/boot/load_data_inactive.png"); 32 | const loadDataHeader = useTexture("/sprites/boot/load_data_header.png"); 33 | 34 | const activeMainMenuElement = useStore( 35 | (state) => state.mainMenuComponent 36 | ); 37 | 38 | const loadDataTexture = useMemo(() => { 39 | if (bootSubscene === BootSubscene.LoadData) { 40 | return loadDataHeader; 41 | } 42 | if (activeMainMenuElement === MainMenuComponent.LoadData) { 43 | return loadDataActive; 44 | } 45 | 46 | return loadDataInactive; 47 | }, [ 48 | activeMainMenuElement, 49 | bootSubscene, 50 | loadDataActive, 51 | loadDataHeader, 52 | loadDataInactive, 53 | ]); 54 | 55 | const authorizeUserTexture = useMemo(() => { 56 | if (bootSubscene === BootSubscene.AuthorizeUser) { 57 | return authorizeUserHeader; 58 | } 59 | if (activeMainMenuElement === MainMenuComponent.AuthorizeUser) { 60 | return authorizeUserActive; 61 | } 62 | 63 | return authorizeUserInactive; 64 | }, [ 65 | activeMainMenuElement, 66 | authorizeUserActive, 67 | authorizeUserInactive, 68 | authorizeUserHeader, 69 | bootSubscene, 70 | ]); 71 | 72 | const loadDataSpring = useSpring({ 73 | opacity: bootSubscene !== BootSubscene.AuthorizeUser ? 1 : 0, 74 | position: 75 | bootSubscene === BootSubscene.LoadData ? [-1.13, -1, 0] : [0, -0.5, 0], 76 | config: { duration: 500 }, 77 | }); 78 | 79 | const authorizeUserSpring = useSpring({ 80 | opacity: bootSubscene !== BootSubscene.LoadData ? 1 : 0, 81 | position: 82 | bootSubscene === BootSubscene.AuthorizeUser 83 | ? [1.13, 1.2, 0] 84 | : [0, 0.5, 0], 85 | config: { duration: 500 }, 86 | }); 87 | 88 | return ( 89 | <> 90 | {props.visible && ( 91 | <> 92 | 101 | {/* @ts-ignore: https://github.com/pmndrs/react-spring/issues/1515 */} 102 | 107 | 108 | 109 | 114 | 119 | 120 | 121 | )} 122 | 123 | ); 124 | }; 125 | 126 | export default BootMainMenuComponents; 127 | -------------------------------------------------------------------------------- /src/components/canvas/objects/EndScene/EndCylinder.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | import { DoubleSide } from "three"; 4 | 5 | const EndCylinder = () => { 6 | const mainCylinder = useTexture("/sprites/end/end_cylinder_1.png"); 7 | 8 | return ( 9 | <> 10 | 11 | 15 | 22 | 23 | 24 | 25 | 29 | 36 | 37 | 38 | 39 | 43 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default memo(EndCylinder); 56 | -------------------------------------------------------------------------------- /src/components/canvas/objects/EndScene/EndSelectionScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef, useState } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { PlainAnimator } from "three-plain-animator/lib/plain-animator"; 4 | import { a, useSpring } from "@react-spring/three"; 5 | import { useStore } from "@/store"; 6 | import { EndComponent } from "@/types"; 7 | import { useTexture } from "@react-three/drei"; 8 | 9 | type EndSelectionScreenProps = { 10 | visible: boolean; 11 | }; 12 | 13 | const EndSelectionScreen = (props: EndSelectionScreenProps) => { 14 | const middleSpritesheetTex: any = useTexture( 15 | "/sprites/end/end_middle_spritesheet.png" 16 | ); 17 | const circleSpritesheetTex: any = useTexture( 18 | "/sprites/end/end_circle_spritesheet.png" 19 | ); 20 | const endText = useTexture("/sprites/end/end_end_text.png"); 21 | const continueText = useTexture("/sprites/end/end_continue_text.png"); 22 | const middleLain = useTexture("/sprites/end/end_middle_lain.png"); 23 | 24 | const activeComponent = useStore((state) => state.endComponent); 25 | 26 | const [middleSpritesheetAnimator] = useState(() => { 27 | const anim = new PlainAnimator(middleSpritesheetTex, 1, 4, 4, 24); 28 | anim.init(0); 29 | return anim; 30 | }); 31 | 32 | const [circleSpritesheetAnimator] = useState(() => { 33 | const anim = new PlainAnimator(circleSpritesheetTex, 4, 2, 8, 24); 34 | anim.init(0); 35 | return anim; 36 | }); 37 | 38 | const [lainVisible, setLainVisible] = useState(0); 39 | 40 | const lastTime = useRef(0); 41 | 42 | const middleBoxRef = useRef(null); 43 | useFrame((state, delta) => { 44 | middleSpritesheetAnimator.animate(); 45 | circleSpritesheetAnimator.animate(); 46 | if (middleBoxRef.current) { 47 | middleBoxRef.current.rotation.z -= delta / 5; 48 | } 49 | const now = Date.now(); 50 | if (now > lastTime.current + 15000) { 51 | lastTime.current = now; 52 | setLainVisible(Number(!lainVisible)); 53 | } 54 | }); 55 | 56 | const { lainOpacityToggle } = useSpring({ 57 | lainOpacityToggle: lainVisible, 58 | config: { duration: 500 }, 59 | }); 60 | 61 | const lainOpacity = lainOpacityToggle.to([0, 1], [0, 0.5]); 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {/* @ts-ignore: https://github.com/pmndrs/react-spring/issues/1515 */} 91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default memo(EndSelectionScreen); 98 | -------------------------------------------------------------------------------- /src/components/canvas/objects/EndScene/EndSphere.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, memo } from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | import { DoubleSide } from "three"; 4 | import { Position } from "@/types"; 5 | import { useFrame } from "@react-three/fiber"; 6 | 7 | type EndSphereProps = { 8 | position: Position; 9 | outroAnim: boolean; 10 | }; 11 | 12 | const EndSphere = (props: EndSphereProps) => { 13 | const secondCylinder = useTexture("/sprites/end/end_cylinder_2.png"); 14 | 15 | const meshRef = useRef(null); 16 | 17 | useFrame((_, delta) => { 18 | if (meshRef.current) { 19 | meshRef.current.rotation.y += 0.5 * delta; 20 | if ( 21 | props.outroAnim && 22 | meshRef.current.scale.x > 0 && 23 | meshRef.current.scale.y > 0 && 24 | meshRef.current.scale.z > 0 25 | ) { 26 | meshRef.current.scale.x -= 0.5 * delta; 27 | meshRef.current.scale.y -= 0.5 * delta; 28 | meshRef.current.scale.z -= 0.5 * delta; 29 | } 30 | } 31 | }); 32 | 33 | return ( 34 | 35 | 36 | 44 | 45 | ); 46 | }; 47 | 48 | export default memo(EndSphere); 49 | -------------------------------------------------------------------------------- /src/components/canvas/objects/GateScene/BlueDigit.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef } from "react"; 2 | import { a, SpringValue } from "@react-spring/three"; 3 | import { useTexture } from "@react-three/drei"; 4 | import vertex from "@/shaders/blue_digit.vert"; 5 | import fragment from "@/shaders/blue_digit.frag"; 6 | import { Position } from "@/types"; 7 | 8 | type BlueDigitProps = { 9 | type: number; 10 | position: SpringValue; 11 | }; 12 | 13 | const BlueDigit = (props: BlueDigitProps) => { 14 | const one = useTexture("/sprites/gate/blue_binary_singular_one.png"); 15 | const zero = useTexture("/sprites/gate/blue_binary_singular_zero.png"); 16 | 17 | const objRef = useRef(null); 18 | const matRef = useRef(null); 19 | 20 | const uniforms = useMemo( 21 | () => ({ 22 | tex: { 23 | type: "t", 24 | value: props.type === 1 ? one : zero, 25 | }, 26 | brightnessMultiplier: { value: 1.5 }, 27 | }), 28 | [one, zero, props.type] 29 | ); 30 | 31 | useEffect(() => { 32 | setTimeout(() => { 33 | if (matRef.current) { 34 | matRef.current.uniforms.brightnessMultiplier.value = 3.5; 35 | matRef.current.uniformsNeedUpdate = true; 36 | } 37 | }, 1400); 38 | setTimeout(() => { 39 | if (objRef.current) objRef.current.visible = true; 40 | }, 150); 41 | }, []); 42 | 43 | return ( 44 | 51 | 52 | 60 | 61 | ); 62 | }; 63 | 64 | export default BlueDigit; 65 | -------------------------------------------------------------------------------- /src/components/canvas/objects/GateScene/GateHUD.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | import { TextType } from "@/types"; 4 | import TextRenderer from "../TextRenderer/TextRenderer"; 5 | import useCappedFrame from "@/hooks/useCappedFrame"; 6 | 7 | type GateMiddleProps = { 8 | intro: boolean; 9 | gateLvl: number; 10 | }; 11 | 12 | const GateHUD = (props: GateMiddleProps) => { 13 | const gatePass = useTexture("/sprites/gate/gate_pass.png"); 14 | const gatePassUnderline = useTexture("/sprites/gate/gate_pass_underline.png"); 15 | const gateAccessPass = useTexture("/sprites/gate/you_got_an_access_pass.png"); 16 | const changeSiteEnable = useTexture("/sprites/gate/change_site_enable.png"); 17 | const gateLeftThing = useTexture("/sprites/gate/left_gate_thing.png"); 18 | const gateRightThing = useTexture("/sprites/gate/right_gate_thing.png"); 19 | 20 | const pressAnyTextRef = useRef(null); 21 | 22 | useCappedFrame(() => { 23 | if (pressAnyTextRef.current) { 24 | pressAnyTextRef.current.visible = !pressAnyTextRef.current.visible; 25 | } 26 | }, 0.5); 27 | 28 | return ( 29 | <> 30 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | 48 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 69 | 75 | 76 | 77 | 82 | 88 | 89 | 90 | 91 | 97 | 98 | 99 | {!props.intro && ( 100 | 101 | 108 | 109 | )} 110 | 111 | ); 112 | }; 113 | 114 | export default GateHUD; 115 | -------------------------------------------------------------------------------- /src/components/canvas/objects/GateScene/GateMiddleObject.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { a, useSpring, useSprings } from "@react-spring/three"; 3 | import blueDigitData from "@/json/gate/blue_digit_positions.json"; 4 | import Mirror from "./Mirror"; 5 | import BlueDigit from "./BlueDigit"; 6 | import { Position } from "@/types"; 7 | 8 | type GateMiddleObjectProps = { 9 | intro: boolean; 10 | gateLvl: number; 11 | }; 12 | 13 | const GateMiddleObject = (props: GateMiddleObjectProps) => { 14 | const [digitSprings, setDigitSprings] = useSprings( 15 | blueDigitData.length, 16 | (i) => ({ 17 | position: blueDigitData[i].initial as Position, 18 | config: { duration: 150 }, 19 | }) 20 | ); 21 | 22 | const [groupSpring, setGroupSpring] = useSpring<{ position: Position }>( 23 | () => ({ 24 | position: [0, 0, 0], 25 | config: { duration: 900 }, 26 | }) 27 | ); 28 | 29 | useEffect(() => { 30 | setDigitSprings((i) => ({ 31 | position: blueDigitData[i].final, 32 | delay: blueDigitData[i].delay, 33 | })); 34 | 35 | setTimeout(() => setGroupSpring({ position: [-0.15, -0.2, 0] }), 1400); 36 | }, [setDigitSprings, setGroupSpring]); 37 | 38 | return ( 39 | <> 40 | 41 | {digitSprings.map((item, idx) => ( 42 | 47 | ))} 48 | 49 | 50 | 0} 52 | position={[0, 0, -0.4]} 53 | rotation={[0, Math.PI / 2, 0]} 54 | /> 55 | 1} 57 | position={[0, 0, 0.5]} 58 | rotation={[0, Math.PI / 2, 0]} 59 | /> 60 | 2} 62 | position={[0.4, 0, 0.05]} 63 | /> 64 | 3} 66 | position={[-0.4, 0, 0.05]} 67 | /> 68 | 69 | 70 | ); 71 | }; 72 | 73 | export default GateMiddleObject; 74 | -------------------------------------------------------------------------------- /src/components/canvas/objects/GateScene/GateSide.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useRef } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { RepeatWrapping, DoubleSide } from "three"; 4 | import { useTexture } from "@react-three/drei"; 5 | import vertex from "@/shaders/gate_side.vert"; 6 | import leftFragment from "@/shaders/gate_side_left.frag"; 7 | import rightFragment from "@/shaders/gate_side_right.frag"; 8 | 9 | const GateSide = () => { 10 | const blueBinary = useTexture("/sprites/gate/blue_binary.png"); 11 | 12 | // coming back to this a year or so later and i have no idea 13 | // what past me meant by this comment so im not gonna touch it :D 14 | // | 15 | // V 16 | // this is really fucking weird 17 | const texture = useMemo(() => { 18 | blueBinary.wrapS = RepeatWrapping; 19 | blueBinary.wrapT = RepeatWrapping; 20 | blueBinary.repeat.set(5, 5); 21 | return blueBinary; 22 | }, [blueBinary]); 23 | 24 | const last = useRef(0); 25 | 26 | useFrame(() => { 27 | const now = Date.now(); 28 | if (matRef.current) { 29 | if (now > last.current + 50) { 30 | matRef.current.uniforms.offset.value += 0.5; 31 | last.current = now; 32 | } 33 | } 34 | }); 35 | 36 | const matRef = useRef(null); 37 | 38 | const uniforms = useMemo( 39 | () => ({ 40 | tex1: { type: "t", value: texture }, 41 | offset: { value: 0 }, 42 | }), 43 | [texture] 44 | ); 45 | 46 | return ( 47 | <> 48 | 54 | 55 | 62 | 63 | 69 | 70 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default GateSide; 84 | -------------------------------------------------------------------------------- /src/components/canvas/objects/GateScene/Mirror.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useRef } from "react"; 2 | import { useGLTF, useTexture } from "@react-three/drei"; 3 | import { GLTF } from "three-stdlib/loaders/GLTFLoader"; 4 | import { RepeatWrapping } from "three"; 5 | import { useFrame } from "@react-three/fiber"; 6 | 7 | type GLTFResult = GLTF & { 8 | nodes: { 9 | GatePass: THREE.Mesh; 10 | }; 11 | materials: { 12 | Material: THREE.MeshStandardMaterial; 13 | }; 14 | }; 15 | 16 | type MirrorProps = { 17 | visible: boolean; 18 | position: [number, number, number]; 19 | rotation?: [number, number, number]; 20 | }; 21 | 22 | const Mirror = (props: MirrorProps) => { 23 | const mirror = useTexture("/sprites/gate/gate_object_texture.png"); 24 | const { nodes } = useGLTF("models/gate_mirror.glb") as GLTFResult; 25 | 26 | const mirrorGroupRef = useRef(null); 27 | 28 | const texture = useMemo(() => { 29 | mirror.wrapS = mirror.wrapT = RepeatWrapping; 30 | return mirror; 31 | }, [mirror]); 32 | 33 | useFrame((_, delta) => { 34 | if (mirrorGroupRef.current) { 35 | mirrorGroupRef.current.rotation.y -= delta * 1.5; 36 | texture.offset.x -= 0.1 * delta; 37 | } 38 | }); 39 | 40 | return ( 41 | <> 42 | 43 | 48 | 55 | 61 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default Mirror; 69 | -------------------------------------------------------------------------------- /src/components/canvas/objects/IdleManager.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from "@react-three/fiber"; 2 | import { useStore } from "@/store"; 3 | import { playAudio } from "@/utils/audio"; 4 | import { playIdleMedia, playLainIdleAnim } from "@/core/events"; 5 | import { getRandomIdleLainAnim, getRandomIdle } from "@/utils/idle"; 6 | import handleEvent from "@/core/handleEvent"; 7 | import { useEffect } from "react"; 8 | import { GameScene, MainSubscene } from "@/types"; 9 | 10 | type IdleManagerProps = { 11 | lainIdleTimerRef: any; 12 | idleSceneTimerRef: any; 13 | }; 14 | 15 | const IdleManager = (props: IdleManagerProps) => { 16 | const mainSubscene = useStore((state) => state.mainSubscene); 17 | const site = useStore((state) => state.site); 18 | const gameProgress = useStore((state) => state.gameProgress); 19 | const scene = useStore((state) => state.scene); 20 | 21 | useEffect(() => { 22 | if (scene !== GameScene.Main) { 23 | props.idleSceneTimerRef.current = -1; 24 | } 25 | }, [props.idleSceneTimerRef, scene]); 26 | 27 | useFrame(() => { 28 | const now = Date.now(); 29 | if ( 30 | props.lainIdleTimerRef.current !== -1 && 31 | props.idleSceneTimerRef.current !== -1 && 32 | scene === GameScene.Main && 33 | mainSubscene === MainSubscene.Site 34 | ) { 35 | if (now > props.lainIdleTimerRef.current + 10000) { 36 | // after one idle animation plays, the second comes sooner than it would after a regular keypress 37 | props.lainIdleTimerRef.current = now - 2500; 38 | 39 | const [animation, duration] = getRandomIdleLainAnim(); 40 | const event = playLainIdleAnim(animation, duration); 41 | if (event) handleEvent(event); 42 | } 43 | if (now > props.idleSceneTimerRef.current + 30000) { 44 | // put it on lock until the next action, since while the idle media plays, the 45 | // Date.now() value keeps increasing, which can result in another idle media playing right after one finishes 46 | // one way to work around this would be to modify the value depending on the last played idle media's duration 47 | // but i'm way too lazy for that 48 | props.idleSceneTimerRef.current = -1; 49 | 50 | playAudio("snd_32.mp4"); 51 | 52 | const data = getRandomIdle(site, gameProgress); 53 | const event = playIdleMedia(data); 54 | 55 | handleEvent(event); 56 | } 57 | } 58 | }); 59 | 60 | return null; 61 | }; 62 | 63 | export default IdleManager; 64 | -------------------------------------------------------------------------------- /src/components/canvas/objects/Images.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import { useStore } from "@/store"; 3 | import { a, useSpring } from "@react-spring/three"; 4 | import { TextureLoader } from "three"; 5 | import { GameSite, NodeData } from "@/types"; 6 | import { useTexture } from "@react-three/drei"; 7 | 8 | type ImagesProps = { 9 | imageTableIndices: NodeData["image_table_indices"]; 10 | }; 11 | 12 | const Images = ({ imageTableIndices }: ImagesProps) => { 13 | const [imageScaleY, setImageScaleY] = useState(3.75); 14 | const [activeImage, setActiveImage] = useState(); 15 | 16 | const site = useStore((state) => state.site); 17 | 18 | const dummy = useTexture("/sprites/dummy.png"); 19 | 20 | const mediaPercentageElapsed = useStore( 21 | (state) => state.mediaPercentageElapsed 22 | ); 23 | 24 | const imageScaleState = useSpring({ 25 | imageScaleY: imageScaleY, 26 | config: { duration: 300 }, 27 | }); 28 | 29 | const textureLoader = useMemo(() => new TextureLoader(), []); 30 | 31 | useEffect(() => { 32 | const siteStr = site === GameSite.A ? "a" : "b"; 33 | 34 | let image: number | null = null; 35 | 36 | if (mediaPercentageElapsed === 0) { 37 | image = imageTableIndices[0]; 38 | } 39 | 40 | if (mediaPercentageElapsed === 35) { 41 | image = imageTableIndices[1]; 42 | } 43 | 44 | if (mediaPercentageElapsed === 70) { 45 | image = imageTableIndices[2]; 46 | } 47 | 48 | if (image !== null) { 49 | setImageScaleY(0); 50 | } 51 | 52 | const timer = setTimeout(() => { 53 | if (image) { 54 | const path = `/media/images/${siteStr}/${image}.png`; 55 | textureLoader.load(path, setActiveImage); 56 | setImageScaleY(3.75); 57 | } 58 | }, 300); 59 | 60 | return () => { 61 | clearTimeout(timer); 62 | }; 63 | }, [imageTableIndices, mediaPercentageElapsed, site, textureLoader]); 64 | 65 | return ( 66 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default Images; 77 | -------------------------------------------------------------------------------- /src/components/canvas/objects/InputHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, memo } from "react"; 2 | import { useStore } from "@/store"; 3 | import { handleEvent, handleInput } from "@/core"; 4 | import { GameEvent, GameScene, Key } from "@/types"; 5 | import IdleManager from "@canvas/objects/IdleManager"; 6 | 7 | const InputHandler = () => { 8 | const scene = useStore((state) => state.scene); 9 | const inputCooldown = useStore((state) => state.inputCooldown); 10 | const keybindings = useStore((state) => state.keybindings); 11 | 12 | const timeSinceLastKeyPress = useRef(-1); 13 | 14 | const lainIdleTimerRef = useRef(-1); 15 | const idleSceneTimerRef = useRef(-1); 16 | 17 | const handleKeyPress = useCallback( 18 | (key: Key) => { 19 | const now = Date.now(); 20 | 21 | if ( 22 | now > timeSinceLastKeyPress.current + inputCooldown && 23 | inputCooldown !== -1 24 | ) { 25 | timeSinceLastKeyPress.current = now; 26 | if (scene === GameScene.Main) { 27 | lainIdleTimerRef.current = now; 28 | idleSceneTimerRef.current = now; 29 | } 30 | 31 | const ctx = useStore.getState(); 32 | const event: GameEvent | null = handleInput(ctx, key); 33 | 34 | if (event) handleEvent(event); 35 | } 36 | }, 37 | [inputCooldown, scene] 38 | ); 39 | 40 | const firedRef = useRef(false); 41 | 42 | const handleKeyBoardEvent = useCallback( 43 | (event: KeyboardEvent) => { 44 | if (!firedRef.current) { 45 | firedRef.current = true; 46 | 47 | let key = event.key; 48 | // make the keybinds work with caps lock on aswell 49 | if (key.length === 1) { 50 | key = key.toLowerCase(); 51 | } 52 | 53 | if (key in keybindings) handleKeyPress(keybindings[key]); 54 | } 55 | }, 56 | [handleKeyPress, keybindings] 57 | ); 58 | 59 | useEffect(() => { 60 | window.addEventListener("keydown", handleKeyBoardEvent); 61 | 62 | window.addEventListener("keyup", () => { 63 | firedRef.current = false; 64 | }); 65 | 66 | return () => { 67 | window.removeEventListener("keydown", handleKeyBoardEvent); 68 | }; 69 | }, [handleKeyBoardEvent]); 70 | 71 | return ( 72 | 76 | ); 77 | }; 78 | 79 | export default memo(InputHandler); 80 | -------------------------------------------------------------------------------- /src/components/canvas/objects/LainSpeak.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useRef } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { useStore } from "@/store"; 4 | import { LainConstructor } from "./MainScene/Lain"; 5 | import { useTexture } from "@react-three/drei"; 6 | 7 | type LainTaKProps = { 8 | intro: boolean; 9 | outro: boolean; 10 | }; 11 | 12 | const LainSpeak = (props: LainTaKProps) => { 13 | const mouth1 = useTexture("/sprites/lain/mouth_1.png"); 14 | const mouth2 = useTexture("/sprites/lain/mouth_2.png"); 15 | const mouth3 = useTexture("/sprites/lain/mouth_3.png"); 16 | const mouth4 = useTexture("/sprites/lain/mouth_4.png"); 17 | const takIntro = useTexture("/sprites/lain/lain_speak_intro.png"); 18 | const takOutro = useTexture("/sprites/lain/lain_speak_outro.png"); 19 | 20 | const mouthRef = useRef(null); 21 | const audioAnalyser = useStore((state) => state.audioAnalyser); 22 | 23 | useFrame(() => { 24 | if (audioAnalyser) { 25 | const buffer = new Uint8Array(audioAnalyser.analyser.fftSize / 2); 26 | audioAnalyser.analyser.getByteTimeDomainData(buffer); 27 | 28 | let rms = 0; 29 | for (let i = 0; i < buffer.length; i++) { 30 | rms += buffer[i] * buffer[i]; 31 | } 32 | 33 | rms = Math.sqrt(rms / buffer.length); 34 | 35 | if (mouthRef.current) { 36 | if (rms >= 130) { 37 | mouthRef.current.map = mouth4; 38 | } else if (rms >= 129 && rms <= 130) { 39 | mouthRef.current.map = mouth3; 40 | } else if (rms > 128 && rms <= 129) { 41 | mouthRef.current.map = mouth2; 42 | } else { 43 | mouthRef.current.map = mouth1; 44 | } 45 | } 46 | } 47 | }); 48 | 49 | return ( 50 | <> 51 | 52 | {props.intro && ( 53 | 59 | )} 60 | {props.outro && ( 61 | 67 | )} 68 | 69 | 74 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export default LainSpeak; 86 | -------------------------------------------------------------------------------- /src/components/canvas/objects/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { PlainAnimator } from "three-plain-animator/lib/plain-animator"; 4 | import { useTexture } from "@react-three/drei"; 5 | 6 | const Loading = () => { 7 | const loading = useTexture("/sprites/loading/loading_spritesheet.png"); 8 | const lifeInstinct = useTexture( 9 | "/sprites/loading/life_instinct_function_os.png" 10 | ); 11 | 12 | const [animator] = useState(() => { 13 | const anim = new PlainAnimator(loading, 10, 3, 29, 60); 14 | anim.init(0); 15 | return anim; 16 | }); 17 | 18 | useFrame(() => { 19 | animator.animate(); 20 | }); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default memo(Loading); 46 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/About.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { useStore } from "@/store"; 4 | import { useTexture } from "@react-three/drei"; 5 | import { handleEvent } from "@/core"; 6 | import { setShowingAbout } from "@/core/events"; 7 | import {playAudio} from "@/utils/audio"; 8 | 9 | const About = () => { 10 | const showingAbout = useStore((state) => state.showingAbout); 11 | const aboutBg = useTexture("/sprites/main/about_background.png"); 12 | 13 | const bgRef = useRef(null); 14 | 15 | useFrame((_, delta) => { 16 | if (bgRef.current) { 17 | bgRef.current.position.y += delta; 18 | if (Math.round(bgRef.current.position.y) === 14) { 19 | handleEvent(setShowingAbout(false)); 20 | } 21 | } 22 | }); 23 | 24 | useEffect(() => { 25 | let audio: HTMLAudioElement; 26 | if (showingAbout) { 27 | audio = playAudio("about_theme.mp4", true); 28 | } 29 | return () => { 30 | if (audio) { 31 | audio.pause(); 32 | } 33 | }; 34 | }, [showingAbout]); 35 | 36 | return ( 37 | <> 38 | {showingAbout && ( 39 | <> 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | )} 53 | 54 | ); 55 | }; 56 | 57 | export default About; 58 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/CyanCrystal.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { DoubleSide } from "three"; 3 | import { GLTF } from "three-stdlib/loaders/GLTFLoader"; 4 | import { useGLTF } from "@react-three/drei"; 5 | 6 | type GLTFResult = GLTF & { 7 | nodes: { 8 | gron: THREE.Mesh; 9 | }; 10 | materials: {}; 11 | }; 12 | 13 | const CyanCrystal = () => { 14 | const { nodes } = useGLTF("models/cyan_crystal.glb") as GLTFResult; 15 | 16 | return ( 17 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default memo(CyanCrystal); 29 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/GrayPlane.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { DoubleSide } from "three"; 3 | import { useFrame } from "@react-three/fiber"; 4 | import { Position } from "@/types"; 5 | 6 | type GrayPlaneProps = { 7 | position: Position; 8 | }; 9 | 10 | const GrayPlane = (props: GrayPlaneProps) => { 11 | const meshRef = useRef(null); 12 | 13 | useFrame((state, delta) => { 14 | if (meshRef.current) { 15 | meshRef.current.rotation.y += delta / 4; 16 | } 17 | }); 18 | 19 | return ( 20 | 21 | 22 | 28 | 29 | ); 30 | }; 31 | 32 | export default GrayPlane; 33 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/GrayRing.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import { UniformsUtils, UniformsLib, DoubleSide } from "three"; 3 | import { useTexture } from "@react-three/drei"; 4 | import vertex from "@/shaders/gray_ring.vert"; 5 | import fragment from "@/shaders/gray_ring.frag"; 6 | 7 | const GrayRing = () => { 8 | const lof = useTexture("/sprites/main/gray_ring_lof.png"); 9 | const hole = useTexture("/sprites/main/hole.png"); 10 | const life = useTexture("/sprites/main/life.png"); 11 | 12 | const uniforms = useMemo(() => { 13 | const uniform = UniformsUtils.merge([UniformsLib["lights"]]); 14 | uniform.lof = { type: "t", value: lof }; 15 | uniform.hole = { type: "t", value: hole }; 16 | uniform.life = { type: "t", value: life }; 17 | 18 | return uniform; 19 | }, [hole, life, lof]); 20 | 21 | return ( 22 | 28 | 32 | 40 | 41 | ); 42 | }; 43 | 44 | export default memo(GrayRing); 45 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/LevelNodes.tsx: -------------------------------------------------------------------------------- 1 | import usePrevious from "@/hooks/usePrevious"; 2 | import { useStore } from "@/store"; 3 | import { 4 | FlattenedSiteLayout, 5 | GameSite, 6 | MainSubscene, 7 | NodeID, 8 | Position, 9 | } from "@/types"; 10 | import { getLevelY } from "@/utils/site"; 11 | import React, { memo, useEffect, useState } from "react"; 12 | import Node from "./Node"; 13 | 14 | type LevelNodesProps = { 15 | flattenedLayout: FlattenedSiteLayout; 16 | site: GameSite; 17 | }; 18 | 19 | const LevelNodes = (props: LevelNodesProps) => { 20 | const currentLevel = useStore((state) => state.level); 21 | const paused = useStore((state) => state.mainSubscene === MainSubscene.Pause); 22 | const currentNode = useStore((state) => state.node); 23 | const prevData = usePrevious({ level: currentLevel }); 24 | 25 | const [nodes, setNodes] = useState( 26 | props.flattenedLayout[currentLevel] 27 | ); 28 | const [pos, setPos] = useState([0, getLevelY(currentLevel), 0]); 29 | 30 | useEffect(() => { 31 | if (prevData?.level !== currentLevel && prevData?.level !== undefined) { 32 | if (Math.abs(prevData.level - currentLevel) === 1) { 33 | // if only changed one level 34 | setNodes(props.flattenedLayout[currentLevel]); 35 | setPos([0, getLevelY(currentLevel), 0]); 36 | } else { 37 | // if changed from level selection 38 | setTimeout(() => { 39 | setNodes(props.flattenedLayout[currentLevel]); 40 | setPos([0, getLevelY(currentLevel), 0]); 41 | }, 1500); 42 | } 43 | } 44 | }, [currentLevel, prevData?.level, props.flattenedLayout, props.site]); 45 | 46 | return ( 47 | 48 | {nodes.map((nodeId) => ( 49 | 54 | ))} 55 | 56 | ); 57 | }; 58 | 59 | export default memo(LevelNodes); 60 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { useStore } from "@/store"; 3 | import { useTexture } from "@react-three/drei"; 4 | 5 | const NotFound = () => { 6 | const notFound = useTexture("/sprites/main/not_found.png"); 7 | const notFoundLof = useTexture("/sprites/main/not_found_lof.png"); 8 | 9 | // TOOD ? 10 | const wordNotFound = useStore((state) => state.wordNotFound); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default memo(NotFound); 26 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/PauseSquare.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import { FrontSide, BackSide, PlaneBufferGeometry } from "three"; 3 | import { a, useSpring } from "@react-spring/three"; 4 | import { useTexture } from "@react-three/drei"; 5 | import { MatrixIndex2D, Position, Rotation } from "@/types"; 6 | 7 | export type PauseSquareProps = { 8 | indices: MatrixIndex2D; 9 | position: Position; 10 | isLetterSquare: boolean; 11 | active: boolean; 12 | introFinished: boolean; 13 | exit: boolean; 14 | }; 15 | 16 | const PauseSquare = (props: PauseSquareProps) => { 17 | const square = useTexture("/sprites/main/pause_gray_boxes.png"); 18 | const { row, col } = props.indices; 19 | 20 | const introSpring = useSpring<{ position: Position; rotation: Rotation }>({ 21 | from: { 22 | position: [1.05, 1.07, 0], 23 | rotation: [Math.PI, Math.PI, -1], 24 | }, 25 | to: async (next) => { 26 | await next({ 27 | position: props.position, 28 | rotation: [Math.PI, Math.PI, 0], 29 | config: { duration: 500 }, 30 | }); 31 | await next({ 32 | rotation: [0, props.isLetterSquare ? Math.PI / 2 : 0, 0], 33 | delay: (row + col) * 100, 34 | config: { duration: 200 }, 35 | }); 36 | }, 37 | }); 38 | 39 | const targetExitPos = useMemo(() => { 40 | let x, y; 41 | if (col < 3) x = -1; 42 | else if (col > 3) x = 1; 43 | else x = 0; 44 | 45 | if (row < 3) y = -1; 46 | else if (row > 3) y = 1; 47 | else y = 0; 48 | 49 | return [props.position[0] + x * 2.2, props.position[1] + y * 2.2, 0]; 50 | }, [col, props.position, row]); 51 | 52 | const spring = useSpring<{ position: Position; rotation: Rotation }>({ 53 | position: props.exit ? targetExitPos : props.position, 54 | rotation: [ 55 | props.active ? Math.PI : 0, 56 | props.isLetterSquare 57 | ? Math.PI / 2 58 | : props.active || props.exit 59 | ? Math.PI / 2 60 | : 0, 61 | 0, 62 | ], 63 | config: { duration: 500 }, 64 | }); 65 | 66 | const geom = useMemo(() => { 67 | let vOffset = col; 68 | if (vOffset > 3) { 69 | vOffset = 6 - col; 70 | } 71 | 72 | let uOffset = row; 73 | if (uOffset > 3) { 74 | uOffset = 6 - row; 75 | } 76 | 77 | const geometry = new PlaneBufferGeometry(); 78 | const uvAttribute = geometry.attributes.uv; 79 | 80 | for (let i = 0; i < uvAttribute.count; i++) { 81 | let u = uvAttribute.getX(i); 82 | let v = uvAttribute.getY(i); 83 | 84 | u = (u * 16) / 256 + (uOffset * 64) / 256 + (vOffset * 16) / 256; 85 | 86 | uvAttribute.setXY(i, u, v); 87 | } 88 | return geometry; 89 | }, [col, row]); 90 | 91 | return ( 92 | 3 ? -0.25 : 0.25, row <= 3 ? -0.25 : 0.25, 0]} 101 | renderOrder={100} 102 | > 103 | 3 && row <= 3) || (col <= 3 && row > 3) ? FrontSide : BackSide 107 | } 108 | transparent={true} 109 | depthTest={false} 110 | /> 111 | 112 | ); 113 | }; 114 | 115 | export default memo(PauseSquare); 116 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/PermissionDenied.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { useStore } from "@/store"; 3 | import { useTexture } from "@react-three/drei"; 4 | import TextRenderer from "../TextRenderer/TextRenderer"; 5 | import { TextType } from "@/types"; 6 | 7 | const PermissionDenied = () => { 8 | const headerContainer = useTexture( 9 | "/sprites/prompt/prompt_question_container.png" 10 | ); 11 | 12 | const permissionDenied = useStore((state) => state.permissionDenied); 13 | 14 | return ( 15 | 16 | 17 | 23 | 24 | 25 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default memo(PermissionDenied); 38 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/PurpleRing.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useMemo, useRef } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { UniformsLib, UniformsUtils, DoubleSide } from "three"; 4 | import { GameSite } from "@/types"; 5 | import { useTexture } from "@react-three/drei"; 6 | import { getLevelDigits } from "@/utils/site"; 7 | import vertex from "@/shaders/purple_ring.vert"; 8 | import fragment from "@/shaders/purple_ring.frag"; 9 | 10 | type PurpleRingProps = { 11 | level: number; 12 | site: GameSite; 13 | }; 14 | 15 | const PurpleRing = (props: PurpleRingProps) => { 16 | const siteA = useTexture("/sprites/main/site_a.png"); 17 | const siteB = useTexture("/sprites/main/site_b.png"); 18 | const siteLevels = useTexture("/sprites/main/site_levels.png"); 19 | 20 | const purpleRingRef = useRef(null); 21 | 22 | const levelTextureOffsets = useMemo(() => { 23 | const offsets = [ 24 | 0.031, 0.026, 0.0218, 0.0176, 0.0131, 0.009, 0.005, 0.001, 0.039, 0.035, 25 | ]; 26 | 27 | const [firstDigit, secondDigit] = getLevelDigits(props.level); 28 | return [offsets[firstDigit], offsets[secondDigit]]; 29 | }, [props.level]); 30 | 31 | const uniforms = useMemo(() => { 32 | const uniform = UniformsUtils.merge([UniformsLib["lights"]]); 33 | 34 | uniform.tex = { type: "t", value: null }; 35 | uniform.siteLevels = { type: "t", value: siteLevels }; 36 | uniform.siteLevelFirstCharacterOffset = { 37 | value: levelTextureOffsets[0], 38 | }; 39 | uniform.siteLevelSecondCharacterOffset = { 40 | value: levelTextureOffsets[1], 41 | }; 42 | 43 | return uniform; 44 | }, [siteLevels, levelTextureOffsets]); 45 | 46 | const matRef = useRef(null); 47 | 48 | const siteTexture = useMemo(() => { 49 | switch (props.site) { 50 | case GameSite.A: 51 | return siteA; 52 | case GameSite.B: 53 | return siteB; 54 | } 55 | }, [props.site, siteA, siteB]); 56 | 57 | useFrame((state, delta) => { 58 | purpleRingRef.current!.rotation.y += delta / 3; 59 | }); 60 | 61 | useEffect(() => { 62 | if (matRef.current) { 63 | matRef.current.uniforms.tex.value = siteTexture; 64 | matRef.current.uniformsNeedUpdate = true; 65 | } 66 | }, [siteTexture, props.site, siteA, siteB]); 67 | 68 | return ( 69 | 75 | 79 | 88 | 89 | ); 90 | }; 91 | 92 | export default memo(PurpleRing); 93 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/Rings.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import PurpleRing from "./PurpleRing"; 3 | import GrayRing from "./GrayRing"; 4 | import CyanCrystal from "./CyanCrystal"; 5 | import { useStore } from "@/store"; 6 | import { getLevelLimit, getLevelY } from "@/utils/site"; 7 | import range from "@/utils/range"; 8 | import { GameSite } from "@/types"; 9 | 10 | type RingsProps = { 11 | activateAllRings: boolean; 12 | site: GameSite; 13 | }; 14 | 15 | const Rings = (props: RingsProps) => { 16 | const level = useStore((state) => state.level); 17 | 18 | const visibleLevels: number[] = useMemo(() => { 19 | if (props.activateAllRings) { 20 | return range(1, getLevelLimit(props.site) + 1); 21 | } else { 22 | const start = Math.max(0, level - 3); 23 | const end = Math.min(getLevelLimit(props.site) + 1, level + 3); 24 | 25 | return range(start, end); 26 | } 27 | }, [level, props.activateAllRings, props.site]); 28 | 29 | return ( 30 | <> 31 | {visibleLevels.map((level: number) => ( 32 | 33 | 34 | 35 | 36 | 37 | ))} 38 | 39 | ); 40 | }; 41 | 42 | export default memo(Rings); 43 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/Site.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from "react"; 2 | import { a, useSpring } from "@react-spring/three"; 3 | import { useStore } from "@/store"; 4 | import { FlattenedSiteLayout, MainSubscene, NodeID } from "@/types"; 5 | import { getLevelY } from "@/utils/site"; 6 | import { getRotationForSegment } from "@/utils/site"; 7 | import Rings from "./Rings"; 8 | import StaticLevelNodes from "./StaticLevelNodes"; 9 | import LevelNodes from "./LevelNodes"; 10 | 11 | type SiteProps = { 12 | introFinished: boolean; 13 | }; 14 | 15 | const Site = (props: SiteProps) => { 16 | const wordSelected = useStore((state) => state.wordSelected); 17 | const level = useStore((state) => state.level); 18 | const siteSegment = useStore((state) => state.siteSegment); 19 | const prev = useStore((state) => state.prev); 20 | 21 | const [rotationSpring, setRotationSpring] = useSpring(() => ({ 22 | x: 0, 23 | y: getRotationForSegment(wordSelected ? prev.siteSegment : siteSegment), 24 | config: { duration: 1200 }, 25 | })); 26 | 27 | const [positionSpring, setPositionSpring] = useSpring(() => ({ 28 | y: -getLevelY(wordSelected ? prev.level : level), 29 | delay: 1300, 30 | config: { duration: 1200 }, 31 | })); 32 | 33 | const [tiltState, setTiltState] = useSpring(() => ({ 34 | tilt: 0, 35 | config: { duration: 200 }, 36 | })); 37 | 38 | useEffect( 39 | () => 40 | useStore.subscribe( 41 | (state) => state.siteSegment, 42 | (siteSegment) => { 43 | setRotationSpring((_, controller) => ({ 44 | y: getRotationForSegment(siteSegment, controller.get().y), 45 | delay: 1100, 46 | })); 47 | } 48 | ), 49 | [setRotationSpring] 50 | ); 51 | 52 | useEffect(() => { 53 | if (wordSelected) { 54 | setPositionSpring({ y: -getLevelY(level), delay: 1300 }); 55 | setRotationSpring({ y: getRotationForSegment(siteSegment), delay: 1300 }); 56 | } 57 | }, [level, setPositionSpring, setRotationSpring, siteSegment, wordSelected]); 58 | 59 | useEffect( 60 | () => 61 | useStore.subscribe( 62 | (state) => state.mainSubscene, 63 | (mainSubscene) => { 64 | if (mainSubscene === MainSubscene.Pause) { 65 | setRotationSpring({ x: Math.PI / 2, delay: 3600 }); 66 | } else { 67 | setRotationSpring({ x: 0 }); 68 | } 69 | } 70 | ), 71 | [setRotationSpring] 72 | ); 73 | 74 | useEffect( 75 | () => 76 | useStore.subscribe( 77 | (state) => state.level, 78 | (level) => { 79 | setPositionSpring({ 80 | y: -getLevelY(level), 81 | delay: 1300, 82 | }); 83 | } 84 | ), 85 | [setPositionSpring] 86 | ); 87 | 88 | useEffect( 89 | () => 90 | useStore.subscribe( 91 | (state) => state.cameraTiltValue, 92 | (cameraTilt) => { 93 | setTiltState({ 94 | tilt: cameraTilt, 95 | }); 96 | } 97 | ), 98 | [setTiltState] 99 | ); 100 | 101 | const siteLayout = useStore((state) => state.siteLayouts[state.site]); 102 | const site = useStore((state) => state.site); 103 | const layout: FlattenedSiteLayout = useMemo(() => { 104 | return siteLayout.map((level) => 105 | level.flat().filter((e): e is NodeID => e !== null) 106 | ); 107 | }, [siteLayout]); 108 | 109 | return ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | }; 121 | 122 | export default Site; 123 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/StaticLevelNodes.tsx: -------------------------------------------------------------------------------- 1 | import usePrevious from "@/hooks/usePrevious"; 2 | import { useStore } from "@/store"; 3 | import { FlattenedSiteLayout, GameSite } from "@/types"; 4 | import range from "@/utils/range"; 5 | import { getLevelLimit, getLevelY } from "@/utils/site"; 6 | import React, { memo, useEffect, useState } from "react"; 7 | import StaticNode from "./StaticNode"; 8 | 9 | type StaticLevelNodesProps = { 10 | flattenedLayout: FlattenedSiteLayout; 11 | site: GameSite; 12 | }; 13 | 14 | const StaticLevelNodes = (props: StaticLevelNodesProps) => { 15 | const currentLevel = useStore((state) => state.level); 16 | const prevData = usePrevious({ level: currentLevel }); 17 | 18 | const [visibleLevels, setVisibleLevels] = useState( 19 | range( 20 | Math.max(currentLevel - 3, 0), 21 | Math.min(currentLevel + 3, getLevelLimit(props.site)) 22 | ) 23 | ); 24 | 25 | useEffect(() => { 26 | if (prevData?.level !== currentLevel && prevData?.level !== undefined) { 27 | const start = Math.max(currentLevel - 3, 1); 28 | const end = Math.min(currentLevel + 3, getLevelLimit(props.site)); 29 | if (Math.abs(prevData.level - currentLevel) === 1) { 30 | // if only changed one level 31 | setVisibleLevels(range(start, end)); 32 | } else { 33 | // if changed from level selection 34 | setTimeout(() => setVisibleLevels(range(start, end)), 1500); 35 | } 36 | } 37 | }, [currentLevel, prevData?.level, props.site]); 38 | 39 | return ( 40 | <> 41 | {visibleLevels.map((level) => ( 42 | 43 | {props.flattenedLayout[level].map((nodeId) => ( 44 | 45 | ))} 46 | 47 | ))} 48 | 49 | ); 50 | }; 51 | 52 | export default memo(StaticLevelNodes); 53 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/StaticNode.tsx: -------------------------------------------------------------------------------- 1 | import useNodeTexture from "@/hooks/useNodeTexture"; 2 | import { useStore } from "@/store"; 3 | import { LainAnimation, NodeID } from "@/types"; 4 | import { 5 | getNode, 6 | getNodeWorldPosition, 7 | getNodeWorldRotation, 8 | isNodeViewed, 9 | } from "@/utils/node"; 10 | import { memo, useEffect, useMemo, useRef } from "react"; 11 | import { DoubleSide } from "three"; 12 | 13 | type StaticNodeProps = { 14 | id: NodeID; 15 | }; 16 | 17 | const StaticNode = (props: StaticNodeProps) => { 18 | const currentNode = useStore((state) => state.node); 19 | const node = useMemo(() => getNode(props.id), [props.id]); 20 | const ref = useRef(null); 21 | 22 | useEffect( 23 | () => 24 | useStore.subscribe( 25 | (state) => state.lainAnimation, 26 | (lainAnimation) => { 27 | switch (lainAnimation) { 28 | case LainAnimation.ThrowNode: 29 | case LainAnimation.RipNode: 30 | case LainAnimation.TouchNodeAndGetScared: 31 | case LainAnimation.KnockAndFall: 32 | case LainAnimation.Knock: 33 | if ( 34 | ref.current && 35 | props.id === currentNode?.id && 36 | ref.current.visible 37 | ) { 38 | ref.current.visible = false; 39 | } 40 | break; 41 | default: 42 | if (ref.current && !ref.current.visible) { 43 | ref.current.visible = true; 44 | } 45 | break; 46 | } 47 | } 48 | ), 49 | [currentNode?.id, props.id] 50 | ); 51 | 52 | const { position, type } = node; 53 | 54 | const worldPosition = getNodeWorldPosition(position); 55 | const rotation = getNodeWorldRotation(position); 56 | const gameProgress = useStore((state) => state.gameProgress); 57 | 58 | const { viewedTexture, normalTexture } = useNodeTexture(type); 59 | 60 | const isViewed = useMemo( 61 | () => isNodeViewed(props.id, gameProgress), 62 | [gameProgress, props.id] 63 | ); 64 | 65 | return ( 66 | 73 | 74 | 79 | 80 | ); 81 | }; 82 | 83 | export default memo(StaticNode); 84 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MainScene/YellowOrb.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo, useRef } from "react"; 2 | import { Vector3, QuadraticBezierCurve3, MathUtils } from "three"; 3 | import { useStore } from "@/store"; 4 | import { useTexture } from "@react-three/drei"; 5 | import { useFrame } from "@react-three/fiber"; 6 | import { Direction } from "@/types"; 7 | 8 | type YellowOrbProps = { 9 | visible: boolean; 10 | }; 11 | 12 | const YellowOrb = (props: YellowOrbProps) => { 13 | const texture = useTexture("/sprites/main/orb.png"); 14 | 15 | const idleStarting = useStore((state) => state.idleStarting); 16 | // ref for the object itself 17 | const orbRef = useRef(null); 18 | // position on the curve 19 | const idxRef = useRef(0); 20 | // how many times the orb changed direction 21 | const directionChangeCountRef = useRef(0); 22 | // current direction - left or right 23 | const directionRef = useRef(Direction.Left); 24 | 25 | // first curve or second curve (0/1) 26 | // first one goes from up to down left to right 27 | // second one goes from down to up left to right 28 | const curveRef = useRef<0 | 1>(0); 29 | const curves = useMemo( 30 | () => [ 31 | new QuadraticBezierCurve3( 32 | new Vector3(1.2, 0, 0), 33 | new Vector3(0.5, -0.8, 0), 34 | new Vector3(-1.2, 1, 0) 35 | ), 36 | new QuadraticBezierCurve3( 37 | new Vector3(-1.2, -0.8, 0), 38 | new Vector3(-0.5, -0.1, 0), 39 | new Vector3(1.2, 0.8, 0) 40 | ), 41 | ], 42 | [] 43 | ); 44 | 45 | const bigOrbScale = useMemo(() => new Vector3(2, 2, 2), []); 46 | 47 | useFrame(() => { 48 | if (props.visible && orbRef.current) { 49 | if (idxRef.current >= 265) { 50 | if (curveRef.current === 0) { 51 | orbRef.current.renderOrder = 0; 52 | } 53 | directionRef.current = 54 | curveRef.current === 0 ? Direction.Right : Direction.Left; 55 | directionChangeCountRef.current++; 56 | } 57 | 58 | if (idxRef.current <= -41) { 59 | if (curveRef.current === 1) { 60 | orbRef.current.renderOrder = -1; 61 | } 62 | directionRef.current = 63 | curveRef.current === 0 ? Direction.Left : Direction.Right; 64 | directionChangeCountRef.current++; 65 | } 66 | 67 | if (directionRef.current === 0) { 68 | if (curveRef.current === 0) { 69 | idxRef.current++; 70 | } else { 71 | idxRef.current--; 72 | } 73 | } else { 74 | if (curveRef.current === 0) { 75 | idxRef.current--; 76 | } else { 77 | idxRef.current++; 78 | } 79 | } 80 | 81 | if ( 82 | directionChangeCountRef.current % 3 === 0 && 83 | directionChangeCountRef.current !== 0 84 | ) { 85 | directionChangeCountRef.current = 0; 86 | if (curveRef.current === 0) { 87 | idxRef.current = 250; 88 | curveRef.current = 1; 89 | } else { 90 | idxRef.current = 0; 91 | curveRef.current = 0; 92 | } 93 | directionRef.current = 0; 94 | } 95 | 96 | if (idleStarting) { 97 | orbRef.current.scale.lerp(bigOrbScale, 0.01); 98 | orbRef.current.position.x = MathUtils.lerp( 99 | orbRef.current.position.x, 100 | 0, 101 | 0.01 102 | ); 103 | orbRef.current.position.y = MathUtils.lerp( 104 | orbRef.current.position.y, 105 | 0, 106 | 0.01 107 | ); 108 | } else { 109 | const curve = curveRef.current; 110 | const { x, y } = curves[curve].getPoint(idxRef.current / 250); 111 | orbRef.current.position.x = x; 112 | orbRef.current.position.y = y; 113 | } 114 | } 115 | }); 116 | 117 | return ( 118 | 119 | 120 | 121 | 122 | 123 | ); 124 | }; 125 | 126 | export default memo(YellowOrb); 127 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaPlayer.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, useCallback, useEffect, useRef } from "react"; 2 | import { useStore } from "@/store"; 3 | import { handleEvent } from "@/core"; 4 | import { setPercentageElapsed } from "@/core/events"; 5 | 6 | const MediaPlayer = () => { 7 | // use it as a guard to avoid multiple set states inside updateTime 8 | const lastSetPercentageRef = useRef(undefined); 9 | const language = useStore((state) => state.language); 10 | 11 | const requestRef = useRef(); 12 | const videoRef = createRef(); 13 | const trackRef = createRef(); 14 | const subtitleRef = createRef(); 15 | 16 | useEffect(() => { 17 | const handleCueChange = (e: any) => { 18 | const { track } = e.target; 19 | const { activeCues } = track; 20 | const text = [...activeCues].map( 21 | (cue) => cue.getCueAsHTML().textContent 22 | )[0]; 23 | if (subtitleRef.current && videoRef.current) { 24 | if (!text || videoRef.current.currentTime === 0) 25 | subtitleRef.current.textContent = text; 26 | else subtitleRef.current.textContent = text; 27 | } 28 | }; 29 | 30 | if (trackRef.current) { 31 | trackRef.current.addEventListener("cuechange", handleCueChange); 32 | } 33 | }, [subtitleRef, trackRef, videoRef]); 34 | 35 | const updateTime = useCallback(() => { 36 | requestRef.current = requestAnimationFrame(updateTime); 37 | if (videoRef.current) { 38 | const timeElapsed = videoRef.current.currentTime; 39 | const duration = videoRef.current.duration; 40 | const percentageElapsed = Math.floor((timeElapsed / duration) * 100); 41 | 42 | if ( 43 | percentageElapsed % 5 === 0 && 44 | lastSetPercentageRef.current !== percentageElapsed 45 | ) { 46 | handleEvent(setPercentageElapsed(percentageElapsed)); 47 | lastSetPercentageRef.current = percentageElapsed; 48 | 49 | if (subtitleRef.current) { 50 | if (percentageElapsed === 0) { 51 | subtitleRef.current.style.visibility = "visible"; 52 | } else if (percentageElapsed === 100) { 53 | subtitleRef.current.style.visibility = "hidden"; 54 | } 55 | } 56 | } 57 | } 58 | }, [videoRef, subtitleRef]); 59 | 60 | useEffect(() => { 61 | requestRef.current = requestAnimationFrame(updateTime); 62 | const curr = requestRef.current; 63 | return () => { 64 | cancelAnimationFrame(curr); 65 | handleEvent(setPercentageElapsed(0)); 66 | }; 67 | }, [updateTime]); 68 | 69 | return ( 70 | <> 71 | 74 |
75 |

84 |

85 | 86 | ); 87 | }; 88 | 89 | export default MediaPlayer; 90 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/AudioVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, useMemo, memo } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import AudioVisualizerColumn from "./AudioVisualizerColumn"; 4 | import { useStore } from "@/store"; 5 | 6 | const AudioVisualizer = () => { 7 | const audioAnalyser = useStore((state) => state.audioAnalyser); 8 | const columnRefs: React.RefObject[][] = useMemo( 9 | () => 10 | Array.from({ length: 15 }, () => [ 11 | createRef(), 12 | createRef(), 13 | createRef(), 14 | createRef(), 15 | ]), 16 | [] 17 | ); 18 | 19 | useFrame(() => { 20 | if (audioAnalyser) { 21 | const frequencyData = audioAnalyser.getFrequencyData(); 22 | 23 | columnRefs.forEach((refArray, idx) => { 24 | const [ref1, ref2, ref3, ref4] = refArray; 25 | 26 | const currentFrequency = frequencyData[16 * idx]; 27 | 28 | switch (true) { 29 | case currentFrequency >= 255: 30 | ref1.current!.visible = true; 31 | ref2.current!.visible = true; 32 | ref3.current!.visible = true; 33 | ref4.current!.visible = true; 34 | break; 35 | case currentFrequency >= 192: 36 | ref1.current!.visible = true; 37 | ref2.current!.visible = true; 38 | ref3.current!.visible = true; 39 | ref4.current!.visible = false; 40 | break; 41 | case currentFrequency >= 128: 42 | ref1.current!.visible = true; 43 | ref2.current!.visible = true; 44 | ref3.current!.visible = false; 45 | ref4.current!.visible = false; 46 | break; 47 | case currentFrequency >= 64: 48 | ref1.current!.visible = true; 49 | ref2.current!.visible = false; 50 | ref3.current!.visible = false; 51 | ref4.current!.visible = false; 52 | break; 53 | default: 54 | ref1.current!.visible = false; 55 | ref2.current!.visible = false; 56 | ref3.current!.visible = false; 57 | ref4.current!.visible = false; 58 | break; 59 | } 60 | }); 61 | } 62 | }); 63 | 64 | return ( 65 | 66 | {columnRefs.map((refArray, idx: number) => ( 67 | 72 | ))} 73 | 74 | ); 75 | }; 76 | 77 | export default memo(AudioVisualizer); 78 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/AudioVisualizerColumn.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | import { Position } from "@/types"; 4 | 5 | type AudioVisualizerColumnProps = { 6 | position: Position; 7 | refs: React.RefObject[]; 8 | }; 9 | 10 | const AudioVisualizerColumn = (props: AudioVisualizerColumnProps) => { 11 | const orangeAudioVisualizerOrb = useTexture( 12 | "/sprites/media/audio_visual_orb_orange.png" 13 | ); 14 | const yellowAudioVisualizerOrb = useTexture( 15 | "/sprites/media/audio_visual_orb_yellow.png" 16 | ); 17 | 18 | const [ref1, ref2, ref3, ref4] = props.refs; 19 | 20 | return ( 21 | 22 | 29 | 35 | 36 | 37 | 44 | 50 | 51 | 52 | 53 | 60 | 66 | 67 | 68 | 75 | 81 | 82 | 83 | 84 | ); 85 | }; 86 | 87 | export default AudioVisualizerColumn; 88 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/Cube.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from "@react-three/fiber"; 2 | import React, { memo, useRef } from "react"; 3 | import { useTexture } from "@react-three/drei"; 4 | import { Position } from "@/types"; 5 | 6 | type CubeProps = { 7 | position: Position; 8 | selectable?: boolean; 9 | active?: boolean; 10 | }; 11 | 12 | const Cube = (props: CubeProps) => { 13 | const gray = useTexture("/sprites/media/gray_box.png"); 14 | const darkGray = useTexture("/sprites/media/dark_gray_box.png"); 15 | 16 | const cubeRef = useRef(null); 17 | 18 | useFrame((_, delta) => { 19 | if (props.selectable && props.active) { 20 | cubeRef.current!.rotation.y -= delta; 21 | } else { 22 | cubeRef.current!.rotation.y = 0; 23 | } 24 | }); 25 | 26 | return ( 27 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default memo(Cube); 40 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/LeftSide.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import TriangularPrism from "./TriangularPrism"; 3 | import Cube from "./Cube"; 4 | import { a, useSpring } from "@react-spring/three"; 5 | import { useStore } from "@/store"; 6 | import { MediaComponent, Position } from "@/types"; 7 | 8 | type LeftSideSpring = { 9 | topFront: Position; 10 | bottomFront: Position; 11 | topBack: Position; 12 | bottomBack: Position; 13 | }; 14 | 15 | const LeftSide = () => { 16 | const activeComponent = useStore((state) => state.mediaComponent); 17 | 18 | const cubesActive = useMemo( 19 | () => activeComponent === MediaComponent.Exit, 20 | [activeComponent] 21 | ); 22 | 23 | const trianglesActive = useMemo( 24 | () => activeComponent === MediaComponent.Play, 25 | [activeComponent] 26 | ); 27 | 28 | const positionSpring = useSpring<{ 29 | from: LeftSideSpring; 30 | to: LeftSideSpring; 31 | }>({ 32 | from: { 33 | topFront: [4, 2, 1], 34 | bottomFront: [0, 2, 0], 35 | topBack: [0, -3, 2], 36 | bottomBack: [4, -2, 1], 37 | }, 38 | to: { 39 | topFront: [0, 0, 0], 40 | bottomFront: [0, 0, 0], 41 | topBack: [0, 0, 0], 42 | bottomBack: [0, 0, 0], 43 | }, 44 | config: { duration: 500 }, 45 | }); 46 | 47 | return ( 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | 77 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default memo(LeftSide); 87 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/Lof.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { PlainAnimator } from "three-plain-animator/lib/plain-animator"; 4 | import { useTexture } from "@react-three/drei"; 5 | 6 | const Lof = () => { 7 | const lofTex: any = useTexture("/sprites/media/lof_spritesheet.png"); 8 | 9 | const [animator] = useState(() => { 10 | const anim = new PlainAnimator(lofTex, 8, 1, 8, 24); 11 | anim.init(0); 12 | return anim; 13 | }); 14 | 15 | useFrame(() => { 16 | animator.animate(); 17 | }); 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default memo(Lof); 27 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/NodeNameContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | 4 | const NodeNameContainer = () => { 5 | const container = useTexture("/sprites/media/media_node_name_container.png"); 6 | 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default NodeNameContainer; 15 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/RightSide.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import { useStore } from "@/store"; 3 | import Word from "./Word"; 4 | import { a, useSpring } from "@react-spring/three"; 5 | import { Vector3 } from "three"; 6 | import Lof from "./Lof"; 7 | import { MediaComponent, NodeData, Position } from "@/types"; 8 | 9 | export const WORD_STATES = [ 10 | { 11 | active: MediaComponent.FirstWord, 12 | cross: [-2, 2, 0], 13 | first: [0, 0, 0], 14 | second: [3, -3, 0], 15 | third: [3.7, -4.3, 0], 16 | }, 17 | { 18 | active: MediaComponent.SecondWord, 19 | cross: [-0.5, 0.5, 0], 20 | first: [1.8, -2.5, 0], 21 | second: [1.5, -1.5, 0], 22 | third: [3.3, -3.7, 0], 23 | }, 24 | { 25 | active: MediaComponent.ThirdWord, 26 | cross: [1, -1, 0], 27 | first: [3.7, -4.3, 0], 28 | second: [0, 0, 0], 29 | third: [3, -3, 0], 30 | }, 31 | { 32 | active: MediaComponent.FirstWord, 33 | cross: [1.3, -1.7, 0], 34 | first: [3.3, -3.7, 0], 35 | second: [1.8, -2.5, 0], 36 | third: [1.5, -1.5, 0], 37 | }, 38 | { 39 | active: MediaComponent.SecondWord, 40 | cross: [1.7, -2.3, 0], 41 | first: [3, -3, 0], 42 | second: [3.7, -4.3, 0], 43 | third: [0, 0, 0], 44 | }, 45 | { 46 | active: MediaComponent.ThirdWord, 47 | cross: [-0.4, -0.5, 0], 48 | first: [1.5, -1.5, 0], 49 | second: [3.3, -3.7, 0], 50 | third: [1.8, -2.5, 0], 51 | }, 52 | ]; 53 | 54 | type RightSideProps = { 55 | words: NodeData["words"]; 56 | }; 57 | 58 | const RightSide = ({ words }: RightSideProps) => { 59 | const currState = useStore((state) => WORD_STATES[state.wordStateIdx]); 60 | const positionSpring = useSpring<{ 61 | cross: Position; 62 | first: Position; 63 | second: Position; 64 | third: Position; 65 | }>({ 66 | ...currState, 67 | config: { duration: 300 }, 68 | }); 69 | 70 | const horizontalPoints = useMemo( 71 | () => [new Vector3(-10, 0, 0), new Vector3(10, 0, 0)], 72 | [] 73 | ); 74 | 75 | const verticalPoints = useMemo( 76 | () => [new Vector3(0, 10, 0), new Vector3(0, -10, 0)], 77 | [] 78 | ); 79 | 80 | const activeComponent = useStore((state) => state.mediaComponent); 81 | 82 | const onHorizontalUpdate = (geometry: THREE.BufferGeometry) => { 83 | geometry.setFromPoints(horizontalPoints); 84 | }; 85 | 86 | const onVerticalUpdate = (geometry: THREE.BufferGeometry) => { 87 | geometry.setFromPoints(verticalPoints); 88 | }; 89 | 90 | return ( 91 | 92 | 93 | 94 | 95 | 96 | 101 | 102 | 103 | 104 | 109 | 110 | 111 | {words[0] !== null && words[1] !== null && words[2] !== null && ( 112 | <> 113 | 118 | 123 | 128 | 129 | )} 130 | 131 | ); 132 | }; 133 | 134 | export default memo(RightSide); 135 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/TriangularPrism.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from "@react-three/fiber"; 2 | import React, { memo, useRef } from "react"; 3 | import { GLTF } from "three-stdlib/loaders/GLTFLoader"; 4 | import { useGLTF, useTexture } from "@react-three/drei"; 5 | import { Position } from "@/types"; 6 | 7 | type GLTFResult = GLTF & { 8 | nodes: { 9 | Cube001: THREE.Mesh; 10 | }; 11 | materials: { 12 | ["Material.001"]: THREE.MeshStandardMaterial; 13 | }; 14 | }; 15 | 16 | type TriangularPrismProps = { 17 | position: Position; 18 | selectable?: boolean; 19 | active?: boolean; 20 | }; 21 | 22 | const TriangularPrism = (props: TriangularPrismProps) => { 23 | const gray = useTexture("/sprites/media/gray_box.png"); 24 | const darkGray = useTexture("/sprites/media/dark_gray_box.png"); 25 | 26 | const { nodes } = useGLTF("models/cut_cube.glb") as GLTFResult; 27 | 28 | const prismRef = useRef(null); 29 | 30 | useFrame((_, delta) => { 31 | if (props.selectable && props.active) { 32 | prismRef.current!.rotation.y -= delta; 33 | } else { 34 | prismRef.current!.rotation.y = 0; 35 | } 36 | }); 37 | 38 | return ( 39 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default memo(TriangularPrism); 53 | -------------------------------------------------------------------------------- /src/components/canvas/objects/MediaScene/Word.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { a, SpringValue } from "@react-spring/three"; 3 | import { useTexture } from "@react-three/drei"; 4 | import { Position, TextType } from "@/types"; 5 | import TextRenderer from "../TextRenderer/TextRenderer"; 6 | 7 | type WordProps = { 8 | word: string; 9 | position: SpringValue; 10 | active: boolean; 11 | }; 12 | 13 | const Word = (props: WordProps) => { 14 | const inactive = useTexture("/sprites/media/word_background.png"); 15 | const active = useTexture("/sprites/media/word_background_active.png"); 16 | 17 | return ( 18 | 19 | 20 | 25 | 26 | 27 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Word; 37 | -------------------------------------------------------------------------------- /src/components/canvas/objects/PolytanScene/PolytanBackground.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | 4 | const PolytanBackground = () => { 5 | const header = useTexture("/sprites/polytan/polytan_header.png"); 6 | const background = useTexture("/sprites/polytan/polytan_background.png"); 7 | const leftArm = useTexture("/sprites/polytan/poly_larm_hud.png"); 8 | const rightArm = useTexture("/sprites/polytan/poly_rarm_hud.png"); 9 | const rightLeg = useTexture("/sprites/polytan/poly_rleg_hud.png"); 10 | const leftLeg = useTexture("/sprites/polytan/poly_lleg_hud.png"); 11 | const head = useTexture("/sprites/polytan/poly_head_hud.png"); 12 | const body = useTexture("/sprites/polytan/poly_body_hud.png"); 13 | 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default PolytanBackground; 45 | -------------------------------------------------------------------------------- /src/components/canvas/objects/PolytanScene/PolytanBear.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { useStore } from "@/store"; 3 | import { useTexture } from "@react-three/drei"; 4 | 5 | const PolytanBear = () => { 6 | const skeleton = useTexture("/sprites/polytan/polytan_skeleton.png"); 7 | const head = useTexture("/sprites/polytan/head.png"); 8 | const body = useTexture("/sprites/polytan/body.png"); 9 | const leftArm = useTexture("/sprites/polytan/left_arm.png"); 10 | const leftLeg = useTexture("/sprites/polytan/left_leg.png"); 11 | const rightArm = useTexture("/sprites/polytan/right_arm.png"); 12 | const rightLeg = useTexture("/sprites/polytan/right_leg.png"); 13 | 14 | const unlockedParts = useStore( 15 | (state) => state.gameProgress.polytan_unlocked_parts 16 | ); 17 | 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default memo(PolytanBear); 48 | -------------------------------------------------------------------------------- /src/components/canvas/objects/Preloader.tsx: -------------------------------------------------------------------------------- 1 | import { useThree } from "@react-three/fiber"; 2 | import { memo, useLayoutEffect, useState } from "react"; 3 | import { useTexture } from "@react-three/drei"; 4 | 5 | // this function just preloads spritesheets and other assets cuz they're big and lazy loading them 6 | // used to make the suspense run for a couple milliseconds, resulting in flickering 7 | const Preloader = () => { 8 | useTexture("/sprites/fonts/orange_jp_font.png"); 9 | useTexture("/sprites/main/big_hud.png"); 10 | useTexture("/sprites/main/long_hud.png"); 11 | useTexture("/sprites/main/boring_hud.png"); 12 | useTexture("/sprites/lain/intro.png"); 13 | useTexture("/sprites/lain/jump_down.png"); 14 | useTexture("/sprites/lain/jump_up.png"); 15 | useTexture("/sprites/lain/move_left.png"); 16 | useTexture("/sprites/lain/move_right.png"); 17 | useTexture("/sprites/lain/standing.png"); 18 | useTexture("/sprites/lain/throw_node.png"); 19 | useTexture("/sprites/lain/rip_middle_ring.png"); 20 | useTexture("/sprites/lain/rip_node.png"); 21 | useTexture("/sprites/lain/prayer.png"); 22 | useTexture("/sprites/lain/knock.png"); 23 | useTexture("/sprites/lain/knock_and_fall.png"); 24 | useTexture("/sprites/lain/touch_node_and_get_scared.png"); 25 | useTexture("/sprites/lain/touch_sleeve.png"); 26 | useTexture("/sprites/lain/thinking.png"); 27 | useTexture("/sprites/lain/stretch.png"); 28 | useTexture("/sprites/lain/stretch_2.png"); 29 | useTexture("/sprites/lain/spin.png"); 30 | useTexture("/sprites/lain/scratch_head.png"); 31 | useTexture("/sprites/lain/blush.png"); 32 | useTexture("/sprites/lain/hands_behind_head.png"); 33 | useTexture("/sprites/lain/hands_on_hips.png"); 34 | useTexture("/sprites/lain/hands_on_hips_2.png"); 35 | useTexture("/sprites/lain/hands_together.png"); 36 | useTexture("/sprites/lain/lean_forward.png"); 37 | useTexture("/sprites/lain/lean_left.png"); 38 | useTexture("/sprites/lain/lean_right.png"); 39 | useTexture("/sprites/lain/look_around.png"); 40 | useTexture("/sprites/lain/play_with_hair.png"); 41 | 42 | return null; 43 | }; 44 | 45 | export default memo(Preloader); 46 | -------------------------------------------------------------------------------- /src/components/canvas/objects/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | 4 | type ProgressBarProps = { 5 | progress: number; 6 | }; 7 | 8 | const SsknProgressBar = (props: ProgressBarProps) => { 9 | const first = useTexture("/sprites/progressbar/progress_bar1.png"); 10 | const second = useTexture("/sprites/progressbar/progress_bar2.png"); 11 | const third = useTexture("/sprites/progressbar/progress_bar3.png"); 12 | const fourth = useTexture("/sprites/progressbar/progress_bar4.png"); 13 | const fifth = useTexture("/sprites/progressbar/progress_bar5.png"); 14 | 15 | const full = useTexture("/sprites/progressbar/progress_bar0.png"); 16 | 17 | const texture = useMemo(() => { 18 | switch (props.progress) { 19 | case 0: 20 | return first; 21 | case 1: 22 | return first; 23 | case 2: 24 | return second; 25 | case 3: 26 | return third; 27 | case 4: 28 | return fourth; 29 | case 5: 30 | return fifth; 31 | default: 32 | return full; 33 | } 34 | }, [fifth, first, fourth, full, props.progress, second, third]); 35 | 36 | return ( 37 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default SsknProgressBar; 49 | -------------------------------------------------------------------------------- /src/components/canvas/objects/Prompt.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { useStore } from "@/store"; 3 | import { PromptComponent } from "@/types"; 4 | import { useTexture } from "@react-three/drei"; 5 | 6 | const Prompt = () => { 7 | const questionContainer = useTexture( 8 | "/sprites/prompt/prompt_question_container.png" 9 | ); 10 | const answerContainer = useTexture( 11 | "/sprites/prompt/prompt_answer_container.png" 12 | ); 13 | const question = useTexture("/sprites/prompt/prompt_question.png"); 14 | const yes = useTexture("/sprites/prompt/prompt_yes.png"); 15 | const no = useTexture("/sprites/prompt/prompt_no.png"); 16 | 17 | const activeComponent = useStore((state) => state.promptComponent); 18 | 19 | const promptVisible = useStore((state) => state.promptVisible); 20 | 21 | return ( 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 53 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default memo(Prompt); 72 | -------------------------------------------------------------------------------- /src/components/canvas/objects/SaveStatusDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | 3 | import { useStore } from "@/store"; 4 | import { useTexture } from "@react-three/drei"; 5 | import { SaveStatus } from "@/types"; 6 | 7 | const SaveStatusDisplay = () => { 8 | const status = useStore((state) => state.saveStatus); 9 | 10 | const statusContainer = useTexture("/sprites/status/status_container.png"); 11 | 12 | const loadSuccessful = useTexture("/sprites/status/load_successful.png"); 13 | const loadFail = useTexture("/sprites/status/load_fail.png"); 14 | const saveSuccessful = useTexture("/sprites/status/save_successful.png"); 15 | 16 | const texture = useMemo(() => { 17 | switch (status) { 18 | case SaveStatus.SaveSuccessful: 19 | return saveSuccessful; 20 | case SaveStatus.LoadFailure: 21 | return loadFail; 22 | case SaveStatus.LoadSuccessful: 23 | return loadSuccessful; 24 | } 25 | }, [loadFail, loadSuccessful, saveSuccessful, status]); 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default memo(SaveStatusDisplay); 40 | -------------------------------------------------------------------------------- /src/components/canvas/objects/SsknScene/SsknBackground.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | 4 | const SsknBackground = () => { 5 | const bg = useTexture("/sprites/sskn/sskn_background.png"); 6 | const bgText = useTexture("/sprites/sskn/sskn_background_text.png"); 7 | const topLabel = useTexture("/sprites/sskn/sskn_top_label.png"); 8 | const dango = useTexture("/sprites/sskn/sskn_dango.png"); 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default memo(SsknBackground); 30 | -------------------------------------------------------------------------------- /src/components/canvas/objects/SsknScene/SsknHUD.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from "react"; 2 | import SsknProgressBar from "@canvas/objects/ProgressBar"; 3 | import { useStore } from "@/store"; 4 | import { SsknComponent } from "@/types"; 5 | import { useTexture } from "@react-three/drei"; 6 | import useCappedFrame from "@/hooks/useCappedFrame"; 7 | 8 | const SsknHUD = () => { 9 | const ok = useTexture("/sprites/sskn/sskn_ok.png"); 10 | const okInactive = useTexture("/sprites/sskn/sskn_ok_inactive.png"); 11 | const cancel = useTexture("/sprites/sskn/sskn_cancel.png"); 12 | const cancelInactive = useTexture("/sprites/sskn/sskn_cancel_inactive.png"); 13 | const upgrade = useTexture("/sprites/sskn/sskn_upgrade.png"); 14 | const arrow = useTexture("/sprites/sskn/sskn_arrow.png"); 15 | const textWrapper = useTexture("/sprites/sskn/sskn_text_wrapper.png"); 16 | const textWrapperInactive = useTexture( 17 | "/sprites/sskn/sskn_text_wrapper_inactive.png" 18 | ); 19 | const loadingContainer = useTexture("/sprites/sskn/sskn_progress_bar.png"); 20 | const line = useTexture("/sprites/sskn/sskn_line.png"); 21 | 22 | const activeComponent = useStore((state) => state.ssknComponent); 23 | 24 | const loading = useStore((state) => state.ssknLoading); 25 | 26 | const [loadProgress, setLoadProgress] = useState(0); 27 | 28 | useCappedFrame(() => { 29 | if (loading) { 30 | setLoadProgress((prev) => prev + 1); 31 | } 32 | }, 0.26); 33 | 34 | return ( 35 | <> 36 | {loading ? ( 37 | <> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ) : ( 46 | 47 | 48 | 51 | 52 | 53 | 60 | 61 | 62 | 69 | 70 | 71 | 78 | 79 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | )} 95 | 96 | 97 | 98 | 99 | ); 100 | }; 101 | 102 | export default memo(SsknHUD); 103 | -------------------------------------------------------------------------------- /src/components/canvas/objects/SsknScene/SsknIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef } from "react"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { DoubleSide } from "three"; 4 | import { useTexture } from "@react-three/drei"; 5 | 6 | const SsknIcon = () => { 7 | const icon = useTexture("/sprites/sskn/SSkn_icon.png"); 8 | const ssknIconRef = useRef(null); 9 | const ssknIconShadowRef = useRef(null); 10 | 11 | useFrame((state, delta) => { 12 | if (ssknIconRef.current && ssknIconShadowRef.current) { 13 | ssknIconRef.current.rotation.y += delta * 2; 14 | ssknIconShadowRef.current.rotation.y += delta * 2; 15 | } 16 | }); 17 | return ( 18 | <> 19 | 25 | 26 | 32 | 33 | 39 | 40 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default memo(SsknIcon); 53 | -------------------------------------------------------------------------------- /src/components/canvas/objects/TextRenderer/AnimatedBigTextRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { Position, Scale, TextType } from "@/types"; 2 | import { a, useTrail, useSprings } from "@react-spring/three"; 3 | import Letter from "./Letter"; 4 | import { useTexture } from "@react-three/drei"; 5 | import useText from "@/hooks/useText"; 6 | 7 | type BigTextRendererProps = { 8 | position: Position; 9 | scale: Scale; 10 | text: string; 11 | shrinked: boolean; 12 | type: TextType.BigOrange | TextType.BigYellow; 13 | }; 14 | 15 | const AnimatedBigTextRenderer = (props: BigTextRendererProps) => { 16 | const { font, texture } = useText(props.type); 17 | const orangeFont = useTexture("/sprites/fonts/orange_font_texture.png"); 18 | 19 | const trail = useTrail(props.text.length, { 20 | position: props.position, 21 | config: { duration: 100 }, 22 | }); 23 | 24 | const springs = useSprings( 25 | props.text.length, 26 | props.text.split("").map((_, i) => ({ 27 | x: props.shrinked ? 0 : i + 0.3, 28 | config: { duration: 200 }, 29 | })) 30 | ); 31 | 32 | return ( 33 | 34 | {trail.map(({ position }, idx) => ( 35 | 36 | 44 | 45 | ))} 46 | 47 | ); 48 | }; 49 | 50 | export default AnimatedBigTextRenderer; 51 | -------------------------------------------------------------------------------- /src/components/canvas/objects/TextRenderer/Letter.tsx: -------------------------------------------------------------------------------- 1 | import { a, SpringValue } from "@react-spring/three"; 2 | import React, { memo, useMemo } from "react"; 3 | import useLetterGeometry from "@/hooks/useLetterGeometry"; 4 | import { FontData } from "@/types"; 5 | 6 | export type LetterProps = { 7 | font: FontData; 8 | texture: THREE.Texture; 9 | letter: string; 10 | posX: SpringValue | number; 11 | scale: [number, number, number]; 12 | renderOrder?: number; 13 | depthTest?: boolean; 14 | }; 15 | 16 | const Letter = (props: LetterProps) => { 17 | const letterData = useMemo(() => props.font.glyphs[props.letter], [props]); 18 | 19 | const geometry = useLetterGeometry( 20 | letterData, 21 | props.texture.image.width, 22 | props.texture.image.height 23 | ); 24 | 25 | return ( 26 | 33 | 38 | 39 | ); 40 | }; 41 | 42 | export default memo(Letter); 43 | -------------------------------------------------------------------------------- /src/components/canvas/objects/TextRenderer/TextRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import Letter from "./Letter"; 3 | import { Scale, TextType } from "@/types"; 4 | import useText from "@/hooks/useText"; 5 | 6 | type TextRendererProps = { 7 | type: TextType; 8 | text: string; 9 | scale: Scale; 10 | renderOrder?: number 11 | depthTest?: boolean; 12 | }; 13 | 14 | const TextRenderer = (props: TextRendererProps) => { 15 | const { font, texture } = useText(props.type); 16 | 17 | return ( 18 | <> 19 | {props.text.split("").map((letter, idx) => ( 20 | 30 | ))} 31 | 32 | ); 33 | }; 34 | 35 | export default memo(TextRenderer); 36 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/BootScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import BootAccela from "@canvas/objects/BootScene/BootAccela"; 3 | import BootAnimation from "@canvas/objects/BootScene/BootAnimation"; 4 | import BootMainMenuComponents from "@canvas/objects/BootScene/BootMainMenuComponents"; 5 | import { useStore } from "@/store"; 6 | import BootAuthorizeUser from "@canvas/objects/BootScene/BootAuthorizeUser"; 7 | import BootLoadData from "@canvas/objects/BootScene/BootLoadData"; 8 | import { BootSubscene } from "@/types"; 9 | import { handleEvent } from "@/core"; 10 | import { setInputCooldown } from "@/core/events"; 11 | 12 | const BootScene = () => { 13 | const bootSubscene = useStore((state) => state.bootSubscene); 14 | 15 | const [accelaVisible, setAccelaVisible] = useState(true); 16 | const [mainMenuVisible, setMainMenuVisible] = useState(false); 17 | 18 | useEffect(() => { 19 | handleEvent(setInputCooldown(-1)); 20 | setTimeout(() => setAccelaVisible(false), 2000); 21 | setTimeout(() => setMainMenuVisible(true), 6200); 22 | setTimeout(() => handleEvent(setInputCooldown(0)), 6500); 23 | }, []); 24 | 25 | return ( 26 | 27 | 28 | 32 | 33 | 36 | 37 | 38 | ); 39 | }; 40 | export default BootScene; 41 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/ChangeDiscScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from "react"; 2 | import { useStore } from "@/store"; 3 | import { NearestFilter } from "three"; 4 | import { GameSite, GameScene } from "@/types"; 5 | import { useTexture } from "@react-three/drei"; 6 | import { handleEvent } from "@/core"; 7 | import { enterScene } from "@/core/events"; 8 | 9 | const ChangeDiscScene = () => { 10 | const site = useStore((state) => state.site); 11 | 12 | const lof = useTexture("/sprites/change_disc/disc_lof.png"); 13 | const changeSite = useTexture("/sprites/change_disc/disc_change_site.png"); 14 | const line = useTexture("/sprites/change_disc/disc_line.png"); 15 | const slopeLine = useTexture("/sprites/change_disc/disc_slope_line.png"); 16 | const checkingInProgress = useTexture( 17 | "/sprites/change_disc/disc_checking_in_progress.png" 18 | ); 19 | const text = useTexture("/sprites/change_disc/disc_disc.png"); 20 | const disc1 = useTexture("/sprites/change_disc/disc_1.png"); 21 | const disc2 = useTexture("/sprites/change_disc/disc_2.png"); 22 | 23 | const fixedTextures = useMemo(() => { 24 | changeSite.magFilter = NearestFilter; 25 | 26 | checkingInProgress.magFilter = NearestFilter; 27 | 28 | return { 29 | changeSite: changeSite, 30 | checkingInProgress: checkingInProgress, 31 | }; 32 | }, [changeSite, checkingInProgress]); 33 | 34 | useEffect(() => { 35 | setTimeout(() => handleEvent(enterScene(GameScene.Main)), 3500); 36 | }, [site]); 37 | 38 | return ( 39 | <> 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {[...Array(2).keys()].map((idx) => ( 51 | 56 | 57 | 58 | ))} 59 | 60 | 61 | 62 | 63 | 64 | {[...Array(7).keys()].map((idx) => ( 65 | 70 | 71 | 72 | ))} 73 | 74 | {[...Array(2).keys()].map((idx) => ( 75 | 80 | 81 | 82 | ))} 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | 95 | export default ChangeDiscScene; 96 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/GateScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import GateSide from "@canvas/objects/GateScene/GateSide"; 3 | import GateHUD from "@canvas/objects/GateScene/GateHUD"; 4 | import GateMiddleObject from "@canvas/objects/GateScene/GateMiddleObject"; 5 | import { useStore } from "@/store"; 6 | import { handleEvent } from "@/core"; 7 | import { resetInputCooldown } from "@/core/events"; 8 | 9 | const GateScene = () => { 10 | const gateLvl = useStore((state) => state.gameProgress.gate_level); 11 | const [introAnim, setIntroAnim] = useState(true); 12 | 13 | useEffect(() => { 14 | setTimeout(() => setIntroAnim(false), 2500); 15 | setTimeout(() => handleEvent(resetInputCooldown), 3500); 16 | }, [gateLvl]); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | export default GateScene; 28 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/IdleMediaScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useStore } from "@/store"; 3 | import Images from "@canvas/objects/Images"; 4 | import { handleEvent } from "@/core"; 5 | import { exitIdleScene, resetInputCooldown } from "@/core/events"; 6 | import { isAudioOnly } from "@/utils/node"; 7 | 8 | const IdleMediaScene = () => { 9 | const mediaPercentageElapsed = useStore( 10 | (state) => state.mediaPercentageElapsed 11 | ); 12 | 13 | const language = useStore((state) => state.language); 14 | const data = useStore((state) => state.idleSceneData); 15 | 16 | useEffect(() => { 17 | setTimeout(() => handleEvent(resetInputCooldown), 1500); 18 | }, []); 19 | 20 | useEffect(() => { 21 | if (mediaPercentageElapsed === 100) { 22 | handleEvent(exitIdleScene); 23 | } 24 | }, [mediaPercentageElapsed]); 25 | 26 | useEffect(() => { 27 | const mediaElement = document.getElementById("media") as HTMLMediaElement; 28 | const trackElement = document.getElementById("track") as HTMLTrackElement; 29 | 30 | if (mediaElement) { 31 | mediaElement.currentTime = 0; 32 | 33 | if (data.nodeName) { 34 | trackElement.src = `/media/webvtt/${language}/${data.nodeName}.vtt`; 35 | } 36 | 37 | const media = data.mediaFile; 38 | if (isAudioOnly(data.mediaFile)) { 39 | mediaElement.src = `media/audio/${media}.mp4`; 40 | } else { 41 | mediaElement.src = `media/movie/${media}.mp4`; 42 | } 43 | mediaElement.load(); 44 | mediaElement.play(); 45 | } 46 | }, [data.mediaFile, data.nodeName, language]); 47 | 48 | return ( 49 | <> 50 | {data.imageTableIndices && ( 51 | 52 | )} 53 | 54 | ); 55 | }; 56 | 57 | export default IdleMediaScene; 58 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/PolytanScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import PolytanBear from "@canvas/objects/PolytanScene/PolytanBear"; 3 | import PolytanBackground from "@canvas/objects/PolytanScene/PolytanBackground"; 4 | import { handleEvent } from "@/core"; 5 | import { resetInputCooldown } from "@/core/events"; 6 | import TextRenderer from "../objects/TextRenderer/TextRenderer"; 7 | import useCappedFrame from "@/hooks/useCappedFrame"; 8 | import { TextType } from "@/types"; 9 | 10 | const PolytanScene = () => { 11 | useEffect(() => { 12 | setTimeout(() => handleEvent(resetInputCooldown), 1000); 13 | }, []); 14 | 15 | const pressAnyTextRef = useRef(null); 16 | 17 | useCappedFrame(() => { 18 | if (pressAnyTextRef.current) { 19 | pressAnyTextRef.current.visible = !pressAnyTextRef.current.visible; 20 | } 21 | }, 0.5); 22 | return ( 23 | <> 24 | 25 | 26 | 27 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default PolytanScene; 40 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/SsknScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import SsknIcon from "@canvas/objects/SsknScene/SsknIcon"; 3 | import SsknBackground from "@canvas/objects/SsknScene/SsknBackground"; 4 | import SsknHUD from "@canvas/objects/SsknScene/SsknHUD"; 5 | import {resetInputCooldown} from "@/core/events"; 6 | import {handleEvent} from "@/core"; 7 | 8 | const SsknScene = () => { 9 | useEffect(() => { 10 | setTimeout(() => handleEvent(resetInputCooldown), 500); 11 | }, []); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default SsknScene; 23 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/TaKScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import LainSpeak from "@canvas/objects/LainSpeak"; 3 | import { useStore } from "@/store"; 4 | import { GameScene } from "@/types"; 5 | import { createAudioAnalyser } from "@/utils/audio"; 6 | import { handleEvent } from "@/core"; 7 | import { enterScene, setAudioAnalyser } from "@/core/events"; 8 | 9 | const TaKScene = () => { 10 | const language = useStore((state) => state.language); 11 | 12 | const node = useStore((state) => state.node); 13 | 14 | const [isIntro, setIsIntro] = useState(true); 15 | const [isOutro, setIsOutro] = useState(false); 16 | 17 | const percentageElapsed = useStore((state) => state.mediaPercentageElapsed); 18 | 19 | useEffect(() => { 20 | if (percentageElapsed === 100) { 21 | setIsOutro(true); 22 | 23 | setTimeout(() => handleEvent(enterScene(GameScene.Main)), 4600); 24 | } 25 | }, [percentageElapsed]); 26 | 27 | useEffect(() => { 28 | setTimeout(() => { 29 | const mediaElement = document.getElementById("media") as HTMLMediaElement; 30 | const trackElement = document.getElementById("track") as HTMLTrackElement; 31 | 32 | if (mediaElement) { 33 | handleEvent(setAudioAnalyser(createAudioAnalyser())); 34 | mediaElement.currentTime = 0; 35 | 36 | if (node) { 37 | trackElement.src = `/media/webvtt/${language}/${node.name}/.vtt`; 38 | 39 | mediaElement.src = `/media/audio/${node.media_file}.mp4`; 40 | mediaElement.load(); 41 | mediaElement.play(); 42 | } 43 | 44 | setIsIntro(false); 45 | } 46 | }, 3800); 47 | }, [node, language]); 48 | 49 | return ; 50 | }; 51 | 52 | export default TaKScene; 53 | -------------------------------------------------------------------------------- /src/components/canvas/scenes/index.ts: -------------------------------------------------------------------------------- 1 | import MainScene from "./MainScene"; 2 | import MediaScene from "./MediaScene"; 3 | import BootScene from "./BootScene"; 4 | import ChangeDiscScene from "./ChangeDiscScene"; 5 | import EndScene from "./EndScene"; 6 | import GateScene from "./GateScene"; 7 | import IdleMediaScene from "./IdleMediaScene"; 8 | import PolytanScene from "./PolytanScene"; 9 | import SsknScene from "./SsknScene"; 10 | import TaKScene from "./TaKScene"; 11 | 12 | export { 13 | MainScene, 14 | MediaScene, 15 | BootScene, 16 | ChangeDiscScene, 17 | EndScene, 18 | GateScene, 19 | IdleMediaScene, 20 | PolytanScene, 21 | SsknScene, 22 | TaKScene, 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/dom/Credit.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type CreditProps = { 4 | name: string; 5 | credit: string; 6 | }; 7 | 8 | const Credit = (props: CreditProps) => ( 9 | <> 10 | {props.name} - {props.credit} 11 |
12 |
13 | 14 | ); 15 | 16 | export default Credit; 17 | -------------------------------------------------------------------------------- /src/components/dom/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | 4 | const Header = () => { 5 | return ( 6 |
7 | 8 | main 9 | 10 | 11 | notes 12 | 13 | 14 | start 15 | 16 | 17 | guide 18 | 19 | 20 | options 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /src/components/dom/Keybinding.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from "react"; 2 | import { useStore } from "@/store"; 3 | import { handleEvent } from "@/core"; 4 | import { setKeybindings } from "@/core/events"; 5 | import { Key } from "@/types"; 6 | 7 | const formatKey = (key: Key): string => { 8 | switch (key) { 9 | case Key.Down: 10 | return "↓"; 11 | case Key.Left: 12 | return "←"; 13 | case Key.Up: 14 | return "↑"; 15 | case Key.Right: 16 | return "→"; 17 | case Key.Circle: 18 | return "◯"; 19 | case Key.Cross: 20 | return "✖"; 21 | case Key.Square: 22 | return "◼"; 23 | case Key.Triangle: 24 | return "▲"; 25 | case Key.R1: 26 | return "R1"; 27 | case Key.R2: 28 | return "R2"; 29 | case Key.L1: 30 | return "L1"; 31 | case Key.L2: 32 | return "L2"; 33 | case Key.Select: 34 | return "Select"; 35 | case Key.Start: 36 | return "start"; 37 | } 38 | }; 39 | 40 | const Keybinding = () => { 41 | const keybindings = useStore((state) => state.keybindings); 42 | 43 | const handleRemap = useCallback( 44 | (keyToRemap: Key, to: string) => { 45 | if (to.length === 1) { 46 | to = to.toLowerCase(); 47 | } 48 | 49 | const newBindings = { 50 | ...Object.fromEntries( 51 | Object.entries(keybindings).filter(([_, v]) => v !== keyToRemap) 52 | ), 53 | [to]: keyToRemap, 54 | }; 55 | 56 | handleEvent(setKeybindings(newBindings)); 57 | localStorage.setItem("lainKeybindings", JSON.stringify(newBindings)); 58 | }, 59 | [keybindings] 60 | ); 61 | 62 | const startKeybindListener = useCallback( 63 | (keyToRemap: Key) => { 64 | window.addEventListener( 65 | "keydown", 66 | (event) => handleRemap(keyToRemap, event.key), 67 | { once: true } 68 | ); 69 | }, 70 | [handleRemap] 71 | ); 72 | 73 | const resetToDefault = () => { 74 | handleEvent( 75 | setKeybindings({ 76 | ArrowDown: Key.Down, 77 | ArrowLeft: Key.Left, 78 | ArrowUp: Key.Up, 79 | ArrowRight: Key.Right, 80 | x: Key.Circle, 81 | z: Key.Cross, 82 | d: Key.Triangle, 83 | s: Key.Square, 84 | t: Key.R2, 85 | e: Key.L2, 86 | w: Key.L1, 87 | r: Key.R1, 88 | v: Key.Start, 89 | c: Key.Select, 90 | }) 91 | ); 92 | localStorage.removeItem("lainKeybindings"); 93 | }; 94 | 95 | const getBoundKey = useCallback( 96 | (k: Key) => Object.keys(keybindings).find((key) => keybindings[key] === k), 97 | [keybindings] 98 | ); 99 | 100 | return ( 101 | <> 102 |

103 | To change a keybinding, just click on it and press the button you wish 104 | to bind it to after. 105 |
106 | In order for this to take effect, you must refresh the game page. 107 |

108 |
109 |
110 | 111 | 112 | {[ 113 | Key.Left, 114 | Key.Right, 115 | Key.Up, 116 | Key.Down, 117 | Key.L1, 118 | Key.L2, 119 | Key.R1, 120 | Key.R2, 121 | Key.Circle, 122 | Key.Triangle, 123 | Key.Cross, 124 | Key.Square, 125 | Key.Select, 126 | Key.Start, 127 | ].map((key) => ( 128 | startKeybindListener(key)} key={key}> 129 | 130 | 131 | 132 | ))} 133 | 134 |
{formatKey(key)}{getBoundKey(key)}
135 |
136 |
137 | 140 | 141 | ); 142 | }; 143 | 144 | export default Keybinding; 145 | -------------------------------------------------------------------------------- /src/components/dom/Language.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useStore } from "@/store"; 3 | import { handleEvent } from "@/core"; 4 | import { setLanguage } from "@/core/events"; 5 | 6 | const Language = () => { 7 | const currentLanguage = useStore((state) => state.language); 8 | const supportedLanguages = [ 9 | { language: "English", code: "en" }, 10 | { language: "Korean", code: "ko" }, 11 | { language: "French", code: "fr" }, 12 | ]; 13 | 14 | const updateLanguage = (langCode: string) => { 15 | handleEvent(setLanguage(langCode)); 16 | localStorage.setItem("lainLanguage", JSON.stringify(langCode)); 17 | }; 18 | 19 | return ( 20 | <> 21 |

22 | From here you can select which language you want the game's 23 | subtitles to be in. 24 |
25 | This list will be updated gradually as more translations get completed. 26 |

27 |
28 | {supportedLanguages.map((entry) => ( 29 |

updateLanguage(entry.code)} 34 | > 35 | {entry.language} 36 |

37 | ))} 38 | 39 | ); 40 | }; 41 | 42 | export default Language; 43 | -------------------------------------------------------------------------------- /src/components/dom/QA.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type QAProps = { 4 | question: string; 5 | answer: string; 6 | }; 7 | 8 | const QA = (props: QAProps) => ( 9 | <> 10 | Q:{" "} 11 | 15 |
16 | A: 17 |

18 | 19 | ); 20 | 21 | export default QA; 22 | -------------------------------------------------------------------------------- /src/components/dom/Savefile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | const Savefile = () => { 4 | const [textAreaValue, setTextAreaValue] = useState(""); 5 | 6 | useEffect(() => { 7 | setTextAreaValue(localStorage.getItem("lainSaveStateV2") || ""); 8 | }, []); 9 | 10 | const loadState = () => { 11 | if (textAreaValue) { 12 | localStorage.setItem("lainSaveStateV2", textAreaValue); 13 | } else { 14 | localStorage.setItem("lainSaveStateV2", ""); 15 | } 16 | }; 17 | 18 | const handleTextValueChange = (e: React.ChangeEvent) => { 19 | setTextAreaValue(e.target.value); 20 | }; 21 | 22 | return ( 23 | <> 24 |
25 |

26 | If you've saved the game during the playthrough, the text provided 27 | in the box below is your "save file". To export it for future 28 | use, just copy everything inside it and paste it inside a file 29 | somewhere locally. To re-import it later, take the contents of the 30 | file, paste them here, and press "Load state". After that, 31 | reload the website. 32 |
33 |
34 | If you're here simply to reset your progress, just delete 35 | everything inside the textbox below and press "Load state". 36 |
37 |
38 | Keep in mind, manually modifying the contents without being 39 | careful/setting it to something random will result in a crash while 40 | trying to load the state in-game. 41 |

42 |