Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553fbf72fa | ||
|
|
9382345c39 | ||
|
|
ad0a3b69f2 | ||
|
|
8ea7934a73 | ||
|
|
a0e57c6e23 | ||
|
|
8d106e1df5 | ||
|
|
0f2aa5ddb4 | ||
|
|
c8b6568d9a | ||
|
|
e256d42f9c | ||
|
|
54afedc34b | ||
|
|
80f4726c00 | ||
|
|
1c5d258314 | ||
|
|
0eea45143e | ||
|
|
65066bbe46 | ||
|
|
37b8bd609b | ||
|
|
67f7c48d6e | ||
|
|
4e79c6eed6 | ||
|
|
8b6dff1189 | ||
|
|
7ba7e142ae | ||
|
|
638b8b37c5 | ||
|
|
c13226e3b3 | ||
|
|
2dc940c6aa | ||
|
|
d5ef78970e | ||
|
|
ecfaed05f7 | ||
|
|
1a39365e21 | ||
|
|
a4a107f894 | ||
|
|
7da693af35 |
No files matched your search
|
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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -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": {
|
||||
@@ -109,7 +137,8 @@
|
||||
"text": {
|
||||
"ja": "{0}曲目",
|
||||
"en": "Song {0}",
|
||||
"zh": "{0}曲目"
|
||||
"zh": "{0}曲目",
|
||||
"ko": "{0}곡째"
|
||||
},
|
||||
"outline": 2.3333333333333335
|
||||
},
|
||||
@@ -120,7 +149,8 @@
|
||||
"text": {
|
||||
"ja": "もどる",
|
||||
"en": "BACK",
|
||||
"zh": "返回"
|
||||
"zh": "返回",
|
||||
"ko": "돌아가기"
|
||||
}
|
||||
},
|
||||
"transition_title": {
|
||||
@@ -141,7 +171,8 @@
|
||||
"text": {
|
||||
"ja": "フリープレイ",
|
||||
"en": "FREE PLAY",
|
||||
"zh": "自由游戏"
|
||||
"zh": "自由游戏",
|
||||
"ko": "프리 플레이"
|
||||
}
|
||||
},
|
||||
"entry_overlay_entry": {
|
||||
@@ -155,7 +186,8 @@
|
||||
"text": {
|
||||
"ja": "2人プレイ",
|
||||
"en": "2P",
|
||||
"zh": "2P"
|
||||
"zh": "2P",
|
||||
"ko": "2인 플레이"
|
||||
}
|
||||
},
|
||||
"credit_side_msg": {
|
||||
@@ -165,7 +197,8 @@
|
||||
"text": {
|
||||
"ja": "太鼓をたたいてスタート!",
|
||||
"en": "Hit the taiko to start!",
|
||||
"zh": "敲打太鼓開始!"
|
||||
"zh": "敲打太鼓开始!",
|
||||
"ko": "북을 쳐서 시작!"
|
||||
}
|
||||
},
|
||||
"crown_message_miss": {
|
||||
@@ -175,7 +208,8 @@
|
||||
"text": {
|
||||
"ja": "もう少し\nがんばるドン!",
|
||||
"en": "Good try.",
|
||||
"zh": "下次\n加油鼓咚!"
|
||||
"zh": "下次\n加油鼓咚!",
|
||||
"ko": "조금 더\n힘내라 동!"
|
||||
}
|
||||
},
|
||||
"crown_message_near": {
|
||||
@@ -185,7 +219,8 @@
|
||||
"text": {
|
||||
"ja": "おしかったドン",
|
||||
"en": "Almost.",
|
||||
"zh": "可惜!\n就差一點了鼓咚…"
|
||||
"zh": "可惜!\n就差一点了鼓咚…",
|
||||
"ko": "아쉬웠다 동"
|
||||
}
|
||||
},
|
||||
"crown_message_success": {
|
||||
@@ -195,7 +230,8 @@
|
||||
"text": {
|
||||
"ja": "ノリノリだドン",
|
||||
"en": "Cleared!\nCongrats, Don!",
|
||||
"zh": "過關\n恭喜鼓咚!"
|
||||
"zh": "过关\n恭喜鼓咚!",
|
||||
"ko": "신난다 동"
|
||||
}
|
||||
},
|
||||
"crown_message_perfect": {
|
||||
@@ -205,7 +241,8 @@
|
||||
"text": {
|
||||
"ja": "よくできたドン!",
|
||||
"en": "Awesome!",
|
||||
"zh": "魂槽全滿!\n太厲害了鼓咚!"
|
||||
"zh": "魂槽全满!\n太厉害了鼓咚!",
|
||||
"ko": "잘했다 동!"
|
||||
}
|
||||
},
|
||||
"song_select_indicator": {
|
||||
@@ -249,7 +286,8 @@
|
||||
"text": {
|
||||
"en": "Select your outfit!",
|
||||
"ja": "きせかえをえらぼう!",
|
||||
"zh": "選擇服飾吧!"
|
||||
"zh": "选择服饰吧!",
|
||||
"ko": "옷을 골라 보자!"
|
||||
}
|
||||
},
|
||||
"result_chara": {
|
||||
@@ -288,7 +326,8 @@
|
||||
"text": {
|
||||
"ja": "ベストスコア更新!",
|
||||
"en": "New High Score!",
|
||||
"zh": "最高分更新!"
|
||||
"zh": "最高分更新!",
|
||||
"ko": "베스트 스코어 갱신!"
|
||||
}
|
||||
},
|
||||
"course_name_0": {
|
||||
@@ -298,7 +337,8 @@
|
||||
"text": {
|
||||
"ja": "かんたん",
|
||||
"en": "Easy",
|
||||
"zh": "簡單"
|
||||
"zh": "简单",
|
||||
"ko": "쉬움"
|
||||
}
|
||||
},
|
||||
"course_name_1": {
|
||||
@@ -308,7 +348,8 @@
|
||||
"text": {
|
||||
"ja": "ふつう",
|
||||
"en": "Normal",
|
||||
"zh": "普通"
|
||||
"zh": "普通",
|
||||
"ko": "보통"
|
||||
}
|
||||
},
|
||||
"course_name_2": {
|
||||
@@ -318,7 +359,8 @@
|
||||
"text": {
|
||||
"ja": "むずかしい",
|
||||
"en": "Hard",
|
||||
"zh": "困難"
|
||||
"zh": "困难",
|
||||
"ko": "어려움"
|
||||
}
|
||||
},
|
||||
"course_name_3": {
|
||||
@@ -328,7 +370,8 @@
|
||||
"text": {
|
||||
"ja": "おに",
|
||||
"en": "Oni",
|
||||
"zh": "魔王"
|
||||
"zh": "魔王",
|
||||
"ko": "오니"
|
||||
}
|
||||
},
|
||||
"course_name_4": {
|
||||
@@ -338,7 +381,8 @@
|
||||
"text": {
|
||||
"ja": "おに(裏)",
|
||||
"en": "Ura",
|
||||
"zh": "裏魔王"
|
||||
"zh": "裏魔王",
|
||||
"ko": "오니(우라)"
|
||||
}
|
||||
},
|
||||
"guide_select_label": {
|
||||
@@ -348,7 +392,8 @@
|
||||
"text": {
|
||||
"ja": "えらぶ",
|
||||
"en": "Select",
|
||||
"zh": "選擇"
|
||||
"zh": "选择",
|
||||
"ko": "선택"
|
||||
}
|
||||
},
|
||||
"guide_decide_label": {
|
||||
@@ -358,7 +403,8 @@
|
||||
"text": {
|
||||
"ja": "決定",
|
||||
"en": "Decide",
|
||||
"zh": "決定"
|
||||
"zh": "决定",
|
||||
"ko": "결정"
|
||||
}
|
||||
},
|
||||
"nameplate_entry_left": {
|
||||
@@ -376,7 +422,8 @@
|
||||
"text": {
|
||||
"ja": "かんたん",
|
||||
"en": "Easy",
|
||||
"zh": "簡單"
|
||||
"zh": "简单",
|
||||
"ko": "쉬움"
|
||||
}
|
||||
},
|
||||
"chip_course_name_1": {
|
||||
@@ -386,7 +433,8 @@
|
||||
"text": {
|
||||
"ja": "ふつう",
|
||||
"en": "Normal",
|
||||
"zh": "普通"
|
||||
"zh": "普通",
|
||||
"ko": "보통"
|
||||
}
|
||||
},
|
||||
"chip_course_name_2": {
|
||||
@@ -396,7 +444,8 @@
|
||||
"text": {
|
||||
"ja": "むずかしい",
|
||||
"en": "Hard",
|
||||
"zh": "困難"
|
||||
"zh": "困难",
|
||||
"ko": "어려움"
|
||||
}
|
||||
},
|
||||
"chip_course_name_3": {
|
||||
@@ -406,7 +455,8 @@
|
||||
"text": {
|
||||
"ja": "おに",
|
||||
"en": "Oni",
|
||||
"zh": "魔王"
|
||||
"zh": "魔王",
|
||||
"ko": "오니"
|
||||
}
|
||||
},
|
||||
"chip_course_name_4": {
|
||||
@@ -416,7 +466,8 @@
|
||||
"text": {
|
||||
"ja": "おに(裏)",
|
||||
"en": "Ura",
|
||||
"zh": "裏魔王"
|
||||
"zh": "裏魔王",
|
||||
"ko": "오니(우라)"
|
||||
}
|
||||
},
|
||||
"modifier_text": {
|
||||
@@ -462,7 +513,8 @@
|
||||
"text": {
|
||||
"en": "Used Skip Song",
|
||||
"ja": "演奏スキップをつかいました",
|
||||
"zh": "使用了跳略演奏"
|
||||
"zh": "使用了跳略演奏",
|
||||
"ko": "연주 스킵을 사용했습니다"
|
||||
}
|
||||
},
|
||||
"game_nameplate_1p": {
|
||||
@@ -477,21 +529,24 @@
|
||||
"text": {
|
||||
"en": "Skip Song",
|
||||
"ja": "演奏スキップ",
|
||||
"zh": "跳略演奏"
|
||||
"zh": "跳略演奏",
|
||||
"ko": "연주 스킵"
|
||||
}
|
||||
},
|
||||
"modifier_name_neiro": {
|
||||
"text": {
|
||||
"en": "Sound",
|
||||
"ja": "音色",
|
||||
"zh": "音色"
|
||||
"zh": "音色",
|
||||
"ko": "음색"
|
||||
}
|
||||
},
|
||||
"modifier_text_muon": {
|
||||
"text": {
|
||||
"en": "No sound",
|
||||
"ja": "無音",
|
||||
"zh": "無聲"
|
||||
"zh": "无声",
|
||||
"ko": "무음"
|
||||
}
|
||||
},
|
||||
"modifier_title": {
|
||||
@@ -501,7 +556,8 @@
|
||||
"text": {
|
||||
"ja": "演奏オプション",
|
||||
"en": "Play Options",
|
||||
"zh": "演奏選項"
|
||||
"zh": "演奏选项",
|
||||
"ko": "연주 옵션"
|
||||
}
|
||||
},
|
||||
"song_select_chara_1p": {
|
||||
@@ -662,7 +718,8 @@
|
||||
"text": {
|
||||
"en": "1-Player",
|
||||
"ja": "1人プレイ",
|
||||
"zh": "1人遊玩"
|
||||
"zh": "1人游玩",
|
||||
"ko": "1인 플레이"
|
||||
}
|
||||
},
|
||||
"entry_credit_2p": {
|
||||
@@ -672,7 +729,8 @@
|
||||
"text": {
|
||||
"en": "2-Player",
|
||||
"ja": "2人プレイ",
|
||||
"zh": "2人遊玩"
|
||||
"zh": "2人游玩",
|
||||
"ko": "2인 플레이"
|
||||
}
|
||||
},
|
||||
"entry_credit_start": {
|
||||
@@ -682,7 +740,8 @@
|
||||
"text": {
|
||||
"en": "Hit the drum to start!",
|
||||
"ja": "太鼓をたたいてスタート!",
|
||||
"zh": "敲打太鼓開始遊戲!"
|
||||
"zh": "敲打太鼓开始游戏!",
|
||||
"ko": "북을 쳐서 시작!"
|
||||
}
|
||||
},
|
||||
"entry_cancel": {
|
||||
@@ -692,7 +751,8 @@
|
||||
"text": {
|
||||
"en": "Cancel",
|
||||
"ja": "つかわない",
|
||||
"zh": "不使用"
|
||||
"zh": "不使用",
|
||||
"ko": "사용 안 함"
|
||||
}
|
||||
},
|
||||
"chara_entry": {
|
||||
@@ -774,14 +834,14 @@
|
||||
"text": {
|
||||
"ja": "{0}曲",
|
||||
"zh": "{0}曲",
|
||||
"en": "of {0}"
|
||||
"en": "of {0}",
|
||||
"ko": "{0}곡"
|
||||
},
|
||||
"outline": 2.0
|
||||
},
|
||||
"song_num_max_game": {
|
||||
"x": 1814,
|
||||
"y": 122
|
||||
|
||||
},
|
||||
"note_arc_curve_height": {
|
||||
"height": 608
|
||||
@@ -873,8 +933,8 @@
|
||||
"height": 132
|
||||
},
|
||||
"dan_exam_seg": {
|
||||
"x": 14,
|
||||
"y": 48,
|
||||
"x": 11,
|
||||
"y": 47,
|
||||
"width": 327
|
||||
},
|
||||
"dan_select_exam_border_text": {
|
||||
@@ -926,22 +986,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 +1034,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": "段位道场"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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()
|
||||
@@ -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()
|
||||