Skip to content

Commit

Permalink
Merge pull request #6 from helloyork/develop
Browse files Browse the repository at this point in the history
narraleaf-react-0.0.1-beta.5
  • Loading branch information
helloyork authored Sep 14, 2024
2 parents e8e42c1 + b83a07d commit c1a79ce
Show file tree
Hide file tree
Showing 21 changed files with 505 additions and 47 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export default nextConfig;

## Documentation

- [Quick Start](./docs/quick-start.md)
- [Customization](./docs/customization.md)

in progress...

## License
Expand Down
97 changes: 97 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Customization

## Custom Elements Styles

There are two ways you can customize your elements styles:

### Add classes to the elements

You can add classes to the elements to customize the styles.

```tsx
"use client";

import {Game, GameProviders, Player} from "narraleaf-react";
import {useState} from "react";

export default function App() {
const [game] = useState<Game>(new Game({
elementStyles: {
say: {
// make sure you have tailwindcss installed to use these classes
container: "rounded-lg shadow-md p-4 bg-white",
textSpan: "text-lg text-black font-bold",

// or use your own class
// container: "my-say-container",
// textSpan: "my-say-text",
},
}
}));

return (
<GameProviders game={game}>
<Player />
</GameProviders>
);
}
```

### Use your own components (advanced)

You can use your own components to render the elements.
You can copy the default components from the [source code](/src/game/player/elements) and modify them.

For example, you can create a custom `say` component using [Say.tsx](/src/game/player/elements/say/Say.tsx)

```tsx
"use client";

import {Game, GameProviders, MenuElementProps, Isolated, Say} from "narraleaf-react";
import {useState} from "react";

function myMenuComponent(
{
prompt,
choices,
afterChoose,
}: Readonly<MenuElementProps>) {

const {game} = useGame();

function choose(choice: Choice) {
afterChoose(choice);
}

return (
<>
{/* menu prompt */}
<Isolated className={"absolute"}>
<div className="absolute w-full h-full">
{prompt && <Say action={{sentence: prompt}} useTypeEffect={false} className="z-10"/>}
</div>
</Isolated>

{/* menu choices */}
<Isolated className={"absolute"}>
<div className="absolute flex flex-col items-center justify-center w-full h-full">
{/* read more in examples */}
</div>
</Isolated>
</>
);
}

export default function App() {
function handlePlayerReady(game: Game) {
game.useComponent<"menu">(Game.ComponentTypes.say, myMenuComponent);
}

return (
<GameProviders>
<Player onReady={handlePlayerReady} />
</GameProviders>
);
}
```

238 changes: 238 additions & 0 deletions docs/quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Quick Start

## Installation

```bash
npm install narraleaf-react
```

## Basic Concepts

NarraLeaf-React is a React framework for creating NarraLeaf stories.
It is based on OOP and provides a set of classes (called "elements") to help you build your story.

The biggest difference between NarraLeaf-React and other visual novel engines is that NarraLeaf-React is a front-end
framework.
It doesn't use any rendering libraries, and **it does not care about the UI**.
So you can customize the UI as you like.

NarraLeaf use elements to represent the game objects, such as characters, images, sounds, and scripts.
The top-level element is the `Story`, which contains multiple `Scene`s.
Each `Scene` contains multiple elements, such as `Character`, `Image`, `Sound`, `Menu`, `Script`, `Condition`,
and `Control`.

Here is a table of the elements:

| Element | Description |
|-----------|----------------------------------------------------------------|
| Story | The top-level element that contains multiple scenes. |
| Scene | A scene that contains multiple **actions**. |
| Character | A character that can say something. |
| Image | An image that can be shown on the screen. |
| Sound | A sound that can be played. |
| Menu | A menu that contains multiple options. |
| Script | A script that can be executed. |
| Condition | A condition that can be used to control the flow of the story. |
| Control | A control that can be used to control the flow of the story. |

and this is a tree that shows the relationship between the elements:

```
Story
└── Scene
├── Character
│ └── Sentence
│ └── Word
├── Image
├── Sound
├── Menu
├── Script
│ └── Lambda (deprecated)
├── Condition
└── Control
Animation
├── Transform
└── Transition
```

Not to be confused, there are some difference between other engines and NarraLeaf:

- Image **only display the image**, character **only say the text**, they are separated.
- Scene represents "label" in Ren'Py or procedure in other engines. **It is not a scene in the traditional sense.** So
you should use `Scene` to represent a procedure, not a scene.
- Transform and Transition can be used on both Image and Scene.

## Set up Player

To set up a player, you have to place the `GameProvider` to the parent component of the player.

Here is an example:

```tsx
import {GameProvider, Player} from "narraleaf-react";

export default function Parent() {
return (
<GameProvider>
<Player/>
</GameProvider>
);
}
```

If you want to initialize the game manually, you can use the `useGame` hook:

