Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
49f80d0
feat: New image transition: `Darkness`
helloyork May 11, 2025
4d1ce44
feat: Add window event listener support for improved user experience
helloyork May 11, 2025
f32bb41
feat: Added method layer.`setZIndex`
helloyork May 11, 2025
8f12545
fix: image resizing not responding
helloyork May 12, 2025
7898a24
update: Menu constructor signature
helloyork May 12, 2025
b4af5d8
breaking change: deprecate `game.config.cps`, introduce `GamePreferen…
helloyork May 12, 2025
2d04466
feat: Add `isNarrator` property to dialog state for enhanced context
helloyork May 12, 2025
76c4831
feat: Added `voiceVolume`, `bgmVolume`, `soundVolume`, and `musicVolu…
helloyork May 13, 2025
1e0a01d
update: AudioManager logic
helloyork May 13, 2025
464478d
feat: global volume
helloyork May 13, 2025
67a528e
fix: Background music is not playing
helloyork May 13, 2025
fb6f170
feat: Using raw text for narrator instead of using Character instance
helloyork May 13, 2025
8b57cfc
feat: character tag function calling
helloyork May 13, 2025
270ec9c
feat: Added `waitForRouterExit` to wait for the page exit animation t…
helloyork May 14, 2025
17adc73
fix: Control.repeat not working
helloyork May 14, 2025
26e6e14
fix: Visual errors after applying transitions and before the elements…
helloyork May 15, 2025
497fd42
fix: Abort Events are not propagated correctly
helloyork May 16, 2025
085a0ac
fix: Incorrect behavior of `router.back`
helloyork May 16, 2025
22160ef
fix: The game state is not flushed correctly
helloyork May 18, 2025
30eab5e
fix: Different behavior between autoForward and user clicking
helloyork May 19, 2025
12a9039
fix: Incorrect transform repeat behavior
helloyork May 19, 2025
d1e93e2
fix: Update style synchronization
helloyork May 20, 2025
701ca45
fix: unexpected NaN when converting align to percentage
helloyork May 20, 2025
cc6cc9b
update: RenderEventAnnoucer.tsx
helloyork May 21, 2025
1395157
update README
helloyork May 21, 2025
13d6e08
Merge pull request #79 from NarraLeaf/dev_nomen
helloyork May 21, 2025
b35f3d8
update: Bump version to 0.5.0
helloyork May 21, 2025
4519540
Merge pull request #80 from NarraLeaf/dev_nomen
helloyork May 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## [0.5.0]

### _Incompatible Changes_

- `game.config.cps` is deprecated, use `GamePreference.cps` instead
- Menu GameElementHistory.`selected` may be null

### _Feature_

- New image transition: `Darkness`
- Added method image.`darken`
- Added method layer.`setZIndex`
- Added `voiceVolume`, `bgmVolume`, `soundVolume`, and `globalVolume` to the game preferences
- Using raw text for narrator instead of using Character instance
- Added `waitForRouterExit` to wait for the page exit animation to complete

### Update

- The skip action will now listen to the window events instead of the player element by default
- Added `isNarrator` to the dialog state

### Fixed

- Background music is not playing
- Visual errors after applying transitions and before the elements are painted
- Transform state is not updated correctly when the transform is skipped
- Abort Events are not propagated correctly
- Incorrect behavior of `router.back`
- The game state is not flushed correctly
- Different behavior between autoForward and user clicking
- Incorrect transform repeat behavior
- Unexpected NaN when converting align to percentage

## [0.4.4] - 2025/5/9

### Fixed
Expand Down
64 changes: 31 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-light.png">
<img alt="Fallback image description" src="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-light.png">
<img alt="NarraLeaf Logo" src="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-light.png">
</picture>

<h1 align="center">NarraLeaf-React</h1>
Expand Down Expand Up @@ -31,49 +31,36 @@ NarraLeaf-React uses TypeScript for all scripting, so you don't have to learn a
It also has a highly abstracted and easy-to-use API, for example:

```typescript
import {Character, Menu, Scene, Word, c, b} from "narraleaf-react";
import {Character, Menu, Scene, c, b} from "narraleaf-react";
```

