Compare commits

...
Author SHA1 Message Date
TaikoZhongandClaude Fable 5.1 ad0a3b69f2 entry: the dan board's strings are mode_dan, not entry_dan
The engine puts a Dan-i Dojo board on the entry screen whenever the skin
defines an entry_dan text; this skin lists the dojo inside song select
instead (75f5d27), so the strings moved into skin_config must not use that
key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 01:46:19 +08:00
TaikoZhongandClaude Fable 5.1 8ea7934a73 Lua: every user-visible string comes from skin_config (tex.skin_text)
entry board titles/comments (entry_* / entry_*_comment_N), costume-menu guide words
(guide_select_label / guide_decide_label), the sort window (sort_window_* /
folder_sort_select / course_name_*), the dan HUD plate (dan_gauge_name /
dan_exam_text_gauge), the N曲 song limit (song_num_max) and the 演奏スキップON
badge (skip_badge_text) are read through the engine's tex.skin_text; the only
Japanese left in Lua are folder-name matchers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 01:45:48 +08:00
Yonokid 8d106e1df5 Merge pull request 'zh is Simplified Chinese: convert the zh strings, translate the last Japanese-only labels' (#34) from GanYiZhong/YataiDONNijiiro:zh-simplified into main
Reviewed-on: Yonokid/YataiDONNijiiro#34
2026-09-15 17:48:00 +02:00
Yonokid 0f2aa5ddb4 Merge branch 'main' into zh-simplified 2026-09-15 17:47:49 +02:00
Yonokid c8b6568d9a Merge pull request 'Loading transition: the game hint block follows the interface language' (#33) from GanYiZhong/YataiDONNijiiro:hint-i18n into main
Reviewed-on: Yonokid/YataiDONNijiiro#33
2026-09-15 17:47:24 +02:00
Yonokid e256d42f9c Merge branch 'main' into hint-i18n 2026-09-15 17:46:35 +02:00
Yonokid 54afedc34b Merge pull request 'font.ttf 3.000: all five cabinet atlases (adds Simplified Chinese and Korean)' (#32) from GanYiZhong/YataiDONNijiiro:font-all-atlases into main
Reviewed-on: Yonokid/YataiDONNijiiro#32
2026-09-15 17:46:12 +02:00
Yonokid 80f4726c00 Merge branch 'main' into font-all-atlases 2026-09-15 17:44:59 +02:00
Yonokid 1c5d258314 Merge pull request 'Dan gauge: 1P red / 2P blue unfilled track' (#31) from GanYiZhong/YataiDONNijiiro:dan-gauge-player-colors into main
Reviewed-on: Yonokid/YataiDONNijiiro#31
2026-09-15 17:44:46 +02:00
Yonokid 0eea45143e Merge branch 'main' into dan-gauge-player-colors 2026-09-15 17:44:35 +02:00
Yonokid 65066bbe46 Merge pull request 'Dan result: 未到達 plates per language and the "not recorded" banner' (#30) from GanYiZhong/YataiDONNijiiro:dan-result-nosave into main
Reviewed-on: Yonokid/YataiDONNijiiro#30
2026-09-15 17:44:28 +02:00
Yonokid 37b8bd609b Merge branch 'main' into dan-result-nosave 2026-09-15 17:44:17 +02:00
Yonokid 67f7c48d6e Merge pull request 'Dan select: the cabinet's exam grid (soul gauge box + per-song segments)' (#29) from GanYiZhong/YataiDONNijiiro:dan-exam-grid into main
Reviewed-on: Yonokid/YataiDONNijiiro#29
2026-09-15 17:44:09 +02:00
Yonokid 4e79c6eed6 Merge branch 'main' into dan-exam-grid 2026-09-15 17:43:15 +02:00
TaikoZhongandClaude Fable 5.1 8b6dff1189 fonts: Han from the language atlas first, kana / Latin from jp (--han-order)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 15:45:15 +08:00
TaikoZhongandClaude Fable 5.1 7ba7e142ae fonts: every family keeps the jp brush glyphs first, its own atlas is the first fallback
font_cn / font_tw / font_kr are now jp first (kana, Latin, digits and every
kanji the jp atlas has stay in the brush face); cn / tw / kr only supply
the characters jp lacks. build_font.py gains --han-order for a separate
ideograph priority.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 14:23:51 +08:00
TaikoZhongandClaude Fable 5.1 638b8b37c5 zh is Simplified Chinese: convert the zh strings, translate the last Japanese-only labels
- every zh string in skin_config.json and the Lua scripts converted Traditional ->
  Simplified (OpenCC TSPhrases + TSCharacters; 裏 kept for 裏魔王); the sort window
  uses the cabinet's own Simplified rows (根据难度选择 / ★的个数 / 排序)
- dan_exam_text_more/less/gauge/gauge_less gain en / zh / ko
- dan_info 魂ゲージ + %以上 and the N曲 song limit follow the interface language
  (cabinet wordlist: clear_theme_soul_gauge, sort_window_songs)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 06:31:22 +08:00
TaikoZhongandClaude Fable 5.1 c13226e3b3 One font per cabinet family: font_cn / font_tw / font_en / font_kr
font_zh.ttf becomes font_cn.ttf (cn, tw, jp, en, kr) and gains siblings built the
same way: font_tw (tw, jp, cn, en, kr), font_en (en, jp, tw, cn, kr), font_kr
(kr, jp, en, tw, cn). font.ttf (jp first) is unchanged and stays the fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 06:16:49 +08:00
TaikoZhongandClaude Fable 5.1 2dc940c6aa Loading transition: the game hint block follows the interface language
The 「ゲームのヒント」 block is a baked Japanese texture. For any other language the
three lines are drawn as text (skin_config transition_hint_title / _line1 / _line2,
en / zh / ko) at the rows measured off the texture, on the same fade; Japanese keeps
the texture. The title strings are the cabinet's own wordlist entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 06:15:35 +08:00
TaikoZhongandClaude Fable 5.1 d5ef78970e font_zh.ttf: Simplified Chinese glyph set first, tw/jp only for what it lacks
With the interface language set to Simplified Chinese the engine now loads
Graphics/font_zh.ttf, built with the cn atlas first (7 588 glyphs from cn,
11 966 from tw, 8 from jp) so shared characters take the Simplified face too,
as the cabinet does with its per-language atlas families.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 05:56:37 +08:00
TaikoZhongandClaude Fable 5.1 ecfaed05f7 font.ttf 3.000: all five cabinet atlases (adds Simplified Chinese and Korean)
Rebuilt from the cabinet's five _64 glyph atlases (jp, tw, cn, kr, en) instead of
jp+tw only: 22 382 glyphs, +1 956 Simplified Chinese and +2 737 Hangul, so zh-CN
and ko text no longer falls back to '?'. Same face, metrics and baseline as 2.000.
tools/font/build_font.py + FONT.md document the cabinet's font selection and the build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 05:37:03 +08:00
TaikoZhongandClaude Fable 5.1 1a39365e21 Dan gauge: 1P red / 2P blue unfilled track
The dan gauge track was the same yellow for both sides. The cabinet colours
it per player like the normal gauge below the clear line: dark red for 1P,
dark teal-blue for 2P. Same body colours as gauge/1p_unfilled_normal and
gauge/2p_unfilled_normal; frame and alpha untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 02:15:03 +08:00
TaikoZhongandClaude Fable 5.1 a4a107f894 Dan result: 未到達 plates per language and the "not recorded" banner
From the cabinet's result movie: the unreached plate (ja / en / ko / zh
frames, 400x128) placed over a song row's counters, and the 610x70 notice
plate at the top right for a skipped run, with the notice text per language
(dan_result_nosave) drawn by the engine (Yonokid/YataiDON#180).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 11:33:08 +08:00
TaikoZhongandClaude Fable 5.1 7da693af35 Dan select: the cabinet's exam grid (soul gauge box + per-song segments)
Turn on the engine's dan_exam_grid layout: the soul gauge in its own box on
the left, the other conditions stacked on the right, and per-song conditions
split into 1st / 2nd / 3rd segments with each song's own border. The art has
shipped with the skin since the board rework; only the option was missing.
The segment strips are re-cut on a window that keeps the 2nd / 3rd badges'
rim (163x34) and the strip anchor moves by the same 3 / 1 px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 10:22:45 +08:00
Yonokid c3f197352a Merge pull request 'Result: fill the soul gauge cell by cell' (#27) from GanYiZhong/YataiDONNijiiro:result-gauge-cells into main
Reviewed-on: Yonokid/YataiDONNijiiro#27
2026-09-11 03:54:26 +02:00
Yonokid 19d1fcc859 Merge branch 'main' into result-gauge-cells 2026-09-11 03:54:19 +02:00
Yonokid f5f35d6b8f Merge pull request 'Dan gauge: one 21 px cell per bar piece' (#26) from GanYiZhong/YataiDONNijiiro:dan-gauge-cells into main
Reviewed-on: Yonokid/YataiDONNijiiro#26
2026-09-11 03:54:08 +02:00
Yonokid bdfe482fe8 Merge branch 'main' into dan-gauge-cells 2026-09-11 03:53:58 +02:00
Yonokid ad52950374 Merge pull request 'Game HUD: song title on the cabinet line' (#25) from GanYiZhong/YataiDONNijiiro:result-rank-skip into main
Reviewed-on: Yonokid/YataiDONNijiiro#25
2026-09-11 03:53:51 +02:00
Yonokid 634896b4c1 Merge branch 'main' into result-rank-skip 2026-09-11 03:53:40 +02:00
TaikoZhongandClaude Fable 5.1 ed7f7224f7 Result: fill the soul gauge cell by cell on the cabinet's 50-cell grid
The result gauge stepped in 87 engine units of 8.4 px, so the fill crept
across the 50-cell / 21 px groove instead of lighting one cell at a time.
Convert the fill target to cells (points * 50 / max, floored like the game
HUD), step one cell per 7 frames (kGageSpeed_), place the norma at cell
30 / 35 / 40, and give the fill pieces the HUD's one-cell-wide art on the
groove's grid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 09:37:40 +08:00
TaikoZhongandClaude Fable 5.1 f93a063a4a Dan gauge: one 21 px cell per bar piece, anchored on the cell grid
The dan gauge fill pieces were 12 px wide at x 726 while the border's cell
grid is 50 x 21 px from x 738; make the bar one cell wide at 738 so the
engine's per-cell stretch lands on the grid, and centre the 32 px gauge-up
glow on its cell (733).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 08:13:34 +08:00
TaikoZhongandClaude Fable 5.1 20838c114e Game HUD: song title on the cabinet's line
song_info y 58 -> 68: the cabinet's title field (y 0..98, 54 pt, border 6.5,
text vertically centred then inset by the border) puts the ink centre near
y 56; the engine anchors the text object's box, whose ink sits ~12 px above
its centre.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 08:13:26 +08:00
Yonokid e347c9d409 Update checksums.sha256 2026-09-10 14:35:16 -04:00
37 changed files with 797 additions and 162 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -128,5 +128,25 @@
"rank_plate": {
"x": 64,
"y": 57
},
"unreached_ja": {
"x": 1252,
"y": 171
},
"unreached_en": {
"x": 1252,
"y": 171
},
"unreached_ko": {
"x": 1252,
"y": 171
},
"unreached_zh": {
"x": 1252,
"y": 171
},
"nosave": {
"x": 1344,
"y": 22
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 B

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 B

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+4 -4
View File
@@ -70,19 +70,19 @@
]
},
"1p_bar": {
"x": 726,
"x": 738,
"y": 248
},
"2p_bar": {
"x": 726,
"x": 738,
"y": 248
},
"1p_bar_fade": {
"x": 716,
"x": 733,
"y": 241
},
"2p_bar_fade": {
"x": 716,
"x": 733,
"y": 241
},
"tamashii": {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 B

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 B

After

Width:  |  Height:  |  Size: 122 B

+19 -10
View File
@@ -272,21 +272,21 @@
"1p_bar": [
{
"x": 87,
"y": 241
"y": 240
},
{
"x": 1034,
"y": 241
"y": 240
}
],
"2p_bar": [
{
"x": 87,
"y": 241
"y": 240
},
{
"x": 1034,
"y": 241
"y": 240
}
],
"bar_clear_bottom": [
@@ -302,20 +302,20 @@
"bar_clear_top": [
{
"x": 87,
"y": 216
"y": 217
},
{
"x": 1034,
"y": 216
"y": 217
}
],
"bar_clear_transition": [
{
"x": 89,
"x": 87,
"y": 215
},
{
"x": 1036,
"x": 1034,
"y": 215
}
],
@@ -349,7 +349,16 @@
"y": 207
}
],
"tamashii_fire": [{"x": 794, "y": 118}, {"x": 1741, "y": 118}],
"tamashii_fire": [
{
"x": 794,
"y": 118
},
{
"x": 1741,
"y": 118
}
],
"footer": [
{
"x": 0,
@@ -464,4 +473,4 @@
"y": 210
}
]
}
}
+252 -26
View File
@@ -1,4 +1,31 @@
{
"transition_hint_title": {
"font_size": 32,
"text": {
"ja": "ゲームのヒント",
"en": "Hints",
"zh": "游戏提示",
"ko": "게임의 힌트"
}
},
"transition_hint_line1": {
"font_size": 26,
"text": {
"ja": "太鼓の面とフチをたたいてリズムにのろう!",
"en": "Hit the drum face and rim and ride the rhythm!",
"zh": "敲击鼓面和鼓边,跟上节奏吧!",
"ko": "북면과 테두리를 두드려 리듬을 타자!"
}
},
"transition_hint_line2": {
"font_size": 26,
"text": {
"ja": "黄色い連打音符はたたいた数だけ得点アップ!",
"en": "Yellow drumroll notes: every hit adds to your score!",
"zh": "黄色连打音符敲得越多得分越高!",
"ko": "노란 연타 음표는 두드린 만큼 점수 UP!"
}
},
"screen": {
"parent": "PyTaikoGreen",
"width": 1920,
@@ -18,7 +45,8 @@
"costume_preset_slots": true,
"dan_title_horizontal": true,
"combo_color_tiers": true,
"gauge_cell_fade_in": true
"gauge_cell_fade_in": true,
"dan_exam_grid": true
}
},
"score_counter_pos": {
@@ -39,7 +67,7 @@
},
"song_info": {
"x": 1872,
"y": 58,
"y": 68,
"font_size": 54,
"outline": 4.333333
},
@@ -165,7 +193,7 @@
"text": {
"ja": "太鼓をたたいてスタート!",
"en": "Hit the taiko to start!",
"zh": "敲打太鼓開始!"
"zh": "敲打太鼓开始!"
}
},
"crown_message_miss": {
@@ -185,7 +213,7 @@
"text": {
"ja": "おしかったドン",
"en": "Almost.",
"zh": "可惜!\n就差一點了鼓咚…"
"zh": "可惜!\n就差一点了鼓咚…"
}
},
"crown_message_success": {
@@ -195,7 +223,7 @@
"text": {
"ja": "ノリノリだドン",
"en": "Cleared!\nCongrats, Don!",
"zh": "過關\n恭喜鼓咚!"
"zh": "过关\n恭喜鼓咚!"
}
},
"crown_message_perfect": {
@@ -205,7 +233,7 @@
"text": {
"ja": "よくできたドン!",
"en": "Awesome!",
"zh": "魂槽全滿!\n太厲害了鼓咚!"
"zh": "魂槽全满!\n太厉害了鼓咚!"
}
},
"song_select_indicator": {
@@ -249,7 +277,7 @@
"text": {
"en": "Select your outfit!",
"ja": "きせかえをえらぼう!",
"zh": "選擇服飾吧!"
"zh": "选择服饰吧!"
}
},
"result_chara": {
@@ -298,7 +326,7 @@
"text": {
"ja": "かんたん",
"en": "Easy",
"zh": "簡單"
"zh": "简单"
}
},
"course_name_1": {
@@ -318,7 +346,7 @@
"text": {
"ja": "むずかしい",
"en": "Hard",
"zh": "困難"
"zh": "困难"
}
},
"course_name_3": {
@@ -348,7 +376,7 @@
"text": {
"ja": "えらぶ",
"en": "Select",
"zh": "選擇"
"zh": "选择"
}
},
"guide_decide_label": {
@@ -358,7 +386,7 @@
"text": {
"ja": "決定",
"en": "Decide",
"zh": "決定"
"zh": "决定"
}
},
"nameplate_entry_left": {
@@ -376,7 +404,7 @@
"text": {
"ja": "かんたん",
"en": "Easy",
"zh": "簡單"
"zh": "简单"
}
},
"chip_course_name_1": {
@@ -396,7 +424,7 @@
"text": {
"ja": "むずかしい",
"en": "Hard",
"zh": "困難"
"zh": "困难"
}
},
"chip_course_name_3": {
@@ -491,7 +519,7 @@
"text": {
"en": "No sound",
"ja": "無音",
"zh": "無聲"
"zh": "无声"
}
},
"modifier_title": {
@@ -501,7 +529,7 @@
"text": {
"ja": "演奏オプション",
"en": "Play Options",
"zh": "演奏選項"
"zh": "演奏选项"
}
},
"song_select_chara_1p": {
@@ -544,7 +572,7 @@
},
"gauge_cells": {
"x": 50
},
},
"mod_badge_grid": {
"x": 170,
"y": 77,
@@ -662,7 +690,7 @@
"text": {
"en": "1-Player",
"ja": "1人プレイ",
"zh": "1人遊玩"
"zh": "1人游玩"
}
},
"entry_credit_2p": {
@@ -672,7 +700,7 @@
"text": {
"en": "2-Player",
"ja": "2人プレイ",
"zh": "2人遊玩"
"zh": "2人游玩"
}
},
"entry_credit_start": {
@@ -682,7 +710,7 @@
"text": {
"en": "Hit the drum to start!",
"ja": "太鼓をたたいてスタート!",
"zh": "敲打太鼓開始遊戲!"
"zh": "敲打太鼓开始游戏!"
}
},
"entry_cancel": {
@@ -781,7 +809,6 @@
"song_num_max_game": {
"x": 1814,
"y": 122
},
"note_arc_curve_height": {
"height": 608
@@ -873,8 +900,8 @@
"height": 132
},
"dan_exam_seg": {
"x": 14,
"y": 48,
"x": 11,
"y": 47,
"width": 327
},
"dan_select_exam_border_text": {
@@ -926,22 +953,45 @@
},
"dan_exam_text_more": {
"text": {
"ja": "%s 以上"
"ja": "%s 以上",
"en": "%s or more",
"zh": "%s 以上",
"ko": "%s 이상"
}
},
"dan_exam_text_less": {
"text": {
"ja": "%s 未満"
"ja": "%s 未満",
"en": "under %s",
"zh": "%s 未满",
"ko": "%s 미만"
}
},
"dan_result_nosave": {
"text": {
"ja": "今回の挑戦は記録されません",
"en": "This attempt will not be recorded",
"zh": "本次挑戰不會被記錄",
"ko": "이번 도전은 기록되지 않습니다"
}
},
"dan_result_nosave_text": {
"font_size": 24
},
"dan_exam_text_gauge": {
"text": {
"ja": "%s %以上"
"ja": "%s %以上",
"en": "%s%% or more",
"zh": "%s%以上",
"ko": "%s% 이상"
}
},
"dan_exam_text_gauge_less": {
"text": {
"ja": "%s %未満"
"ja": "%s %未満",
"en": "under %s%%",
"zh": "%s%未满",
"ko": "%s% 미만"
}
},
"note_arc_pivot": {
@@ -951,5 +1001,181 @@
"note_arc_pivot_2p": {
"x": 1282.19,
"y": -167.83
},
"entry_game_comment_1": {
"text": {
"ja": "すきな曲や、むずかしさを",
"en": "Select a song and",
"zh": "可选择喜爱的"
}
},
"entry_game_comment_2": {
"text": {
"ja": "えらんであそべるよ!",
"en": "difficulty to play!",
"zh": "歌曲、难度游玩哦!"
}
},
"entry_practice_comment_1": {
"text": {
"ja": "曲の一部をくりかえし練習して",
"en": "Practice a section of a song",
"zh": "反复练习歌曲的片段"
}
},
"entry_practice_comment_2": {
"text": {
"ja": "腕をみがこう!",
"en": "over and over to improve!",
"zh": "磨练技巧吧!"
}
},
"entry_costume_comment_1": {
"text": {
"ja": "どんちゃんのきせかえを",
"en": "Choose an outfit",
"zh": "可为咚酱选择"
}
},
"entry_costume_comment_2": {
"text": {
"ja": "えらんであそべるよ!",
"en": "for your Don-chan!",
"zh": "服饰游玩哦!"
}
},
"entry_ai_battle_comment_1": {
"text": {
"ja": "「AIどん」と勝つか負けるか",
"en": "See if you can beat AI-DON",
"zh": "与「AI咚」来一场"
}
},
"entry_ai_battle_comment_2": {
"text": {
"ja": "ギリギリのバトルをたのしもう!",
"en": "in this neck-and-neck competition!",
"zh": "不到最后难分胜负的对战吧!"
}
},
"mode_dan_comment_1": {
"text": {
"ja": "3曲連続して演奏しながら、",
"en": "Play three songs in a row",
"zh": "连续演奏3首指定曲"
}
},
"mode_dan_comment_2": {
"text": {
"ja": "お題の合格を目指そう!",
"en": "and aim to clear the task!",
"zh": "并达成题目的合格条件吧!"
}
},
"mode_dan_comment_3": {
"text": {
"ja": "合格すると腕前が認定されるよ!",
"en": "Your skills will be rewarded!",
"zh": "只要合格实力就会备受肯定哦!"
}
},
"entry_settings_comment_1": {
"text": {
"ja": "ゲームのせっていを",
"en": "Change the game's",
"zh": "可以变更"
}
},
"entry_settings_comment_2": {
"text": {
"ja": "かえられるよ!",
"en": "settings here!",
"zh": "游戏设定哦!"
}
},
"sort_window_diff": {
"text": {
"ja": "むずかしさ",
"en": "Difficulty",
"zh": "难度"
}
},
"sort_window_star": {
"text": {
"ja": "★の数",
"en": "Number of \"★\"",
"zh": "★的个数"
}
},
"sort_window_order": {
"text": {
"ja": "表示順",
"en": "Sort by",
"zh": "排序"
}
},
"sort_window_order_0": {
"text": {
"ja": "いつもどおり",
"en": "Default",
"zh": "通常"
}
},
"sort_window_order_1": {
"text": {
"ja": "未クリア優先",
"en": "Not cleared",
"zh": "未通关优先"
}
},
"sort_window_order_2": {
"text": {
"ja": "未フルコンボ優先",
"en": "Not Full Combo",
"zh": "未全连段优先"
}
},
"sort_window_order_3": {
"text": {
"ja": "未ドンダフルコンボ優先",
"en": "Not Donderful Combo",
"zh": "未全良连段优先"
}
},
"sort_window_songs": {
"text": {
"ja": "曲",
"en": "Songs",
"zh": "首"
}
},
"folder_sort_select": {
"text": {
"ja": "むずかしさからえらぶ",
"en": "Select by difficulty",
"zh": "根据难度选择"
}
},
"dan_gauge_name": {
"text": {
"ja": "魂ゲージ",
"en": "Soul Gauge",
"zh": "魂槽",
"ko": "클리어 게이지"
}
},
"skip_badge_text": {
"text": {
"ja": "演奏スキップON",
"en": "演奏スキップON",
"zh": "演奏スキップON"
}
},
"mode_dan": {
"text": {
"ja": "段位道場",
"en": "Dan-i Dojo",
"zh": "段位道场"
}
}
}
+5 -2
View File
@@ -251,10 +251,13 @@ function M:draw()
local x = GAUGE_X0 + CELL_PITCH * n - CELL_PITCH / 2 - 1
if self.label_for ~= ex.red then
self.label = text.create_raw_text(tostring(ex.red) .. "%以上",
-- skin_config: dan_gauge_name (魂ゲージ) and dan_exam_text_gauge ("%s %以上", %% = %)
local fmt = tex.skin_text("dan_exam_text_gauge"):gsub("%%%%", "\1")
local caption = fmt:gsub("%%s", tostring(ex.red)):gsub("\1", "%%")
self.label = text.create_raw_text(caption,
VALUE_SIZE, CAP_FILL, VALUE_OUTLINE, false,
4.0 / tex.get_screen_scale())
self.name_label = text.create_raw_text("魂ゲージ",
self.name_label = text.create_raw_text(tex.skin_text("dan_gauge_name"),
NAME_SIZE, CAP_FILL, CAP_OUTLINE, false,
3.0 / tex.get_screen_scale())
self.label_for = ex.red
+20 -40
View File
@@ -60,39 +60,23 @@ local TITLE_SPACING, INFO_SPACING = 2.0, 1.0
local MODES = {
game = {
frame = 0, outline = {251, 1, 29, 255},
titles = { ja = "演奏ゲーム", en = "Game", zh = "节奏游戏" },
comment = {
ja = { "すきな曲や、むずかしさを", "えらんであそべるよ!" },
en = { "Select a song and", "difficulty to play!" },
zh = { "可選擇喜愛的", "歌曲、難度遊玩哦!" },
},
title_key = "entry_game",
comment_keys = { "entry_game_comment_1", "entry_game_comment_2" },
},
practice = {
frame = 2, outline = {34, 138, 117, 255},
titles = { ja = "特訓モード", en = "Practice", zh = "练习游戏" },
comment = {
ja = { "曲の一部をくりかえし練習して", "腕をみがこう!" },
en = { "Practice a section of a song", "over and over to improve!" },
zh = { "反覆練習歌曲的片段", "磨練技巧吧!" },
},
title_key = "entry_practice",
comment_keys = { "entry_practice_comment_1", "entry_practice_comment_2" },
},
costume = {
frame = 4, outline = {200, 120, 222, 255},
titles = { ja = "きせかえ", en = "Costume", zh = "装扮" },
comment = {
ja = { "どんちゃんのきせかえを", "えらんであそべるよ!" },
en = { "Choose an outfit", "for your Don-chan!" },
zh = { "可為咚醬選擇", "服飾遊玩哦!" },
},
title_key = "entry_costume",
comment_keys = { "entry_costume_comment_1", "entry_costume_comment_2" },
},
ai = {
frame = 2, outline = {34, 138, 117, 255},
titles = { ja = "AIバトル", en = "AI Battle", zh = "AI斗争" },
comment = {
ja = { "「AIどん」と勝つか負けるか", "ギリギリのバトルをたのしもう!" },
en = { "See if you can beat AI-DON", "in this neck-and-neck competition!" },
zh = { "與「AI咚」來一場", "不到最後難分勝負的對戰吧!" },
},
title_key = "entry_ai_battle",
comment_keys = { "entry_ai_battle_comment_1", "entry_ai_battle_comment_2" },
},
-- ROUND 83 (r83-dandojo-as-mode) — 段位道場 is a MODE on the cabinet
-- (`mode_select.lua` CreateBoardList l.289-296 appends GameMode.kDani right after
@@ -103,24 +87,16 @@ local MODES = {
-- mode_select_comment_dani (three lines; every other board here has two).
dan = {
frame = 6, outline = {76, 60, 151, 255},
titles = { ja = "段位道場", en = "Dan-i Dojo", zh = "段位道場" },
comment = {
ja = { "3曲連続して演奏しながら、", "お題の合格を目指そう!", "合格すると腕前が認定されるよ!" },
en = { "Play three songs in a row", "and aim to clear the task!", "Your skills will be rewarded!" },
zh = { "連續演奏3首指定曲", "並達成題目的合格條件吧!", "只要合格實力就會備受肯定哦!" },
},
title_key = "mode_dan",
comment_keys = { "mode_dan_comment_1", "mode_dan_comment_2", "mode_dan_comment_3" },
},
-- ROUND 83: ゲーム設定 has no cabinet counterpart and was wearing 段位道場's board
-- (dani, frames 6/7) since ROUND 6. Moved onto `aprilfool`, baked as frames 9/10 by
-- bake_entry.py's EXTRA list; its title colour-add at label `aprilfool` is (0,132,212).
settings = {
frame = 9, outline = {0, 132, 212, 255},
titles = { ja = "ゲーム設定", en = "Settings", zh = "游戏設定" },
comment = {
ja = { "ゲームのせっていを", "かえられるよ!" },
en = { "Change the game's", "settings here!" },
zh = { "可以變更", "遊戲設定哦!" },
},
title_key = "entry_settings",
comment_keys = { "entry_settings_comment_1", "entry_settings_comment_2" },
},
}
@@ -129,16 +105,19 @@ local MODES = {
-- (pairs() order is unspecified). Ask the engine which language it is actually running
-- (`tex.language()`, script.cpp:693 -> config.general.language) and try that key first;
-- the old scan stays as the fallback for a language the table has no column for.
-- Titles and comments live in skin_config (entry_* / entry_*_comment_N); the engine hands
-- us the title it drew, so the mode is the one whose skin_config title matches.
local LANGS = { "ja", "en", "zh", "ko" }
local function find_mode(text_str)
local ok, cur = pcall(function() return tex.language() end)
if ok and type(cur) == "string" then
for key, m in pairs(MODES) do
if m.titles[cur] == text_str then return m, cur end
if tex.skin_text(m.title_key, cur) == text_str then return m, cur end
end
end
for key, m in pairs(MODES) do
for lang, t in pairs(m.titles) do
if t == text_str then return m, lang end
for _, lang in ipairs(LANGS) do
if tex.skin_text(m.title_key, lang) == text_str then return m, lang end
end
end
return MODES.settings, "ja", true
@@ -297,7 +276,8 @@ function EntryBox.new(text_str, font_size)
self.title = text.create_raw_text(text_str, TITLE_SIZE, { 255, 255, 255, 255 }, mode.outline, false, TITLE_INNER_T, TITLE_SPACING)
self.info = {}
if not unknown then
local lines = mode.comment[lang] or mode.comment.ja
local lines = {}
for i, key in ipairs(mode.comment_keys) do lines[i] = tex.skin_text(key, lang) end
for i, line in ipairs(lines) do
self.info[i] = text.create_raw_text(line, INFO_SIZE, INFO_COLOR, INFO_OUTLINE, false,
INFO_BORDER / tex.get_screen_scale(), INFO_SPACING)
+3 -19
View File
@@ -86,21 +86,6 @@ local ITEM_KEYS = {
"preset_1", "preset_2", "preset_3", "random_item",
}
local GUIDE_TEXT = {
ja = { "えらぶ", "決定" },
en = { "Move", "Confirm" },
zh = { "選擇", "決定" },
}
local function detect_lang(s)
if s:find("[\227-\239]") then
-- kana block (U+3040..U+30FF) => ja, otherwise CJK => zh
if s:find("\227[\129-\131]") then return "ja" end
return "zh"
end
return "en"
end
local function clamp(v, lo, hi)
if v < lo then return lo end
if v > hi then return hi end
@@ -140,10 +125,9 @@ function CostumeMenu.new(is_2p, text_str, title_x, title_y)
-- arcade: font 54 white, outline 6 (renderer default = black); thickness here is x1.5 on screen
self.title_text = text.create_raw_text(text_str, TITLE_SIZE, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2.0)
local lang = detect_lang(text_str)
local gt = GUIDE_TEXT[lang] or GUIDE_TEXT.ja
self.guide_sel = text.create_raw_text(gt[1], GUIDE_SIZE, { 30, 30, 30, 255 }, { 30, 30, 30, 255 }, false, 0, 1.0)
self.guide_dec = text.create_raw_text(gt[2], GUIDE_SIZE, { 30, 30, 30, 255 }, { 30, 30, 30, 255 }, false, 0, 1.0)
-- guide words from skin_config (guide_select_label / guide_decide_label)
self.guide_sel = text.create_raw_text(tex.skin_text("guide_select_label"), GUIDE_SIZE, { 30, 30, 30, 255 }, { 30, 30, 30, 255 }, false, 0, 1.0)
self.guide_dec = text.create_raw_text(tex.skin_text("guide_decide_label"), GUIDE_SIZE, { 30, 30, 30, 255 }, { 30, 30, 30, 255 }, false, 0, 1.0)
self.costume_select_mode = false
-- arcade cursor_idx_ (1..5, here 0-based) and select_offset_
+1 -1
View File
@@ -61,7 +61,7 @@ function AllNetIcon:draw_song_max()
local limit = tex.song_limit()
if not limit or limit <= 0 then return end
if self._song_max_n ~= limit then
self._song_max = text.create_raw_text(tostring(limit) .. "曲",
self._song_max = text.create_raw_text((tex.skin_text("song_num_max"):gsub("{0}", tostring(limit))),
SONG_MAX_FONT, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, SONG_MAX_BORDER / tex.get_screen_scale(), 2.0)
self._song_max_n = limit
end
+23 -2
View File
@@ -153,6 +153,16 @@ function SongTransition.new(title, subtitle, is_second)
self.chara_l = tex.load_texture("global/rainbow_transition/chara_left")
self.chara_r = tex.load_texture("global/rainbow_transition/chara_right")
self.hint = tex.load_texture("global/rainbow_transition/chara_center")
-- The hint block is baked in Japanese. Any other interface language draws the
-- three lines as text (skin_config transition_hint_title / _line1 / _line2) at the
-- rows measured off the texture, on the same fade.
local lang = tex.language and tex.language() or "ja"
if lang ~= "ja" and text and text.create_text then
local white, black = { 255, 255, 255, 255 }, { 0, 0, 0, 255 }
self.hint_title = text.create_text("transition_hint_title", white, black, false, 4.0, 2.0)
self.hint_line1 = text.create_text("transition_hint_line1", white, black, false, 3.0, 2.0)
self.hint_line2 = text.create_text("transition_hint_line2", white, black, false, 3.0, 2.0)
end
-- 演奏スキップON badge (the reference build loading/loading_song.nulm, mc `enso_skip_on`)
self.skip_badge = tex.load_texture("loading/skip_badge/badge")
-- NOT latched here: GameScreen::on_screen_start builds the transition
@@ -170,7 +180,7 @@ function SongTransition.new(title, subtitle, is_second)
-- locale and so do we. Built HERE and not in skip_armed(): the glyph
-- atlas needs a frame to upload, so creating it on the first drawn frame
-- left the first frames showing an empty pill.
self.skip_text = text.create_raw_text("演奏スキップON", SKIP_TEXT_SIZE,
self.skip_text = text.create_raw_text(tex.skin_text("skip_badge_text"), SKIP_TEXT_SIZE,
{ 0, 0, 0, 255 }, { 0, 0, 0, 0 }, false, 0, 2)
end
return self
@@ -276,7 +286,18 @@ function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down)
put(self.atlas_b, ORIGIN, 960 - bw * 0.5, 400 - bh * 0.5, bw, bh, info, SRC_BAND)
-- 6. the hint block (「ゲームのヒント」 + 3 lines) on the same curve: the
-- arcade turns every text field on at `stop` and off at `out`.
put(self.hint, REST_HINT, 0, 568, 1920, 512, info)
if self.hint_title then
-- texture rows (alpha > 200, outline included): title 165..202, line 1 228..257,
-- line 2 282..310, all centred on x = 960; the texture sits at y = 568.
local function centred(t, y)
if t then t:draw({ x = math.floor(960 - t.width / 2 + 0.5), y = y, fade = info }) end
end
centred(self.hint_title, 568 + 165)
centred(self.hint_line1, 568 + 228)
centred(self.hint_line2, 568 + 282)
else
put(self.hint, REST_HINT, 0, 568, 1920, 512, info)
end
end
-- 7. 演奏スキップON badge, top-left, on the same fade as the band. The arcade
+15 -11
View File
@@ -10,7 +10,8 @@ ResultPlayer = {}
ResultPlayer.__index = ResultPlayer
local GAUGE_SCALE = 0.7 -- arcade places tamashii_gage at 0.7 on the result board
local GAUGE_UNIT = 12 * GAUGE_SCALE -- 12 px per YataiDON gauge unit in the plate groove (87 units)
local GAUGE_CELLS = 50 -- the cabinet's gauge is 50 cells (kMaxGage_), like the game HUD
local GAUGE_UNIT = 21 * GAUGE_SCALE -- one 21 px cell of the groove, at the board's 0.7
local OPTION_PITCH = 43
-- ROUND 15 — the whole result sequence is now the cabinet's own state machine, read out of
@@ -37,7 +38,7 @@ local OPTION_PITCH = 43
local F = 1000 / 120 -- ROUND 24 (r24-fpsaudit): Common.FPS is the cabinet's
-- 120 fps script-logic clock, not 60 -- see ENGINE_BINDINGS.md
local WAIT_GAUGE_MS = 100 * F -- 833.3 fade-in done -> the gauge starts filling
local GAUGE_MS_UNIT = 7 * F * (50 / 87) -- 33.53 ms per YataiDON unit == 7 f per arcade unit
local GAUGE_MS_UNIT = 7 * F -- 58.33 ms per cell (kGageSpeed_ = 7 f per arcade unit)
local WAIT_SCORE_MS = 100 * F -- 833.3 gauge full -> StartScoreDisplay
local SCORE_TO_CROWN = 500 * F -- 4166.7 UpdateScore state length
local CROWN_TO_ACTION = 150 * F -- 1250.0 Crown state length -> MoveResultAction
@@ -169,6 +170,9 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.is_2p = is_2p
self.has_2p = has_2p
self.gauge_length = gauge_length
-- Fill target in cells: gauge_length is the engine's 87-unit display value
-- (points / max * 87), so cells = points * 50 / max, floored like the HUD gauge.
self.gauge_cells = math.floor(gauge_length * GAUGE_CELLS / 87 + 1e-4)
self.difficulty = difficulty
self.selected_difficulty = selected_difficulty
self.crown_type = crown_type
@@ -214,7 +218,7 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.gauge_started = false
self.gauge_start_ms = -1
self.gauge_shown = 0 -- units currently filled (animates up to gauge_length)
self.gauge_shown = 0 -- cells currently filled (animates up to gauge_cells)
self.gauge_done = false
self.score_delay = nil
self.fade_in_started = false -- read by C++: starts the score roll
@@ -349,7 +353,7 @@ function ResultPlayer:update(current_ms, fade_in_finished,
-- isEffectSkip_ (ResultMain): every remaining state collapses at once.
if is_skipped and not self.skipped then
self.skipped = true
self.gauge_shown = math.floor(self.gauge_length)
self.gauge_shown = self.gauge_cells
-- ROUND 52: skip cuts the count-up loop (UpdateTamashiiGage's
-- isEffectSkip branch does TSound:StopSlot, ResultMain.lua:1069) and
-- lands everything on one don_big (ScoreBase UpdateScoreDisplay's
@@ -372,7 +376,7 @@ function ResultPlayer:update(current_ms, fade_in_finished,
if self.gauge_started then
self.tamashii_fire:update(current_ms)
local target = math.floor(self.gauge_length)
local target = self.gauge_cells
-- WaitUpdateTamashiiGage: 100 frames of nothing, then 7 frames per arcade unit
local elapsed = current_ms - self.gauge_start_ms - WAIT_GAUGE_MS
if self.skipped then
@@ -388,7 +392,7 @@ function ResultPlayer:update(current_ms, fade_in_finished,
self.gauge_shown = math.min(target, math.floor(elapsed / GAUGE_MS_UNIT))
-- ROUND 52: the fill reaching the rainbow top plays
-- achieve_tamashii (ResultMain.lua:1086-1088; non-skip path only).
if not self.achieve_played and self.gauge_state == 2 and self.gauge_shown >= 87 then
if not self.achieve_played and self.gauge_state == 2 and self.gauge_shown >= GAUGE_CELLS then
self.achieve_played = true
audio.play_sound("achieve_tamashii" .. self.seat_lr, "sound")
end
@@ -410,7 +414,7 @@ function ResultPlayer:update(current_ms, fade_in_finished,
if audio.stop_sound then audio.stop_sound("count_up_loop" .. self.seat_c) end
end
end
if self.gauge_state == 2 and self.gauge_shown >= 87 then
if self.gauge_state == 2 and self.gauge_shown >= GAUGE_CELLS then
if self.rainbow_start_ms < 0 then self.rainbow_start_ms = current_ms end
self.rainbow_frac = math.fmod((current_ms - self.rainbow_start_ms) / 75.0, 8.0)
end
@@ -618,7 +622,7 @@ function ResultPlayer:draw_gauge()
local gl_int = self.gauge_shown
-- ROUND 48: the clear-zone position follows the norma art tier
-- (hard = normal's segment-35 zone), see the init block above.
local clear_pts = { 52, 60, 69 }
local clear_pts = { 30, 35, 40 } -- easy / normal+hard / oni norma cells
local clear_pt = clear_pts[self.art_index + 1]
local u = GAUGE_UNIT
@@ -626,7 +630,7 @@ function ResultPlayer:draw_gauge()
tex.draw_texture(self.t_gauge_unfilled, { scale = scale, fade = base_fade, index = p_idx })
end
if self.gauge_state == 2 and gl_int >= 87 then
if self.gauge_state == 2 and gl_int >= GAUGE_CELLS then
-- ROUND 53: rainbow cycle sampled from anim/gauge_rainbow (sprite 31): 83.33 ms per
-- step, base cel at 1 with the next fading in over it — the cabinet's own display
-- list, drawn leaf by leaf in depth order. Fallback = pre-R53 75 ms hand cycle.
@@ -692,7 +696,7 @@ function ResultPlayer:draw_gauge()
-- findings stand: the flame holds local (+43,-43) scale **0.800** through the
-- `fever` loop, which is why the drawn scale stays a constant 0.8 here.
local fire_sheen = nil
if self.gauge_state == 2 and gl_int >= 87 and self.t_gauge_tamashii_fire then
if self.gauge_state == 2 and gl_int >= GAUGE_CELLS and self.t_gauge_tamashii_fire then
local ff, fa2, sheen_a = fire_state(self.rainbow_start_ms, tex.get_current_ms())
if ff then
fire_sheen = sheen_a
@@ -708,7 +712,7 @@ function ResultPlayer:draw_gauge()
if self.t_gauge_tamashii then
tex.draw_texture(self.t_gauge_tamashii, { scale = scale, fade = base_fade, index = p_idx })
end
if self.gauge_state == 2 and gl_int >= 87 and self.t_gauge_tamashii_overlay then
if self.gauge_state == 2 and gl_int >= GAUGE_CELLS and self.t_gauge_tamashii_overlay then
if fire_sheen ~= nil then
if fire_sheen > 0.0 then
tex.draw_texture(self.t_gauge_tamashii_overlay,
+15 -22
View File
@@ -329,7 +329,7 @@ function SongSelect:draw_song_max()
if self._song_max_n ~= limit then
local sc = tex.get_skin_config("song_num") or {}
self._song_max_y = sc.y or 252
self._song_max = text.create_raw_text(tostring(limit) .. "曲",
self._song_max = text.create_raw_text((tex.skin_text("song_num_max"):gsub("{0}", tostring(limit))),
-- `common_song_select_number_of_songs` id 3 `text_total_songs`: size 24, border 3.5
sc.font_size or 24, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, ol(3.5), 2.0)
self._song_max_n = limit
@@ -2498,30 +2498,23 @@ local SW_CURSOR_CYCLE = 51.0
-- The strings are the cabinet's own word table (the reference build `datatable` wordlist), not invented:
-- sort_window_diff / _star / _order, course_easy..course_mania, sort_window_order_*,
-- folder_sort_select, sort_window_songs. Keyed by `tex.language()` with en as the fallback.
local SW_WORDS = {
header = {
ja = { "むずかしさ", "★の数", "表示順" },
en = { "Difficulty", 'Number of "★"', "Sort by" },
zh = { "難度", "★數", "顯示順序" },
},
course = {
ja = { "かんたん", "ふつう", "むずかしい", "おに", "おに" },
en = { "Easy", "Normal", "Hard", "Extreme", "Extreme" },
zh = { "簡單", "普通", "困難", "魔王", "魔王" },
},
order = {
ja = { "いつもどおり", "未クリア優先", "未フルコンボ優先", "未ドンダフルコンボ優先" },
en = { "Default", "Not cleared", "Not Full Combo", "Not Donderful Combo" },
zh = { "通常", "未通關優先", "未全連段優先", "未全良連段優先" },
},
title = { ja = "むずかしさからえらぶ", en = "Select by difficulty", zh = "以難易度選擇" },
songs = { ja = "曲", en = "Songs", zh = "首" },
-- skin_config keys (the strings themselves live there, per language)
local SW_KEYS = {
header = { "sort_window_diff", "sort_window_star", "sort_window_order" },
course = { "course_name_0", "course_name_1", "course_name_2", "course_name_3", "course_name_3" },
order = { "sort_window_order_0", "sort_window_order_1", "sort_window_order_2", "sort_window_order_3" },
title = "folder_sort_select",
songs = "sort_window_songs",
}
local function sw_words(group)
local lang = (tex.language and tex.language()) or "en"
local t = SW_WORDS[group]
return t[lang] or t.en
local k = SW_KEYS[group]
if type(k) == "table" then
local out = {}
for i, key in ipairs(k) do out[i] = tex.skin_text(key) end
return out
end
return tex.skin_text(k)
end
-- OutlinedText cache: key = group .. "\0" .. index (the strings never change at runtime).
+25 -25
View File
@@ -70,7 +70,7 @@ beafcc1a9146a1db31efab49f83fe5c076772e635625f1e562295494ebebe6cf *./Graphics/bac
9cd825ef8c220d21e03ae64a5cc2dafdb16d8e4495fb404af5d705d3569f202c *./Graphics/background/bga_bg_fever_2/s3.png
ef0c3da5d56e528cbb82e597b0e8c2625baa1128ed8447e99e56652d5c2c5e2e *./Graphics/background/bga_bg_fever_2/s5.png
3eaf190b9018644c24b889296aa15268bc4f57827ef597bd4c4331421d04b056 *./Graphics/background/bga_bg_fever_2/s9.png
42692e59d714c56aae4d46fa24dafbb583e3f52c9347b6812761d0c4a999aed7 *./Graphics/background/bga_bg_fever_2/texture.json
8fe3634c792f5e22d301d5775e61243ba4ae1d2e131a77efa6b5cb8d2257e3c0 *./Graphics/background/bga_bg_fever_2/texture.json
a7a94b9ef7b1d47df59851a30f8ff121997ad57f1a9987255549eed285a620a0 *./Graphics/background/bg_fever/bg_fever_0/background.png
e0f89dc98bc3cd5e36fe43afbf3d9ce142f69be881931e0a9733700e0df30628 *./Graphics/background/bg_fever/bg_fever_0/corner.png
e0f89dc98bc3cd5e36fe43afbf3d9ce142f69be881931e0a9733700e0df30628 *./Graphics/background/bg_fever/bg_fever_0/footer.png
@@ -1696,7 +1696,7 @@ e0f89dc98bc3cd5e36fe43afbf3d9ce142f69be881931e0a9733700e0df30628 *./Graphics/ent
e0f89dc98bc3cd5e36fe43afbf3d9ce142f69be881931e0a9733700e0df30628 *./Graphics/entry/background/shops_left.png
e0f89dc98bc3cd5e36fe43afbf3d9ce142f69be881931e0a9733700e0df30628 *./Graphics/entry/background/shops_right.png
c66ae3897ad679e05b07c4ba9ab4800f28c9aac7411c6286f1ddb8fae157e75e *./Graphics/entry/background/street_lit.png
671f31ba324eae4b68797ead149b72e691e158492c9541327b07c5fc2727acea *./Graphics/entry/background/texture.json
7bf2b9ac45c4ee6ce5073d910e291eb922174cba954be90696c74fda27317835 *./Graphics/entry/background/texture.json
e0f89dc98bc3cd5e36fe43afbf3d9ce142f69be881931e0a9733700e0df30628 *./Graphics/entry/background/tower.png
88d921ef4f651a0660de0289bb6dfe8c0a3d79cbdc0b7eb0662b74d59e8b2156 *./Graphics/entry/background/twinkle/0.png
b89344fe65f113ee23416d253aff7772b5690df8423aeafa35d64962527a5933 *./Graphics/entry/background/twinkle/1.png
@@ -2682,7 +2682,7 @@ d21dc53600bab83b80352bad38cd598d216c6e1279a4d54a0a085ad82ad1430d *./Graphics/res
925c61b2744b0540cea4a0f4124df64bb7cd990a94834ffb219c5f05a2302485 *./Graphics/result/score/score_num.png
684c22481d2a0e4bb2d6dd67b5df87650e081edfeadffcc70c377c393334c06d *./Graphics/result/score/score_shinuchi_en.png
1c160a1a14016ea3a5f761b040db433420476363224011d615613af660e7d0e1 *./Graphics/result/score/score_shinuchi_ja.png
b309049109d5ba5a35a29b06373f13dc93ec8591e93a3267616b7b33e0b8d18e *./Graphics/result/score/texture.json
89c4d6afafb8010ec4da0b9f72f10f677ec8c87677fcb3d2c9edde923a5b5f9f *./Graphics/result/score/texture.json
2e58e24c6920473fcb992280392f418d1d6058b8933a6fdc72ec2e1b986776bd *./Graphics/skin_config.json
0dce4781f045f0cd197797f76f0914af71e748d1cd661b44c760a479fa91a71c *./Graphics/song_select/animation.json
27e427b29774e04dc739b1c58a7205565de88cea6370876b623f8e13a7106b5c *./Graphics/song_select/box/back_graphic.png
@@ -3171,7 +3171,7 @@ e08f82230e9e8f8c00deed9a7d0d0eaba6933021b9960b782a8b77eca3200b06 *./Scripts/anim
eb2bb15e87cc83d66b320e0e4cbcbd2d5385881f36eca27fe73740439055ef6c *./Scripts/anim/sort_cursor.lua
5789c13c7958010f2878f9a0b119a50af4f362e754dc06ca3683ce742d507d25 *./Scripts/anim/title_idle.lua
24e7afe3e93935774c14aebce3e635970ec045952224ea5246633ff05b37e3af *./Scripts/anim/title_idle_map.lua
2a160f3c8902d329effb39fb342087b6f7dca56b15c13d1b605b7cf3ca275b1c *./Scripts/anim/wheel_decide.lua
66797bb60d6bacde5d882136a7dee9cda2f3e02282dd2ebaf2ee37346db14f70 *./Scripts/anim/wheel_decide.lua
4e61d41a6f50228b0d320290a99c7f6658843936448c61aff89c5a1e86891121 *./Scripts/background/background.lua
1d35116ef8edb2c9acf9ccc26024e1e788cf3cb010b87a930f1f76108d3c6b69 *./Scripts/background/bg_collabs/a3.lua
784052aa0ba1881415d1468174453cbee33b2cc58c6283b0a53758759a750668 *./Scripts/background/bg_collabs/animal.lua
@@ -3183,14 +3183,14 @@ c519641b04ec00ca74a1d1ce74f5cf81ec52aa5edd52ade83469250079b81147 *./Scripts/back
5c206f19a5da213bb6437eb5806238bef3615da19370843edf83ef3eda1b0877 *./Scripts/background/bg_collabs/oshiri.lua
91295c79c701f668c367fa43da1e6886c7164d6d701d26f32f8368bcab47a8b9 *./Scripts/background/bg_collabs/practice.lua
8e4f5625a08cde1c3d935024193cb44b9227888f3ff345f1e91eef74e2937f23 *./Scripts/background/bg_collabs/tutorial.lua
5fdaf3e32df20ab01f31f1c47248e6e9322b5bb1328684163ebb3333ad3b56b8 *./Scripts/background/bg_data/bg_anim/bg_0.lua
cb9f8b43f330485e3a8a61efe6b466d8c245de21400d5e3f39bd291d7225a29c *./Scripts/background/bg_data/bg_anim/bg_1.lua
b9c69a7db45b7449be537d46d9383785ce391574d0441d31d6771a244f4ff7a3 *./Scripts/background/bg_data/bg_anim/bg_2.lua
57e7cdf3c869bfb62d7063db3fbecf0c47c6de285187d44396ab80c3b2108af6 *./Scripts/background/bg_data/bg_anim/bg_3.lua
e1bfcb2a5782ff5397c2327fa3089aec6ef4cd4188b48ac6cc2b2cc1485bbac0 *./Scripts/background/bg_data/bg_anim/bg_4.lua
2d3b9768a070c23c1eff3d5be95cc74f39b1ac1e0f17ae72118bc80c8c259176 *./Scripts/background/bg_data/bg_anim/bg_fever_0.lua
b7ca6b3ea867ce111b4d6851199942410a0c898260ab61750a0de5ef2e74ce94 *./Scripts/background/bg_data/bg_anim/bg_fever_1.lua
6bf1ff661b31052db9d3bf7be1b91ee255e15c846dffd8597e41aff041f65b22 *./Scripts/background/bg_data/bg_anim/bg_fever_2.lua
4ff647ed2d5e89b4065ea3741c897c4e5f82847803b9716a4c2738d69662f932 *./Scripts/background/bg_data/bg_anim/bg_0.lua
8903b5109295abeafd333618da1a6d8469fccc765528f8df2ecd0a66bbcecc06 *./Scripts/background/bg_data/bg_anim/bg_1.lua
3e59921fa641cdf04b95d7a88069d98bcdcc71fe152157d380fcbca6501da36d *./Scripts/background/bg_data/bg_anim/bg_2.lua
3aee9ee68b0398ae69e094d91b627371c80eb7f192053223000df95ded445fb0 *./Scripts/background/bg_data/bg_anim/bg_3.lua
b3ef868c08d9c0ba0eb21c7540a198ae6f24884cce614d74a63d93aaed70b226 *./Scripts/background/bg_data/bg_anim/bg_4.lua
707b6845d9fb2467a0b2a69ed985c5a40c05178e05a1a8923e3aaa9a90ca9189 *./Scripts/background/bg_data/bg_anim/bg_fever_0.lua
f8a08c701525496d0babe753620675f076a9a2a4a1b08250d27da923164e3b55 *./Scripts/background/bg_data/bg_anim/bg_fever_1.lua
150bdb8d419fd8859f6cf21e7e0bff24488f9fba35bc41b5ad4561814113d9be *./Scripts/background/bg_data/bg_anim/bg_fever_2.lua
2eef6d469e79072285d2e2bef30fda69e30eaefb1d314cf858d948946371fc77 *./Scripts/background/bg_data/bg_fever.lua
41ca8d897d3ad67bc4f56b0807b9527814b135033b6bc0649d38ad55641bd2ad *./Scripts/background/bg_data/donbg_anim/a_01_1p.lua
854d59cdadd48bfba15ce2b99101dbbd7692955cc56d62e789bb4953d39a5a7e *./Scripts/background/bg_data/donbg_anim/a_01_2p.lua
@@ -3208,7 +3208,7 @@ b85248097bf701da651957088fa2944b1bce90d0be89b4ad00c5561a5cbcc284 *./Scripts/back
f1a738f092b1aa4aeb869dd11ec2b3bdf4fe0788f2d1796b622038d1df530ba9 *./Scripts/background/bg_data/fever_anim/f_1.lua
f154e429cb171d7ce72b029a4803ba74051ac23a3d7489f46fdcc45da72462e8 *./Scripts/background/bg_data/fever_anim/f_2.lua
06f2a3bd86d692bf21f143df0bf2f5d5e775c23b903e01de3f7812e4129a9c83 *./Scripts/background/bg_objects/arcade_gauge.lua
735bec40edcaa27036a8b0d83ba34ac57a40ae985bf538edf98d06e95b54c3d7 *./Scripts/background/bg_objects/bg_anim.lua
8c15718242606d122b6f9766c105500b3187f6ab0f4fd01bb84946f77ac39a34 *./Scripts/background/bg_objects/bg_anim.lua
f5f817baee833398cfbb5c0ab8d7e1868474fb94390ce1de2bd7a01c088f4520 *./Scripts/background/bg_objects/bg_fever.lua
a935ef8d768cf09b43b4b20672eb2621d94fe16085798ae25812bd88dddea61b *./Scripts/background/bg_objects/bg_normal.lua
09d76920ca920ef021dd40e06dfa4c9c0e665bf392316a92f2b5855254389ee3 *./Scripts/background/bg_objects/chibi/0.lua
@@ -3232,7 +3232,7 @@ c6dad415f43ae162eeda6ddc6cccafffadb98a842a2a7ad71288502f70d1a8a3 *./Scripts/back
d375fa5fcfd156fd74968b841423fe4618a1b3046d37dacb0b1c3d399dd471f6 *./Scripts/background/bg_objects/donbg/anim_player.lua
0bb1e1b3fd82e6b525dc951313fb4df87aae96cdbb3d9cad175bcbe368e84330 *./Scripts/background/bg_objects/donbg/base.lua
63bd3b3d984c5c970fbe1a3b45aadb0eee6e1409a790889a3585b7e8fda44f4a *./Scripts/background/bg_objects/donbg.lua
a2798341597901c488120539ba791c26bdbec329d5bc8126acbca67a2e2344f2 *./Scripts/background/bg_objects/ending.lua
149dc01c612347cdfcdf439315ef68dfc29faab2ea82453f5590b5583551c0da *./Scripts/background/bg_objects/ending.lua
7421cecba17c2d02d2fa9d80fbdeab1b6235420edcef6ee75bda4fb1797aabb0 *./Scripts/background/bg_objects/enso_skip.lua
78a051013339f7b2376561baa3a5d591e1435446b06721bbc02ac4b02e2ed137 *./Scripts/background/bg_objects/fever_anim.lua
bbc5dd4b156adaf9ee014885090623937935845aea4e356bb19d5b7f85a1a46c *./Scripts/background/bg_objects/fever.lua
@@ -3248,18 +3248,18 @@ f204455c907d3e0e7affb89db3649a709a27d528d84cc58d51a7d8cfaa1d3deb *./Scripts/back
510225154288da646a6becc9df9f3e2beb00af165815c0e9545a0363dddba589 *./Scripts/dan_select/dan_select.lua
616d72c0192f4a5a0309d4d16d481688a4c6a284faa4866b6e32ffa1608bddad *./Scripts/entry/box.lua
744599a57762ca2a669240caee885431c4216945c05bcd4e85c45850f2e931a3 *./Scripts/entry/costume_menu.lua
57b8f54e505b005cd7cace9fcf0622fdecf5d85a63cf7cbe3a8ea80f9a1412a3 *./Scripts/entry/entry.lua
64771d8bda30f17fa8e01862e722b7398c65d314fa2482adec348074859acf7f *./Scripts/entry/entry.lua
3d8c25402fff69adbfa1f6796807665f6cd61528583d0b7c625e09af752d5160 *./Scripts/entry/player.lua
aae54e0a31c77eac3fbf7e31db7e0136e9b440115ee5433c0b632b9871f13097 *./Scripts/game/clip_player.lua
62fabd365bc0240603bc719f89bf7b42cce8324a810ccc4d63e99a1a0d49b0da *./Scripts/game/ending_anim/ending_clear.lua
3f6f738f19b6475664bdba856711517e3c3a7c868d00184722e30c8222f413b6 *./Scripts/game/ending_anim/ending_donderful.lua
282af6e13da1b6974ec2b746073feee5fab48226d8c8c6e064bec20c6e19b35f *./Scripts/game/ending_anim/ending_fail.lua
4766cb18243dbfa38c3653962a8db4dd17b8724560c15ae9c62b7339266a8507 *./Scripts/game/ending_anim/ending_fullcombo.lua
719b9279ce4787dd3a96dcd80fabe228af814bb5b438f90d8639fd83518ee168 *./Scripts/game/clip_player.lua
5749994b631898c91e246110bb4b5e67aa3fdc395c4d59b49c4ee72a44e0043d *./Scripts/game/ending_anim/ending_clear.lua
33764ed8566bf003802816710a84f7bb01ef7a6ccd549b130d896f20c6e2a3b0 *./Scripts/game/ending_anim/ending_donderful.lua
1e5c509028b1a67acc2e40eed6222b51bc0e87a3d5e7eb8a4e3a3b1da4bc319b *./Scripts/game/ending_anim/ending_fail.lua
88a006d073f226f610f77c9cb45da966532eccdf97454ccf73570f2a73520f43 *./Scripts/game/ending_anim/ending_fullcombo.lua
60aad9f07edee45abb2dca0803acc0f46a519d6a0f6e0c7625b8dd127447cfb9 *./Scripts/game/ending_anim/rank_hud_in.lua
9a55c93ebd302db1bb4c7f55093582a1743ae29b8a34b7544e2e5117f78b865d *./Scripts/game/ending_anim/rank_hud.lua
f4de8faceba1417bbdf0c408275f36b91022c93539178bd2d59018623bac759b *./Scripts/game/ending_anim/rank_hud.lua
826c02cac90412a42722d0fa23e5f08878b00f32ca564607d08bd36fdecc0de4 *./Scripts/game/ending_anim/rank_hud_out.lua
5010747253fcca353b124290d1874b8a1ef5f733b77bd1ada14fd07e0f55e57e *./Scripts/game/ending_anim/rank_result_kiwami.lua
ff7652f6d7bab5ebf7391d360a625a7cfe9a694bc40951aa7a1fa096fa4fe9a6 *./Scripts/game/ending_anim/rank_result.lua
6c885099876d77ea14d4588edb3de5105ecef7f8b83e2787a3bdb25918e5b920 *./Scripts/game/ending_anim/rank_result_kiwami.lua
02a2a424b68fce7b172807ce60df5c2b41121cb65f2edcc5a1e43b77702a4008 *./Scripts/game/ending_anim/rank_result.lua
373351a486510d5dd4c5a3249ad0421a52908ac11a36ae53ae12ed3f6f481793 *./Scripts/global/allnet_indicator.lua
690e4e84a519bc3cdc39286a433b2dbc500834022a2b8bffa7b3513156d087ca *./Scripts/global/coin_overlay.lua
1eaebfd5dc7ec74571c9872d1817cd51e360db5c02cad6bf49d3c851ef9b70dc *./Scripts/global/entry_overlay.lua
@@ -3272,12 +3272,12 @@ bcd31928563102097ed1b2610f097dff26983f7209822b6a7294daa14e68d08a *./Scripts/resu
c5762b46c786d95cead9d69fda59cba844b0ebb007aca1b00fbad14a6a93d362 *./Scripts/result/high_score_indicator.lua
2a60a921314965c89ee2cdb29928955aca59c18bb691eae8f0ceceb28bed5d9d *./Scripts/result/result_background.lua
bae62b0f451139b880d57d4b214156a8deaadedb2aacc1f7421d4e21189a810d *./Scripts/result/result_bg_layers.lua
8cc680fde26a887539c6335bbf29a4ea2f96ef95164ec84580ed821f6b2355e5 *./Scripts/result/result_crown.lua
141a453f447f176f13f6c35f20335261d98f78ec97671e9bdb54cd5deece802c *./Scripts/result/result_crown.lua
18d140a70c3816578d523913b60155ea208ff91a67ded089b75ad401bd5cb625 *./Scripts/result/result_crown_message.lua
fe5bc0a6cff271012d0de9ec7fc5cc7a94b3f1268ea1aa4a18e72c38b1a0e6c3 *./Scripts/result/result_fade_in.lua
9447dea62976348429b38b22104c68708b21cfbbf61f5d37a5a8d082d69f7a1c *./Scripts/result/result_player.lua
1033652c20bd62a07f645c94961227dfe73fb49df9bc581c460739c96c109c08 *./Scripts/song_select/dan_doors.lua
698d1ba46c4be558b59624e339a5e1971d12bff5119daed2bc22bdeb5b305286 *./Scripts/song_select/song_select.lua
093ad96b9aebc98e6b1fb2ce8f8f3f9e9ab4e0f24f7328e6eb831d8c5e90c87e *./Scripts/song_select/song_select.lua
89047ab5b642696922ca2c6e098ca288580a9c0bb3e197a08f17b821a31fc519 *./.skin-repo
40c378cdcaea2e2c9ec3da4c7fa72911d13a849a5b51e9645300f9571a7e12c8 *./Sounds/dan_result/achieve_tamashii.ogg
606f2d3558badc06e54b2c59a36f0580f4954774918d69a712ad6de45518cba0 *./Sounds/dan_result/advance_intro.ogg
+83
View File
@@ -0,0 +1,83 @@
# `Graphics/font.ttf` — how it is made, and how the cabinet actually uses its fonts
## What the cabinet ships
The cabinet has **no vector font**. `Data\x64\font\` holds pre-rasterised BC7 glyph atlases,
one family per language and one atlas per size/weight bucket, each with an XML sidecar
(`codepoint → rect`; no bearings, no kerning):
| language | buckets on disk | distinct codepoints | face (as drawn in the atlas) |
|---|---|---|---|
| `jp` | `30`, `32_B`, `32_DB`, `32_EB`, `64` | 7 532 | 30/32: rounded gothic in three weights; 64: brush/勘亭流-style display face |
| `tw` | `30`, `32`, `64` | 17 678 | same split |
| `cn` | `30`, `32`, `64` | 7 608 | same split (the 64 face is a heavier brush) |
| `kr` | `30`, `32`, `64` | 8 407 | same split |
| `en` | `30`, `32`, `64` | 299 | same split |
(The JP 39.06 dump has the same files minus `cn_*`; the CN 05 dump adds `cn_*`. The
`jp/tw/kr/en` XMLs are byte-identical between the two.)
## How a text record picks its atlas
* **Language** comes from the cabinet language setting: wordlist rows are looked up in the
language's column and rendered from that language's atlas family. Rows flagged
`fontType 0` (untranslated) always render from `jp`.
* **Size/weight bucket** is authored per text record in the `.nulm` (`DefineFont` entry
symbol `"0".."4"` → `64`, `30`, `32_EB`, `32_B`, `32_DB`). It is not language dependent.
For `en/tw/kr/cn` the three weighted `32_*` buckets are registered onto the plain `32`
atlas, so a record that asks for `32_EB` on a `cn` cabinet is drawn from `cn_32`.
* The only fallback is `jp_64` when the language itself is unknown; a codepoint missing from
the chosen atlas is simply not drawn. Of the 692 text records in the shipped movies, 449
use the 30/32 (rounded gothic) buckets and 168 use the 64 (brush) bucket.
YataiDON draws every string through a single `.ttf`, so one face has to serve all of it.
This skin's `font.ttf` is built from the **`_64` (brush) atlases**, which is the face used for
headings, song titles and most of the large text.
## Building `font.ttf`
`tools/font/build_font.py` merges the five `_64` atlases into one TrueType font:
1. Decode each `<lang>_64.nutexb` to a DDS (BC7, `nutexb_dump -o`) next to its XML.
2. For each language in priority order `jp, tw, cn, kr, en`, every codepoint that does not
yet have an inked glyph takes its bitmap from that atlas (blank placeholder cells are
skipped so a later atlas can supply the character).
3. Each bitmap is upscaled 4× (bicubic), thresholded at alpha ≥ 128, traced with potrace
and converted to quadratic outlines (`cu2qu`).
4. Metrics: `unitsPerEm 1024`, `scale = 1024 / fontPoint` of the atlas (16 units per pixel
for `jp_64`), `advance = cell width × scale`, baseline = the atlas's median bottom ink row
of its Han/Hangul glyphs, so the five families sit on one baseline with equal Han height.
`ascender 880 / descender −144`, as the earlier build.
```
pip install potracer fonttools pillow numpy
python build_font.py <dir with *_64.dds + *_64.xml> font.ttf --order jp,tw,cn,kr,en
```
Result (version 3.000): 22 382 glyphs — 7 499 from `jp`, 10 107 from `tw`, 1 956 from `cn`,
2 737 from `kr`, 82 from `en`. Coverage of each atlas's repertoire: jp 7521/7532,
tw 17651/17678, cn 7589/7608, kr 8070/8407, en 268/295 (the remainder are blank cells in
the atlases themselves). Version 2.000 had no `cn` or `kr` glyphs, which is why Simplified
Chinese and Korean text showed `?`.
## Per-language fonts (`Graphics/font_<family>.ttf`)
The engine loads `Graphics/font_<language code>.ttf` if present, else `Graphics/font_<cabinet family>.ttf`
(zh → cn, ko → kr, ja → jp), else `font.ttf`. The settings screen reloads the skin, so switching
language switches the font. This mirrors the cabinet, which draws everything from the language's own
atlas family and only lacks what that family does not have.
| file | atlas priority | used for |
|---|---|---|
| `font.ttf` | `jp, tw, cn, kr, en` | Japanese, and any language without its own file (unchanged 3.000) |
| `font_cn.ttf` | `cn, tw, jp, en, kr` | Simplified Chinese (`zh`) |
| `font_tw.ttf` | `tw, jp, cn, en, kr` | Traditional Chinese (`zh_tw` / `tw`) |
| `font_en.ttf` | `en, jp, tw, cn, kr` | English |
| `font_kr.ttf` | `kr, jp, en, tw, cn` | Korean (`ko`) |
```
python build_font.py <dir> font_cn.ttf --order cn,tw,jp,en,kr
python build_font.py <dir> font_tw.ttf --order tw,jp,cn,en,kr
python build_font.py <dir> font_en.ttf --order en,jp,tw,cn,kr
python build_font.py <dir> font_kr.ttf --order kr,jp,en,tw,cn
```
+157
View File
@@ -0,0 +1,157 @@
"""Build a TrueType font from the cabinet's bitmap glyph atlases (font/<lang>_64.{xml,nutexb}).
The cabinet ships no vector font: each language has its own BC7 atlas + XML (codepoint ->
rect, no bearings, no kerning), and RenderText picks the atlas family by the cabinet
language (fontType 0 rows -> jp) and the size/weight bucket per text record. YataiDON
draws everything through one .ttf, so this merges the five 64pt atlases into one font:
every codepoint takes its glyph from the first atlas (in --order) that has ink for it.
Per atlas: scale = upm / fontPoint (jp_64: 16 units per pixel)
advance = glyph width * scale (A: 51 px -> 816, same as the earlier build)
baseline row = median bottom ink row of the Han/Hangul glyphs (+1), so the
different atlases sit on one baseline; Latin-only atlases use 'A'.
Tracing: alpha >= 128 at 4x bicubic upscale -> potrace -> cubic -> quadratic (cu2qu).
usage: python build_font.py <atlas_dir with *_64.dds/*_64.xml> <out.ttf> [--order jp,tw,cn,kr,en] [--jobs N]
"""
import sys, os, argparse, statistics, time
import xml.etree.ElementTree as ET
import numpy as np
from PIL import Image
import potrace
from multiprocessing import Pool
UPM = 1024
UPSCALE = 4
ASCENT, DESCENT = 880, -144
def parse_xml(path):
r = ET.parse(path).getroot().find('font')
glyphs = {int(x.get('index')): (int(x.get('offsetU')), int(x.get('offsetV')),
int(x.get('width')), int(x.get('height'))) for x in r.findall('glyph')}
return glyphs, {k: int(v) for k, v in r.attrib.items()}
_A = None
def _init(alpha_path):
global _A
_A = np.load(alpha_path, mmap_mode='r')
def _trace(job):
cp, u, v, w, h, scale, baseline_px = job
a = np.asarray(_A[v:v + h, u:u + w])
rows = np.where(a.max(axis=1) >= 128)[0]
if rows.size == 0:
return cp, int(round(w * scale)), None # blank: advance only
big = np.array(Image.fromarray(a).resize((w * UPSCALE, h * UPSCALE), Image.BICUBIC)) < 128 # potracer: True = white
path = potrace.Bitmap(big).trace(turdsize=4,
alphamax=1.0, opticurve=True, opttolerance=0.2)
k = scale / UPSCALE
def P(p): return (p.x * k, (baseline_px * UPSCALE - p.y) * k)
contours = []
for curve in path.curves:
segs = [('M', P(curve.start_point))]
for s in curve.segments:
if s.is_corner:
segs.append(('L', P(s.c))); segs.append(('L', P(s.end_point)))
else:
segs.append(('C', P(s.c1), P(s.c2), P(s.end_point)))
contours.append(segs)
return cp, int(round(w * scale)), contours
def main():
ap = argparse.ArgumentParser()
ap.add_argument('atlas_dir'); ap.add_argument('out')
ap.add_argument('--order', default='jp,tw,cn,kr,en')
ap.add_argument('--han-order', default='', help='separate priority for CJK ideographs (U+2E80-9FFF, F900-FAFF, 20000+); --order then applies to everything else')
ap.add_argument('--jobs', type=int, default=max(1, os.cpu_count() - 4))
ap.add_argument('--limit', type=int, default=0, help='debug: glyphs per atlas')
ap.add_argument('--embolden', default='', help='lang=px: dilate that atlas alpha by px before tracing')
args = ap.parse_args()
langs = args.order.split(',')
han_langs = args.han_order.split(',') if args.han_order else langs
def is_han(cp): return 0x2E80 <= cp <= 0x9FFF or 0xF900 <= cp <= 0xFAFF or cp >= 0x20000
def rank(lang, cp): return (han_langs if is_han(cp) else langs).index(lang) if lang in (han_langs if is_han(cp) else langs) else 99
taken = {} # cp -> (advance, contours or None, lang)
all_langs = list(dict.fromkeys(langs + han_langs))
for lang in all_langs:
t0 = time.time()
glyphs, attr = parse_xml(os.path.join(args.atlas_dir, f'{lang}_64.xml'))
im = Image.open(os.path.join(args.atlas_dir, f'{lang}_64.dds'))
A = np.array(im)[:, :, 3]
emb = dict(kv.split('=') for kv in args.embolden.split(',') if kv)
if lang in emb and int(emb[lang]) > 0:
from scipy.ndimage import grey_dilation
k = 2 * int(emb[lang]) + 1; A = grey_dilation(A, size=(k, k))
scale = UPM / attr['fontPoint']
# baseline: where the Han / Hangul glyphs sit (Latin-only atlas: 'A')
bots = []
for cp, (u, v, w, h) in glyphs.items():
if 0x4E00 <= cp <= 0x9FFF or 0xAC00 <= cp <= 0xD7A3:
r = np.where(A[v:v + h, u:u + w].max(axis=1) >= 128)[0]
if r.size: bots.append(int(r.max()))
if bots:
baseline_px = statistics.median(bots) + 1
else:
u, v, w, h = glyphs[0x41]
r = np.where(A[v:v + h, u:u + w].max(axis=1) >= 128)[0]
baseline_px = int(r.max()) + 1 + 2 * 64 / attr['fontPoint'] # 'A' sits ~2 jp-px above the Han bottom
alpha_path = os.path.join(args.atlas_dir, f'_{lang}_alpha.npy')
np.save(alpha_path, A)
jobs = [(cp, u, v, w, h, scale, baseline_px) for cp, (u, v, w, h) in sorted(glyphs.items())
if (cp not in taken or taken[cp][1] is None and cp not in (0x20, 0x3000, 0xA0) or rank(lang, cp) < rank(taken[cp][2], cp))]
if args.limit: jobs = jobs[:args.limit]
got = 0
with Pool(args.jobs, initializer=_init, initargs=(alpha_path,)) as pool:
for cp, adv, contours in pool.imap_unordered(_trace, jobs, chunksize=64):
if contours is None:
if cp in (0x20, 0x3000, 0xA0) and cp not in taken:
taken[cp] = (adv, None, lang)
continue
if cp in taken and taken[cp][1] is not None and rank(lang, cp) >= rank(taken[cp][2], cp): continue
taken[cp] = (adv, contours, lang); got += 1
os.remove(alpha_path)
print(f'{lang}_64: fontPoint {attr["fontPoint"]} scale {scale:.2f} baseline {baseline_px:.1f}px '
f'-> {got} new glyphs ({time.time() - t0:.0f}s)', flush=True)
# ---- assemble
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.cu2quPen import Cu2QuPen
order = ['.notdef'] + [(f'u{cp:05X}' if cp > 0xFFFF else f'uni{cp:04X}') for cp in sorted(taken)]
fb = FontBuilder(UPM, isTTF=True)
fb.setupGlyphOrder(order)
cmap, glyf, hmtx = {}, {}, {}
pen = TTGlyphPen(None); glyf['.notdef'] = pen.glyph(); hmtx['.notdef'] = (512, 0)
for name, cp in zip(order[1:], sorted(taken)):
adv, contours, _ = taken[cp]
pen = TTGlyphPen(None)
if contours:
qpen = Cu2QuPen(pen, max_err=1.0, reverse_direction=False)
for segs in contours:
for s in segs:
if s[0] == 'M': qpen.moveTo(s[1])
elif s[0] == 'L': qpen.lineTo(s[1])
else: qpen.curveTo(s[1], s[2], s[3])
qpen.closePath()
g = pen.glyph()
glyf[name] = g
g.recalcBounds(glyf) if contours else None
hmtx[name] = (adv, getattr(g, 'xMin', 0) if contours else 0)
cmap[cp] = name
fb.setupCharacterMap(cmap)
fb.setupGlyf(glyf)
fb.setupHorizontalMetrics(hmtx)
fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT)
fb.setupNameTable({'familyName': 'YataiDON HSS Atlas', 'styleName': 'Regular',
'uniqueFontIdentifier': 'YataiDON HSS Atlas 3.000', 'fullName': 'YataiDON HSS Atlas Regular',
'version': 'Version 3.000', 'psName': 'YataiDONHSSAtlas-Regular'})
fb.setupOS2(sTypoAscender=ASCENT, sTypoDescender=DESCENT, sTypoLineGap=0,
usWinAscent=ASCENT, usWinDescent=-DESCENT, fsType=0)
fb.setupPost()
fb.save(args.out)
from collections import Counter
print('saved', args.out, len(order), 'glyphs;', dict(Counter(v[2] for v in taken.values())))
if __name__ == '__main__':
main()
+155
View File
@@ -0,0 +1,155 @@
"""Build a TrueType font from the cabinet's bitmap glyph atlases (font/<lang>_64.{xml,nutexb}).
The cabinet ships no vector font: each language has its own BC7 atlas + XML (codepoint ->
rect, no bearings, no kerning), and RenderText picks the atlas family by the cabinet
language (fontType 0 rows -> jp) and the size/weight bucket per text record. YataiDON
draws everything through one .ttf, so this merges the five 64pt atlases into one font:
every codepoint takes its glyph from the first atlas (in --order) that has ink for it.
Per atlas: scale = upm / fontPoint (jp_64: 16 units per pixel)
advance = glyph width * scale (A: 51 px -> 816, same as the earlier build)
baseline row = median bottom ink row of the Han/Hangul glyphs (+1), so the
different atlases sit on one baseline; Latin-only atlases use 'A'.
Tracing: alpha >= 128 at 4x bicubic upscale -> potrace -> cubic -> quadratic (cu2qu).
usage: python build_font.py <atlas_dir with *_64.dds/*_64.xml> <out.ttf> [--order jp,tw,cn,kr,en] [--jobs N]
"""
import sys, os, argparse, statistics, time
import xml.etree.ElementTree as ET
import numpy as np
from PIL import Image
import potrace
from multiprocessing import Pool
UPM = 1024
UPSCALE = 4
ASCENT, DESCENT = 880, -144
def parse_xml(path):
r = ET.parse(path).getroot().find('font')
glyphs = {int(x.get('index')): (int(x.get('offsetU')), int(x.get('offsetV')),
int(x.get('width')), int(x.get('height'))) for x in r.findall('glyph')}
return glyphs, {k: int(v) for k, v in r.attrib.items()}
_A = None
def _init(alpha_path):
global _A
_A = np.load(alpha_path, mmap_mode='r')
def _trace(job):
cp, u, v, w, h, scale, baseline_px = job
a = np.asarray(_A[v:v + h, u:u + w])
rows = np.where(a.max(axis=1) >= 128)[0]
if rows.size == 0:
return cp, int(round(w * scale)), None # blank: advance only
big = np.array(Image.fromarray(a).resize((w * UPSCALE, h * UPSCALE), Image.BICUBIC)) < 128 # potracer: True = white
path = potrace.Bitmap(big).trace(turdsize=4,
alphamax=1.0, opticurve=True, opttolerance=0.5)
k = scale / UPSCALE
def P(p): return (p.x * k, (baseline_px * UPSCALE - p.y) * k)
contours = []
for curve in path.curves:
segs = [('M', P(curve.start_point))]
for s in curve.segments:
if s.is_corner:
segs.append(('L', P(s.c))); segs.append(('L', P(s.end_point)))
else:
segs.append(('C', P(s.c1), P(s.c2), P(s.end_point)))
contours.append(segs)
return cp, int(round(w * scale)), contours
def main():
ap = argparse.ArgumentParser()
ap.add_argument('atlas_dir'); ap.add_argument('out')
ap.add_argument('--order', default='jp,tw,cn,kr,en')
ap.add_argument('--jobs', type=int, default=max(1, os.cpu_count() - 4))
ap.add_argument('--limit', type=int, default=0, help='debug: glyphs per atlas')
ap.add_argument('--files', default='', help='lang=atlas stem overrides, e.g. jp=jp_32_EB,tw=tw_32')
ap.add_argument('--embolden', default='', help='lang=px: dilate that atlas alpha by px before tracing')
args = ap.parse_args()
langs = args.order.split(',')
stems = dict(kv.split('=') for kv in args.files.split(',') if kv)
taken = {} # cp -> (advance, contours or None, lang)
for lang in langs:
t0 = time.time()
stem = stems.get(lang, f'{lang}_64')
glyphs, attr = parse_xml(os.path.join(args.atlas_dir, f'{stem}.xml'))
im = Image.open(os.path.join(args.atlas_dir, f'{stem}.dds'))
A = np.array(im)[:, :, 3]
emb = dict(kv.split('=') for kv in args.embolden.split(',') if kv)
if lang in emb and int(emb[lang]) > 0:
from scipy.ndimage import grey_dilation
k = 2 * int(emb[lang]) + 1; A = grey_dilation(A, size=(k, k))
scale = UPM / attr['fontPoint']
# baseline: where the Han / Hangul glyphs sit (Latin-only atlas: 'A')
bots = []
for cp, (u, v, w, h) in glyphs.items():
if 0x4E00 <= cp <= 0x9FFF or 0xAC00 <= cp <= 0xD7A3:
r = np.where(A[v:v + h, u:u + w].max(axis=1) >= 128)[0]
if r.size: bots.append(int(r.max()))
if bots:
baseline_px = statistics.median(bots) + 1
else:
u, v, w, h = glyphs[0x41]
r = np.where(A[v:v + h, u:u + w].max(axis=1) >= 128)[0]
baseline_px = int(r.max()) + 1 + 2 * 64 / attr['fontPoint'] # 'A' sits ~2 jp-px above the Han bottom
alpha_path = os.path.join(args.atlas_dir, f'_{lang}_alpha.npy')
np.save(alpha_path, A)
jobs = [(cp, u, v, w, h, scale, baseline_px) for cp, (u, v, w, h) in sorted(glyphs.items())
if cp not in taken or taken[cp][1] is None and cp not in (0x20, 0x3000, 0xA0)]
if args.limit: jobs = jobs[:args.limit]
got = 0
with Pool(args.jobs, initializer=_init, initargs=(alpha_path,)) as pool:
for cp, adv, contours in pool.imap_unordered(_trace, jobs, chunksize=64):
if contours is None:
if cp in (0x20, 0x3000, 0xA0) and cp not in taken:
taken[cp] = (adv, None, lang)
continue
if cp in taken and taken[cp][1] is not None: continue
taken[cp] = (adv, contours, lang); got += 1
os.remove(alpha_path)
print(f'{stem}: fontPoint {attr["fontPoint"]} scale {scale:.2f} baseline {baseline_px:.1f}px '
f'-> {got} new glyphs ({time.time() - t0:.0f}s)', flush=True)
# ---- assemble
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.cu2quPen import Cu2QuPen
order = ['.notdef'] + [(f'u{cp:05X}' if cp > 0xFFFF else f'uni{cp:04X}') for cp in sorted(taken)]
fb = FontBuilder(UPM, isTTF=True)
fb.setupGlyphOrder(order)
cmap, glyf, hmtx = {}, {}, {}
pen = TTGlyphPen(None); glyf['.notdef'] = pen.glyph(); hmtx['.notdef'] = (512, 0)
for name, cp in zip(order[1:], sorted(taken)):
adv, contours, _ = taken[cp]
pen = TTGlyphPen(None)
if contours:
qpen = Cu2QuPen(pen, max_err=2.0, reverse_direction=False)
for segs in contours:
for s in segs:
if s[0] == 'M': qpen.moveTo(s[1])
elif s[0] == 'L': qpen.lineTo(s[1])
else: qpen.curveTo(s[1], s[2], s[3])
qpen.closePath()
g = pen.glyph()
glyf[name] = g
g.recalcBounds(glyf) if contours else None
hmtx[name] = (adv, getattr(g, 'xMin', 0) if contours else 0)
cmap[cp] = name
fb.setupCharacterMap(cmap)
fb.setupGlyf(glyf)
fb.setupHorizontalMetrics(hmtx)
fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT)
fb.setupNameTable({'familyName': os.environ.get('FONT_FAMILY', 'YataiDON HSS Atlas'), 'styleName': 'Regular',
'uniqueFontIdentifier': os.environ.get('FONT_FAMILY', 'YataiDON HSS Atlas') + ' 3.000', 'fullName': os.environ.get('FONT_FAMILY', 'YataiDON HSS Atlas') + ' Regular',
'version': 'Version 3.000', 'psName': os.environ.get('FONT_FAMILY', 'YataiDON HSS Atlas').replace(' ', '') + '-Regular'})
fb.setupOS2(sTypoAscender=ASCENT, sTypoDescender=DESCENT, sTypoLineGap=0,
usWinAscent=ASCENT, usWinDescent=-DESCENT, fsType=0)
fb.setupPost()
fb.save(args.out)
from collections import Counter
print('saved', args.out, len(order), 'glyphs;', dict(Counter(v[2] for v in taken.values())))
if __name__ == '__main__':
main()