A pixel-art visual novel (AVG) engine written in Rust, rendered in the terminal.
Driven by a script interpreter, supporting dialogue, characters, audio, and save/load.
See the example game "TerminalLove"
- Dual rendering modes: Terminal TUI (
tmj_terminal) + GPU-accelerated window (tmj_egui) - Terminal rendering: TUI drawing and event handling via
ratatui+crossterm - GPU rendering: Fullscreen, 1:2 character grid, CJK font support via
eframe+soft_ratatui - Script-driven: Built-in script parser with assignment, calls,
wait, chaining, and more - Modular workspace:
tmj_app,tmj_core,tmj_macro,tmj_terminal,tmj_egui - Configurable startup: Resolution, font, resource paths, and layout via
setting.toml - Audio & resources: Character sprites, expressions, and audio playback
- Rust toolchain (stable, supporting
edition = "2024") - Windows / Linux / macOS terminal
git clone https://github.com/rabitank/TermAVG.git
cd TermAVG
cargo buildTerminal TUI mode (redirect logs to avoid UI conflicts):
cargo run -p tmj_terminal 2> debug.txtGPU window mode (fullscreen, CJK font support):
cargo run -p tmj_eguiIf
setting.tomlis missing on first run, it will be created with defaults automatically.
engine/
├─ tmj_app/ # Game logic, pages, script variables, rendering pipeline
├─ tmj_core/ # Script system, event system, resource paths, common utilities
├─ tmj_macro/ # Procedural macros
├─ tmj_terminal/ # TUI mode entry (crossterm terminal rendering)
├─ tmj_egui/ # GPU mode entry (eframe + soft_ratatui window rendering)
├─ resource/ # Scripts and assets (scripts, fonts, images, etc.)
├─ setting.toml # Runtime configuration
├─ layout.toml # Layout configuration
└─ README.md
Example fields:
resolution = [240, 67]
font = "resource/font/SarasaTermCL-Regular.ttf"
font_bold = "resource/font/SarasaTermCL-Bold.ttf"
preprogress_script = ["resource/script_example.fs"]
is_force_skipable = false
save_dir = "save"
gallery_dir = "resource/gallery"
about_file = "resource/about.txt"
entre_script = "resource/script_example.fss"
mainmenu_title_file = "resource/mainmenu_title.txt"
mainmenu_default_bg_img = "resource/main_menu_bg.png"
mainmenu_session_bg_map = []
default_bg_img = "resource/bg_0.png"
default_face_img = "resource/default_face_img.png"
max_history_ls = 60| Field | Description |
|---|---|
resolution |
Logical render resolution [w, h] (character cells); the main画面 is centered accordingly |
font |
GPU mode font path (CJK monospace recommended, e.g. Sarasa Term CL) |
font_bold |
GPU mode bold font path (optional) |
preprogress_script |
Scripts to preprocess: *.fs → numbered *.fss on startup |
is_force_skipable |
Reserved, not yet used |
save_dir |
Save directory (normal slots + temp.save) |
gallery_dir |
Gallery resource directory |
about_file |
About popup content file (each line centered) |
entre_script |
Entry script path (usually points to preprocessed *.fss) |
mainmenu_title_file |
Main menu title file (optional, falls back to default) |
mainmenu_default_bg_img |
Main menu default background |
mainmenu_session_bg_map |
Session-based background mapping: session_id_min, session_id_max, bg_img |
default_bg_img / default_face_img |
Legacy fields, not directly read by the main flow |
max_history_ls |
Legacy field, history limit is internally hardcoded |
All paths are relative to the project root.
Defines the logical coordinate layout for story pages, main menu, and popups.
Coordinate shorthands:
ltwh:(left, top, width, height)twh:(top, width, height)(horizontally centered, x derived from character count and spacing)lw:(left, width)wh:(width, height)
| Field | Meaning |
|---|---|
character_twh |
Character sprite box (top, width, height) |
two_character_spec |
Spacing for 2 characters on screen |
x_character_spec |
Spacing for 3+ characters on screen |
vertical_dark_edge |
Top/bottom dark bar height |
frame_face_ltwh |
Avatar area |
frame_content_ltwh |
Dialogue box body |
text_ltwh |
Text area (clipped inside frame_content) |
frame_name_ltwh |
Speaker name area |
short_key_ltwh |
Bottom shortcut bar area |
chapter_title_ltwh |
Chapter title area |
chapter_subtitle_ltwh |
Chapter subtitle area |
paragraph_ltwh |
Narration / paragraph text area |
history_wh |
History popup dimensions |
mainmenu_lw |
Main menu list panel (left, width) |
mainmenu_load_pop_lw |
Load popup (left, width); width = 0 uses remaining space |
Scripts use #number as section separators (e.g. #1, #2). The engine reads story content section by section. Entry scripts should use *.fss. If you maintain *.fs (no numbers after #), set preprogress_script to auto-number them into *.fss on startup.
- Preprocessing (optional):
Game::new()iteratespreprogress_script, converts#markers to#1/#2/..., outputs toresource/<name>.fss. - Initialize script context: Creates
ScriptContext, registers global variables, functions, types (character,text_obj, etc.) and behavior mappings. Entry viaentre_script. - Streaming section reader:
StreamSectionReaderreads one section bysession_id(from#Nto next marker). Returns EOF at end of file. - Parse commands:
ScriptParser(lexer + parser) converts section text into command sequences (set/once/wait/call/assignment/chain). - Frame-by-frame execution:
Interpreterinjects commands intoSessionExecutor, executed each frame viaupdate().waitpauses for time or input;oncechanges auto-revert at section end.
| Type | Description |
|---|---|
character |
Character object; use character.say(text) to drive dialogue, with sprite, expression, position, etc. |
layer |
Dynamic layer object; manage via the layers global, supports visual effects (alpha fade, glitch, heartbeat, etc.) |
| Object | Description |
|---|---|
bg |
Background state (image, edge toggle) |
bgm |
Background music state |
env_effect |
Ambient sound state |
frame |
Dialogue box state (visibility, content, typewriter params, etc.) |
paragraph |
Narration / long text area state |
chapter |
Chapter title / subtitle state |
character_ls |
Currently displayed character list |
layers |
Dynamic layer table (add/remove) |
| Function | Description |
|---|---|
text(content) |
Writes to frame.content, drives FrameBehaviour (narration mode, no speaker or avatar) |
voice(path, [seconds], [volume]) |
Play audio; seconds>0 fades out then in; volume: 0~1; empty string stops the track |
see(name) |
Print current info of a visual element (debug) |
log(path_or_expr) |
Print a script path value (debug) |
save_to(table, target_path) |
Serialize a script table to file |
create_default_character(path) |
Generate a default character config template |
bg.set(path)/bg.trans_to(path, duration)/bg.show_edge()/bg.hide_edge()bgm.set(path, [fade_type], [seconds], [volume])/bgm.stop([seconds])env_effect.set(path, [seconds], [volume])/env_effect.stop([seconds])frame.show()/frame.hide()/frame.set_mode(mode)paragraph.show()/paragraph.hide()/paragraph.print(text)/paragraph.new(text)/paragraph.clear()chapter.show_title(title, [duration])/chapter.show_sub_title(subtitle, [duration])character.say(text)(instance method)character_ls.set_characters(c1, c2, ...)
| Syntax | Description |
|---|---|
变量 = 值 |
Assignment |
变量 = 命令 参数... |
Command return value assignment |
对象.方法 参数... |
Method call |
set 路径 参数... |
Set value |
once 路径 参数... |
One-shot command, auto-reverted at section end |
wait 0.5 |
Wait for specified seconds |
命令1 -> 命令2 |
Chained call |
Runtime environment info is written to
script_env.txton startup for debugging. See the example script.
- The workspace contains multiple crates; run
cargo check/cargo testfrom the project root - Core script code is in
tmj_core/src/script/ - Engine pages and rendering are in
tmj_app/src/pages/andtmj_app/src/game.rs
| Key | Function |
|---|---|
↑/↓ or ←/→ |
Move / select |
Enter |
Confirm / continue |
Esc or q |
Back / quit |
A shortcut bar is displayed at the bottom of every page.
- TUI: ratatui, crossterm
- GPU rendering: eframe, soft_ratatui
- Serialization: serde, toml
- Audio: rodio
- Other:
tracing,anyhow,image,strum,fontdue,cosmic-text
Issues and PRs are welcome.
This project is open-sourced under the MIT license.
Copyright (c) 2024 rabitank