```typescript
const scene1 = new Scene("scene1_hello_world", {
background: "/background/scene1_hello_world.jpg",
const scene1 = new Scene("Scene1: Hello World", {
background: "/link/to/background.jpg",
});

const johnSmith = new Character("John Smith");
const johnDoe = new Character("John Doe");
const jS = new Character("John Smith");
const jD = new Character("John Doe");

scene1.action([
/**
* John Smith: Hello, world!
* John Smith: This is my first **NarraLeaf** story.
* John Smith: Start editing src/story.js and enjoy the journey!
*/
johnSmith
.say("Hello, world!")
.say`This is my first ${b("NarraLeaf")} story.`
.say`Start editing ${c("src/story.js", "#00f")} and enjoy the journey!`,

/**
* John Doe: Also, don't forget to check out the documentation!
*/
johnDoe.say("Also, don't forget to check out the documentation!"),

/**
* Menu: Start the journey
* > Yes I will!
* - John Smith: Great! Let's start the journey!
* - John Smith: You can open issues on GitHub if you have any questions.
* > No, I'm going to check the documentation
* - John Smith: Sure! Take your time!
*/
jS`Hello, world!`,
jS`This is my first ${b("NarraLeaf")} story.`,
jS`Start editing ${c("src/story.js", "#00f")} and enjoy the journey!`,

jD`Also, don't forget to check out the ${c("documentation", "#00f")}!`,

"By the way, the documentation is available on https://react.narraleaf.com/documentation",
"You can also visit the website for demo and more information.",

Menu.prompt("Start the journey")

.choose("Yes I will!", [
johnSmith
.say("Great! Let's start the journey!")
.say("You can open issues on GitHub if you have any questions.")
jS`Great! Let's start the journey!`,
jS`You can open issues on GitHub if you have any questions.`
])

.choose("No, I'm going to check the documentation", [
johnSmith.say("Sure! Take your time!")
jS`Sure! Take your time!`
])
]);
```
Expand Down Expand Up @@ -133,6 +120,17 @@ npm install narraleaf-react
- [Plugin](https://react.narraleaf.com/documentation/core/plugin)
- [Utils](https://react.narraleaf.com/documentation/core/utils)
- [Player](https://react.narraleaf.com/documentation/player)
- [Player](https://react.narraleaf.com/documentation/player/player)
- [GameProviders](https://react.narraleaf.com/documentation/player/game-providers)
- Hooks
- [useGame](https://react.narraleaf.com/documentation/player/hooks/useGame)
- [usePreferences](https://react.narraleaf.com/documentation/player/hooks/usePreferences)
- [useRouter](https://react.narraleaf.com/documentation/player/hooks/useRouter)
- [useDialog](https://react.narraleaf.com/documentation/player/hooks/useDialog)
- [Page Router](https://react.narraleaf.com/documentation/player/page-router)
- [Dialog](https://react.narraleaf.com/documentation/player/dialog)
- [Notification](https://react.narraleaf.com/documentation/player/notification)
- [Menu](https://react.narraleaf.com/documentation/player/menu)
- About
- [License](https://react.narraleaf.com/documentation/info/license)
- [Incompatible Changes](https://react.narraleaf.com/documentation/info/incompatible-changes)
Expand Down
74 changes: 36 additions & 38 deletions docs/README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-light.png">
<img alt="Fallback image description" src="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-light.png">
<img alt="NarraLeaf Logo" src="https://raw.githubusercontent.com/NarraLeaf/.github/refs/heads/master/doc/banner-md-light.png">
</picture>

<h1 align="center">NarraLeaf-React</h1>
Expand Down Expand Up @@ -35,46 +35,33 @@ import {Character, Menu, Scene, Word, c, b} from "narraleaf-react";
```

