├── .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 |
72 |
73 |
74 |
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 |
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 | {formatKey(key)}
130 | {getBoundKey(key)}
131 |
132 | ))}
133 |
134 |
135 |
136 |
137 |
138 | Reset to default bindings
139 |
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 |
43 |
44 |
Load State
45 |
46 | >
47 | );
48 | };
49 |
50 | export default Savefile;
51 |
--------------------------------------------------------------------------------
/src/core/handleEvent.ts:
--------------------------------------------------------------------------------
1 | import { useStore } from "@/store";
2 | import { GameEvent } from "@/types";
3 | import { playAudio } from "@/utils/audio";
4 | import sleep from "@/utils/sleep";
5 |
6 | // the async/await here might be misleading for some, it functions as a
7 | // setTimeout that fires multiple async calls without stopping the execution,
8 | // which is what we want.
9 | const handleEvent = (event: GameEvent) => {
10 | const applyMutation = useStore.getState().applyMutation;
11 | const { state, effects, audio, additionalEvents } = event;
12 |
13 | if (state) {
14 | state.forEach(async (mutationData) => {
15 | const { delay, mutation } = mutationData;
16 | if (delay) await sleep(delay);
17 |
18 | applyMutation(mutation);
19 | });
20 | }
21 |
22 | if (effects) {
23 | effects.forEach((effect) => effect());
24 | }
25 |
26 | if (audio) {
27 | audio.forEach(async (audio) => {
28 | const { delay, sfx } = audio;
29 | if (delay) await sleep(delay);
30 | sfx.forEach((soundEffect) => {
31 | playAudio(soundEffect);
32 | });
33 | });
34 | }
35 |
36 | if (additionalEvents) {
37 | additionalEvents.forEach(async (gameEvent) => {
38 | const { delay, event } = gameEvent;
39 | if (delay) await sleep(delay);
40 | handleEvent(event);
41 | });
42 | }
43 | };
44 |
45 | export default handleEvent;
46 |
--------------------------------------------------------------------------------
/src/core/index.ts:
--------------------------------------------------------------------------------
1 | import handleInput from "./handleInput";
2 | import handleEvent from "./handleEvent";
3 |
4 | export { handleEvent, handleInput };
5 |
--------------------------------------------------------------------------------
/src/hooks/useCappedFrame.tsx:
--------------------------------------------------------------------------------
1 | import { RenderCallback, useFrame } from "@react-three/fiber";
2 | import { useRef } from "react";
3 |
4 | const useCappedFrame = (
5 | callback: RenderCallback,
6 | framerate: number = 0.016,
7 | renderPriority?: number
8 | ) => {
9 | const deltaRef = useRef(0);
10 | useFrame((state, delta) => {
11 | deltaRef.current += delta;
12 | if (deltaRef.current > framerate) {
13 | callback(state, deltaRef.current, renderPriority);
14 | deltaRef.current = deltaRef.current % framerate;
15 | }
16 | });
17 | };
18 |
19 | export default useCappedFrame;
20 |
--------------------------------------------------------------------------------
/src/hooks/useLetterGeometry.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { PlaneBufferGeometry } from "three";
3 |
4 | const useLetterGeometry = (
5 | letterData: number[],
6 | atlasWidth: number,
7 | atlasHeight: number
8 | ) => {
9 | const [letterGeometry, setLetterGeometry] = useState();
10 |
11 | useEffect(() => {
12 | const geometry = new PlaneBufferGeometry();
13 |
14 | const uvAttribute = geometry.attributes.uv;
15 |
16 | for (let i = 0; i < uvAttribute.count; i++) {
17 | let u = uvAttribute.getX(i);
18 | let v = uvAttribute.getY(i);
19 |
20 | u = (u * letterData[2]) / atlasWidth + letterData[0] / atlasWidth;
21 |
22 | v =
23 | (v * letterData[3]) / atlasHeight +
24 | (1 - letterData[3] / atlasHeight - letterData[1] / atlasHeight) -
25 | letterData[4] / atlasHeight;
26 |
27 | uvAttribute.setXY(i, u, v);
28 | }
29 |
30 | setLetterGeometry(geometry);
31 | }, [atlasHeight, atlasWidth, letterData]);
32 |
33 | return letterGeometry;
34 | };
35 |
36 | export default useLetterGeometry;
37 |
--------------------------------------------------------------------------------
/src/hooks/useNodeTexture.tsx:
--------------------------------------------------------------------------------
1 | import { useTexture } from "@react-three/drei";
2 |
3 | // mapping node type to filename
4 | const nodeTypeToName = (type: number) => {
5 | switch (type) {
6 | case 3:
7 | return "Cou";
8 | case 4:
9 | return "Dc";
10 | case 7:
11 | return "SSkn";
12 | case 2:
13 | return "Tda";
14 | case 5:
15 | return "Dia";
16 | case 0:
17 | return "Lda";
18 | default:
19 | return "MULTI";
20 | }
21 | };
22 |
23 | const useNodeTexture = (nodeType: number) => {
24 | const typeName = nodeTypeToName(nodeType);
25 |
26 | const normal = `/sprites/nodes/${typeName}.png`;
27 | const active = `/sprites/nodes/${typeName}_active.png`;
28 | const viewed = `/sprites/nodes/${typeName}_viewed.png`;
29 | const gold = `/sprites/nodes/${typeName}_gold.png`;
30 |
31 | return {
32 | activeTexture: useTexture(active),
33 | normalTexture: useTexture(normal),
34 | viewedTexture: useTexture(viewed),
35 | goldTexture: useTexture(gold),
36 | };
37 | };
38 |
39 | export default useNodeTexture;
40 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | const usePrevious = (value: T): T | undefined => {
4 | const ref = useRef();
5 | useEffect(() => {
6 | ref.current = value;
7 | });
8 | return ref.current;
9 | };
10 |
11 | export default usePrevious;
12 |
--------------------------------------------------------------------------------
/src/hooks/useText.tsx:
--------------------------------------------------------------------------------
1 | import { FontData, TextType } from "@/types";
2 | import { useTexture } from "@react-three/drei";
3 | import mediumFontJson from "@/json/font/medium_font.json";
4 | import bigFontJson from "@/json/font/big_font.json";
5 | import jpFontJson from "@/json/font/jp_font.json";
6 |
7 | const getFontDataAndTexture = (textType: TextType) => {
8 | switch (textType) {
9 | case TextType.MediumGreen:
10 | return {
11 | texture: "/sprites/fonts/white_and_green_texture.png",
12 | font: mediumFontJson,
13 | };
14 | case TextType.MediumBlack:
15 | return {
16 | texture: "/sprites/fonts/orange_font_texture.png",
17 | font: mediumFontJson,
18 | };
19 | case TextType.MediumOrange:
20 | return {
21 | texture: "/sprites/fonts/white_and_orange_texture.png",
22 | font: mediumFontJson,
23 | };
24 | case TextType.MediumWhite:
25 | return {
26 | texture: "/sprites/fonts/yellow_font_texture.png",
27 | font: mediumFontJson,
28 | };
29 | case TextType.BigOrange:
30 | return {
31 | texture: "/sprites/fonts/orange_font_texture.png",
32 | font: bigFontJson,
33 | };
34 | case TextType.BigYellow:
35 | return {
36 | texture: "/sprites/fonts/yellow_font_texture.png",
37 | font: bigFontJson,
38 | };
39 | case TextType.Jp:
40 | return {
41 | texture: "/sprites/fonts/orange_jp_font.png",
42 | font: jpFontJson,
43 | };
44 | }
45 | };
46 |
47 | const useText = (
48 | textType: TextType
49 | ): { texture: THREE.Texture; font: FontData } => {
50 | const { font, texture } = getFontDataAndTexture(textType);
51 | return {
52 | texture: useTexture(texture),
53 | font: font,
54 | };
55 | };
56 |
57 | export default useText;
58 |
--------------------------------------------------------------------------------
/src/json/font/big_font.json:
--------------------------------------------------------------------------------
1 | {
2 | "glyphs": {
3 | "A": [0, 2, 17, 14, 0],
4 | "B": [18, 2, 12, 14, 0],
5 | "C": [32, 2, 15, 14, 0],
6 | "D": [48, 2, 15, 14, 0],
7 | "E": [64, 2, 11, 14, 0],
8 | "F": [76, 2, 11, 14, 0],
9 | "G": [88, 2, 15, 14, 0],
10 | "H": [104, 2, 15, 14, 0],
11 | "I ": [121, 2, 5, 14, 0],
12 | "J": [128, 2, 11, 14, 0],
13 | "K": [140, 2, 14, 14, 0],
14 | "L": [156, 2, 12, 14, 0],
15 | "M": [170, 2, 15, 14, 0],
16 | "N": [188, 2, 14, 14, 0],
17 | "O": [204, 2, 17, 14, 0],
18 | "P": [222, 2, 12, 14, 0],
19 | "Q": [236, 2, 17, 14, 0],
20 | "R": [0, 18, 12, 14, 0],
21 | "S": [14, 18, 11, 14, 0],
22 | "T": [26, 18, 15, 14, 0],
23 | "U": [42, 18, 15, 14, 0],
24 | "V": [58, 18, 17, 14, 0],
25 | "W": [76, 18, 21, 14, 0],
26 | "X": [98, 18, 15, 14, 0],
27 | "Y": [114, 18, 15, 14, 0],
28 | "Z": [130, 18, 11, 14, 0],
29 | "0": [142, 16, 8, 16, 0],
30 | "1": [158, 16, 6, 16, 0],
31 | "2": [168, 16, 8, 16, 0],
32 | "3": [182, 16, 8, 16, 0],
33 | "4": [196, 16, 8, 16, 0],
34 | "5": [210, 16, 8, 16, 0],
35 | "6": [224, 16, 8, 16, 0],
36 | "7": [238, 16, 8, 16, 0],
37 | "8": [0, 32, 8, 16, 0],
38 | "9": [14, 32, 8, 16, 0],
39 | "a": [28, 34, 11, 14, 0],
40 | "b": [40, 34, 11, 14, 0],
41 | "c": [52, 34, 10, 14, 0],
42 | "d": [64, 34, 11, 14, 0],
43 | "e": [76, 34, 12, 14, 0],
44 | "f": [90, 34, 9, 14, 3],
45 | "g": [100, 34, 11, 14, 4],
46 | "h": [112, 34, 11, 14, 0],
47 | "i": [123, 34, 7, 14, 0],
48 | "j": [132, 34, 7, 17, 3],
49 | "k": [140, 34, 11, 14, 0],
50 | "l": [152, 34, 7, 15, 0],
51 | "m": [160, 34, 14, 14, 0],
52 | "n": [176, 34, 11, 14, 0],
53 | "o": [188, 34, 12, 14, 0],
54 | "p": [202, 34, 11, 14, 4],
55 | "q": [214, 34, 11, 18, 4],
56 | "r": [226, 34, 9, 14, 0],
57 | "s": [236, 34, 8, 14, 0],
58 | "t": [0, 58, 9, 14, 0],
59 | "u": [10, 58, 11, 14, 0],
60 | "v": [22, 58, 13, 14, 0],
61 | "w": [36, 58, 17, 14, 0],
62 | "x": [54, 58, 11, 14, 0],
63 | "y": [65, 58, 13, 14, 3],
64 | "z": [80, 58, 10, 14, 0],
65 | ",": [92, 58, 5, 16, 2],
66 | ".": [100, 58, 5, 14, 0],
67 | "*": [109, 58, 9, 14, 0],
68 | ":": [120, 58, 5, 14, 0],
69 | "@": [128, 58, 17, 14, 0],
70 | "'": [146, 58, 5, 14, 0],
71 | "(": [155, 58, 6, 17, 3],
72 | ")": [162, 58, 6, 17, 3],
73 | "-": [171, 58, 11, 14, 0],
74 | "!": [185, 58, 4, 14, 0],
75 | "\\": [192, 58, 10, 14, 0],
76 | "#": [204, 58, 17, 14, 0],
77 | "%": [222, 58, 17, 14, 0],
78 | "&": [0, 82, 16, 14, 0],
79 | "?": [18, 82, 9, 14, 0],
80 | "/": [28, 82, 11, 16, 2],
81 | " ": [0, 0, 0, 0, 0, 0]
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/json/font/jp_font.json:
--------------------------------------------------------------------------------
1 | {
2 | "glyphs": {
3 | "ア": [0, 0, 16, 16, 0],
4 | "イ": [16, 0, 16, 16, 0],
5 | "ウ": [32, 0, 16, 16, 0],
6 | "エ": [48, 0, 16, 16, 0],
7 | "オ": [64, 0, 16, 16, 0],
8 | "カ": [80, 0, 16, 16, 0],
9 | "キ": [96, 0, 16, 16, 0],
10 | "ク": [112, 0, 16, 16, 0],
11 | "ケ": [128, 0, 16, 16, 0],
12 | "コ": [144, 0, 16, 16, 0],
13 | "サ": [160, 0, 16, 16, 0],
14 | "シ": [176, 0, 16, 16, 0],
15 | "ス": [192, 0, 16, 16, 0],
16 | "セ": [208, 0, 16, 16, 0],
17 | "ソ": [224, 0, 16, 16, 0],
18 |
19 | "タ": [0, 16, 16, 16, 0],
20 | "チ": [16, 16, 16, 16, 0],
21 | "ツ": [32, 16, 16, 16, 0],
22 | "テ": [48, 16, 16, 16, 0],
23 | "ト": [64, 16, 16, 16, 0],
24 | "ナ": [80, 16, 16, 16, 0],
25 | "ニ": [96, 16, 16, 16, 0],
26 | "ヌ": [112, 16, 16, 16, 0],
27 | "ネ": [128, 16, 16, 16, 0],
28 | "ノ": [144, 16, 16, 16, 0],
29 | "ハ": [160, 16, 16, 16, 0],
30 | "ヒ": [176, 16, 16, 16, 0],
31 | "フ": [192, 16, 16, 16, 0],
32 | "ヘ": [208, 16, 16, 16, 0],
33 | "ホ": [224, 16, 16, 16, 0],
34 |
35 | "マ": [0, 32, 16, 16, 0],
36 | "ミ": [16, 32, 16, 16, 0],
37 | "ム": [32, 32, 16, 16, 0],
38 | "メ": [48, 32, 16, 16, 0],
39 | "モ": [64, 32, 16, 16, 0],
40 | "ヤ": [80, 32, 16, 16, 0],
41 | "ユ": [96, 32, 16, 16, 0],
42 | "ヨ": [112, 32, 16, 16, 0],
43 | "ラ": [128, 32, 16, 16, 0],
44 | "リ": [144, 32, 16, 16, 0],
45 | "ル": [160, 32, 16, 16, 0],
46 | "レ": [176, 32, 16, 16, 0],
47 | "ロ": [192, 32, 16, 16, 0],
48 | "ワ": [208, 32, 16, 16, 0],
49 | "ン": [224, 32, 16, 16, 0],
50 |
51 | "ッ": [0, 48, 16, 16, 0],
52 | "ャ": [16, 48, 16, 16, 0],
53 | "ュ": [32, 48, 16, 16, 0],
54 | "ョ": [48, 48, 16, 16, 0],
55 | "ガ": [64, 48, 16, 16, 0],
56 | "ギ": [80, 48, 16, 16, 0],
57 | "グ": [96, 48, 16, 16, 0],
58 | "ゲ": [112, 48, 16, 16, 0],
59 | "ゴ": [128, 48, 16, 16, 0],
60 | "ザ": [144, 48, 16, 16, 0],
61 | "ジ": [160, 48, 16, 16, 0],
62 | "ズ": [176, 48, 16, 16, 0],
63 | "ゼ": [192, 48, 16, 16, 0],
64 | "ゾ": [208, 48, 16, 16, 0],
65 | "ダ": [224, 48, 16, 16, 0],
66 |
67 | "ヂ": [0, 64, 16, 16, 0],
68 | "ヅ": [16, 64, 16, 16, 0],
69 | "デ": [32, 64, 16, 16, 0],
70 | "ド": [48, 64, 16, 16, 0],
71 | "バ": [64, 64, 16, 16, 0],
72 | "ビ": [80, 64, 16, 16, 0],
73 | "ブ": [96, 64, 16, 16, 0],
74 | "ベ": [112, 64, 16, 16, 0],
75 | "ボ": [128, 64, 16, 16, 0],
76 | "パ": [144, 64, 16, 16, 0],
77 | "ピ": [160, 64, 16, 16, 0],
78 | "プ": [176, 64, 16, 16, 0],
79 | "ペ": [192, 64, 16, 16, 0],
80 | "ポ": [208, 64, 16, 16, 0],
81 |
82 | "ァ": [0, 80, 16, 16, 0],
83 | "ィ": [16, 80, 16, 16, 0],
84 | "ゥ": [32, 80, 16, 16, 0],
85 | "ェ": [48, 80, 16, 16, 0],
86 | "ォ": [64, 80, 16, 16, 0],
87 | "ー": [80, 80, 16, 16, 0]
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/json/font/medium_font.json:
--------------------------------------------------------------------------------
1 | {
2 | "glyphs": {
3 | "A": [64, 80, 8, 8, 0],
4 | "B": [72, 80, 8, 8, 0],
5 | "C": [80, 80, 8, 8, 0],
6 | "D": [88, 80, 8, 8, 0],
7 | "E": [96, 80, 8, 8, 0],
8 | "F": [104, 80, 8, 8, 0],
9 | "G": [112, 80, 8, 8, 0],
10 | "H": [120, 80, 8, 8, 0],
11 | "I": [128, 80, 8, 8, 0],
12 | "J": [136, 80, 8, 8, 0],
13 | "K": [144, 80, 8, 8, 0],
14 | "L": [152, 80, 8, 8, 0],
15 | "M": [160, 80, 8, 8, 0],
16 | "N": [168, 80, 8, 8, 0],
17 | "O": [176, 80, 8, 8, 0],
18 | "P": [184, 80, 8, 8, 0],
19 | "Q": [192, 80, 8, 8, 0],
20 | "R": [200, 80, 8, 8, 0],
21 | "S": [208, 80, 8, 8, 0],
22 | "T": [216, 80, 8, 8, 0],
23 | "U": [224, 80, 8, 8, 0],
24 | "V": [232, 80, 8, 8, 0],
25 | "W": [240, 80, 8, 8, 0],
26 | "X": [65, 88, 8, 8, 0],
27 | "Y": [72, 88, 8, 8, 0],
28 | "Z": [80, 88, 8, 8, 0],
29 | "0": [88, 88, 8, 8, 0],
30 | "1": [96, 88, 8, 8, 0],
31 | "2": [104, 88, 8, 8, 0],
32 | "3": [112, 88, 8, 8, 0],
33 | "4": [120, 88, 8, 8, 0],
34 | "5": [128, 88, 8, 8, 0],
35 | "6": [136, 88, 8, 8, 0],
36 | "7": [144, 88, 8, 8, 0],
37 | "8": [152, 88, 8, 8, 0],
38 | "9": [160, 88, 8, 8, 0],
39 | "a": [168, 88, 8, 8, 0],
40 | "b": [176, 88, 8, 8, 0],
41 | "c": [184, 88, 8, 8, 0],
42 | "d": [192, 88, 8, 8, 0],
43 | "e": [200, 88, 8, 8, 0],
44 | "f": [208, 88, 8, 8, 0],
45 | "g": [217, 88, 8, 9, 0.2],
46 | "h": [224, 88, 8, 8, 0],
47 | "i": [232, 88, 8, 8, 0],
48 | "j": [240, 88, 8, 9, 0.2],
49 | "k": [64, 96, 8, 8, 0],
50 | "l": [72, 96, 8, 8, 0],
51 | "m": [80, 96, 8, 8, 0],
52 | "n": [88, 96, 8, 8, 0],
53 | "o": [96, 96, 8, 8, 0],
54 | "p": [104, 96, 8, 9, 0.2],
55 | "q": [112, 96, 8, 9, 0.2],
56 | "r": [120, 96, 8, 8, 0],
57 | "s": [128, 96, 8, 8, 0],
58 | "t": [136, 96, 8, 8, 0],
59 | "u": [144, 96, 8, 8, 0],
60 | "v": [152, 96, 8, 8, 0],
61 | "w": [160, 96, 8, 8, 0],
62 | "x": [168, 96, 8, 8, 0],
63 | "y": [176, 96, 8, 9, 0.2],
64 | "z": [184, 96, 8, 8, 0],
65 | ",": [192, 96, 8, 8, 0],
66 | ".": [200, 96, 8, 8, 0],
67 | "*": [208, 96, 8, 8, 0],
68 | ":": [0, 104, 8, 8, 0],
69 | "@": [8, 104, 8, 8, 0],
70 | "'": [16, 104, 8, 8, 0],
71 | "(": [24, 104, 8, 8, 0],
72 | ")": [32, 104, 8, 8, 0],
73 | "-": [40, 104, 8, 8, 0],
74 | "!": [48, 104, 8, 8, 0],
75 | "\"": [56, 104, 8, 8, 0],
76 | "#": [64, 104, 8, 8, 0],
77 | "%": [72, 104, 8, 8, 0],
78 | "&": [80, 104, 8, 8, 0],
79 | "?": [88, 104, 8, 8, 0],
80 | "/": [96, 104, 8, 9, 0],
81 | " ": [0, 0, 0, 0, 0, 0],
82 | "_": [0, 0, 0, 0, 0, 0]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/json/node_positions.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "position": [-1.145, -0.33, -0.5],
4 | "rotation": [0, -1.95, 0]
5 | },
6 | {
7 | "position": [-0.5, -0.33, -1.15],
8 | "rotation": [0, -2.75, 0]
9 | },
10 | {
11 | "position": [0.45, -0.33, -1.15],
12 | "rotation": [0, -3.55, 0]
13 | },
14 | {
15 | "position": [1.15, -0.33, -0.45],
16 | "rotation": [0, -4.35, 0]
17 | },
18 | {
19 | "position": [1.15, -0.33, 0.45],
20 | "rotation": [0, 1.2, 0]
21 | },
22 | {
23 | "position": [0.45, -0.33, 1.1],
24 | "rotation": [0, 0.35, 0]
25 | },
26 | {
27 | "position": [-0.49, -0.33, 1.12],
28 | "rotation": [0, -0.37, 0]
29 | },
30 | {
31 | "position": [-1.15, -0.33, 0.45],
32 | "rotation": [0, -1.17, 0]
33 | },
34 | {
35 | "position": [-1.145, 0, -0.5],
36 | "rotation": [0, -1.95, 0]
37 | },
38 | {
39 | "position": [-0.5, 0, -1.15],
40 | "rotation": [0, -2.75, 0]
41 | },
42 | {
43 | "position": [0.45, 0, -1.15],
44 | "rotation": [0, -3.55, 0]
45 | },
46 | {
47 | "position": [1.15, 0, -0.45],
48 | "rotation": [0, -4.35, 0]
49 | },
50 | {
51 | "position": [1.15, 0, 0.45],
52 | "rotation": [0, 1.2, 0]
53 | },
54 | {
55 | "position": [0.45, 0, 1.1],
56 | "rotation": [0, 0.35, 0]
57 | },
58 | {
59 | "position": [-0.49, 0, 1.12],
60 | "rotation": [0, -0.37, 0]
61 | },
62 | {
63 | "position": [-1.15, 0, 0.45],
64 | "rotation": [0, -1.17, 0]
65 | },
66 | {
67 | "position": [-1.145, 0.33, -0.5],
68 | "rotation": [0, -1.95, 0]
69 | },
70 | {
71 | "position": [-0.5, 0.33, -1.15],
72 | "rotation": [0, -2.75, 0]
73 | },
74 | {
75 | "position": [0.45, 0.33, -1.15],
76 | "rotation": [0, -3.55, 0]
77 | },
78 | {
79 | "position": [1.15, 0.33, -0.45],
80 | "rotation": [0, -4.35, 0]
81 | },
82 | {
83 | "position": [1.15, 0.33, 0.45],
84 | "rotation": [0, 1.2, 0]
85 | },
86 | {
87 | "position": [0.45, 0.33, 1.1],
88 | "rotation": [0, 0.35, 0]
89 | },
90 | {
91 | "position": [-0.49, 0.33, 1.12],
92 | "rotation": [0, -0.37, 0]
93 | },
94 | {
95 | "position": [-1.15, 0.33, 0.45],
96 | "rotation": [0, -1.17, 0]
97 | }
98 | ]
99 |
--------------------------------------------------------------------------------
/src/json/site_b_layout.json:
--------------------------------------------------------------------------------
1 | [
2 | [],
3 | [
4 | ["0116b", null, null, "0119b", null, "0121b", "0122b", "0123b"],
5 | [null, null, "0110b", null, "0112b", "0113b", "0114b", "0115b"],
6 | ["0100b", "0101b", null, "0103b", "0104b", "0105b", "0106b", null]
7 | ],
8 | [
9 | ["0216b", null, null, null, "0220b", "0221b", "0222b", null],
10 | [null, "0209b", "0210b", "0211b", "0212b", "0213b", "0214b", "0215b"],
11 | ["0200b", "0201b", "0202b", "0203b", "0204b", "0205b", null, null]
12 | ],
13 | [
14 | ["0316b", "0317b", "0318b", "0319b", "0320b", "0321b", null, "0323b"],
15 | ["0308b", "0309b", "0310b", "0311b", "0312b", "0313b", "0314b", "0315b"],
16 | [null, "0301b", "0302b", "0303b", "0304b", "0305b", "0306b", "0307b"]
17 | ],
18 | [
19 | ["0416b", "0417b", "0418b", "0419b", "0420b", "0421b", "0422b", "0423b"],
20 | ["0408b", "0409b", "0410b", "0411b", null, "0413b", "0414b", "0415b"],
21 | ["0400b", "0401b", "0402b", "0403b", "0404b", "0405b", "0406b", null]
22 | ],
23 | [
24 | [null, "0517b", "0518b", "0519b", "0520b", "0521b", "0522b", "0523b"],
25 | ["0508b", "0509b", "0510b", "0511b", "0512b", "0513b", "0514b", null],
26 | ["0500b", "0501b", "0502b", "0503b", "0504b", "0505b", "0506b", "0507b"]
27 | ],
28 | [
29 | ["0616b", "0617b", "0618b", "0619b", "0620b", "0621b", "0622b", "0623b"],
30 | ["0608b", "0609b", "0610b", "0611b", null, "0613b", "0614b", null],
31 | ["0600b", "0601b", null, "0603b", "0604b", "0605b", "0606b", "0607b"]
32 | ],
33 | [
34 | ["0716b", "0717b", "0718b", "0719b", "0720b", "0721b", "0722b", "0723b"],
35 | ["0708b", "0709b", "0710b", "0711b", null, "0713b", "0714b", "0715b"],
36 | ["0700b", "0701b", "0702b", "0703b", null, "0705b", "0706b", "0707b"]
37 | ],
38 | [
39 | ["0816b", "0817b", "0818b", "0819b", "0820b", "0821b", "0822b", "0823b"],
40 | ["0808b", "0809b", "0810b", "0811b", "0812b", "0813b", "0814b", "0815b"],
41 | ["0800b", "0801b", "0802b", "0803b", "0804b", "0805b", "0806b", "0807b"]
42 | ],
43 | [
44 | ["0916b", "0917b", null, "0919b", "0920b", null, "0922b", "0923b"],
45 | ["0908b", "0909b", "0910b", "0911b", "0912b", "0913b", "0914b", "0915b"],
46 | ["0900b", "0901b", "0902b", "0903b", "0904b", "0905b", "0906b", "0907b"]
47 | ],
48 | [
49 | ["1016b", "1017b", "1018b", "1019b", "1020b", "1021b", "1022b", null],
50 | ["1008b", "1009b", "1010b", "1011b", "1012b", "1013b", null, "1015b"],
51 | ["1000b", "1001b", "1002b", "1003b", "1004b", null, "1006b", "1007b"]
52 | ],
53 | [
54 | ["1116b", "1117b", "1118b", "1119b", "1120b", "1121b", "1122b", "1123b"],
55 | ["1108b", "1109b", "1110b", "1111b", null, "1113b", "1114b", "1115b"],
56 | [null, null, "1102b", "1103b", "1104b", "1105b", "1106b", "1107b"]
57 | ],
58 | [
59 | [null, null, "1218b", "1219b", "1220b", "1221b", null, null],
60 | [null, "1209b", "1210b", null, "1212b", null, "1214b", "1215b"],
61 | ["1200b", "1201b", null, "1203b", "1204b", "1205b", "1206b", "1207b"]
62 | ],
63 | [
64 | ["1316b", null, null, "1319b", "1320b", "1321b", null, null],
65 | [null, "1309b", "1310b", "1311b", "1312b", "1313b", null, "1315b"],
66 | ["1300b", "1301b", "1302b", "1303b", "1304b", "1305b", "1306b", null]
67 | ]
68 | ]
69 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import type { AppProps } from "next/app";
3 | import { useEffect } from "react";
4 | import { handleEvent } from "@/core";
5 | import { setKeybindings, setLanguage } from "@/core/events";
6 | import { upgradeLegacySave } from "@/utils/save";
7 |
8 | const MyApp = ({ Component, pageProps }: AppProps) => {
9 | useEffect(() => {
10 | const keybindingSettings = localStorage.getItem("lainKeybindings");
11 | if (keybindingSettings) {
12 | handleEvent(setKeybindings(JSON.parse(keybindingSettings)));
13 | }
14 |
15 | const language = localStorage.getItem("lainLanguage");
16 | if (language) {
17 | handleEvent(setLanguage(JSON.parse(language)));
18 | }
19 |
20 | const saveState = localStorage.getItem("lainSaveStateV2");
21 | if (!saveState) {
22 | // if we find an old version, upgrade it
23 | const oldSaveState = localStorage.getItem("lainSaveState");
24 | if (oldSaveState) {
25 | localStorage.setItem(
26 | "lainSaveStateV2",
27 | JSON.stringify(upgradeLegacySave(JSON.parse(oldSaveState)))
28 | );
29 | }
30 | }
31 | }, []);
32 |
33 | return (
34 | <>
35 |
36 | >
37 | );
38 | };
39 |
40 | export default MyApp;
41 |
42 |
--------------------------------------------------------------------------------
/src/pages/options.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import React from "react";
3 | import Keybinding from "@/components/dom/Keybinding";
4 | import Language from "@/components/dom/Language";
5 | import Savefile from "@/components/dom/Savefile";
6 | import Header from "@/components/dom/Header";
7 |
8 | const Options = () => {
9 | return (
10 | <>
11 |
12 | {" < options >"}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | >
21 | );
22 | };
23 |
24 | export default Options;
25 |
--------------------------------------------------------------------------------
/src/shaders/blue_digit.frag:
--------------------------------------------------------------------------------
1 | uniform sampler2D tex;
2 | uniform float brightnessMultiplier;
3 | varying vec2 vUv;
4 |
5 | void main() {
6 | gl_FragColor = texture2D(tex, vUv) * brightnessMultiplier;
7 | }
8 |
--------------------------------------------------------------------------------
/src/shaders/blue_digit.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | void main() {
4 | vUv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/src/shaders/explosion_line.frag:
--------------------------------------------------------------------------------
1 | uniform vec3 color1;
2 | uniform vec3 color2;
3 | uniform float alpha;
4 |
5 | varying vec2 vUv;
6 |
7 | void main() {
8 | float alpha = smoothstep(1.0, 0.0, vUv.y);
9 | float colorMix = smoothstep(1.0, 2.0, 1.8);
10 |
11 | gl_FragColor = vec4(mix(color1, color2, colorMix), alpha) * 0.6;
12 | }
13 |
--------------------------------------------------------------------------------
/src/shaders/explosion_line.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | void main() {
4 | vUv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/src/shaders/gate_side.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | void main() {
4 | vUv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/src/shaders/gate_side_left.frag:
--------------------------------------------------------------------------------
1 | uniform sampler2D tex1;
2 | uniform float alpha;
3 | uniform float offset;
4 |
5 | varying vec2 vUv;
6 |
7 | void main() {
8 | float alpha = smoothstep(0.9, 1.0, vUv.x);
9 |
10 | vec4 t1 = texture2D(tex1,vUv * 5.0 + offset);
11 |
12 | gl_FragColor = mix(t1, vec4(0,0,0,0), alpha) * 0.8;
13 | }
14 |
--------------------------------------------------------------------------------
/src/shaders/gate_side_right.frag:
--------------------------------------------------------------------------------
1 | uniform sampler2D tex1;
2 | uniform float alpha;
3 | uniform float offset;
4 |
5 | varying vec2 vUv;
6 |
7 | void main() {
8 | float alpha = smoothstep(1.0, 0.9, vUv.x);
9 |
10 | vec4 t1 = texture2D(tex1,vUv * 5.0 + offset);
11 |
12 | gl_FragColor = mix(vec4(0,0,0,0), t1, alpha) * 0.8;
13 | }
14 |
--------------------------------------------------------------------------------
/src/shaders/gray_ring.frag:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 | uniform sampler2D lof;
3 | uniform sampler2D hole;
4 | uniform sampler2D life;
5 |
6 | // lights
7 | varying vec3 vPos;
8 | varying vec3 vNormal;
9 |
10 | struct PointLight {
11 | vec3 position;
12 | vec3 color;
13 | float distance;
14 | };
15 |
16 | uniform PointLight pointLights[ NUM_POINT_LIGHTS ];
17 |
18 | // transform coordinates to uniform within segment
19 | float tolocal(float x, int segments, float step) {
20 | float period = 1.0/step*float(segments);
21 | return mod(x, period) / period;
22 | }
23 |
24 | // check if coordinate is within the given height
25 | bool isheight(float y, float thin) {
26 | return y > 0.5-thin/2.0 && y < 0.5+thin/2.0;
27 | }
28 |
29 | // sloping function
30 | float slope(float x, float thin) {
31 | return x*(1.0-thin)/2.0;
32 | }
33 |
34 | // frag color / texture
35 | // #424252 hex in original textures
36 | vec4 color(vec2 vUv, int quadnum, bool textureexists, int thinperiod, int quadlen, float step) {
37 | if (!textureexists) {
38 | return vec4(0.259,0.259,0.322, 1);
39 | } else if (mod(float(quadnum), 2.0) == 1.0) {
40 | return texture2D(hole, vec2(tolocal(vUv.x, quadlen-thinperiod, step), vUv.y));
41 | // return vec4(tolocal(vUv.x, quadlen-thinperiod, step), 0, 0, 1);
42 | } else if (quadnum == 0) {
43 | return texture2D(lof, vec2(tolocal(vUv.x, quadlen-thinperiod, step), vUv.y));
44 | } else {
45 | return texture2D(life, vec2(tolocal(vUv.x, quadlen-thinperiod, step), vUv.y));
46 | }
47 | }
48 |
49 | void main() {
50 |
51 | //lights
52 | vec4 addedLights = vec4(0.0,
53 | 0.0,
54 | 0.0,
55 | 1.0);
56 |
57 | for(int l = 0; l < NUM_POINT_LIGHTS; l++) {
58 | vec3 lightDirection = normalize(vPos
59 | - pointLights[l].position);
60 | addedLights.rgb += clamp(dot(-lightDirection,
61 | vNormal), 0.0, 1.0)
62 | * pointLights[l].color
63 | * 20.0;
64 | }
65 |
66 |
67 | // number of segments
68 | float step = 64.0;
69 |
70 | // thin line height
71 | float thin = 0.3;
72 |
73 | // segment within circle
74 | int segment = int(floor(vUv.x * step));
75 |
76 | int quadlen = int(step)/4;
77 |
78 | // segment within circle's quad
79 | int quadel = int(mod(float(segment), float(quadlen)));
80 |
81 | // which quad
82 | int quadnum = int(int(segment) / int(quadlen));
83 |
84 | // how big thin part is
85 | int thinperiod = 8;
86 |
87 | if (quadel < thinperiod && isheight(vUv.y, thin)) {
88 | // thin line
89 | gl_FragColor = color(vUv, quadnum, false, thinperiod, quadlen, step) * addedLights;
90 | } else if (quadel == thinperiod) {
91 | // slope up
92 | float dist = tolocal(vUv.x, 1, step);
93 | if (vUv.y > slope(1.0-dist, thin) && vUv.y < 1.0-slope(1.0-dist, thin)) {
94 | gl_FragColor = color(vUv, quadnum, true, thinperiod, quadlen, step) * addedLights;
95 | } else {
96 | gl_FragColor = vec4(0, 0, 0, 0);
97 | }
98 | } else if (quadel == quadlen-1) {
99 | // slope down
100 | float dist = tolocal(vUv.x, 1, step);
101 | if (vUv.y > slope(dist, thin) && vUv.y < 1.0-slope(dist, thin)) {
102 | gl_FragColor = color(vUv, quadnum, true, thinperiod, quadlen, step) * addedLights;
103 | } else {
104 | gl_FragColor = vec4(0, 0, 0, 0);
105 | }
106 | } else if (quadel > thinperiod) {
107 | gl_FragColor = color(vUv, quadnum, true, thinperiod, quadlen, step) * addedLights;
108 | } else {
109 | // transparent
110 | gl_FragColor = vec4(0, 0, 0, 0);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/shaders/gray_ring.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | varying vec3 vPos;
4 | varying vec3 vNormal;
5 |
6 | void main() {
7 | vUv = uv;
8 |
9 | vPos = (modelMatrix * vec4(position, 1.0 )).xyz;
10 | vNormal = normalMatrix * normal;
11 |
12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
13 | }
14 |
--------------------------------------------------------------------------------
/src/shaders/intro_star.frag:
--------------------------------------------------------------------------------
1 | uniform vec3 color1;
2 | uniform vec3 color2;
3 | uniform float alpha;
4 |
5 | varying vec2 vUv;
6 |
7 | void main() {
8 | float alpha = smoothstep(0.0, 1.0, vUv.y);
9 | float colorMix = smoothstep(1.0, 2.0, 1.8);
10 |
11 | gl_FragColor = vec4(mix(color1, color2, colorMix), alpha) * 0.8;
12 | }
13 |
--------------------------------------------------------------------------------
/src/shaders/intro_star.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | void main() {
4 | vUv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/src/shaders/middle_ring.frag:
--------------------------------------------------------------------------------
1 | uniform sampler2D tex;
2 |
3 | varying vec2 vUv;
4 |
5 | void main() {
6 | gl_FragColor = texture2D( tex, vUv);
7 | }
8 |
--------------------------------------------------------------------------------
/src/shaders/middle_ring.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 | uniform float uTime;
3 | uniform float wobbleStrength;
4 | uniform float noiseAmp;
5 |
6 | //
7 | // Description : Array and textureless GLSL 2D/3D/4D simplex
8 | // noise functions.
9 | // Author : Ian McEwan, Ashima Arts.
10 | // Maintainer : ijm
11 | // Lastmod : 20110822 (ijm)
12 | // License : Copyright (C) 2011 Ashima Arts. All rights reserved.
13 | // Distributed under the MIT License. See LICENSE file.
14 | // https://github.com/ashima/webgl-noise
15 | //
16 |
17 | vec3 mod289(vec3 x) {
18 | return x - floor(x * (1.0 / 289.0)) * 289.0;
19 | }
20 |
21 | vec4 mod289(vec4 x) {
22 | return x - floor(x * (1.0 / 289.0)) * 289.0;
23 | }
24 |
25 | vec4 permute(vec4 x) {
26 | return mod289(((x*34.0)+1.0)*x);
27 | }
28 |
29 | vec4 taylorInvSqrt(vec4 r)
30 | {
31 | return 1.79284291400159 - 0.85373472095314 * r;
32 | }
33 |
34 | float snoise(vec3 v) {
35 | const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
36 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
37 |
38 | // First corner
39 | vec3 i = floor(v + dot(v, C.yyy) );
40 | vec3 x0 = v - i + dot(i, C.xxx) ;
41 |
42 | // Other corners
43 | vec3 g = step(x0.yzx, x0.xyz);
44 | vec3 l = 1.0 - g;
45 | vec3 i1 = min( g.xyz, l.zxy );
46 | vec3 i2 = max( g.xyz, l.zxy );
47 |
48 | // x0 = x0 - 0.0 + 0.0 * C.xxx;
49 | // x1 = x0 - i1 + 1.0 * C.xxx;
50 | // x2 = x0 - i2 + 2.0 * C.xxx;
51 | // x3 = x0 - 1.0 + 3.0 * C.xxx;
52 | vec3 x1 = x0 - i1 + C.xxx;
53 | vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
54 | vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
55 |
56 | // Permutations
57 | i = mod289(i);
58 | vec4 p = permute( permute( permute(
59 | i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
60 | + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
61 | + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
62 |
63 | // Gradients: 7x7 points over a square, mapped onto an octahedron.
64 | // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
65 | float n_ = 0.142857142857; // 1.0/7.0
66 | vec3 ns = n_ * D.wyz - D.xzx;
67 |
68 | vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
69 |
70 | vec4 x_ = floor(j * ns.z);
71 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
72 |
73 | vec4 x = x_ *ns.x + ns.yyyy;
74 | vec4 y = y_ *ns.x + ns.yyyy;
75 | vec4 h = 1.0 - abs(x) - abs(y);
76 |
77 | vec4 b0 = vec4( x.xy, y.xy );
78 | vec4 b1 = vec4( x.zw, y.zw );
79 |
80 | //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
81 | //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
82 | vec4 s0 = floor(b0)*2.0 + 1.0;
83 | vec4 s1 = floor(b1)*2.0 + 1.0;
84 | vec4 sh = -step(h, vec4(0.0));
85 |
86 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
87 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
88 |
89 | vec3 p0 = vec3(a0.xy,h.x);
90 | vec3 p1 = vec3(a0.zw,h.y);
91 | vec3 p2 = vec3(a1.xy,h.z);
92 | vec3 p3 = vec3(a1.zw,h.w);
93 |
94 | // Normalise gradients
95 | vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
96 | p0 *= norm.x;
97 | p1 *= norm.y;
98 | p2 *= norm.z;
99 | p3 *= norm.w;
100 |
101 | // Mix final noise value
102 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
103 | m = m * m;
104 | return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
105 | dot(p2,x2), dot(p3,x3) ) );
106 | }
107 |
108 | void main() {
109 | vUv = uv;
110 |
111 | // offset of the wobble when jumping
112 | const float angleOffset = -0.8;
113 |
114 | // compute world position of the vertex
115 | // (ie, position after model rotation and translation)
116 | vec4 worldPos = modelMatrix * vec4(position, 0.0);
117 | float wobbleAngle = atan(worldPos.x, worldPos.z) + angleOffset;
118 |
119 | vec3 pos = position;
120 |
121 | // noise modifiers
122 | float noiseFreq = 0.5;
123 |
124 | vec3 noisePos = vec3(pos.x * noiseFreq + uTime, pos.y, pos.z);
125 | pos.y += snoise(noisePos) * noiseAmp + wobbleStrength * sin(wobbleAngle * 2.0);
126 |
127 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.);
128 | }
129 |
--------------------------------------------------------------------------------
/src/shaders/node.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform sampler2D normalTexture;
4 | uniform sampler2D activeTexture;
5 | uniform float timeMSeconds;
6 |
7 | varying vec2 vUv;
8 |
9 | // lights
10 | varying vec3 vPos;
11 | varying vec3 vNormal;
12 |
13 | struct PointLight {
14 | vec3 position;
15 | vec3 color;
16 | float distance;
17 | };
18 |
19 | uniform PointLight pointLights[ NUM_POINT_LIGHTS ];
20 |
21 | void main() {
22 | vec4 addedLights = vec4(0.0, 0.0, 0.0, 1.0);
23 |
24 | for (int l = 0; l < NUM_POINT_LIGHTS; l++) {
25 | vec3 lightDirection = normalize(vPos - pointLights[l].position);
26 | addedLights.rgb += clamp(dot(-lightDirection, vNormal), 0.0, 1.0) *
27 | pointLights[l].color * 50.0;
28 | }
29 |
30 | vec4 t1 = texture2D(normalTexture, vUv);
31 | vec4 t2 = texture2D(activeTexture, vUv);
32 | float bias = (1.0 - timeMSeconds) - floor(1.0 - timeMSeconds);
33 | gl_FragColor = mix(t1, t2, bias);
34 | }
35 |
--------------------------------------------------------------------------------
/src/shaders/node.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | varying vec3 vPos;
4 | varying vec3 vNormal;
5 |
6 | void main() {
7 | vUv = uv;
8 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
9 | }
10 |
--------------------------------------------------------------------------------
/src/shaders/purple_ring.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | varying vec3 vPos;
4 | varying vec3 vNormal;
5 |
6 | void main() {
7 | vUv = uv;
8 |
9 | vPos = (modelMatrix * vec4(position, 1.0 )).xyz;
10 | vNormal = normalMatrix * normal;
11 |
12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
13 | }
14 |
--------------------------------------------------------------------------------
/src/shaders/star.frag:
--------------------------------------------------------------------------------
1 | uniform vec3 color1;
2 | uniform vec3 color2;
3 | uniform float alpha;
4 |
5 | varying vec2 vUv;
6 |
7 | void main() {
8 | float alpha = smoothstep(0.0, 1.0, vUv.y);
9 | float colorMix = smoothstep(1.0, 2.0, 1.8);
10 |
11 | gl_FragColor = vec4(mix(color1, color2, colorMix), alpha) * 0.8;
12 | }
13 |
--------------------------------------------------------------------------------
/src/shaders/star.vert:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | void main() {
4 | vUv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/audio.ts:
--------------------------------------------------------------------------------
1 | import { Audio as ThreeAudio, AudioAnalyser, AudioListener } from "three";
2 |
3 | export const playAudio = (filename: string, loop = false) => {
4 | const path = `/media/sfx/${filename}`;
5 | const audio = new Audio(path);
6 |
7 | audio.currentTime = 0;
8 | audio.volume = 0.5;
9 | audio.loop = loop;
10 | audio.play();
11 |
12 | return audio;
13 | };
14 |
15 | export const createAudioAnalyser = () => {
16 | const mediaElement = document.getElementById("media") as HTMLMediaElement;
17 | const listener = new AudioListener();
18 | const audio = new ThreeAudio(listener);
19 |
20 | audio.setMediaElementSource(mediaElement);
21 |
22 | return new AudioAnalyser(audio, 2048);
23 | };
24 |
--------------------------------------------------------------------------------
/src/utils/end.ts:
--------------------------------------------------------------------------------
1 | import voiceJson from "@/json/voice.json";
2 |
3 | export const getVoiceFilenames = (name: String) => {
4 | const { translation_table, vowels, voice_file_list } = voiceJson;
5 | // convert name from katakana to romaji
6 | Object.keys(translation_table).forEach((translated) => {
7 | name = name.replaceAll(
8 | translation_table[translated as keyof typeof translation_table],
9 | translated
10 | );
11 | });
12 |
13 | let currentVowel = "";
14 | let filesToPlay = [];
15 | let currentSyllable = "";
16 |
17 | for (let i = 0; i < name.length; ) {
18 | let filename = "";
19 | // current character is a vowel
20 | if (vowels.includes(name[i])) {
21 | // long vowel
22 | if (name[i] === "-") {
23 | filename = `${currentVowel}_${currentVowel}`;
24 | }
25 | // two vowels in a row
26 | else if (currentVowel !== "") {
27 | filename = `${currentVowel}_${name[i]}`;
28 | // if double vowel sound does not exist fall back to single vowel
29 | if (!voice_file_list.includes(filename)) filename = `${name[i]}`;
30 | }
31 | // single vowel
32 | else filename = `${name[i]}`;
33 | currentVowel = name[i];
34 | i += 1;
35 | } else {
36 | // 2 character long syllable by default
37 | currentSyllable = name.slice(i, i + 2);
38 |
39 | if (currentSyllable[1] === "Y" || currentSyllable[1] === "H") {
40 | // 3 character long syllable
41 | currentSyllable = name.slice(i, i + 3);
42 | i += 1;
43 | }
44 |
45 | i += 2;
46 |
47 | if (currentVowel === "") filename = `${currentSyllable}`;
48 | else {
49 | filename = `${currentVowel}_${currentSyllable}`;
50 |
51 | if (!voice_file_list.includes(filename))
52 | filename = `${currentSyllable}`;
53 | }
54 |
55 | if (currentSyllable[1] === "Y" || currentSyllable[1] === "H")
56 | currentVowel = currentSyllable[2];
57 | else currentVowel = currentSyllable[1];
58 | }
59 |
60 | // convert filename to katakana
61 | filesToPlay.push(
62 | filename
63 | .split("_")
64 | .map((c) => translation_table[c as keyof typeof translation_table])
65 | .join("_")
66 | );
67 | }
68 | return filesToPlay;
69 | };
70 |
--------------------------------------------------------------------------------
/src/utils/idle.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GameSite,
3 | GameProgress,
4 | LainAnimation,
5 | NodeID,
6 | IdleSceneData,
7 | } from "@/types";
8 |
9 | import { getNode } from "./node";
10 | import { randomFrom } from "./random";
11 |
12 | export const getRandomIdle = (
13 | site: GameSite,
14 | gameProgress: GameProgress
15 | ): IdleSceneData => {
16 | const audioNodes: { a: NodeID[]; b: NodeID[] } = {
17 | a: [
18 | "0000a",
19 | "0001a",
20 | "0002a",
21 | "0003a",
22 | "0004a",
23 | "0005a",
24 | "0006a",
25 | "0007a",
26 | "0008a",
27 | "0009a",
28 | ],
29 | b: ["1015b", "1219b", "0419b", "0500b", "0501b", "0508b", "0510b", "0513b"],
30 | };
31 |
32 | const videoFiles = {
33 | a: [
34 | "INS01.STR[0]",
35 | "INS02.STR[0]",
36 | "INS03.STR[0]",
37 | "INS04.STR[0]",
38 | "INS05.STR[0]",
39 | "INS06.STR[0]",
40 | "INS07.STR[0]",
41 | "INS08.STR[0]",
42 | "INS09.STR[0]",
43 | "INS10.STR[0]",
44 | "INS11.STR[0]",
45 | "INS12.STR[0]",
46 | "INS13.STR[0]",
47 | "INS14.STR[0]",
48 | "INS15.STR[0]",
49 | "INS16.STR[0]",
50 | "INS17.STR[0]",
51 | "INS18.STR[0]",
52 | "INS19.STR[0]",
53 | "INS20.STR[0]",
54 | "INS21.STR[0]",
55 | "INS22.STR[0]",
56 | ],
57 | b: [
58 | "INS16.STR[0]",
59 | "INS17.STR[0]",
60 | "INS18.STR[0]",
61 | "INS19.STR[0]",
62 | "INS20.STR[0]",
63 | "INS21.STR[0]",
64 | "INS22.STR[0]",
65 | ],
66 | };
67 |
68 | if (Math.random() < 0.5) {
69 | // audio
70 | switch (site) {
71 | case GameSite.A: {
72 | const node = getNode(randomFrom(audioNodes.a));
73 | return {
74 | nodeName: node.name,
75 | mediaFile: node.media_file,
76 | imageTableIndices: node.image_table_indices,
77 | };
78 | }
79 | case GameSite.B: {
80 | const node = getNode(randomFrom(audioNodes.b));
81 | return {
82 | nodeName: node.name,
83 | mediaFile: node.media_file,
84 | imageTableIndices: node.image_table_indices,
85 | };
86 | }
87 | }
88 | } else {
89 | // video
90 | const polytanProgress = gameProgress.polytan_unlocked_parts;
91 | const isPolytanFullyUnlocked = Object.values(polytanProgress).every(
92 | (v) => v
93 | );
94 | if (site === GameSite.B && isPolytanFullyUnlocked && Math.random() < 0.3) {
95 | return {
96 | nodeName: "",
97 | mediaFile: randomFrom(["PO1.STR[0]", "PO2.STR[0]"]),
98 | };
99 | } else {
100 | switch (site) {
101 | case GameSite.A: {
102 | return { nodeName: "", mediaFile: randomFrom(videoFiles.a) };
103 | }
104 | case GameSite.B: {
105 | return { nodeName: "", mediaFile: randomFrom(videoFiles.b) };
106 | }
107 | }
108 | }
109 | }
110 | };
111 |
112 | export const getRandomIdleLainAnim = (): [LainAnimation, number] => {
113 | const moves: [LainAnimation, number][] = [
114 | [LainAnimation.Prayer, 3500],
115 | [LainAnimation.TouchSleeve, 3000],
116 | [LainAnimation.Thinking, 3900],
117 | [LainAnimation.Stretch2, 3900],
118 | [LainAnimation.Stretch, 3000],
119 | [LainAnimation.Spin, 3000],
120 | [LainAnimation.ScratchHead, 3900],
121 | [LainAnimation.Blush, 3000],
122 | [LainAnimation.HandsBehindHead, 2300],
123 | [LainAnimation.HandsOnHips, 3000],
124 | [LainAnimation.HandsOnHips2, 3900],
125 | [LainAnimation.HandsTogether, 2500],
126 | [LainAnimation.LeanForward, 2700],
127 | [LainAnimation.LeanLeft, 2700],
128 | [LainAnimation.LeanRight, 3500],
129 | [LainAnimation.LookAround, 3000],
130 | [LainAnimation.PlayWithHair, 2900],
131 | ];
132 |
133 | return randomFrom(moves);
134 | };
135 |
--------------------------------------------------------------------------------
/src/utils/lain.ts:
--------------------------------------------------------------------------------
1 | import { Direction, LainAnimation } from "@/types";
2 |
3 | export const getAnimationForDirection = (direction: Direction) => {
4 | switch (direction) {
5 | case Direction.Up:
6 | return LainAnimation.JumpUp;
7 | case Direction.Down:
8 | return LainAnimation.JumpDown;
9 | case Direction.Left:
10 | return LainAnimation.MoveLeft;
11 | case Direction.Right:
12 | return LainAnimation.MoveRight;
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/lcg.ts:
--------------------------------------------------------------------------------
1 | const LCG = (a: number, c: number, m: number, s: number) => () =>
2 | (s = (s * a + c) % m);
3 |
4 | export default LCG;
5 |
--------------------------------------------------------------------------------
/src/utils/log.ts:
--------------------------------------------------------------------------------
1 | export const logError = (ctx: any, msg: string) => {
2 | console.log(`${msg}`);
3 | console.log(ctx);
4 | };
5 |
--------------------------------------------------------------------------------
/src/utils/media.ts:
--------------------------------------------------------------------------------
1 | export const playMediaElement = () => {
2 | const mediaElement = document.getElementById("media") as HTMLMediaElement;
3 |
4 | if (mediaElement && mediaElement.paused) mediaElement.play();
5 | };
6 |
7 | export const resetMediaElement = () => {
8 | const mediaElement = document.getElementById("media") as HTMLMediaElement;
9 | if (mediaElement) {
10 | mediaElement.pause();
11 |
12 | mediaElement.currentTime = 0;
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/random.ts:
--------------------------------------------------------------------------------
1 | export const randomFrom = (arr: T[]) => {
2 | return arr[Math.floor(Math.random() * arr.length)];
3 | };
4 |
5 | export const randomBetween = (min: number, max: number) => {
6 | return Math.random() * (max - min) + min;
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/range.ts:
--------------------------------------------------------------------------------
1 | const range = (start: number, end: number) => {
2 | return Array.from(new Array(end - start), (_, i) => i + start);
3 | };
4 |
5 | export default range;
6 |
--------------------------------------------------------------------------------
/src/utils/site.ts:
--------------------------------------------------------------------------------
1 | import { GameSite, Direction, SiteLayout } from "@/types";
2 | import siteBLayoutJson from "@/json/site_b_layout.json";
3 | import siteALayoutJson from "@/json/site_a_layout.json";
4 |
5 | // if prev rotation value is provided it will return a value based on the nearest coterminal angle.
6 | // we do this because when rotation "resets" (i.e. goes from previous value to 0)
7 | // spring (the library used to animate stuff) will animate the full rotation
8 | // which will make it go back AROUND the site, instead of only 45 degrees
9 | // so we use coterminal angles to substitute the original rotation.
10 |
11 | // for example when at 315 and rotating towards 360, the original calculation would return
12 | // 0, which would rotate the site backwards for the entire -315 degrees
13 | // whereas we only needed it to rotate another 45 (till 360).
14 |
15 | const FULL_CIRCLE = Math.PI * 2;
16 |
17 | export const getRotationForSegment = (
18 | segment: number,
19 | prevRotation?: number
20 | ) => {
21 | const rotation = (Math.PI / 4) * (segment - 6);
22 |
23 | if (prevRotation !== undefined) {
24 | // circle count
25 | const cc = Math.floor(prevRotation / FULL_CIRCLE);
26 |
27 | // original angle
28 | const o = rotation + cc * FULL_CIRCLE;
29 | // positive coterminal angle
30 | const pc = rotation + FULL_CIRCLE + cc * FULL_CIRCLE;
31 | // negative coterminal
32 | const nc = rotation - FULL_CIRCLE - cc * FULL_CIRCLE;
33 |
34 | // distance from originally calculated value
35 | const od = Math.abs(o - prevRotation);
36 | // distance from positive coterminal
37 | const pcd = Math.abs(pc - prevRotation);
38 | // distance from negative
39 | const ncd = Math.abs(nc - prevRotation);
40 |
41 | switch (Math.min(od, pcd, ncd)) {
42 | case od:
43 | return o;
44 | case pcd:
45 | return pc;
46 | case ncd:
47 | return nc;
48 | }
49 | }
50 |
51 | return rotation;
52 | };
53 |
54 | export const getLevelY = (level: number) => {
55 | return -4.5 + level * 1.5;
56 | };
57 |
58 | export const getLevelLimit = (site: GameSite) => {
59 | switch (site) {
60 | case GameSite.A:
61 | return 22;
62 | case GameSite.B:
63 | return 13;
64 | }
65 | };
66 |
67 | export const getLevelDigits = (level: number): [number, number] => {
68 | return [Math.floor(level / 10), level % 10];
69 | };
70 |
71 | export const getChangeDirection = (
72 | segment: number,
73 | prevSegment: number
74 | ): Direction.Right | Direction.Left => {
75 | if (prevSegment === 7 && segment === 0) {
76 | return Direction.Left;
77 | }
78 |
79 | if (prevSegment === 0 && segment === 7) {
80 | return Direction.Right;
81 | }
82 |
83 | if (prevSegment < segment) {
84 | return Direction.Left;
85 | } else {
86 | return Direction.Right;
87 | }
88 | };
89 |
90 | export const getLayout = (site: GameSite): SiteLayout => {
91 | switch (site) {
92 | case GameSite.A:
93 | return siteALayoutJson as SiteLayout;
94 | case GameSite.B:
95 | return siteBLayoutJson as SiteLayout;
96 | }
97 | };
98 |
99 |
--------------------------------------------------------------------------------
/src/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
2 |
3 | export default sleep;
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["src/*"],
7 | "@canvas/*": ["src/components/canvas/*"],
8 | },
9 | "lib": ["dom", "dom.iterable", "esnext"],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "strict": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "noEmit": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "downlevelIteration": true,
21 | "jsx": "preserve",
22 | "incremental": true
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------