```tsx
import {useGame} from "narraleaf-react";

export default function Child() {
// this have to be called in a child component of the `GameProvider`
const {game, setGame} = useGame();

useEffect(() => {
game.getLiveGame().newGame(); // create new game
}, []);

// or instantiate the game manually
useEffect(() => {
setGame(new Game({
elements: {
say: {
textSpeed: 100.
}
}
}));
}, []);

return <Player/>;
}

```

If you don't want to use the hook, you can also use the `onReady` callback of the `Player` component:

```tsx
import {Game} from "narraleaf-react";

export default function Child() {
const handleOnReady = (game: Game) => {
game.getLiveGame().newGame();
}

return (
<GameProvider>
<Player onReady={handleOnReady}/>
</GameProvider>
);
}
```

## Write Your First Story

### Create a React project

First, create a new React project:

```bash
npx create-react-app my-narraleaf-story
cd my-narraleaf-story
npm install narraleaf-react
```

### Write a simple story

Let's write a simple story that displays an image and says something.

You need to create a `src/story.ts` file _(or somewhere else you like)_:

```ts
// src/story.ts

// First, import the necessary classes
import {Story, Scene, Character, Image} from "narraleaf-react";

// Create a new story
// The name of the story is human-readable and is used for debugging purposes
const story = new Story("My First NarraLeaf Story");

// Create a new scene
// The name of the scene should be unique and is used for debugging purposes
const scene1 = new Scene("scene1_hello_world");

// then let's create a "character" with image
const character1 = new Character("me");
const character1Image = new Image({
src: "https://placehold.it/200x200",
});

// Add actions to the scene
scene1.action([
// Show the image for 1 second
character1Image.show({
duration: 1000,
}).toActions(),

// Say something
character1
.say("Hello, world!")
.say("This is my first NarraLeaf story.")
.say("Start editing this file and enjoy the journey!")
.toActions(),
]);

// Why we use "toActions()"?
// Because we can chain the actions together, and "toActions()" is used to end the chain.
// It is necessary to use "toActions()" at the end of each chain.
// Do not call "toActions()" at other places, it will confuse the framework.

// Add the scene to the story
story.entry(scene1);

export {story};
```

Then, you can use the `Player` component to play the story
You need to edit the `src/App.tsx` file: _(or page.tsx, App.jsx, page.jsx, etc.)_

Replace the content with the following code:

```tsx
// src/App.tsx

import {GameProvider, Player} from "narraleaf-react";
import {story} from "./story";

export default function App() {
return (
<GameProvider>
<Player
story={story}
width="100vw"
height="100vh"
onReady={(game: Game) => {
game.getLiveGame().newGame();
}}
/>
</GameProvider>
);
}
```

### Run the story

Now you can run the story:

```bash
npm dev
```

and open your browser to `http://localhost:3000`.

Congratulations! You have written your first NarraLeaf story!

For other actions, you can check the [Examples](./examples) folder.
You can use NarraLeaf-React to create more complex stories, such as adding sounds, menus, conditions, and animations.
That's so cool!

For customizing the UI, please check the [Customization](./customization.md) document.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "narraleaf-react",
"version": "0.0.1-beta.4",
"version": "0.0.1-beta.5",
"description": "A React visual novel player framework",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -12,8 +12,8 @@
"postbuild": "tsc-alias -p tsconfig.json",
"lint": "eslint \"./src/**/*.{ts,tsx,js,jsx}\"",
"lint:fix": "eslint --fix \"./src/**/*.{ts,tsx,js,jsx}\"",
"publish": "npm run lint && rimraf dist && webpack --config webpack.config.js && tsc-alias -p tsconfig.json && npm publish",
"typedoc": "typedoc src/index.ts --out docs"
"publish": "npm run lint && rimraf dist && webpack --config webpack.config.js && tsc-alias -p tsconfig.json",
"typedoc": "typedoc src/index.ts --out doc"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
8 changes: 6 additions & 2 deletions src/game/nlcore/action/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,23 @@ export const SoundActionTypes = {
fade: "sound:fade",
setVolume: "sound:setVolume",
setRate: "sound:setRate",
pause: "sound:pause",
resume: "sound:resume",
} as const;
export type SoundActionContentType = {
[K in typeof SoundActionTypes[keyof typeof SoundActionTypes]]:
K extends "sound:play" ? [void] :
K extends "sound:stop" ? [void] :
K extends "sound:fade" ? [{
start: number;
start?: number;
end: number;
duration: number;
}] :
K extends "sound:setVolume" ? [number] :
K extends "sound:setRate" ? [number] :
any;
K extends "sound:pause" ? [void] :
K extends "sound:resume" ? [void] :
any;
}
export const ControlActionTypes = {
action: "control:action",
Expand Down
Loading

0 comments on commit c1a79ce

Please sign in to comment.