```typescript
const scene1 = new Scene("场景1_你好_世界", {
background: "/background/scene1_hello_world.jpg",
const scene1 = new Scene("场景1: 你好,世界", {
background: "/link/to/background.jpg",
});

const johnSmith = new Character("约翰·史密斯");
const johnDoe = new Character("约翰·多");
const jS = new Character("John Smith");
const jD = new Character("John Doe");

scene1.action([
/**
* 约翰·史密斯: 你好世界!
* 约翰·史密斯: 这是我的第一个 **NarraLeaf** 视觉小说
* 约翰·史密斯: 开始编辑 src/story.js 并享受旅程!
*/
johnSmith
.say("你好世界!")
.say`这是我的第一个 ${b("NarraLeaf")} 视觉小说`
.say`开始编辑 ${c("src/story.js", "#00f")} 并享受旅程!`,

/**
* 约翰·多: 对了,别忘了查看文档!
*/
johnDoe.say("对了,别忘了查看文档!"),

/**
* Menu: 开始旅程
* > 是的,我会!
* - 约翰·史密斯: 太好了!让我们开始旅程!
* - 约翰·史密斯: 如果您有任何问题,可以在GitHub上提出问题。
* > 不,我要查看文档
* - 约翰·史密斯: 当然!慢慢来!
*/
Menu.promp("开始旅程")
.choose("是的,我会!", [
johnSmith
.say("太好了!让我们开始旅程!")
.say("如果您有任何问题,可以在GitHub上提出问题。")
])
.choose("不,我要查看文档", [
johnSmith.say("当然!慢慢来!")
])
jS`你好,世界!`,
jS`这是我的第一个 ${b("NarraLeaf")} 故事。`,
jS`开始编辑 ${c("src/story.js", "#00f")} 并享受旅程!`,

jD`别忘了检查 ${c("文档", "#00f")}!`,

"顺便说一句,文档在 https://react.narraleaf.com/documentation",
"你也可以访问网站获取更多信息。",

Menu.prompt("开始旅程")

.choose("是的,我愿意!", [
jS`太好了!让我们开始旅程!`,
jS`如果你有任何问题,可以在 GitHub 上提出问题。`
])

.choose("不,我要检查文档", [
jS`好的,请慢慢来!`
])
]);
```

