mirror of
https://github.com/ad044/lainTSX.git
synced 2024-10-22 15:09:05 +00:00
added main page and guide, refactored some functions
This commit is contained in:
parent
a2ebca2e53
commit
862f2ed0e8
21 changed files with 506 additions and 72 deletions
83
package-lock.json
generated
83
package-lock.json
generated
|
@ -2109,6 +2109,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz",
|
||||
"integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA=="
|
||||
},
|
||||
"@tokenizer/token": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz",
|
||||
"integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w=="
|
||||
},
|
||||
"@types/anymatch": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
||||
|
@ -2156,6 +2161,11 @@
|
|||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/debug": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
|
||||
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
|
||||
},
|
||||
"@types/eslint": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz",
|
||||
|
@ -2307,6 +2317,15 @@
|
|||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"@types/readable-stream": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.9.tgz",
|
||||
"integrity": "sha512-sqsgQqFT7HmQz/V5jH1O0fvQQnXAJO46Gg9LRO/JPfjmVmGUlcx831TZZO3Y3HtWhIkzf3kTsNT0Z0kzIhIvZw==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"safe-buffer": "*"
|
||||
}
|
||||
},
|
||||
"@types/resolve": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
|
||||
|
@ -6595,6 +6614,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"file-type": {
|
||||
"version": "16.3.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.3.0.tgz",
|
||||
"integrity": "sha512-ZA0hV64611vJT42ltw0T9IDwHApQuxRdrmQZWTeDmeAUtZBBVSQW3nSQqhhW1cAgpXgqcJvm410BYHXJQ9AymA==",
|
||||
"requires": {
|
||||
"readable-web-to-node-stream": "^3.0.0",
|
||||
"strtok3": "^6.0.3",
|
||||
"token-types": "^2.0.0",
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
|
@ -9963,6 +9993,26 @@
|
|||
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
|
||||
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
|
||||
},
|
||||
"music-metadata": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.8.1.tgz",
|
||||
"integrity": "sha512-Q4PhR788jp2VDh/JgPvEUZ3NCqxgW4+hV0H2XrYKfGwq5aGagm1ek9kbCJvUCvejVnmyn6Rb+ggIPxrIVPfzww==",
|
||||
"requires": {
|
||||
"content-type": "^1.0.4",
|
||||
"debug": "^4.3.1",
|
||||
"file-type": "^16.2.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"strtok3": "^6.0.8",
|
||||
"token-types": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
|
@ -10679,6 +10729,11 @@
|
|||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"peek-readable": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.3.tgz",
|
||||
"integrity": "sha512-mpAcysyRJxmICBcBa5IXH7SZPvWkcghm6Fk8RekoS3v+BpbSzlZzuWbMx+GXrlUwESi9qHar4nVEZNMKylIHvg=="
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
|
@ -12571,6 +12626,15 @@
|
|||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"readable-web-to-node-stream": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.1.tgz",
|
||||
"integrity": "sha512-4zDC6CvjUyusN7V0QLsXVB7pJCD9+vtrM9bYDRv6uBQ+SKfx36rp5AFNPRgh9auKRul/a1iFZJYXcCbwRL+SaA==",
|
||||
"requires": {
|
||||
"@types/readable-stream": "^2.3.9",
|
||||
"readable-stream": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
|
||||
|
@ -14406,6 +14470,16 @@
|
|||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
|
||||
},
|
||||
"strtok3": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.0.8.tgz",
|
||||
"integrity": "sha512-QLgv+oiXwXgCgp2PdPPa+Jpp4D9imK9e/0BsyfeFMr6QL6wMVqoVn9+OXQ9I7MZbmUzN6lmitTJ09uwS2OmGcw==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.1.1",
|
||||
"@types/debug": "^4.1.5",
|
||||
"peek-readable": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"style-loader": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz",
|
||||
|
@ -14859,6 +14933,15 @@
|
|||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"token-types": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-2.1.1.tgz",
|
||||
"integrity": "sha512-wnQcqlreS6VjthyHO3Y/kpK/emflxDBNhlNUPfh7wE39KnuDdOituXomIbyI79vBtF0Ninpkh72mcuRHo+RG3Q==",
|
||||
"requires": {
|
||||
"@tokenizer/token": "^0.1.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/three": "^0.126.0",
|
||||
"music-metadata": "^7.8.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
|
|
33
scripts/extract/convert_sfx.mjs
Normal file
33
scripts/extract/convert_sfx.mjs
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { readdirSync } from "fs";
|
||||
import { spawnSync } from "child_process";
|
||||
import { join } from "path";
|
||||
import * as mm from "music-metadata";
|
||||
|
||||
// stub implementation of upping the pitch of the sfx, still wip
|
||||
export async function convert_sfx() {
|
||||
let i = 0;
|
||||
const dir = join("..", "..", "src", "static", "sfx");
|
||||
for (let file of readdirSync(dir)) {
|
||||
if (file.includes("snd")) {
|
||||
console.log(file);
|
||||
const metaData = await mm.parseFile(`${join(dir, file)}`);
|
||||
|
||||
const sampleRate = metaData.format.sampleRate;
|
||||
|
||||
spawnSync(
|
||||
"ffmpeg",
|
||||
[
|
||||
"-i",
|
||||
join(dir, file),
|
||||
"-filter_complex",
|
||||
`asetrate=${sampleRate}*2^(6/12),atempo=1/2^(6/12)`,
|
||||
"-n",
|
||||
join(dir, "..", "t", file),
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convert_sfx();
|
|
@ -3,13 +3,17 @@ import "./static/css/page.css";
|
|||
import Game from "./dom-components/Game";
|
||||
import { HashRouter, Route, Switch } from "react-router-dom";
|
||||
import Notes from "./dom-components/Notes";
|
||||
import MainPage from "./dom-components/MainPage";
|
||||
import Guide from "./dom-components/Guide";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<HashRouter>
|
||||
<Switch>
|
||||
<Route path={"/"} exact component={Notes} />
|
||||
<Route path={"/"} exact component={MainPage} />
|
||||
<Route path={"/notes"} exact component={Notes} />
|
||||
<Route path={"/game"} exact component={Game} />
|
||||
<Route path={"/guide"} exact component={Guide} />
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useFrame } from "react-three-fiber";
|
||||
import { playAudio, useStore } from "../store";
|
||||
import { useStore } from "../store";
|
||||
import playAudio from "../utils/playAudio";
|
||||
import * as audio from "../static/sfx";
|
||||
import {
|
||||
playIdleAudio,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { playAudio, useStore } from "../store";
|
||||
import { useStore } from "../store";
|
||||
import playAudio from "../utils/playAudio";
|
||||
import sleep from "../utils/sleep";
|
||||
import { GameEvent } from "../types/types";
|
||||
|
||||
|
|
|
@ -130,14 +130,6 @@ const handleMainSceneInput = (
|
|||
|
||||
if (!nodeData) return resetInputCooldown;
|
||||
|
||||
const lainMoveAnimation = `move_${direction}`;
|
||||
const newSiteRot = [
|
||||
0,
|
||||
direction === "left"
|
||||
? siteRotY + Math.PI / 4
|
||||
: siteRotY - Math.PI / 4,
|
||||
0,
|
||||
];
|
||||
const newNode = {
|
||||
...(nodeData.node !== "unknown"
|
||||
? getNodeById(nodeData.node, activeSite)
|
||||
|
@ -147,6 +139,16 @@ const handleMainSceneInput = (
|
|||
|
||||
if (nodeData.didMove) {
|
||||
if (!canLainMove) return resetInputCooldown;
|
||||
|
||||
const lainMoveAnimation = `move_${direction}`;
|
||||
const newSiteRot = [
|
||||
0,
|
||||
direction === "left"
|
||||
? siteRotY + Math.PI / 4
|
||||
: siteRotY - Math.PI / 4,
|
||||
0,
|
||||
];
|
||||
|
||||
return siteMoveHorizontal({
|
||||
lainMoveAnimation: lainMoveAnimation,
|
||||
siteRot: newSiteRot,
|
||||
|
@ -169,11 +171,6 @@ const handleMainSceneInput = (
|
|||
|
||||
if (!nodeData) return resetInputCooldown;
|
||||
|
||||
const lainMoveAnimation = `jump_${direction}`;
|
||||
const newLevel = (direction === "up" ? level + 1 : level - 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
|
||||
const newNode = {
|
||||
...(nodeData.node !== "unknown"
|
||||
? getNodeById(nodeData.node, activeSite)
|
||||
|
@ -183,6 +180,12 @@ const handleMainSceneInput = (
|
|||
|
||||
if (nodeData.didMove) {
|
||||
if (!canLainMove) return resetInputCooldown;
|
||||
|
||||
const lainMoveAnimation = `jump_${direction}`;
|
||||
const newLevel = (direction === "up" ? level + 1 : level - 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
|
||||
return siteMoveVertical({
|
||||
lainMoveAnimation: lainMoveAnimation,
|
||||
activeLevel: newLevel,
|
||||
|
|
16
src/dom-components/Credit.tsx
Normal file
16
src/dom-components/Credit.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
|
||||
type CreditProps = {
|
||||
name: string;
|
||||
credit: string;
|
||||
};
|
||||
|
||||
const Credit = (props: CreditProps) => (
|
||||
<>
|
||||
<span className="cool-text">{props.name}</span> - {props.credit}
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
|
||||
export default Credit;
|
|
@ -20,6 +20,7 @@ import { Canvas } from "react-three-fiber";
|
|||
import Preloader from "../components/Preloader";
|
||||
import InputHandler from "../components/InputHandler";
|
||||
import MediaPlayer from "../components/MediaPlayer";
|
||||
import Header from "./Header";
|
||||
|
||||
const Game = () => {
|
||||
const currentScene = useStore((state) => state.currentScene);
|
||||
|
@ -68,29 +69,40 @@ const Game = () => {
|
|||
};
|
||||
}, [handleGameResize]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflowY = "hidden";
|
||||
|
||||
return () => {
|
||||
document.body.style.overflowY = "visible";
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="game"
|
||||
style={{ width: Math.round(width), height: Math.round(height) }}
|
||||
>
|
||||
<Canvas
|
||||
concurrent
|
||||
gl={{ antialias: false }}
|
||||
pixelRatio={window.devicePixelRatio}
|
||||
className="main-canvas"
|
||||
<>
|
||||
<Header />
|
||||
<div
|
||||
className="game"
|
||||
style={{ width: Math.round(width), height: Math.round(height) }}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<Preloader />
|
||||
{dispatchScene[currentScene as keyof typeof dispatchScene]}
|
||||
<InputHandler />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
{["media", "idle_media", "tak", "end"].includes(currentScene) && (
|
||||
<div style={{ marginTop: -height }}>
|
||||
<MediaPlayer />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Canvas
|
||||
concurrent
|
||||
gl={{ antialias: false }}
|
||||
pixelRatio={window.devicePixelRatio}
|
||||
className="main-canvas"
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<Preloader />
|
||||
{dispatchScene[currentScene as keyof typeof dispatchScene]}
|
||||
<InputHandler />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
{["media", "idle_media", "tak", "end"].includes(currentScene) && (
|
||||
<div style={{ marginTop: -height }}>
|
||||
<MediaPlayer />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
100
src/dom-components/Guide.tsx
Normal file
100
src/dom-components/Guide.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import React from "react";
|
||||
import Header from "./Header";
|
||||
import "../static/css/guide.css";
|
||||
|
||||
const Guide = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<p className="guide-paragraph">
|
||||
<br /> A bit of a disclaimer - as stated on the main page, these are
|
||||
entirely my thoughts from observations while developing the game, some
|
||||
of the information may be innacurate. <br /> <br />
|
||||
First, let's get this out of the way - Serial Experiments Lain PSX isn't
|
||||
a "game" in a traditional sense, it's more like a visual novel which you
|
||||
piece together yourself. The story revolves around Lain and her
|
||||
interactions with her psychiatrist - Touko.
|
||||
<br /> <br />A common misconception about the game is that there's no
|
||||
specific order, and that you just randomly watch stuff and come up with
|
||||
an explanation yourself. From what I've noticed, this is not entirely
|
||||
true. Let me explain:
|
||||
<br />
|
||||
<br />
|
||||
The blue orbs that you navigate through (we'll call those blue orbs
|
||||
"nodes" from now on) contain either:
|
||||
<br />
|
||||
<br />
|
||||
A. Media (audio/video)
|
||||
<br />
|
||||
B. Collectibles
|
||||
<br />
|
||||
C. Upgrades
|
||||
<br />
|
||||
<br />
|
||||
There are also multiple "types" of nodes. You can tell them apart by
|
||||
their names and icons.
|
||||
<br />
|
||||
Here's a basic list of these nodes according to their names in their
|
||||
respective categories:
|
||||
<br />
|
||||
<br />A category (Media) - Tda, Lda, Dia, Cou, TaK, Dc
|
||||
<br />B category (Collectibles) - P2
|
||||
<br />C category (Upgrades) - SSkn, GaTE
|
||||
<br /> <br />
|
||||
Let's step through each of these one by one:
|
||||
<br />
|
||||
<br />
|
||||
Category A - Media:
|
||||
<br /> Tda - Touko's diary - Touko's personal thoughts.
|
||||
<br />
|
||||
Lda - Lain's diary - Lain's personal thoughts.
|
||||
<br />
|
||||
Dia - Diagnosis - Lain's diagnosis after their interactions, provided by
|
||||
Touko.
|
||||
<br />
|
||||
Cou - Counseling - Lain and Touko's interactions.
|
||||
<br />
|
||||
TaK - Not sure what TaK stands for - Random quotes by Lain.
|
||||
<br />
|
||||
Dc - Videos.
|
||||
<br />
|
||||
<br />
|
||||
This is where the no-real-order issue comes into play - each of these
|
||||
separate types are put in chronological order, meaning After Lda001
|
||||
comes Lda002, then Lda003, etc. This is identical for every other node
|
||||
type I mentioned. <br /> <br />
|
||||
What might lead some people to believe that there is no real order is
|
||||
that the way you UNLOCK them may not be entirely chronological, for
|
||||
example, the first node you'll most likely interact with from Tda is
|
||||
Tda028, since that's the closest to where you start from at the
|
||||
beginning of the game.
|
||||
<br /> <br />
|
||||
There's also another issue - despite these separate types being put in a
|
||||
specific order, it is unclear how they interact with each other. For
|
||||
example, there is no way (to my knowledge) to tell which Cou comes after
|
||||
which Lda judging by their names alone. What might help here are the
|
||||
"words" you select while you play them (on the right hand side there are
|
||||
3 floating things on each audio node which you can select).
|
||||
<br /> <br />
|
||||
Category B - Collectibles: <br />
|
||||
P2 - Polytan - You collect parts of Lain's bear, after you collect all
|
||||
the pieces, 2 extra idle media files become available.
|
||||
<br /> <br />
|
||||
Category C - Upgrades:
|
||||
<br />
|
||||
SSkn - Not sure what SSkn stands for - The main upgrade inside the game.
|
||||
Some nodes have an "upgrade requirement" that you need to meet to be
|
||||
able to view them, the way you do that is by collecting these. So, the
|
||||
next time you see Lain try to knock on a node and fall over, know that
|
||||
you need to collect more SSkn nodes.
|
||||
<br />
|
||||
GaTE - Gate (I guess) - A "gate pass" as the game calls it - after
|
||||
collecting all of them, you unlock Site B, which contains
|
||||
more nodes and is the place where you continue the story.
|
||||
<br /> <br />
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Guide;
|
17
src/dom-components/Header.tsx
Normal file
17
src/dom-components/Header.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import "../static/css/header.css";
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<div className="header">
|
||||
<Link to="/">main</Link>
|
||||
<Link to="/notes">notes</Link>
|
||||
<Link to="/game">start</Link>
|
||||
<Link to="/guide">guide</Link>
|
||||
<a href="https://discord.com/invite/W22Ga2R">discord</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
116
src/dom-components/MainPage.tsx
Normal file
116
src/dom-components/MainPage.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import "../static/css/mainpage.css";
|
||||
import Credit from "./Credit";
|
||||
import Header from "./Header";
|
||||
import QA from "./QA";
|
||||
|
||||
const MainPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<p className="header-paragraph">
|
||||
This is a web reimplementation of the Serial Experiments Lain PSX game
|
||||
with the aim to provide multi-language support.
|
||||
<br />
|
||||
Please make sure to read the <Link to="/notes">notes</Link> before you
|
||||
start playing.
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
<p className="faq">
|
||||
FAQ:
|
||||
<br /> <br />
|
||||
<QA
|
||||
question={"I'm confused about the game"}
|
||||
answer={"Amazing! That means the game is working properly."}
|
||||
/>
|
||||
<QA
|
||||
question={
|
||||
"I'm extremely confused about the game and I'm not sure what I'm doing"
|
||||
}
|
||||
answer={`Read the <a href="/#/guide">guide</a>. Keep in mind though that this is only my interpretation of the game and what I pieced together while developing it, it could be wrong.`}
|
||||
/>
|
||||
<QA
|
||||
question={"Source code?"}
|
||||
answer={`<a href="https://github.com/ad044/lain-psx-ts">On my github.</a>`}
|
||||
/>
|
||||
<QA
|
||||
question={"I found an issue/have a suggestion/etc."}
|
||||
answer={`Please join our <a href="https://discord.com/invite/W22Ga2R">discord server</a> and tell us about it!`}
|
||||
/>
|
||||
</p>
|
||||
<h2 className="mainpage-header">credits</h2>
|
||||
<p className="credits">
|
||||
<Credit
|
||||
name={"m35"}
|
||||
credit={"Created jPSXdec/laintools, reverse engineering."}
|
||||
/>
|
||||
<Credit name={"ad"} credit={"Main dev/project lead."} />
|
||||
<Credit
|
||||
name={"elliotcraft79"}
|
||||
credit={
|
||||
"Programming help, reverse engineering, voice/sound effect extraction, extraction automation script, subtitle timing."
|
||||
}
|
||||
/>
|
||||
<Credit
|
||||
name={"Yukkuri"}
|
||||
credit={
|
||||
"Programming/GLSL help, asset extraction, reverse engineering."
|
||||
}
|
||||
/>
|
||||
<Credit
|
||||
name={"spaztron64"}
|
||||
credit={"Reverse engineering, sound extraction."}
|
||||
/>
|
||||
<Credit name={"Popcorn"} credit={"Programming help."} />
|
||||
<Credit
|
||||
name={"lelenium"}
|
||||
credit={"Helped with literally everything."}
|
||||
/>
|
||||
<Credit name={"Bunbuns"} credit={"Fonts, help with japanese."} />
|
||||
<Credit name={"Phenomenal"} credit={"Help with 3D stuff, fonts."} />
|
||||
<Credit name={"oo"} credit={"Help with japanese."} />
|
||||
<Credit name={"JToke"} credit={"Help with shaders."} />
|
||||
<Credit name={"retard"} credit={"Made 3D models."} />
|
||||
<Credit name={"knobluch"} credit={"Made 3D models."} />
|
||||
<Credit name={"ridderhoff"} credit={"Help with 3D stuff."} />
|
||||
<Credit
|
||||
name={"claire"}
|
||||
credit={"Helped with japanese and Lain's voice."}
|
||||
/>
|
||||
<Credit name={"Lorenzo"} credit={"Majority of the subtitle timing."} />
|
||||
<Credit
|
||||
name={"mutronics"}
|
||||
credit={"Subtitle timing, translation, owner of laingame."}
|
||||
/>
|
||||
<Credit name={"Shuji"} credit={"Translation."} />
|
||||
<Credit
|
||||
name={"Magikarp"}
|
||||
credit={"Provided initial models for the rings."}
|
||||
/>
|
||||
<Credit
|
||||
name={
|
||||
"mutronics, lelenium, Lorenzo, elliotcraft79, CosmicKiwii, Mikix, shemishtameshel, espilya, Yokuba, oo, Shuji, Bunbuns, claire, Eternofímero, Cal, Cena"
|
||||
}
|
||||
credit={"Subtitle timing team."}
|
||||
/>{" "}
|
||||
<Credit
|
||||
name={"psx.lain.pl team"}
|
||||
credit={"providing the base translation."}
|
||||
/>{" "}
|
||||
<Credit
|
||||
name={"INITIATE"}
|
||||
credit={"helping the project gain recognition initially."}
|
||||
/>{" "}
|
||||
Special thanks to
|
||||
<a href="https://twitter.com/pmndrs"> Poimandres</a> for answering all
|
||||
the dumb questions I had while programming and creating the amazing
|
||||
libraries used in this project.
|
||||
</p>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPage;
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect } from "react";
|
||||
import "../static/css/notes.css";
|
||||
import { Link } from "react-router-dom";
|
||||
import Header from "./Header";
|
||||
|
||||
const Notes = () => {
|
||||
useEffect(() => {
|
||||
|
@ -9,6 +10,7 @@ const Notes = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<table className="main-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -23,6 +25,25 @@ const Notes = () => {
|
|||
This is especially true if you're using a bad setup, and even
|
||||
more true if you're using Linux on a bad setup, since Firefox's
|
||||
WebGL implementation on it has had issues for a while now.
|
||||
<br />
|
||||
<br />
|
||||
If it's your first time playing the game, the first time loading
|
||||
it might take a while depending on the factors mentioned above.
|
||||
If you're seeing a black screen for a bit, just wait it out.
|
||||
Subsequent website visits will be much faster once the browser
|
||||
caches all the assets.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Sounds</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
Browsers require user permission to autoplay audio. If you're
|
||||
not hearing any sound effects, just click somewhere around the
|
||||
page.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -57,19 +78,17 @@ const Notes = () => {
|
|||
<p>Browser Settings</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
<span className="text-center">Firefox</span>
|
||||
<div className="browser-notes">
|
||||
privacy.resistFingerprinting should be set to false (it should
|
||||
be by default). Otherwise, it limits the maximum WebGL texture
|
||||
size to 2048, resulting in poor sprite quality.
|
||||
<br />
|
||||
<br />
|
||||
Picture-In-Picture functionality should not be used (you most
|
||||
likely have it disabled already). Just having it enabled won't
|
||||
break anything, but actually using it might lead to some funny
|
||||
visual bugs with media files.
|
||||
</div>
|
||||
<span className="text-center">Firefox</span>
|
||||
<p className="browser-notes">
|
||||
privacy.resistFingerprinting should be set to false (it should
|
||||
be by default). Otherwise, it limits the maximum WebGL texture
|
||||
size to 2048, resulting in poor sprite quality.
|
||||
<br />
|
||||
<br />
|
||||
Picture-In-Picture functionality should not be used (you most
|
||||
likely have it disabled already). Just having it enabled won't
|
||||
break anything, but actually using it might lead to some funny
|
||||
visual bugs with media files.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
21
src/dom-components/QA.tsx
Normal file
21
src/dom-components/QA.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
|
||||
type QAProps = {
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
const QA = (props: QAProps) => (
|
||||
<>
|
||||
Q:{" "}
|
||||
<span
|
||||
className="cool-text"
|
||||
dangerouslySetInnerHTML={{ __html: props.question }}
|
||||
></span>
|
||||
<br />
|
||||
A: <span dangerouslySetInnerHTML={{ __html: props.answer }}></span>
|
||||
<br /> <br />
|
||||
</>
|
||||
);
|
||||
|
||||
export default QA;
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useFrame } from "react-three-fiber";
|
||||
import { createAudioAnalyser, useStore } from "../store";
|
||||
import { useStore } from "../store";
|
||||
import createAudioAnalyser from "../utils/createAudioAnalyser";
|
||||
import EndSelectionScreen from "../components/EndScene/EndSelectionScreen";
|
||||
import introSpeech from "../static/media/audio/LAIN21.XA[31].mp4";
|
||||
import outroSpeech from "../static/media/audio/LAIN21.XA[16].mp4";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { playAudio, useStore } from "../store";
|
||||
import { useStore } from "../store";
|
||||
import playAudio from "../utils/playAudio";
|
||||
import LevelSelection from "../components/MainScene/LevelSelection";
|
||||
import HUD from "../components/MainScene/HUD";
|
||||
import MainYellowTextAnimator from "../components/TextRenderer/MainYellowTextAnimator";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createAudioAnalyser, useStore } from "../store";
|
||||
import { useStore } from "../store";
|
||||
import createAudioAnalyser from "../utils/createAudioAnalyser";
|
||||
import LeftSide from "../components/MediaScene/Selectables/LeftSide";
|
||||
import RightSide from "../components/MediaScene/Selectables/RightSide";
|
||||
import AudioVisualizer from "../components/MediaScene/AudioVisualizer/AudioVisualizer";
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import LainSpeak from "../components/LainSpeak";
|
||||
import { createAudioAnalyser, useStore } from "../store";
|
||||
import { useStore } from "../store";
|
||||
import createAudioAnalyser from "../utils/createAudioAnalyser";
|
||||
|
||||
const TaKScene = () => {
|
||||
const setScene = useStore((state) => state.setScene);
|
||||
|
|
18
src/store.ts
18
src/store.ts
|
@ -1,6 +1,5 @@
|
|||
import create from "zustand";
|
||||
import { combine } from "zustand/middleware";
|
||||
import * as THREE from "three";
|
||||
import { AudioAnalyser } from "three";
|
||||
import game_progress from "./resources/initial_progress.json";
|
||||
import { getNodeById } from "./helpers/node-helpers";
|
||||
|
@ -445,23 +444,6 @@ export const getCurrentUserState = (): UserSaveState => {
|
|||
export const saveUserProgress = (state: UserSaveState) =>
|
||||
localStorage.setItem("lainSaveState", JSON.stringify(state));
|
||||
|
||||
export const playAudio = (audio: HTMLAudioElement) => {
|
||||
audio.currentTime = 0;
|
||||
audio.volume = 0.5;
|
||||
audio.loop = false;
|
||||
audio.play();
|
||||
};
|
||||
|
||||
export const createAudioAnalyser = () => {
|
||||
const mediaElement = document.getElementById("media") as HTMLMediaElement;
|
||||
const listener = new THREE.AudioListener();
|
||||
const audio = new THREE.Audio(listener);
|
||||
|
||||
audio.setMediaElementSource(mediaElement);
|
||||
|
||||
return new THREE.AudioAnalyser(audio, 2048);
|
||||
};
|
||||
|
||||
export const isPolytanFullyUnlocked = () => {
|
||||
const polytanProgress = useStore.getState().polytanUnlockedParts;
|
||||
|
||||
|
|
13
src/utils/createAudioAnalyser.ts
Normal file
13
src/utils/createAudioAnalyser.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as THREE from "three";
|
||||
|
||||
const createAudioAnalyser = () => {
|
||||
const mediaElement = document.getElementById("media") as HTMLMediaElement;
|
||||
const listener = new THREE.AudioListener();
|
||||
const audio = new THREE.Audio(listener);
|
||||
|
||||
audio.setMediaElementSource(mediaElement);
|
||||
|
||||
return new THREE.AudioAnalyser(audio, 2048);
|
||||
};
|
||||
|
||||
export default createAudioAnalyser;
|
8
src/utils/playAudio.ts
Normal file
8
src/utils/playAudio.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const playAudio = (audio: HTMLAudioElement) => {
|
||||
audio.currentTime = 0;
|
||||
audio.volume = 0.5;
|
||||
audio.loop = false;
|
||||
audio.play();
|
||||
};
|
||||
|
||||
export default playAudio;
|
Loading…
Reference in a new issue