Expand Down Expand Up @@ -124,7 +111,7 @@ npm install narraleaf-react
- [文本](https://react.narraleaf.com/documentation/core/elements/text)
- [持久化](https://react.narraleaf.com/documentation/core/elements/persistent)
- [故事](https://react.narraleaf.com/documentation/core/elements/story)
- [Displayable](https://react.narraleaf.com/documentation/core/elements/displayable)
- [可视化组件](https://react.narraleaf.com/documentation/core/elements/displayable)
- [图层](https://react.narraleaf.com/documentation/core/elements/layer)
- [服务](https://react.narraleaf.com/documentation/core/elements/service)
- [视频](https://react.narraleaf.com/documentation/core/elements/video)
Expand All @@ -133,6 +120,17 @@ npm install narraleaf-react
- [插件](https://react.narraleaf.com/documentation/core/plugin)
- [实用工具](https://react.narraleaf.com/documentation/core/utils)
- [播放器](https://react.narraleaf.com/documentation/player)
- [Player](https://react.narraleaf.com/documentation/player/player)
- [GameProviders](https://react.narraleaf.com/documentation/player/game-providers)
- 钩子
- [useGame](https://react.narraleaf.com/documentation/player/hooks/useGame)
- [usePreferences](https://react.narraleaf.com/documentation/player/hooks/usePreferences)
- [useRouter](https://react.narraleaf.com/documentation/player/hooks/useRouter)
- [useDialog](https://react.narraleaf.com/documentation/player/hooks/useDialog)
- [页面路由](https://react.narraleaf.com/documentation/player/page-router)
- [对话框](https://react.narraleaf.com/documentation/player/dialog)
- [通知](https://react.narraleaf.com/documentation/player/notification)
- [选项框](https://react.narraleaf.com/documentation/player/menu)
- 关于
- [许可](https://react.narraleaf.com/documentation/info/license)
- [不兼容的更改](https://react.narraleaf.com/documentation/info/incompatible-changes)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "narraleaf-react",
"version": "0.4.4",
"version": "0.5.0",
"description": "A React visual novel player framework",
"main": "./dist/main.js",
"types": "./dist/index.d.ts",
Expand Down
34 changes: 23 additions & 11 deletions src/game/nlcore/action/actionHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Action } from "./action";
import { randId } from "@lib/util/data";
import { LiveGameEventToken } from "../types";
import { LogicAction } from "../game";
import { GameHistory, GameHistoryManager } from "./gameHistory";

export type ActionHistory<T extends Array<unknown> = any> = {
action: Action;
Expand Down Expand Up @@ -32,7 +33,7 @@ export class ActionHistoryManager {
*/
public push<T extends Array<any> = Array<any>>(action: Action, onUndo?: (...args: T) => void, args?: T, timeline?: Timeline): {id: string} {
const id = randId(6);
this.history.push({action, id, args, undo: onUndo, timeline});
this.history.push({action, id, args: args || [], undo: onUndo, timeline});

// Check if the history size exceeds the limit
if (this.history.length > this.maxHistorySize) {
Expand Down Expand Up @@ -63,31 +64,42 @@ export class ActionHistoryManager {

const affected: ActionHistory<any>[] = [];
for (let i = this.history.length - 1; i >= index; i--) {
if (this.history[i].timeline && !this.history[i].timeline!.isSettled) {
if (this.history[i].timeline && !this.history[i].timeline!.isSettled()) {
this.history[i].timeline!.abort();
}
console.log("NarraLeaf-React [ActionHistory] Undoing", this.history[i].action.type, this.history[i]);
this.history[i].undo?.(...(this.history[i].args || []));
affected.push(this.history[i]);
}

this.history.splice(index);
this.history.length = index;
this.hooks.onUndo.forEach(cb => cb(affected));
return (affected[affected.length - 1]?.action || null) as LogicAction.Actions | null;
}

public undo(): LogicAction.Actions | null {
const last = this.history.pop();
if (last) {
if (last.timeline && !last.timeline.isSettled) {
last.timeline.abort();
public undo(gameHistory: GameHistoryManager): LogicAction.Actions | null {
if (!this.ableToUndo(gameHistory)) {
return null;
}

const history = gameHistory.getHistory();
let last: GameHistory | undefined;
for (let i = history.length - 1; i >= 0; i--) {
if (history[i].isPending !== true) {
last = history[i];
break;
}
last.undo?.(...last.args);
this.hooks.onUndo.forEach(cb => cb([last]));
return last.action as LogicAction.Actions;
}
if (last) {
return this.undoUntil(last.token);
}
return null;
}

public ableToUndo(gameHistory: GameHistoryManager): boolean {
return this.history.length > 0 && gameHistory.getHistory().some((h: GameHistory) => h.isPending !== true);
}

public onUndo(callback: (affected: ActionHistory<any>[]) => void): LiveGameEventToken {
this.hooks.onUndo.push(callback);
return {
Expand Down
4 changes: 4 additions & 0 deletions src/game/nlcore/action/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,15 @@ export const ImageActionTypes = {
flush: "image:flush",
initWearable: "image:initWearable",
setAppearance: "image:setAppearance",
setDarkness: "image:setDarkness",
} as const;
export type ImageActionContentType = {
[K in typeof ImageActionTypes[keyof typeof ImageActionTypes]]:
K extends "image:setSrc" ? [ImageSrc | Color] :
K extends "image:flush" ? [] :
K extends "image:initWearable" ? [Image] :
K extends "image:setAppearance" ? [FlexibleTuple<SelectElementFromEach<TagGroupDefinition>> | string[], ImageTransition | undefined] :
K extends "image:setDarkness" ? [darkness: number, duration?: number, easing?: TransformDefinitions.EasingDefinition] :
any;
} & DisplayableActionContentType<ImageTransition>;
/* Condition */
Expand Down Expand Up @@ -187,10 +189,12 @@ export type PersistentActionContentType = {
/* Layer */
export const LayerActionTypes = {
action: "layer:action",
setZIndex: "layer:setZIndex",
} as const;
export type LayerActionContentType = {
[K in typeof LayerActionTypes[keyof typeof LayerActionTypes]]:
K extends "layer:action" ? any :
K extends "layer:setZIndex" ? [number] :
any;
}
/* Video */
Expand Down
10 changes: 8 additions & 2 deletions src/game/nlcore/action/actions/characterAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ export class CharacterAction<T extends typeof CharacterActionTypes[keyof typeof
}

// Create dialog
const dialog = gameState.createDialog(this.getId(), sentence, () => {
const dialogId = gameState.idManager.generateId();
const dialog = gameState.createDialog(dialogId, sentence, () => {
if (voice) {
const task = gameState.audioManager.stop(voice);
timeline.attachChild(task);
}

gameState.gameHistory.resolvePending(id); // accessing id is technically dangerous, but I think it is impossible to happen

awaitable.resolve({
type: this.type,
node: this.contentNode.getChild()
Expand All @@ -69,6 +72,7 @@ export class CharacterAction<T extends typeof CharacterActionTypes[keyof typeof
const task = gameState.audioManager.stop(voice);
timeline.attachChild(task);
}
dialog.cancel();
});
gameState.gameHistory.push({
token: id,
Expand All @@ -77,7 +81,9 @@ export class CharacterAction<T extends typeof CharacterActionTypes[keyof typeof
type: "say",
text: dialog.text,
voice: voice ? voice.getSrc() : null,
}
character: this.callee.state.name,
},
isPending: true,
});

return awaitable;
Expand Down
Loading